diff --git a/common/packets/trait-update-receptacle-item-full.packet.js b/common/packets/trait-update-receptacle-item-full.packet.js new file mode 100644 index 0000000..b0031fa --- /dev/null +++ b/common/packets/trait-update-receptacle-item-full.packet.js @@ -0,0 +1,29 @@ +import msgpack from 'msgpack-lite'; + +import {Packet} from '@avocado/net'; + +export class TraitUpdateReceptacleItemFullPacket extends Packet { + + static pack(packet) { + const data = packet.data[1]; + data.json = msgpack.encode(data.json); + return super.pack(packet); + } + + static get schema() { + return { + ...super.schema, + data: { + slotIndex: 'uint16', + json: 'buffer', + }, + }; + } + + static unpack(packet) { + const data = super.unpack(packet); + data.json = msgpack.decode(data.json); + return data; + } + +} diff --git a/common/traits/lootable.trait.js b/common/traits/lootable.trait.js new file mode 100644 index 0000000..006f4a1 --- /dev/null +++ b/common/traits/lootable.trait.js @@ -0,0 +1,68 @@ +import {compose, Property} from '@avocado/core'; +import {StateProperty, Trait, Entity} from '@avocado/entity'; +import {Rectangle, Vector} from '@avocado/math'; + +const decorate = compose( + StateProperty('lootable', { + track: true, + }), +); + +export class Lootable extends decorate(Trait) { + + static defaultParams() { + return { + table: [], + }; + } + + static defaultState() { + return { + lootable: true, + }; + } + + static type() { + return 'lootable'; + } + + constructor(entity, params, state) { + super(entity, params, state); + } + + calculateLoot() { + const jsons = []; + const roll = Math.random() * 100; + for (const {perc, json} of this.params.table) { + if (perc > roll) { + jsons.push(json); + } + } + return jsons; + } + + listeners() { + const listeners = {}; + if (AVOCADO_SERVER) { + listeners['died'] = () => { + const jsons = this.calculateLoot(); + const position = this.entity.position; + for (let i = 0; i < jsons.length; i++) { + const offset = Vector.sub( + [ + Math.floor(Math.random() * 16), + Math.floor(Math.random() * 16) + ], + [8, 8] + ); + this.entity.spawnRawAt( + Vector.add(position, offset), + jsons[i] + ); + } + }; + } + return listeners; + } + +} diff --git a/common/traits/receptacle.trait.js b/common/traits/receptacle.trait.js index 87f8e49..795a029 100644 --- a/common/traits/receptacle.trait.js +++ b/common/traits/receptacle.trait.js @@ -7,6 +7,9 @@ import { import { TraitUpdateReceptacleItemSwapPacket, } from '../packets/trait-update-receptacle-item-swap.packet'; +import { + TraitUpdateReceptacleItemFullPacket, +} from '../packets/trait-update-receptacle-item-full.packet'; const decorate = compose( StateProperty('slotCount', { @@ -79,6 +82,14 @@ export class Receptacle extends decorate(Trait) { item.qty = packet.data.qty; } } + // Full item. + if (packet instanceof TraitUpdateReceptacleItemFullPacket) { + Entity.loadOrInstance(packet.data.json).then((item) => { + item.hydrate().then(() => { + this.entity.addItemToSlot(item, packet.data.slotIndex); + }); + }); + } // Slot swap. if (packet instanceof TraitUpdateReceptacleItemSwapPacket) { // NULL destination slot === remove. @@ -105,7 +116,7 @@ export class Receptacle extends decorate(Trait) { this.packetUpdates.push(new TraitUpdateReceptacleItemQtyPacket({ slotIndex, qty: item.qty, - }, this.entity)); + })); } // Item was used up. else { @@ -197,6 +208,10 @@ export class Receptacle extends decorate(Trait) { listeners.collisionStart = (other) => { if (other.is('item')) { this.entity.addItemToSlot(other); + const slotIndex = this.itemSlotIndex(other); + if (other.is('listed')) { + other.list.removeEntity(other); + } } }; } @@ -207,12 +222,20 @@ export class Receptacle extends decorate(Trait) { return { addItemToSlot: (item, slotIndex = AUTO_SLOT) => { + slotIndex = slotIndex >> 0; if (AUTO_SLOT === slotIndex) { return this.mergeItem(item); } else { this.addListenersForItem(item); this.slotItems[slotIndex] = item; + if (AVOCADO_SERVER) { + this.packetUpdates.push(new TraitUpdateReceptacleItemFullPacket({ + slotIndex, + // CHEATING: assuming this entity is the informed + json: item.toNetwork(this.entity), + })); + } } this.entity.emit('inventoryChanged'); }, @@ -228,7 +251,7 @@ export class Receptacle extends decorate(Trait) { this.packetUpdates.push(new TraitUpdateReceptacleItemSwapPacket({ firstSlotIndex: slotIndex, secondSlotIndex: NULL_SLOT, - }, this.entity)); + })); } this.entity.emit('inventoryChanged'); return item; diff --git a/resource/yarn-ball.png b/resource/yarn-ball.png new file mode 100644 index 0000000..8c945de Binary files /dev/null and b/resource/yarn-ball.png differ diff --git a/server/create-fixtures.js b/server/create-fixtures.js index ce13c31..92941c6 100644 --- a/server/create-fixtures.js +++ b/server/create-fixtures.js @@ -40,6 +40,8 @@ import {wateringCanJSON} from './fixtures/watering-can.entity'; writeFixture('watering-can.entity.json', wateringCanJSON()); import {tomatoSeedsJSON} from './fixtures/tomato-seeds.entity'; writeFixture('tomato-seeds.entity.json', tomatoSeedsJSON()); +import {yarnBallJSON} from './fixtures/yarn-ball.entity'; +writeFixture('yarn-ball.entity.json', yarnBallJSON()); // Write rooms. import {kittyFireJSON} from './fixtures/kitty-fire.room'; writeFixture('kitty-fire.room.json', kittyFireJSON()); diff --git a/server/fixtures/kitty-fire.room.js b/server/fixtures/kitty-fire.room.js index f0bc4c6..5037b37 100644 --- a/server/fixtures/kitty-fire.room.js +++ b/server/fixtures/kitty-fire.room.js @@ -64,15 +64,15 @@ export function kittyFireJSON() { // for (let i = 0; i < 20; ++i) { // addEntityWithRandomPosition('/rock.entity.json'); // } - for (let i = 0; i < 30; ++i) { - addEntityWithRandomPosition('/flower-barrel.entity.json'); - } + // for (let i = 0; i < 30; ++i) { + // addEntityWithRandomPosition('/flower-barrel.entity.json'); + // } for (let i = 0; i < 5; ++i) { addEntityWithRandomPosition('/mama-kitty-spawner.entity.json'); } - for (let i = 0; i < 50; ++i) { - addEntityWithRandomPosition('/fire.entity.json'); - } + // for (let i = 0; i < 50; ++i) { + // addEntityWithRandomPosition('/fire.entity.json'); + // } for (let i = 0; i < 5; ++i) { addEntityWithRandomPosition('/blue-fire.entity.json'); } diff --git a/server/fixtures/kitty.entity.js b/server/fixtures/kitty.entity.js index 9fcab79..c076919 100644 --- a/server/fixtures/kitty.entity.js +++ b/server/fixtures/kitty.entity.js @@ -111,6 +111,24 @@ export function kittyJSON() { }, layered: {}, listed: {}, + lootable: { + params: { + table: [ + { + perc: 20, + json: { + uri: '/rock.entity.json', + }, + }, + { + perc: 70, + json: { + uri: '/yarn-ball.entity.json', + }, + }, + ], + }, + }, mobile: { state: { speed: 40, @@ -128,6 +146,7 @@ export function kittyJSON() { }, }, }, + spawner: {}, vulnerable: {}, }, }; diff --git a/server/fixtures/rock.entity.js b/server/fixtures/rock.entity.js index 7633383..3b4260e 100644 --- a/server/fixtures/rock.entity.js +++ b/server/fixtures/rock.entity.js @@ -1,7 +1,5 @@ import {buildInvoke, buildTraversal} from '@avocado/behavior'; -import {AFFINITY_NONE} from '../../common/combat/constants'; - // Rock. export function rockJSON() { const spawnProjectile = buildInvoke( diff --git a/server/fixtures/yarn-ball.entity.js b/server/fixtures/yarn-ball.entity.js new file mode 100644 index 0000000..2a4ddca --- /dev/null +++ b/server/fixtures/yarn-ball.entity.js @@ -0,0 +1,54 @@ +import {buildInvoke, buildTraversal} from '@avocado/behavior'; + +// Yarn ball. +export function yarnBallJSON() { + return { + traits: { + collider: { + params: { + isSensor: true, + }, + }, + existent: { + state: { + name: 'Yarn Ball', + }, + }, + item: { + params: { + slotImageUris: { + default: '/yarn-ball.png', + }, + }, + }, + layered: {}, + listed: {}, + magnetic: {}, + mobile: {}, + physical: {}, + pictured: { + params: { + images: { + initial: { + offset: [0, 0], + size: [8, 8], // Derive? + uri: '/yarn-ball.png', + }, + } + }, + }, + positioned: {}, + roomed: {}, + shaped: { + params: { + shape: { + type: 'rectangle', + position: [0, 0], + size: [16, 16], + }, + }, + }, + visible: {}, + }, + }; +}