diff --git a/client/app.js b/client/app.js index a357457..0512727 100644 --- a/client/app.js +++ b/client/app.js @@ -7,7 +7,13 @@ import {Stage} from '@avocado/graphics'; import {ActionRegistry, InputPacket} from '@avocado/input'; import {Vector} from '@avocado/math'; import {SocketClient} from '@avocado/net/client/socket'; -import {BundlePacket, ClientSynchronizer, SocketIoParser} from '@avocado/net'; +import { + BundlePacket, + ClientSynchronizer, + idFromSynchronized, + SocketIoParser, + SynchronizedCreatePacket, +} from '@avocado/net'; import {clearAnimation, setAnimation} from '@avocado/timing'; import {World} from '@avocado/physics/matter/world'; import {Room, RoomView} from '@avocado/topdown'; @@ -59,7 +65,7 @@ export class App extends decorate(class {}) { super(); const config = this.readConfig(); // Room. - this.room = new Room(); + this.room = null; // World time. this.worldTime = new WorldTime(); // Graphics. @@ -70,13 +76,10 @@ export class App extends decorate(class {}) { this.renderHandle = undefined; this.rps = new CycleTracker(1 / 60); // Refresh rate, actually. this.stage = new Stage(config.visibleSize, config.visibleScale); - this.roomView = new RoomView(this.room, this.stage.renderer); - this.stage.addChild(this.roomView); + this.roomView = null; this.on('darknessChanged', this.applyLighting, this); this.on('isFocusedChanged', this.applyMuting, this) this.on('isMenuOpenedChanged', this.applyMuting, this) - // Listen for new entities. - this.room.on('entityAdded', this.onRoomEntityAdded, this); // Input. this.actionRegistry = new ActionRegistry(); this.actionRegistry.mapKeysToActions(config.actionKeyMap); @@ -97,14 +100,49 @@ export class App extends decorate(class {}) { this.tps = new CycleTracker(config.simulationFrequency); this.simulationHandle = undefined; this.world = config.doPhysicsSimulation ? new World() : undefined; - this.room.world = this.world; - this.room.world.stepTime = config.simulationFrequency; + this.world.stepTime = config.simulationFrequency; // State synchronization. this.state = undefined; this.synchronizer = new ClientSynchronizer(); this.synchronizer.addSynchronized(this.worldTime); } + acceptPacket(packet) { + this.synchronizer.acceptPacket(packet); + // Keep refs to new synchronizeds. + if (packet instanceof SynchronizedCreatePacket) { + const roomId = idFromSynchronized(Room); + const {type, id} = packet.data.synchronized; + switch (type) { + // Track room. + case roomId: + const room = this.synchronizer.synchronized(type, id); + this.onRoomCreated(room); + break; + } + } + if (packet instanceof SelfEntityPacket) { + // Set self entity. + const selfEntity = this.room.findEntity(packet.data); + if (selfEntity) { + this.selfEntity = selfEntity; + this.selfEntityUuid = undefined; + // Add back our self entity traits. + const selfEntityOnlyTraits = [ + 'controllable', + 'followed', + ]; + for (const type of selfEntityOnlyTraits) { + selfEntity.addTrait(type); + } + const {camera} = selfEntity; + this.stage.camera = camera; + // Avoid the initial 'lerp. + camera.realPosition = camera.position; + } + } + } + applyLighting() { const roomView = this.roomView; if (!roomView) { @@ -165,7 +203,7 @@ export class App extends decorate(class {}) { }); this.socket.on('connect', () => { this.removeFromDom(document.querySelector('.app')); - this.room.layers.destroy(); + this.room = null; this.selfEntity = undefined; this.selfEntityUuid = undefined; this.isConnected = true; @@ -272,12 +310,11 @@ export class App extends decorate(class {}) { this.hasReceivedState = true; } if (packet instanceof BundlePacket) { - return this.synchronizer.acceptPackets(packet.data); + for (let i = 0; i < packet.data.length; i++) { + this.onPacket(packet.data[i]); + } } - if (packet instanceof SelfEntityPacket) { - this.selfEntityUuid = packet.data; - } - this.synchronizer.acceptPackets([packet]); + this.acceptPacket(packet); } onPointerDown(event) { @@ -301,6 +338,26 @@ export class App extends decorate(class {}) { this.pointingAt = [-1, -1]; } + onRoomCreated(room) { + // Keep tabs on the room. + if (this.room) { + this.room.off('entityAdded', this.onRoomEntityAdded); + } + this.room = room; + // View. + this.stage.removeChild(this.roomView); + this.roomView = new RoomView(this.room, this.stage.renderer); + this.stage.addChild(this.roomView); + // Listen for new entities. + this.room.on('entityAdded', this.onRoomEntityAdded, this); + const allEntities = this.room.allEntities(); + for (let i = 0; i < allEntities.length; i++) { + this.onRoomEntityAdded(allEntities[i]); + } + // Physics. + this.room.world = this.world; + } + onRoomEntityAdded(entity) { // Traits that shouldn't be on client. const noClientTraits = [ @@ -339,21 +396,6 @@ export class App extends decorate(class {}) { entity.container.night(this.darkness); } entity.stage = this.stage; - // Set self entity. - if (this.selfEntityUuid) { - if (entity === this.room.findEntity(this.selfEntityUuid)) { - this.selfEntity = entity; - this.selfEntityUuid = undefined; - // Add back our self entity traits. - for (const type of selfEntityOnlyTraits) { - entity.addTrait(type); - } - const {camera} = entity; - this.stage.camera = camera; - // Avoid the initial 'lerp. - camera.realPosition = camera.position; - } - } } onWheel(event) { @@ -570,7 +612,9 @@ export class App extends decorate(class {}) { } // Tick. this.worldTime.tick(elapsed); - this.room.tick(elapsed); + if (this.room) { + this.room.tick(elapsed); + } // Sample. this.tps.sample(elapsed); }, 1000 * config.simulationFrequency); diff --git a/common/combat/vulnerable.trait.js b/common/combat/vulnerable.trait.js index 4a87261..9ea2c19 100644 --- a/common/combat/vulnerable.trait.js +++ b/common/combat/vulnerable.trait.js @@ -60,9 +60,11 @@ export class Vulnerable extends Trait { } hydrate() { - this._isHydrating = true; - this.addEmitter(); - this.addEmitterRenderer(); + if (AVOCADO_CLIENT) { + this._isHydrating = true; + this.addEmitter(); + this.addEmitterRenderer(); + } } acceptDamage(damage) { diff --git a/common/traits/item.trait.js b/common/traits/item.trait.js index 3ff2a37..e72e18e 100644 --- a/common/traits/item.trait.js +++ b/common/traits/item.trait.js @@ -79,13 +79,15 @@ export class Item extends decorate(Trait) { } hydrate() { - const promises = []; - for (const index in this.params.slotImages) { - promises.push(Image.load(this.params.slotImages[index]).then((image) => { - this._slotImages[index] = image; - })); + if (AVOCADO_CLIENT) { + const promises = []; + for (const index in this.params.slotImages) { + promises.push(Image.load(this.params.slotImages[index]).then((image) => { + this._slotImages[index] = image; + })); + } + return Promise.all(promises); } - return Promise.all(promises); } get itemActions() { diff --git a/common/traits/receptacle.trait.js b/common/traits/receptacle.trait.js index e29b4fa..95c17fe 100644 --- a/common/traits/receptacle.trait.js +++ b/common/traits/receptacle.trait.js @@ -45,16 +45,10 @@ export class Receptacle extends decorate(Trait) { item.qty = slotSpec.qty; // Set wielder. item.wielder = this.entity; - // On the client, hydrate the item before adding it to the inventory. - if (AVOCADO_CLIENT) { - item.hydrate().then(() => { - this.entity.addItemToSlot(item, index); - }); - } - // Server just adds it. - else { + // Hydrate the item before adding it to the inventory. + item.hydrate().then(() => { this.entity.addItemToSlot(item, index); - } + }); }); } } diff --git a/common/world-time.synchronized.js b/common/world-time.synchronized.js index f4ab4fb..4da5123 100644 --- a/common/world-time.synchronized.js +++ b/common/world-time.synchronized.js @@ -28,7 +28,7 @@ export class WorldTime extends decorate(class {}) { acceptPacket(packet) { if (packet instanceof WorldTimePacket) { - this.fromNetwork(packet.data.hour); + this.fromNetwork(packet.data); } } @@ -51,14 +51,10 @@ export class WorldTime extends decorate(class {}) { fromJSON(json) { if (json.hour) { - this._hour = this.fromNetwork(json.hour); + this._hour = json.hour / MAGIC_TO_FIT_HOUR_INTO_USHORT; } } - fromNetwork(hour) { - this._hour = hour / MAGIC_TO_FIT_HOUR_INTO_USHORT; - } - get hour() { return this._hour; } @@ -71,7 +67,7 @@ export class WorldTime extends decorate(class {}) { if (!this._isUpdateReady) { return; } - return new WorldTimePacket(this.toNetwork()); + return new WorldTimePacket(this.toNetwork(informed)); } secondsPerHour() { @@ -84,14 +80,10 @@ export class WorldTime extends decorate(class {}) { this.ticker.tick(elapsed); } - toNetwork() { + toJSON() { return { hour: (this._hour * MAGIC_TO_FIT_HOUR_INTO_USHORT) >> 0, }; } - toJSON() { - return this.toNetwork(); - } - } diff --git a/server/fixtures/kitty-fire.room.js b/server/fixtures/kitty-fire.room.js index 252dd95..58b9475 100644 --- a/server/fixtures/kitty-fire.room.js +++ b/server/fixtures/kitty-fire.room.js @@ -67,12 +67,12 @@ export function kittyFireJSON() { // for (let i = 0; i < 20; ++i) { // addEntityWithRandomPosition('/flower-barrel.entity.json'); // } - for (let i = 0; i < 3; ++i) { - addEntityWithRandomPosition('/mama-kitty-spawner.entity.json'); - } - for (let i = 0; i < 5; ++i) { - addEntityWithRandomPosition('/fire.entity.json'); - } + // for (let i = 0; i < 3; ++i) { + // addEntityWithRandomPosition('/mama-kitty-spawner.entity.json'); + // } + // for (let i = 0; i < 5; ++i) { + // addEntityWithRandomPosition('/fire.entity.json'); + // } // for (let i = 0; i < 1; ++i) { // addEntityWithRandomPosition('/blue-fire.entity.json'); // } diff --git a/server/game.js b/server/game.js index 0ca3073..06f4fab 100644 --- a/server/game.js +++ b/server/game.js @@ -57,14 +57,15 @@ export default class Game { this.informables.splice(index, 1); } }); - // Sync world time. - entity.addSynchronized(this.worldTime); - // Add entity to room. - if (this.room) { - this.room.addEntityToLayer(entity, 0); - } - // Initial information. - entity.inform(); + Promise.resolve(entity.hydrate()).then(() => { + // Sync world time. + entity.addSynchronized(this.worldTime); + // Add entity to room. + if (this.room) { + this.room.addEntityToLayer(entity, 0); + entity.addSynchronized(this.room); + } + }); // Listen for events. socket.on('packet', this.createPacketListener(socket)); socket.on('disconnect', this.createDisconnectionListener(socket)); @@ -110,6 +111,9 @@ export default class Game { this.informables[i].inform(); } // Clean packets. + if (this.room) { + this.room.cleanPackets(); + } this.worldTime.cleanPackets(); } diff --git a/server/traits/informed.trait.js b/server/traits/informed.trait.js index 49dd796..f635d26 100644 --- a/server/traits/informed.trait.js +++ b/server/traits/informed.trait.js @@ -8,6 +8,8 @@ import {EntityCreatePacket, EntityPacket, EntityRemovePacket, Trait} from '@avoc import {Rectangle, Vector} from '@avocado/math'; import {BundlePacket, ServerSynchronizer} from '@avocado/net'; +import {SelfEntityPacket} from '../../common/packets/self-entity.packet'; + const decorate = compose( ); @@ -19,7 +21,7 @@ export class Informed extends decorate(Trait) { constructor(entity, params, state) { super(entity, params, state); - this.seenEntities = []; + this._sentSelfEntityPacket = false; this._socket = undefined; this._synchronizer = new ServerSynchronizer(); } @@ -44,251 +46,6 @@ export class Informed extends decorate(Trait) { ); } - deduplicateEntityCreatePackets(packets) { - const created = new Map(); - return packets.filter((packet) => { - const entity = packet.entity; - if (!entity) { - return true; - } - // Only care about creates. - if (!(packet instanceof EntityCreatePacket)) { - return true; - } - if (created.has(entity)) { - return false; - } - created.set(entity, true); - return true; - }); - } - - filter(packets) { - packets = packets.filter((packet) => { - return !(packet instanceof EntityPacket); - }); - // // Filter invisible entities. - // packets = this.filterInvisibleEntityPackets(packets); - // // Reduce entities by range. - // const [ - // inRangeEntities, - // outOfRangeEntities, - // visibleEntities, - // ] = this.reducePacketEntitiesByRange(packets); - - // // TODO? Upgrade seen entity packets that are out of range to entity - // // remembers. - - // // Filter the out of range entity updates. - // packets = this.filterOutOfRangeEntityPackets( - // packets, - // outOfRangeEntities - // ); - // // Deduplicate entity creates. - // packets = this.deduplicateEntityCreatePackets(packets); - // // Filter known creates. - // packets = this.filterKnownEntityCreatePackets(packets); - // // Inject create packets. - // packets = this.injectEntityCreatePackets(packets, visibleEntities); - // // Inject removes for any previously seen entity that isn't visible - // // anymore. - // packets = this.injectEntityRemovePackets(packets, visibleEntities); - // // "See" entities. - // this.markEntitiesSeen(visibleEntities); - // // Unsee any removed entities. - // this.markEntitiesUnseen(packets); - - return packets; - } - - filterInvisibleEntityPackets(packets) { - return packets.filter((packet) => { - const entity = packet.entity; - if (!entity) { - return true; - } - // Removes could be on destroyed entities, so pass them. - if (packet instanceof EntityRemovePacket) { - return true; - } - return entity.isVisible; - }); - } - - filterKnownEntityCreatePackets(packets) { - return packets.filter((packet) => { - if (!packet.entity) { - return true; - } - if (!(packet instanceof EntityCreatePacket)) { - return true; - } - return !this.hasSeenEntity(packet.entity); - }); - } - - filterOutOfRangeEntityPackets(packets, outOfRangeEntities) { - return packets.filter((packet) => { - const entity = packet.entity; - if (!entity) { - return true; - } - // Send removes even if they're out of range, if client knows about - // them. - if ( - packet instanceof EntityRemovePacket - && this.hasSeenEntity(entity) - ) { - return true; - } - return -1 === outOfRangeEntities.indexOf(packet.entity); - }); - } - - hasSeenEntity(entity) { - return -1 !== this.seenEntities.indexOf(entity); - } - - injectEntityCreatePackets(packets, visibleEntities) { - // Get a list of all visible but not yet seen entities. - const visibleButNotYetSeen = []; - for (let i = 0; i < visibleEntities.length; i++) { - const entity = visibleEntities[i]; - if (!this.hasSeenEntity(entity)) { - visibleButNotYetSeen.push(entity); - } - } - // Get a list of all existing created entities. - const allExistingCreatedEntities = packets.filter((packet) => { - return packet instanceof EntityCreatePacket; - }).map((packet) => { - return packet.entity; - }); - // JIT inject creates before any unknown updates. - for (let i = 0; i < packets.length; i++) { - const packet = packets[i]; - // Only care about entity packets. - if (!(packet instanceof EntityPacket)) { - continue; - } - // Only unknown. - const entity = packet.entity; - if (this.hasSeenEntity(entity)) { - continue; - } - if (-1 !== allExistingCreatedEntities.indexOf(entity)) { - continue; - } - // Not creates nor removes. - if ( - packet instanceof EntityCreatePacket - || packet instanceof EntityRemovePacket - ) { - // This does count as seen. - const index = visibleButNotYetSeen.indexOf(entity); - if (-1 !== index) { - visibleButNotYetSeen.splice(index, 1); - } - continue; - } - // Inject. - packets.splice( - i, - 0, - new EntityCreatePacket(entity.mergeDiff(), entity) - ); - i += 1; - // We've seen it. - const index = visibleButNotYetSeen.indexOf(entity); - if (-1 !== index) { - visibleButNotYetSeen.splice(index, 1); - } - allExistingCreatedEntities.push(entity); - } - // Append creates for any visible-but-not-yet-seen entities. - for (let i = 0; i < visibleButNotYetSeen.length; i++) { - const entity = visibleButNotYetSeen[i]; - // Skip any existing creates. - if (-1 === allExistingCreatedEntities.indexOf(entity)) { - packets.push(new EntityCreatePacket(entity.mergeDiff(), entity)); - } - } - return packets; - } - - injectEntityRemovePackets(packets, visibleEntities) { - const alreadyRemovedEntities = packets.filter((packet) => { - return packet instanceof EntityRemovePacket; - }).map((packet) => { - return packet.entity; - }); - for (let i = 0; i < this.seenEntities.length; i++) { - const entity = this.seenEntities[i]; - if (-1 === visibleEntities.indexOf(entity)) { - if (-1 === alreadyRemovedEntities.indexOf(entity)) { - packets.push(new EntityRemovePacket({}, entity)); - } - } - } - return packets; - } - - markEntitiesSeen(visibleEntities) { - for (let i = 0; i < visibleEntities.length; i++) { - const entity = visibleEntities[i]; - if (-1 === this.seenEntities.indexOf(entity)) { - this.seenEntities.push(entity); - } - } - } - - markEntitiesUnseen(packets) { - const removedEntities = packets.filter((packet) => { - return packet instanceof EntityRemovePacket; - }).map((packet) => { - return packet.entity; - }); - for (let i = 0; i < removedEntities.length; i++) { - const entity = removedEntities[i]; - const index = this.seenEntities.indexOf(entity) - if (-1 !== index) { - this.seenEntities.splice(index, 1); - } - } - } - - reducePacketEntitiesByRange(packets) { - // Unique packet entities. - const packetEntitiesMap = new Map(); - for (let i = 0; i < packets.length; i++) { - const entity = packets[i].entity; - if (entity && !packetEntitiesMap.has(entity)) { - packetEntitiesMap.set(entity, true); - } - } - // Locate visible entities. - const areaToInform = this.areaToInform; - const visibleEntities = this.entity.room.visibleEntities(areaToInform); - // Document out of range entities. - const inRangeEntities = []; - const outOfRangeEntities = []; - let it = packetEntitiesMap.keys(); - for (let value = it.next(); !value.done; value = it.next()) { - const entity = value.value; - if (-1 === visibleEntities.indexOf(entity)) { - if (-1 === outOfRangeEntities.indexOf(entity)) { - outOfRangeEntities.push(entity); - } - } - else { - if (-1 === inRangeEntities.indexOf(entity)) { - inRangeEntities.push(entity); - } - } - } - return [inRangeEntities, outOfRangeEntities, visibleEntities]; - } - get socket() { return this._socket; } @@ -303,42 +60,18 @@ export class Informed extends decorate(Trait) { inform: () => { const payload = this._synchronizer.packetsFor(this.entity); - // const payload = []; - // for (let i = 0; i < this._willWatch.length; i++) { - // const packets = this._willWatch[i].packets(this.entity); - // for (let j = 0; j < packets.length; j++) { - // payload.push(packets[j]); - // } - // } - // for (let i = 0; i < this._watching.length; i++) { - // const packets = this._watching[i].packets(this.entity); - // for (let j = 0; j < packets.length; j++) { - // payload.push(packets[j]); - // } - // } - // for (let i = 0; i < this._willWatch.length; i++) { - // this._watching.push(this._willWatch[i]); - // } - // this._willWatch = []; - // // Clean all packets. - // for (let i = 0; i < this._watching.length; i++) { - // this._watching[i].cleanPackets(); - // } if (0 === payload.length) { return; } + if (!this._sentSelfEntityPacket) { + this._sentSelfEntityPacket = true; + payload.push(new SelfEntityPacket(this.entity.numericUid)); + } if (this._socket) { this._socket.send(new BundlePacket(payload)); } }, - seesEntity: (entity) => { - return Rectangle.isTouching( - this.entity.areaToInform, - entity.visibleAabb - ); - }, - addSynchronized: (synchronized) => { this._synchronizer.addSynchronized(synchronized); },