// Stack.js
(function () {
/* global AdventureJS A */
/**
* @ajspath AdventureJS.Atom.Stack
* @augments AdventureJS.Atom
* @class AdventureJS.Helpers.Stack
* @ajsnavheading HelperClasses
* @param {String} game_name Name of top level game instance that is scoped to window.
* @param {String} name Instance name.
* @summary Class that allows splitting assets into fungible instances.
* @tutorial Helpers_Stacks
* @classdesc
* <p>
* <strong>Stack</strong> is a special class
* that allows entity assets to be split into
* In this example, we'll set up a custom
* class Pistachio and then create a dispenser
* that spawns new instances.
* </p>
* <pre class="display"><code class="language-javascript">MyGame.createClass({
* class: "Pistachio",
* extend: "Edible",
* singular: "pistachio",
* plural: "pistachios",
* description: function () {
* let msg = "";
* msg = `It's a pistachio. `;
* return msg;
* },
* });
* MyGame.createAsset({
* class: "Pistachio",
* name: "pistachio",
* place: { in: "small pouch" },
* stack: {
*
* // If use_stack_name is true, this item will be described
* // using the enumeration property with the class's singular
* // property, eg "three pistachios".
* // If use_stack_name is false, the asset's
* // name will be used, eg "the sack of pistachios".
* // This is intended to help distinguish dispensers.
* // Spawned stacks will default to use_stack_name = true.
* this.use_stack_name = true;
*
* // enumeration determines how stack numbers are
* // presented. Options are: <br/>
* // <li>numeric: 1, 2, 3, 17, 100, etc.</li>
* // <li>spelled: a, two, three, seventeen, one hundred, etc.</li>
* // <li>grouped: a, a couple of, several, a few, some, numerous</li>
* // The enumeration setting carries to spawned stacks.
* this.enumeration = "spelled"; // numeric, spelled, grouped
*
* // Making a stack a dispenser means the whole stack
* // is a fixed object. For example,
* // a sack of peanuts, where the sack is an asset,
* // and the peanuts always reside inside it.
* // New peanuts can be spawned from the stack,
* // but the entire stack can't be removed.
* // Spawned stacks will NOT be dispensers.
* dispenser: false,
*
* // Quantity represents the current count of items
* // in this stack, which could be 1 or 10 or 1000.
* // Setting to -1 makes it infinite which is useful
* // for making bottomless dispensers.
* quantity: -1,
*
* // max_quantity sets the highest quantity that
* // this stack can hold. Setting to -1 makes it
* // infinite. If quantity is set to -1, max_quantity
* // will be ignored.
* max_quantity: -1,
*
* // max_per_take sets the highest quantity that
* // can be taken from this stack at a time.
* this.max_per_take = -1;
*
* // max_stacks_in_game means the total number of
* // stacks allowed in the game world, for
* // example you may want to allow no more
* // than 50 unique breadcrumbs to be created.
* // Spawned stacks will always inherit this property
* // from their originating stack.
* max_stacks_in_game: 50,
*
* // max_quantity_in_game means the total quantity held
* // by all stacks in the game world.
* // For instance, you may want to have 5 quivers
* // of arrows, but no more than 50 arrows between them all.
* // If any stack of this class has infinite quantity,
* // this will be infinite.
* // Spawned stacks will always inherit this property
* // from their originating stack.
* max_quantity_in_game: -1,
*
* // Stacks spawned by this stack will inherit properties
* // from this stack, except for dispenser and use_stack_name.
* // max_quantity and max_per_take can be set uniquely for new stacks.
* spawn_properties: {},
*
* },
* });
* </code></pre>
**/
class Stack extends AdventureJS.Atom {
constructor(name, game_name, context_id) {
super(name, game_name, context_id);
this.class = "Stack";
this.id = `${context_id}|stack`;
/**
* If use_stack_name is true, this item will be described
* using the enumeration property with the class's singular
* property, eg "three pistachios".
* If use_stack_name is false, the asset's
* name will be used, eg "the sack of pistachios".
* This is intended to help distinguish dispensers.
* Spawned stacks will default to use_stack_name = true.
* @var {Boolean} AdventureJS.Helpers.Stack#use_stack_name
*/
this.use_stack_name = true;
/**
* enumeration determines how stack numbers are
* presented. Options are: <br/>
* <li>numeric: 1, 2, 3, 17, 100, etc.</li>
* <li>spelled: a, two, three, seventeen, one hundred, etc.</li>
* <li>grouped: a, a couple of, several, a few, some, numerous</li>
* The enumeration setting carries to spawned stacks.
* @var {string} AdventureJS.Helpers.Stack#enumeration
*/
this.enumeration = "spelled"; // numeric, spelled, grouped
/**
* Making a stack a dispenser means the whole stack
* is a fixed object. For example,
* a sack of peanuts, where the sack is an asset,
* and the peanuts always reside inside it.
* New peanuts can be spawned from the stack,
* but the entire stack can't be removed.
* Spawned stacks will NOT be dispensers.
* @var {Boolean} AdventureJS.Helpers.Stack#dispenser
*/
this.dispenser = false;
/**
* Quantity represents the current count of items
* in this stack, which could be 1 or 10 or 1000.
* Setting to -1 makes it infinite which is useful
* for making bottomless dispensers.
* Spawned stacks will start with the quantity taken.
* @var {int} AdventureJS.Helpers.Stack#quantity
* @default 1
*/
this.quantity = 1;
/**
* max_quantity sets the highest quantity that
* this stack can hold. Setting to -1 makes it
* infinite. If quantity is set to -1, max_quantity
* will be ignored.
* @var {int} AdventureJS.Helpers.Stack#max_quantity
* @default -1
*/
this.max_quantity = -1;
/**
* max_per_take sets the highest quantity that
* can be taken from this stack at a time.
* Spawned stacks will inherit this value.
* @var {int} AdventureJS.Helpers.Stack#max_take
* @default -1
* @note Not yet supported.
*/
this.max_per_take = 1;
/**
* max_stacks_in_game means the total number of
* stacks of this class allowed in the game world.
* For example you may want to allow no more
* than 50 unique breadcrumbs to be created
* Spawned stacks will inherit this value.
* @var {int} AdventureJS.Helpers.Stack#max_stacks_in_game
* @note Not yet supported.
*/
this.max_stacks_in_game = 20;
/**
* max_quantity_in_game means the total quantity held
* by all stacks in the game world, excluding infinite
* stacks. For instance, you may want to have 5 quivers
* of arrows, but no more than 50 arrows between them all.
* Spawned stacks will inherit this value.
* @var {int} AdventureJS.Helpers.Stack#max_quantity_in_game
* @note Not yet supported.
*/
this.max_quantity_in_game = -1;
/**
* Stacks spawned by this stack will inherit properties
* from this stack, except for dispenser and use_stack_name.
* max_quantity and max_per_take can be set uniquely for new stacks.
* @var {object} AdventureJS.Helpers.Stack#spawn_properties
*/
this.spawn_properties = {};
return this;
}
/**
* Get stack quantity. Converts -1 to Infinity
* for purposes of comparison.
*/
getStackName(definite = false) {
let name,
article = "";
if (this.dispenser) {
// the bowl of grapes
if (definite) article = this.context.definite_article;
name = `${article} ${this.context.name}`.trim();
} else if (!this.dispenser) {
name =
this.quantity === 1 ? this.context.singular : this.context.plural;
if ("function" === typeof this.enumeration) {
// if someone wrote a custom function, let that handle it
article = `${article} ${this.enumeration(this.quantity)}`.trim();
} else if ("string" === typeof this.enumeration) {
if (this.quantity === 0 || this.quantity === 1) {
// exclude definite article from 0 and 1
article =
`${this.game.dictionary.enumerate(this.enumeration, this.quantity)}`.trim();
} else {
if (definite) article = `${this.context.definite_article} `;
article +=
`${this.game.dictionary.enumerate(this.enumeration, this.quantity)}`.trim();
}
}
name = `${article} ${name}`.trim();
}
// this is a hacky fix for a sloppy situation where
// some of our enumeration phrase start with "a" like
// "a hundred" or "a score of" and we end up with
// "the a hundred things"
if (name.startsWith("the a ")) {
name = name.replace("the a ", "the ");
}
return name;
}
/**
* Get stack quantity. Converts -1 to Infinity
* for purposes of comparison.
*/
getQuantity() {
return this.quantity > -1 ? this.quantity : Infinity;
}
/**
* subtract from the quantity of this stack.
* @param {int} amount Number to subtract.
*/
subtract(amount = 1) {
if (this.quantity > -1) {
this.quantity = this.quantity - amount;
if (this.quantity === 0) {
// @TODO destroy if empty?
}
}
}
/**
* add to the quantity of this stack.
* @param {int} amount Number to add.
*/
add(amount = 1) {
if (this.quantity > -1) {
if (this.quantity + amount > this.max_quantity) {
// @TODO max quantity check?
// what if some portion of amount can fit?
}
this.quantity = this.quantity + amount;
}
}
/**
* canIncrement(amount) returns true or false.
* @param {int} amount A property to test.
* @returns {Boolean}
*/
canIncrement(amount) {
if (this.quantity > -1) {
if (this.quantity === this.max_quantity) {
return false;
}
if (this.quantity + amount > this.max_quantity) {
// @TODO how to handle if can partially increment?
// return false?
}
}
return true;
}
/**
* Find a compatible stack in a specified asset.
* @param {int} amount A property to test.
* @returns {Boolean}
*/
findCompatibleStack(aspect, asset) {
const contents = asset.getContentsAt(aspect);
for (let i = 0; i < contents.length; i++) {
const item = this.game.getAsset(contents[i]);
// does asset already contain a compatible stack?
if (
item.stack &&
item.class === this.context.class &&
item.id !== this.context_id
) {
// if asset contains a compatible stack, return that
return item;
}
}
return null;
}
/**
* merge the quantity of another stack into this one.
* @param {object} donor The asset to merge into this one.
* @param {bool} clear_donor Whether to zero the donor's quantity.
*/
merge(donor, clear_donor = false) {
if (this.quantity === -1) {
// nothing needs to happen
} else if (donor.stack.quantity === -1) {
this.quantity = -1;
// @TODO check against this.max_quantity?
} else if (this.quantity > -1) {
this.quantity = this.quantity + donor.stack.quantity;
}
if (clear_donor && donor.stack.quantity > -1) {
donor.stack.quantity = 0;
}
}
/**
* Spawns a new stack asset.
* @param {int} quantity The quantity to add to the new stack.
* @returns {object}
*/
spawn(quantity) {
// ensure that quantity is within limits
quantity = Math.min(
quantity,
this.max_per_take < 0 ? Infinity : this.max_per_take,
this.quantity < 0 ? Infinity : this.quantity
);
this.game.log(
"L1686",
"log",
"high",
`[Stack.js] spawn(): ${quantity} ${this.context.class}`,
"Entity"
);
const proto = {
class: this.context.class,
name: this.context.class.toLowerCase(),
id: this.context.class.toLowerCase() + A.FX.UID.get(),
};
const asset = this.game.addAsset(proto);
if (!asset) return null;
// it's possible for a custom class to have a stack
// already so let's check for that before adding
// a stack to the new asset
if (!asset.stack) {
asset.stack = new AdventureJS.Helpers.Stack(
"stack",
this.game_name,
asset.id
);
this.game.world._stacks[asset.class] =
this.game.world._stacks[asset.class] || [];
if (!this.game.world._stacks[asset.class].includes(asset.id)) {
this.game.world._stacks[asset.class].push(asset.id);
}
}
// get spawned stack's properties from dispensing stack
asset.stack.set({
// we don't copy use_stack_name
// we don't copy dispenser
// quantity depends on what player asked for or dispenser limits
quantity: quantity,
// max_quantity and max_per_take can be set differently
// for spawned stacks otherwise they inherit
max_quantity: this.spawn_properties.max_quantity || this.max_quantity,
max_per_take: this.spawn_properties.max_per_take || this.max_per_take,
// spawned stacks always inherit these remaining properties
enumeration: this.enumeration,
max_stacks_in_game: this.max_stacks_in_game,
max_quantity_in_game: this.max_quantity_in_game,
spawn_properties: this.spawn_properties,
});
asset.stack.quantity = quantity;
this.subtract(quantity);
// put the new spawn in its source's location
// asset.place = Object.assign(asset.place, this.context.place);
asset.setPlace(this.context.place);
return asset;
}
/**
* Initializes a new stack.
* @param {int} initialize Initialize this stack.
*/
initialize() {
this.game.log(
"L1451",
"log",
"high",
`[Stack.js] initialize()${this.context_id}.stack`,
"Entity"
);
let asset = this.context;
this.game.world._stacks[asset.class] =
this.game.world._stacks[asset.class] || [];
if (!this.game.world._stacks[asset.class].includes(asset.id)) {
this.game.world._stacks[asset.class].push(asset.id);
}
// ensure that stacks can be taken from
if (!asset.isIOV("take")) {
asset.setIOV("take");
}
}
/**
* Destroys a stack.
* @param {int} destroy Destroy this stack.
*/
destroy() {
this.game.log(
"L1710",
"log",
"high",
`[Stack.js] ${this.context_id}.stack.destroy()`,
"Entity"
);
let asset = this.context;
// if it's a stack, remove from world._stacks
if (this.game.world._stacks[asset.class]?.includes(asset.id)) {
let index = this.game.world._stacks[asset.class].indexOf(asset.id);
this.game.world._stacks[asset.class].splice(index);
}
}
}
AdventureJS.Helpers.Stack = Stack;
})();