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.