Turn-based combat system using JS as an example
Perhaps you, as a programmer, were once interested in turn-based strategies. In this article, I decided to share my own take on the subject using JavaScript.
Who is interested – welcome under cat
Hello everyone, this is stalker320, I was away for a while and just returned from hibernation.
To begin with, we will set tasks that will allow us to decide what we need to develop.
The class that will handle game events. Let’s say it will be called Game;
The class responsible for creating the player and the mob, the Entity class;
inventory class;
Item class;
And finally, the class of the acting effect.
Effect class
You need to start with the element that uses the least number of mentions of other elements. In our case, this is the class Effect
and it will have two properties name
and steps_left
.
class Effect {
constructor(name, steps_left) {
this.name = name;
this.steps_left = steps_left;
}
get_name() {
return this.name;
}
get_steps_left() {
return this.steps_left;
}
count() {
// отсчитывает 1 ход.
this.steps_left -= 1;
}
}
This class does nothing by itself, but lays the potential for the future.
Now we can create different variations of the effects:
// Кровотечение
class BleedingEffect extends Effect {
constructor(name, steps_left, damage) {
super(name, steps_left);
this.damage = damage;
}
get_damage() {
return this.damage;
}
}
// Регенерация
class RegeneartionEffect extends Effect {
constructor(name, steps_left, heal) {
super(name, steps_left);
this.heal = heal;
}
get_heal_count() {
return this.heal;
}
}
// Пассивная броня каждый ход
class PassiveArmorEffect extends Effect {
constructor(name, steps_left, armor) {
super(name, steps_left);
this.armor = armor;
}
get_armor() {
return this.armor;
}
}
And an even more interesting method of creating effects is to create on the spot.
let berserk = new class BerserkEffect extends Effect {
constructor(name, steps_left) {
super(name, steps_left);
}
get_dmg_multiplier() {return 1.5;}
get_resistance() {return -0.5;} // добавляет отрицательное сопротивление урону, что увеличивает получаемый урон на 50%
}();
Item class
The item is a template for many things – weapons, tools, actions.
class Item {
constructor(name) {
this.name = name;
}
get_name() {
return this.name;
}
}
The class itself Item
does not give us anything, but is the starting point for other classes. As an example, I will give a few more classes that inherit from Item
.
class Weapon extends Item {
constructor(name, damage, effect) {
super(name);
this.damage = damage;
this.effect = effect;
}
get_damage() {
return this.damage;
}
get_effect() {
return this.effect;
}
}
Class Weapon
has two properties in addition to the properties Item
. These are the damage and effect properties.
class Shield extends Item {
constructor(name, armor) {
super(name);
this.armor = armor;
}
get_shield() {
return this.armor;
}
}
Shield, allows you to skip a certain amount of damage past the health bar
class Potion extends Item {
constructor(name, effect) {
super(name);
this.effect = effect;
}
get_effect() {
return this.effect;
}
}
// Зелье лечения за один ход, как пример
class HealPotion extends Potion {
constructor(name, heal_count) {
super(name, new RegenerationEffect(name, 1, heal_count));
}
}
class RegeneratonPotion extends Potion {
constructor(name, steps, heal_count) {
super(name, new RegenerationEffect(name, steps, heal_count));
}
}
As well as tons of different types of potions that apply the appropriate effects to the character when applied.
Inventory class
A simple container for an array that limits the incoming data type and size.
class Inventory {
// Инвентарь представляет собой место, где хранятся все предметы
container;
container_size = 16;
constructor(container_size = 16) {
this.container_size = container_size;
this.container = Array();
}
set_item(idx, item) {
/**
* idx - число
* item - объект от класса Item
*/
if ( !(item instanceof Item)) {
throw new Error("item isn't instance of Item");
}
if (idx < 0 || idx > this.container_size) {
throw new Error("idx out of bounds");
}
this.container[idx] = item;
}
get_item(idx) {
/**
* idx - число
*/
if (idx < 0 || idx > this.container_size) {
throw new Error("idx out of bounds");
}
return container[idx];
}
}
Entity class
This class is probably the second most important class. He is responsible for all mobs and the player created in the future.
It will not have many properties, but they will be relevant
– maximum health max_health
– current health health
– Shield shield
– inventory from Inventory
inventory
– effects effects
– action points steps
– maximum action points max_steps
class Entity {
max_health;
health;
shield = 0;
inventory = new Inventory(16);
effects = Array();
steps;
max_steps;
weapon_idx = -1;
constructor(max_health = 100, max_steps = 3) {
this.max_health = max_health;
this.health = max_health;
this.steps = max_steps;
this.max_steps = max_steps;
}
gain_damage(damage, effect) {
if (effect !== null) {
this.effects.push(effect);
}
let dmg = damage;
// ОБРАБОТКА СОПРОТИВЛЕНИЙ
for (const effect in this.effects) {
if (effect.get_resistance !== null) {
dmg *= (1 - effect.get_resistance());
}
}
let dmg_left = dmg - this.shield;
if (dmg_left >= 0) {
this.health = health - dmg_left;
this.reset_shield();
}
else {
this.shield -= damage;
}
if (this.health < 0) this.health = 0;
}
setup_shield(shield_count, effect) {
if (effect !== null) {
this.effects.push(effect);
}
let def = shield_count;
// ОБРАБОТКА ИНКРЕМЕНТОВ в первую очередь
this.effects.forEach((effect) => {
if (effect.get_def_incrementation !== null) {
def += effect.get_def_incrementation();
}
});
// ОБРАБОТКА МНОЖИТЕЛЕЙ далее
this.effects.forEach((effect) => {
if (effect.get_def_multiplier !== null) {
def *= effect.get_def_multiplier();
}
})
this.shield += def;
}
reset_shield() {
this.shield = 0;
}
heal(heal_count, effect) {
if (effect !== null) {
this.effects.push(effect);
}
if (this.health < this.max_health) {
this.health += heal_count;
}
if (this.health > this.max_health) this.health = this.max_health;
}
deal_damage(target, weapon) {
if (weapon instanceof Weapon) {
let dmg = weapon.get_damage();
// ОБРАБОТКА ИНКРЕМЕНТОВ в первую очередь
this.effects.forEach((effect) => {
if (effect.get_dmg_incrementation !== null) {
dmg += effect.get_dmg_incrementation();
}
});
// ОБРАБОТКА МНОЖИТЕЛЕЙ далее
this.effects.forEach((effect) => {
if (effect.get_dmg_multiplier !== null) {
dmg *= effect.get_dmg_multiplier();
}
})
target.gain_damage(dmg, weapon.get_effect());
}
}
is_alive() {
return this.health > 0;
}
step() {
/** считать после действий атаки/защиты/лечения.
*
*/
for (let i = 0; i < this.effects.length; i++) {
const effect = this.effects[i];
if (effect.get_damage !== null) {
this.gain_damage(effect.get_damage(), null);
}
if (effect.get_heal_count !== null) {
this.heal(effect.get_heal_count(), null);
}
if (effect.get_armor !== null) {
this.heal(effect.get_armor(), null);
}
effect.count();
if (effect.get_steps_left() <= 0) {
this.effects[i] = null;
}
}
this.effects = this.effects.filter((elem) => {return elem != null;});
// Здесь использована стрелочная функция для фильтрации по элементам без null
}
get_weapon() {
if (this.weapon_idx >= 0) return this.inventory.get_item(this.weapon_idx);
else return null;
}
}
I hope that comments are superfluous, but if any point is not clear, feel free, dear readers, to clarify incomprehensible or incorrect elements in the comments.
Game class
The most important element in the project. It is responsible for the game loop, which controls the sequence of events. Here is what it should contain:
– Number Entity
now walking step_idx
– an array of Entity
, entities
– Player number player_idx
– List of allies ally_idxes
– List of opponents enemy_idxes
– Quantity Entity
, entities_count
class Game {
entities = Array();
step_idx = -1;
player_idx = -1;
entities_count = 0;
ally_idxes = Array();
enemy_idxes = Array();
constructor(allies_count, enemies_count) {
this.entities_count = allies_count + enemies_count;
this.step_idx = 0;
this.player_idx = 0;
for (let i = 0; i < this.entities_count; i++) {
if (i < allies_count) {
// Составляем списки союзников
ally_idxes[ally_idxes.length] = i;
}
else if (i > allies_count && i < allies_count + enemies_count) {
// ... и противников
enemy_idxes[enemy_idxes.length] = i;
}
}
}
step(action) {
const entity = this.entities[step_idx];
if (action == 0) {
entity.dealDamage(target, entity.get_weapon());
}
else if (action == 1) {
entity.
}
entity.step();
this.step_count(entity);
}
step_count(entity) {
entity.steps -= 1;
if (entity.steps <= 0) {
entity.steps = entity.max_steps;
this.step_idx += 1;
if (this.step_idx >= this.entities_count) this.step_idx = 0;
}
}
}
Conclusion
Today in this article I wrote what is called a framework, if not a JavaScript game engine.
PS
I wanted to express myself too much, so I decided to write this article.