Skip to content

Commit

Permalink
[inventory] Implements a first version of the inventory module (#79)
Browse files Browse the repository at this point in the history
* added @xethya/utils

* implemented basic structure with slotCapacity

* added placeholder Item class

* updated to [email protected]

* implemented minimum `contents` attr

* updaded to [email protected]

* finished first round of implementation

* starting to work on inventory tests

* added isFull(), isEmpty(), stack resizing

* added tests

* bumped inventory to 0.0.2
  • Loading branch information
joelalejandro authored Sep 21, 2019
1 parent 9df91b6 commit fe9dba4
Show file tree
Hide file tree
Showing 4 changed files with 518 additions and 5 deletions.
7 changes: 6 additions & 1 deletion packages/inventory/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@xethya/inventory",
"shortName": "inventory",
"version": "0.0.1",
"version": "0.0.2",
"description": "",
"main": "dist/xethya.inventory.js",
"module": "dist/xethya.inventory.es.js",
Expand All @@ -16,6 +16,7 @@
],
"devDependencies": {
"@types/jest": "^24.0.18",
"@types/uuid": "^3.4.5",
"jest": "^24.9.0",
"ts-jest": "^24.0.2",
"tslint": "^5.19.0",
Expand All @@ -39,5 +40,9 @@
"transformIgnorePatterns": [
"/node_modules/[^@xethya]/"
]
},
"dependencies": {
"@xethya/utils": "^0.1.8",
"uuid": "^3.3.3"
}
}
293 changes: 292 additions & 1 deletion packages/inventory/src/inventory.ts
Original file line number Diff line number Diff line change
@@ -1 +1,292 @@
export class Inventory {}
import { assert, DynamicWeightedStack, Stack } from "@xethya/utils";
import { Item } from "./item";

/**
* By default, any inventory supports a maximum capacity
* of 100 weight units.
*/
export const INVENTORY_DEFAULT_CAPACITY = 100;

/**
* A default capacity provider to use the default capacity
* for inventories.
*/
export const INVENTORY_DEFAULT_CAPACITY_PROVIDER = () => INVENTORY_DEFAULT_CAPACITY;

/**
* A function used to calculate this inventory's capacity.
* Its scope is the inventory's instance.
*
* @this {Inventory}
*/
export type InventoryCapacityProvider = (this: Inventory) => number;

/**
* Customizes the inventory's features.
*/
export type InventoryOptions = {
/**
* A function used to calculate this inventory's capacity.
* Its scope is the inventory's instance. Defaults to a basic
* provider that always return 100.
*
* @default INVENTORY_DEFAULT_CAPACITY_PROVIDER
*/
capacityProvider?: InventoryCapacityProvider;
};

/**
* A lookup result for an entry in the inventory's index.
*/
export type InventoryIndexEntry = {
/**
* Used to preserve the inventory's order.
*/
position: number;

/**
* A reference to the item itself.
*/
item: Item;
};

/**
* Allows to lookup an item by its ID.
*/
export type InventoryIndex = { [key: string]: InventoryIndexEntry };

export class Inventory {
/**
* Contains a list of all the items stored in this inventory.
*/
protected readonly contents: DynamicWeightedStack<Item>;

/**
* Allows to quickly lookup an item by its unique identifier.
*/
protected readonly index: InventoryIndex;

/**
* A function used to calculate this inventory's capacity.
* Its scope is the inventory's instance.
*
* @this {Inventory}
*/
protected readonly capacityProvider: InventoryCapacityProvider;

protected lastCalculatedCapacity: number;

/**
* Counts how many items have been stored in the inventory.
*
* @todo Move this to the DynamicWeightedStack (or maybe any Stack?).
*/
protected count: number = 0;

/**
* Indicates how full this inventory is.
*
* @todo Move this to the DynamicWeightedStack (or maybe any Stack?).
*/
protected occupiedCapacity: number = 0;

/**
* Allows to keep track of items placed in a container, up to
* a certain capacity.
*
* @param options {InventoryOptions}
*/
constructor(options: InventoryOptions = {}) {
this.capacityProvider = options.capacityProvider || INVENTORY_DEFAULT_CAPACITY_PROVIDER;
this.contents = new DynamicWeightedStack<Item>(this.capacity, "weight");
this.index = {};
}

/**
* Indicates the current capacity of the inventory. If the capacity has
* changed since the last time it was calculate, it'll autoresize the
* inventory's stack.
*/
public get capacity(): number {
const capacity = this.capacityProvider.bind(this)();

if (this.lastCalculatedCapacity && this.lastCalculatedCapacity !== capacity) {
this.contents.resize(this.lastCalculatedCapacity);
}

this.lastCalculatedCapacity = capacity;

return this.lastCalculatedCapacity;
}

/**
* Returns the index entries as an array of values.
*/
protected get indexEntries(): InventoryIndexEntry[] {
return Object.values(this.index);
}

/**
* Returns the available capacity on this inventory.
*/
getAvailableSpace(): number {
return this.capacity - this.occupiedCapacity;
}

/**
* Puts an item or items inside this inventory. This will affect
* the inventory's occupied capacity.
*
* @param items {...Item}
*/
put(...items: Item[]): void {
items.forEach(item => {
this.contents.push(item);
this.index[item.id] = { position: this.count, item };

// TODO: These could be exposed from the stack, removing the need for extra state.
this.count += 1;
this.occupiedCapacity += item.weight;
});
}

/**
* Returns information about an item in the inventory,
* looking it up by its unique identifier. This will not
* have any impact on the inventory's occupied capacity.
*
* @param id {string}
*/
peek(id: string): Item | void {
return this.index[id] ? this.index[id].item : undefined;
}

/**
* Returns information about an item in the inventory,
* looking it up by its numeric position. This will not
* have any impact on the inventory's occupied capacity.
*
* @param position {number}
*/
peekAt(position: number): Item | void {
if (this.isEmpty()) {
return;
}

const indexEntry = this.getByPosition(position);

if (!indexEntry) {
return;
}

return indexEntry.item;
}

/**
* Returns information about *all* of this inventory's
* items as an array.
*/
peekAll(): Item[] {
if (this.isEmpty()) {
return [];
}

const items: Item[] = [];
this.indexEntries.forEach(({ position, item }) => (items[position] = item));
return items;
}

/**
* Extracts an item from the inventory by its unique identifier.
* This *will* affect the inventory's occupied capacity.
*
* @param id {string}
*/
retrieve(id: string): Item | void {
if (this.isEmpty()) {
return;
}

const item = this.peek(id);

if (!item) {
return;
}

this.extractFromContents(item);

return item;
}

/**
* Extracts an item from the inventory by its position.
* This *will* affect the inventory's occupied capacity.
*
* @param id {string}
*/
retrieveAt(position: number): Item | void {
if (this.isEmpty()) {
return;
}

const item = this.peekAt(position);

if (!item) {
return;
}

this.extractFromContents(item);

return item;
}

/**
* Returns `true` if the inventory is full, `false` if it's not.
*/
isFull(): boolean {
return this.getAvailableSpace() === 0;
}

/**
* Returns `true` if the inventory is empty, `false` if it's not.
*/
isEmpty(): boolean {
return this.getAvailableSpace() === this.capacity;
}

/**
* Removes an item from the inventory and adjusts the capacity
* accordingly.
*
* @param itemToRetrieve {Item}
*/
protected extractFromContents(itemToRetrieve: Item) {
const temporaryStack = new Stack<Item>();

while (temporaryStack.peek() !== itemToRetrieve && !this.contents.isEmpty()) {
temporaryStack.push(this.contents.pop() as Item);
}

temporaryStack.pop();
delete this.index[itemToRetrieve.id];

while (!temporaryStack.isEmpty()) {
const item = temporaryStack.pop() as Item;
this.contents.push(item);
this.index[item.id].position -= 1;
}

this.count -= 1;
this.occupiedCapacity -= itemToRetrieve.weight;
}

/**
* Returns an item by its position in the inventory index.
*
* @param position {number}
*/
protected getByPosition(position: number) {
assert(position >= 0, "A non-negative index must be used to access the inventory by position");
return this.indexEntries.find(entry => entry.position === position);
}
}
40 changes: 40 additions & 0 deletions packages/inventory/src/item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { v4 as generateUUID } from "uuid";

export const ITEM_DEFAULT_WEIGHT = 1;

/**
* Customizes the item's features.
*/
export type ItemOptions = {
/**
* How much this item weights. Defaults to ITEM_DEFAULT_WEIGHT (1).
*
* @default 1
*/
weight?: number;
};

export class Item {
/**
* A unique identifier for the item.
*/
public readonly id: string;

/**
* How much this item weights. Defaults to ITEM_DEFAULT_WEIGHT (1).
*
* @default 1
*/
public readonly weight: number;

/**
* Represents something an entity can hold and/or use.
*
* @param options {ItemOptions}
* @todo Decouple this into a package of its own.
*/
constructor(options: ItemOptions = {}) {
this.id = generateUUID();
this.weight = options.weight || ITEM_DEFAULT_WEIGHT;
}
}
Loading

0 comments on commit fe9dba4

Please sign in to comment.