flow: inventory and item usage!
This commit is contained in:
parent
238eff70b4
commit
a0249b66b3
|
@ -251,9 +251,20 @@ export class App extends decorate(class {}) {
|
||||||
const slotIndex = (parseInt(key) + 9) % 10;
|
const slotIndex = (parseInt(key) + 9) % 10;
|
||||||
this.setActiveSlotIndex(slotIndex);
|
this.setActiveSlotIndex(slotIndex);
|
||||||
break;
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
if (this.selfEntity) {
|
||||||
|
this.actionRegistry.state = this.actionRegistry.state.set(
|
||||||
|
'UseItem',
|
||||||
|
this.selfEntity.activeSlotIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onKeyUp(key) {
|
||||||
|
}
|
||||||
|
|
||||||
onPacket(packet) {
|
onPacket(packet) {
|
||||||
if (!this.hasReceivedState) {
|
if (!this.hasReceivedState) {
|
||||||
this.renderIntoDom(document.querySelector('.app')).then(() => {
|
this.renderIntoDom(document.querySelector('.app')).then(() => {
|
||||||
|
@ -468,8 +479,10 @@ export class App extends decorate(class {}) {
|
||||||
|
|
||||||
startProcessingInput() {
|
startProcessingInput() {
|
||||||
const config = this.readConfig();
|
const config = this.readConfig();
|
||||||
// Pointer input.
|
// Key input.
|
||||||
this.stage.on('keyDown', this.onKeyDown, this);
|
this.stage.on('keyDown', this.onKeyDown, this);
|
||||||
|
this.stage.on('keyUp', this.onKeyUp, this);
|
||||||
|
// Pointer input.
|
||||||
this.stage.on('pointerDown', this.onPointerDown, this);
|
this.stage.on('pointerDown', this.onPointerDown, this);
|
||||||
this.stage.on('pointerMove', this.onPointerMove, this);
|
this.stage.on('pointerMove', this.onPointerMove, this);
|
||||||
this.stage.on('pointerUp', this.onPointerUp, this);
|
this.stage.on('pointerUp', this.onPointerUp, this);
|
||||||
|
@ -486,6 +499,9 @@ export class App extends decorate(class {}) {
|
||||||
if (this.actionState !== this.actionRegistry.state) {
|
if (this.actionState !== this.actionRegistry.state) {
|
||||||
this.actionState = this.actionRegistry.state;
|
this.actionState = this.actionRegistry.state;
|
||||||
this.socket.send(InputPacket.fromState(this.actionState));
|
this.socket.send(InputPacket.fromState(this.actionState));
|
||||||
|
this.actionRegistry.state = this.actionRegistry.state.delete(
|
||||||
|
'UseItem'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, 1000 * config.inputFrequency);
|
}, 1000 * config.inputFrequency);
|
||||||
// Mouse/touch movement.
|
// Mouse/touch movement.
|
||||||
|
|
18
client/ui/hooks/use-event.js
Normal file
18
client/ui/hooks/use-event.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// 3rd party.
|
||||||
|
import React, {useEffect} from 'react';
|
||||||
|
|
||||||
|
export function useEvent(object, eventName, fn) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!object) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onEvent = (...args) => {
|
||||||
|
fn(...args);
|
||||||
|
};
|
||||||
|
onEvent();
|
||||||
|
object.on(eventName, onEvent);
|
||||||
|
return () => {
|
||||||
|
object.off(eventName, onEvent);
|
||||||
|
};
|
||||||
|
}, [object]);
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
// 3rd party.
|
// 3rd party.
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import * as I from 'immutable';
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
// 2nd party.
|
// 2nd party.
|
||||||
import {compose} from '@avocado/core';
|
import {compose} from '@avocado/core';
|
||||||
import contempo from 'contempo';
|
import contempo from 'contempo';
|
||||||
// 1st party.
|
// 1st party.
|
||||||
|
import {useEvent} from '../hooks/use-event';
|
||||||
import {usePropertyChange} from '../hooks/use-property-change';
|
import {usePropertyChange} from '../hooks/use-property-change';
|
||||||
import ItemSlot from './item-slot';
|
import ItemSlot from './item-slot';
|
||||||
|
|
||||||
|
@ -43,49 +45,34 @@ const decorate = compose(
|
||||||
const HotbarComponent = ({app}) => {
|
const HotbarComponent = ({app}) => {
|
||||||
const selfEntity = usePropertyChange(app, 'selfEntity');
|
const selfEntity = usePropertyChange(app, 'selfEntity');
|
||||||
const activeSlotIndex = usePropertyChange(selfEntity, 'activeSlotIndex');
|
const activeSlotIndex = usePropertyChange(selfEntity, 'activeSlotIndex');
|
||||||
const [slotImageUris, setSlotImageUris] = useState({});
|
const [items, setItems] = useState(I.List());
|
||||||
useEffect(() => {
|
useEvent(selfEntity, 'inventoryChanged', () => {
|
||||||
if (!selfEntity) {
|
setItems(items.withMutations((items) => {
|
||||||
return;
|
for (let i = 0; i < 10; ++i) {
|
||||||
}
|
|
||||||
const onInventoryChanged = () => {
|
|
||||||
const imageUris = {};
|
|
||||||
const itemPromises = [];
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
const item = selfEntity.itemInSlot(i);
|
const item = selfEntity.itemInSlot(i);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
itemPromises.push(item.then((item) => {
|
if (!items.has(i)) {
|
||||||
return item.hydrate().then(() => {
|
items.set(i, I.Map());
|
||||||
const image = item.imageForSlot(0);
|
}
|
||||||
if (image) {
|
items.set(i, items.get(i).withMutations((listItem) => {
|
||||||
imageUris[i] = image.uri;
|
listItem.set('backgroundImage', item.imageForSlot());
|
||||||
}
|
listItem.set('qty', item.qty);
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Promise.all(itemPromises).then(() => {
|
}));
|
||||||
setSlotImageUris(imageUris);
|
});
|
||||||
})
|
|
||||||
};
|
|
||||||
selfEntity.on('inventoryChanged', onInventoryChanged);
|
|
||||||
onInventoryChanged();
|
|
||||||
return () => {
|
|
||||||
selfEntity.off('inventoryChanged', onInventoryChanged);
|
|
||||||
};
|
|
||||||
}, [selfEntity]);
|
|
||||||
|
|
||||||
const hotkeyForSlot = (index) => {
|
const hotkeyForSlot = (index) => {
|
||||||
return (index + 1) % 10;
|
return (index + 1) % 10;
|
||||||
}
|
}
|
||||||
const slots = [];
|
const slots = [];
|
||||||
for (let i = 0; i < 10; ++i) {
|
for (let i = 0; i < 10; ++i) {
|
||||||
const slot = <ItemSlot
|
const slot = <ItemSlot
|
||||||
backgroundImage={slotImageUris[i]}
|
|
||||||
className={classnames(
|
className={classnames(
|
||||||
activeSlotIndex === i ? 'active' : '',
|
activeSlotIndex === i ? 'active' : '',
|
||||||
)}
|
)}
|
||||||
|
item={items.get(i)}
|
||||||
key={i}
|
key={i}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
if (selfEntity) {
|
if (selfEntity) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// 3rd party.
|
// 3rd party.
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import React from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
// 2nd party.
|
// 2nd party.
|
||||||
import {compose} from '@avocado/core';
|
import {compose} from '@avocado/core';
|
||||||
import contempo from 'contempo';
|
import contempo from 'contempo';
|
||||||
|
@ -30,12 +30,62 @@ const decorate = compose(
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
image-rendering: optimizeSpeed; /* Older versions of FF */
|
||||||
|
image-rendering: -moz-crisp-edges; /* FF 6.0+ */
|
||||||
|
image-rendering: -webkit-optimize-contrast; /* Safari */
|
||||||
|
image-rendering: -o-crisp-edges; /* OS X & Windows Opera (12.02+) */
|
||||||
|
image-rendering: pixelated; /* Awesome future-browsers */
|
||||||
|
-ms-interpolation-mode: nearest-neighbor; /* IE */
|
||||||
}
|
}
|
||||||
|
.item-slot-inner .qty {
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0.25px 0.25px 0 black;
|
||||||
|
font-family: joystix;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0.5px;
|
||||||
|
font-size: 5px;
|
||||||
|
line-height: 5px;
|
||||||
|
}
|
||||||
|
.qty.e3 {
|
||||||
|
font-size: 4px;
|
||||||
|
}
|
||||||
|
.qty.e4 {
|
||||||
|
font-size: 3px;
|
||||||
|
bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
|
|
||||||
const ItemSlotComponent = (props) => {
|
const ItemSlotComponent = (props) => {
|
||||||
const {backgroundImage, children, className, ...rest} = props;
|
const {children, className, item, ...rest} = props;
|
||||||
|
let backgroundImageUri;
|
||||||
|
let qty;
|
||||||
|
let qtyClass;
|
||||||
|
if (item) {
|
||||||
|
const backgroundImage = item.get('backgroundImage');
|
||||||
|
if (backgroundImage) {
|
||||||
|
backgroundImageUri = backgroundImage.uri;
|
||||||
|
}
|
||||||
|
qty = item.get('qty');
|
||||||
|
if (qty > 9999) {
|
||||||
|
qtyClass = 'e4';
|
||||||
|
}
|
||||||
|
else if (qty > 999) {
|
||||||
|
qtyClass = 'e3';
|
||||||
|
}
|
||||||
|
else if (qty > 99) {
|
||||||
|
qtyClass = 'e2';
|
||||||
|
}
|
||||||
|
else if (qty > 9) {
|
||||||
|
qtyClass = 'e1';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qtyClass = 'e0';
|
||||||
|
}
|
||||||
|
}
|
||||||
return <div
|
return <div
|
||||||
{...rest}
|
{...rest}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
|
@ -46,8 +96,16 @@ const ItemSlotComponent = (props) => {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="item-slot-inner"
|
className="item-slot-inner"
|
||||||
style={{backgroundImage: `url(${backgroundImage})`}}
|
style={{
|
||||||
>{children}</div>
|
backgroundImage: backgroundImageUri && `url(${backgroundImageUri})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{qty && <div className={classnames(
|
||||||
|
'qty',
|
||||||
|
qtyClass,
|
||||||
|
)}>{qty}</div>}
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,12 @@ export class Controllable extends Trait {
|
||||||
|
|
||||||
set inputState(inputState) {
|
set inputState(inputState) {
|
||||||
this._inputState = I.Map(inputState);
|
this._inputState = I.Map(inputState);
|
||||||
|
if (AVOCADO_SERVER) {
|
||||||
|
if (inputState.has('UseItem')) {
|
||||||
|
const slotIndex = inputState.get('UseItem');
|
||||||
|
this.entity.useItemInSlot(slotIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tick(elapsed) {
|
tick(elapsed) {
|
||||||
|
|
76
common/traits/item.trait.js
Normal file
76
common/traits/item.trait.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import {behaviorItemFromJSON} from '@avocado/behavior';
|
||||||
|
import {compose, TickingPromise} from '@avocado/core';
|
||||||
|
import {StateProperty, Trait} from '@avocado/entity';
|
||||||
|
import {Image} from '@avocado/graphics';
|
||||||
|
|
||||||
|
const decorate = compose(
|
||||||
|
StateProperty('qty', {
|
||||||
|
track: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export class Item extends decorate(Trait) {
|
||||||
|
|
||||||
|
static defaultParams() {
|
||||||
|
return {
|
||||||
|
itemActions: {
|
||||||
|
type: 'actions',
|
||||||
|
traversals: [],
|
||||||
|
},
|
||||||
|
slotImages: {
|
||||||
|
default: '/question-mark.png',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultState() {
|
||||||
|
return {
|
||||||
|
qty: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static type() {
|
||||||
|
return 'item';
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(entity, params, state) {
|
||||||
|
super(entity, params, state);
|
||||||
|
this._itemActions = behaviorItemFromJSON(this.params.itemActions);
|
||||||
|
this._slotImages = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrate() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
get itemActions() {
|
||||||
|
return this._itemActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
methods() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
decrementQuantity: (amount = 1) => {
|
||||||
|
this.entity.qty -= amount;
|
||||||
|
},
|
||||||
|
|
||||||
|
imageForSlot: () => {
|
||||||
|
const qty = this.entity.qty;
|
||||||
|
if (this._slotImages[qty]) {
|
||||||
|
return this._slotImages[qty];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return this._slotImages['default'];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
208
common/traits/receptacle.trait.js
Normal file
208
common/traits/receptacle.trait.js
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
import {compose, TickingPromise} from '@avocado/core';
|
||||||
|
import {Entity, StateProperty, Trait} from '@avocado/entity';
|
||||||
|
|
||||||
|
import {TraitItemQtyPacket} from './trait-item-qty.packet';
|
||||||
|
import {TraitItemSwapPacket} from './trait-item-swap.packet';
|
||||||
|
|
||||||
|
const decorate = compose(
|
||||||
|
StateProperty('slotCount', {
|
||||||
|
track: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const NULL_SLOT = 65535;
|
||||||
|
const AUTO_SLOT = 65535;
|
||||||
|
|
||||||
|
// TODO more localized events; inventoryChanged is too noisy
|
||||||
|
export class Receptacle extends decorate(Trait) {
|
||||||
|
|
||||||
|
static defaultParams() {
|
||||||
|
return {
|
||||||
|
slots: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultState() {
|
||||||
|
return {
|
||||||
|
slotCount: 10,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static type() {
|
||||||
|
return 'receptacle';
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(entity, params, state) {
|
||||||
|
super(entity, params, state);
|
||||||
|
this.packetUpdates = [];
|
||||||
|
this.qtyListeners = new Map();
|
||||||
|
this.slotItems = {};
|
||||||
|
// Load items.
|
||||||
|
for (const index in this.params.slots) {
|
||||||
|
const slotSpec = this.params.slots[index];
|
||||||
|
this.slotItems[index] = Entity.load(slotSpec.uri).then((item) => {
|
||||||
|
// Set quantity.
|
||||||
|
item.qty = slotSpec.qty;
|
||||||
|
// 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 {
|
||||||
|
this.entity.addItemToSlot(item, index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
for (let i = 0; i < this.entity.slotCount; ++i) {
|
||||||
|
const item = this.entity.removeItemFromSlot(i);
|
||||||
|
if (item) {
|
||||||
|
this.removeListenersForItem(item);
|
||||||
|
item.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptPacket(packet) {
|
||||||
|
// Quantity update.
|
||||||
|
if (packet instanceof TraitItemQtyPacket) {
|
||||||
|
const item = this.entity.itemInSlot(packet.data.slotIndex);
|
||||||
|
if (item) {
|
||||||
|
item.qty = packet.data.qty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Slot swap.
|
||||||
|
if (packet instanceof TraitItemSwapPacket) {
|
||||||
|
// NULL destination slot === remove.
|
||||||
|
const {firstSlotIndex, secondSlotIndex} = packet.data;
|
||||||
|
if (NULL_SLOT === secondSlotIndex) {
|
||||||
|
const item = this.entity.removeItemFromSlot(firstSlotIndex);
|
||||||
|
if (item) {
|
||||||
|
item.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Normal slot swap.
|
||||||
|
else {
|
||||||
|
this.entity.swapSlots(firstSlotIndex, secondSlotIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addListenersForItem(item) {
|
||||||
|
const listener = (oldQty, newQty) => {
|
||||||
|
if (AVOCADO_SERVER) {
|
||||||
|
const slotIndex = this.itemSlotIndex(item);
|
||||||
|
// Valid quantity; update client.
|
||||||
|
if (newQty > 0) {
|
||||||
|
this.packetUpdates.push(new TraitItemQtyPacket({
|
||||||
|
slotIndex,
|
||||||
|
qty: item.qty,
|
||||||
|
}, this.entity));
|
||||||
|
}
|
||||||
|
// Item was used up.
|
||||||
|
else {
|
||||||
|
this.entity.removeItemFromSlot(slotIndex);
|
||||||
|
item.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.entity.emit('inventoryChanged');
|
||||||
|
};
|
||||||
|
this.qtyListeners.set(item, listener);
|
||||||
|
item.on('qtyChanged', listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSlotIndexInRange(slotIndex) {
|
||||||
|
return slotIndex >= 0 && slotIndex < this.entity.slotCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemSlotIndex(item) {
|
||||||
|
for (let i = 0; i < this.entity.slotCount; ++i) {
|
||||||
|
if (item === this.slotItems[i]) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
mergeItem(item) {
|
||||||
|
}
|
||||||
|
|
||||||
|
packetsForUpdate() {
|
||||||
|
const packetsForUpdate = this.packetUpdates;
|
||||||
|
this.packetUpdates = [];
|
||||||
|
return packetsForUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListenersForItem(item) {
|
||||||
|
item.off('qtyChanged', this.qtyListeners.get(item));
|
||||||
|
this.qtyListeners.delete(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
methods() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
addItemToSlot: (item, slotIndex = AUTO_SLOT) => {
|
||||||
|
if (AUTO_SLOT === slotIndex) {
|
||||||
|
return this.mergeItem(item);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.addListenersForItem(item);
|
||||||
|
this.slotItems[slotIndex] = item;
|
||||||
|
}
|
||||||
|
this.entity.emit('inventoryChanged');
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItemFromSlot: (slotIndex) => {
|
||||||
|
const item = this.entity.itemInSlot(slotIndex);
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.removeListenersForItem(item);
|
||||||
|
delete this.slotItems[slotIndex];
|
||||||
|
if (AVOCADO_SERVER) {
|
||||||
|
this.packetUpdates.push(new TraitItemSwapPacket({
|
||||||
|
firstSlotIndex: slotIndex,
|
||||||
|
secondSlotIndex: NULL_SLOT,
|
||||||
|
}, this.entity));
|
||||||
|
}
|
||||||
|
this.entity.emit('inventoryChanged');
|
||||||
|
return item;
|
||||||
|
},
|
||||||
|
|
||||||
|
itemInSlot: (slotIndex) => {
|
||||||
|
if (this.isSlotIndexInRange(slotIndex)) {
|
||||||
|
// While it's loading, it'll be a promise.
|
||||||
|
if (this.slotItems[slotIndex] instanceof Entity) {
|
||||||
|
return this.slotItems[slotIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
swapSlots: (leftIndex, rightIndex) => {
|
||||||
|
if (
|
||||||
|
!this.isSlotIndexInRange(leftIndex)
|
||||||
|
|| !this.isSlotIndexInRange(rightIndex)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Swap items.
|
||||||
|
const leftItem = this.entity.itemInSlot(leftIndex);
|
||||||
|
const rightItem = this.entity.itemInSlot(rightIndex);
|
||||||
|
this.removeListenersForItem(leftItem);
|
||||||
|
this.removeListenersForItem(rightItem);
|
||||||
|
this.slotItems[leftIndex] = rightItem;
|
||||||
|
this.slotItems[rightIndex] = leftItem;
|
||||||
|
this.addListenersForItem(leftItem);
|
||||||
|
this.addListenersForItem(rightItem);
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
12
common/traits/trait-item-qty.packet.js
Normal file
12
common/traits/trait-item-qty.packet.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import {EntityPacket} from '@avocado/entity';
|
||||||
|
|
||||||
|
export class TraitItemQtyPacket extends EntityPacket {
|
||||||
|
|
||||||
|
static get schema() {
|
||||||
|
const schema = super.schema;
|
||||||
|
schema.data.slotIndex = 'uint16';
|
||||||
|
schema.data.qty = 'uint16';
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
common/traits/trait-item-swap.packet.js
Normal file
12
common/traits/trait-item-swap.packet.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import {EntityPacket} from '@avocado/entity';
|
||||||
|
|
||||||
|
export class TraitItemSwapPacket extends EntityPacket {
|
||||||
|
|
||||||
|
static get schema() {
|
||||||
|
const schema = super.schema;
|
||||||
|
schema.data.firstSlotIndex = 'uint16';
|
||||||
|
schema.data.secondSlotIndex = 'uint16';
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
84
common/traits/wielder.trait.js
Normal file
84
common/traits/wielder.trait.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import {createContext} from '@avocado/behavior';
|
||||||
|
import {compose} from '@avocado/core';
|
||||||
|
import {StateProperty, Trait} from '@avocado/entity';
|
||||||
|
|
||||||
|
const decorate = compose(
|
||||||
|
StateProperty('activeSlotIndex', {
|
||||||
|
track: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export class Wielder extends decorate(Trait) {
|
||||||
|
|
||||||
|
static dependencies() {
|
||||||
|
return [
|
||||||
|
'receptacle',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultState() {
|
||||||
|
return {
|
||||||
|
activeSlotIndex: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static type() {
|
||||||
|
return 'wielder';
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(entity, params, state) {
|
||||||
|
super(entity, params, state);
|
||||||
|
this.itemActions = undefined;
|
||||||
|
this.itemActionsContext = createContext();
|
||||||
|
this.itemInUse = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
methods() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
useItemInActiveSlot: () => {
|
||||||
|
if (-1 === this.entity.activeSlotIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.entity.useItemInSlot(this.entity.activeSlotIndex)
|
||||||
|
},
|
||||||
|
|
||||||
|
useItemInSlot: (slotIndex) => {
|
||||||
|
// Already using an item? TODO: cancel on active slot change?
|
||||||
|
if (this.itemInUse) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const item = this.entity.itemInSlot(slotIndex);
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.itemInUse = item;
|
||||||
|
// Set up context.
|
||||||
|
this.itemActionsContext.add('user', this.entity);
|
||||||
|
this.itemActionsContext.add('item', item);
|
||||||
|
this.itemActionsContext.add('slotIndex', slotIndex);
|
||||||
|
// Keep a reference to the item.
|
||||||
|
this.itemActions = item.itemActions;
|
||||||
|
// Defer until the item actions are finished.
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.itemActions.once('actionsFinished', () => {
|
||||||
|
// Check if the item was used up.
|
||||||
|
this.itemActions = undefined;
|
||||||
|
this.itemActionsContext.clear();
|
||||||
|
this.itemInUse = undefined;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(elapsed) {
|
||||||
|
if (this.itemActions) {
|
||||||
|
this.itemActions.tick(this.itemActionsContext, elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
1
resource/potion.entity.json
Normal file
1
resource/potion.entity.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"traits":{"damaging":{"params":{"damageSpecs":[{"affinity":0,"lock":0,"power":-50,"variance":0.1}]}},"existent":{},"item":{"params":{"itemActions":{"type":"actions","traversals":[{"type":"traversal","steps":[{"type":"key","key":"user"},{"type":"key","key":"takeDamageFrom"},{"type":"invoke","args":[{"type":"traversal","steps":[{"type":"key","key":"item"}]}]}]},{"type":"traversal","steps":[{"type":"key","key":"item"},{"type":"key","key":"decrementQuantity"},{"type":"invoke","args":[]}]},{"type":"traversal","steps":[{"type":"key","key":"global"},{"type":"key","key":"wait"},{"type":"invoke","args":[{"type":"literal","value":0.5}]}]}]},"slotImages":{"default":"/potion.png"}}}}}
|
BIN
resource/potion.png
Normal file
BIN
resource/potion.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
BIN
resource/question-mark.png
Normal file
BIN
resource/question-mark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 429 B |
|
@ -59,6 +59,16 @@ export function createEntityForConnection(socket) {
|
||||||
y: y * 4,
|
y: y * 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
receptacle: {
|
||||||
|
params: {
|
||||||
|
slots: {
|
||||||
|
0: {
|
||||||
|
qty: 10,
|
||||||
|
uri: '/potion.entity.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
roomed: {},
|
roomed: {},
|
||||||
shaped: {
|
shaped: {
|
||||||
params: {
|
params: {
|
||||||
|
@ -70,6 +80,11 @@ export function createEntityForConnection(socket) {
|
||||||
},
|
},
|
||||||
visible: {},
|
visible: {},
|
||||||
vulnerable: {},
|
vulnerable: {},
|
||||||
|
wielder: {
|
||||||
|
state: {
|
||||||
|
activeSlotIndex: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// Embed socket.
|
// Embed socket.
|
||||||
|
|
|
@ -25,6 +25,9 @@ import {mamaKittySpawnerJSON} from './fixtures/mama-kitty-spawner.entity';
|
||||||
writeFixture('mama-kitty-spawner.entity.json', mamaKittySpawnerJSON());
|
writeFixture('mama-kitty-spawner.entity.json', mamaKittySpawnerJSON());
|
||||||
import {mamaKittyJSON} from './fixtures/mama-kitty.entity';
|
import {mamaKittyJSON} from './fixtures/mama-kitty.entity';
|
||||||
writeFixture('mama-kitty.entity.json', mamaKittyJSON());
|
writeFixture('mama-kitty.entity.json', mamaKittyJSON());
|
||||||
|
// Write items.
|
||||||
|
import {potionJSON} from './fixtures/potion.entity';
|
||||||
|
writeFixture('potion.entity.json', potionJSON());
|
||||||
// Write rooms.
|
// Write rooms.
|
||||||
import {kittyFireJSON} from './fixtures/kitty-fire.room';
|
import {kittyFireJSON} from './fixtures/kitty-fire.room';
|
||||||
writeFixture('kitty-fire.room.json', kittyFireJSON());
|
writeFixture('kitty-fire.room.json', kittyFireJSON());
|
||||||
|
|
49
server/fixtures/potion.entity.js
Normal file
49
server/fixtures/potion.entity.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import {buildInvoke, buildTraversal} from '@avocado/behavior';
|
||||||
|
|
||||||
|
import {AFFINITY_NONE} from '../../common/combat/constants';
|
||||||
|
|
||||||
|
// Healing potion.
|
||||||
|
export function potionJSON() {
|
||||||
|
const causeHealing = buildInvoke(['user', 'takeDamageFrom'], [
|
||||||
|
buildTraversal(['item']),
|
||||||
|
]);
|
||||||
|
const decrement = buildInvoke(
|
||||||
|
['item', 'decrementQuantity'],
|
||||||
|
);
|
||||||
|
const cooldown = buildInvoke(
|
||||||
|
['global', 'wait'],
|
||||||
|
[0.5],
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
traits: {
|
||||||
|
damaging: {
|
||||||
|
params: {
|
||||||
|
damageSpecs: [
|
||||||
|
{
|
||||||
|
affinity: AFFINITY_NONE,
|
||||||
|
lock: 0,
|
||||||
|
power: -50,
|
||||||
|
variance: 0.1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
existent: {},
|
||||||
|
item: {
|
||||||
|
params: {
|
||||||
|
itemActions: {
|
||||||
|
type: 'actions',
|
||||||
|
traversals: [
|
||||||
|
causeHealing,
|
||||||
|
decrement,
|
||||||
|
cooldown,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
slotImages: {
|
||||||
|
default: '/potion.png',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -247,6 +247,9 @@ export class Informed extends decorate(Trait) {
|
||||||
if (0 === packets.length) {
|
if (0 === packets.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: filter packets that are only delivered to self entity.
|
||||||
|
|
||||||
// Filter invisible entities.
|
// Filter invisible entities.
|
||||||
packets = this.filterInvisibleEntityPackets(packets);
|
packets = this.filterInvisibleEntityPackets(packets);
|
||||||
// Reduce entities by range.
|
// Reduce entities by range.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user