For this particular problem it's better to use duck typing instead of object composition. Duck typing makes use of the duck test to ensure type safety:
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
For this problem, we'll make use of an analogous "mage test" and "fighter test" respectively:
- If it has mana and is holding a staff then it is a mage.
- If it has stamina and is holding a sword then it is a fighter.
Note that we can still use object composition to keep our code modular. We'll create prototypes for character
, fighter
and mage
and then compose them together to get the final prototype:
const character = {
health: 100,
right: null,
left: null,
equip(item) {
const {name, right, left} = this;
if (right === null) this.right = item;
else if (left === null) this.left = item;
else console.error(`${name} is already holding ${right} and ${left}.`);
}
};
First, we have the prototype for characters. Every character has at least four properties: name
, health
, right
(i.e. the item equipped in the right hand) and left
(i.e. the item equipped in the left hand). We provide default values for health
, right
and left
. However, we don't provide any default value for name
. Hence, when we create a new character we must provide it a name.
const fighter = {
stamina: 100,
fight(foe) {
const {name, stamina, right, left} = this;
if (right !== "a sword" && left !== "a sword")
console.error(`${name} is not holding a sword.`);
else if (stamina === 0) console.error(`${name} has no stamina.`);
else { this.stamina--; console.log(`${name} slashes at ${foe}.`); }
}
};
Then, we have the prototype for fighters. Note that since a fighter is also a character, we can use the name
, right
and left
properties in the fight
method. In addition, fighters have a stamina
property which has a default value of 100.
const mage = {
mana: 100,
cast(spell) {
const {name, mana, right, left} = this;
if (right !== "a staff" && left !== "a staff")
console.error(`${name} is not holding a staff.`);
else if (mana === 0) console.error(`${name} has no mana.`);
else { this.mana--; console.log(`${name} casts ${spell}.`); }
}
};
Next, we have the prototype for mages. Like fighters, mages are also characters and hence they too can make use of character-specific properties. In addition, mages have a mana
property with a default value of 100.
Object.assign(character, fighter, mage);
Object.prototype.create = function (properties) {
return Object.assign(Object.create(this), properties);
};
const gandalf = character.create({ name: "Gandalf" });
gandalf.equip("a sword");
gandalf.equip("a staff");
gandalf.fight("the goblin");
gandalf.cast("a blinding light");
Finally, we use Object.assign
to compose all the prototypes together by extending the character
prototype with the fighter
and mage
prototypes. We also extend Object.prototype
with a useful create
function to easily create instances of prototypes. We use this method to create an instance of character
named Gandalf and we make him fight a goblin.
const mage = {
mana: 100,
cast(spell) {
const {name, mana, right, left} = this;
if (right !== "a staff" && left !== "a staff")
console.error(`${name} is not holding a staff.`);
else if (mana === 0) console.error(`${name} has no mana.`);
else { this.mana--; console.log(`${name} casts ${spell}.`); }
}
};
const fighter = {
stamina: 100,
fight(foe) {
const {name, stamina, right, left} = this;
if (right !== "a sword" && left !== "a sword")
console.error(`${name} is not holding a sword.`);
else if (stamina === 0) console.error(`${name} has no stamina.`);
else { this.stamina--; console.log(`${name} slashes at ${foe}.`); }
}
};
const character = {
health: 100,
right: null,
left: null,
equip(item) {
const {name, right, left} = this;
if (right === null) this.right = item;
else if (left === null) this.left = item;
else console.error(`${name} is already holding ${right} and ${left}.`);
}
};
Object.assign(character, fighter, mage);
Object.prototype.create = function (properties) {
return Object.assign(Object.create(this), properties);
};
const gandalf = character.create({ name: "Gandalf" });
gandalf.equip("a sword");
gandalf.equip("a staff");
gandalf.fight("the goblin");
gandalf.cast("a blinding light");
Above is the demo of the entire script put together, demonstrating how it works. As you can see, you can break up your character prototype into several different prototypes such as mage
and fighter
and then put them all back together using Object.assign
. This makes adding new character types much easier and much more manageable. Duck typing is used to ensure that a fighter (a character equipped with a sword) can't cast a spell, etc. Hope that helps.