Возможно, вы, как программист, когда-то интересовались пошаговыми стратегиями. В этой статье я решил рассказать о собственном взгляде на эту тему, используя JavaScript
Кто заинтересован - добро пожаловать под кат
Всем привет, это stalker320, я отсутствовал какое-то время и только вернулся из спячки.
Для начала поставим задачи, которые позволят решить что нам нужно разработать.
Класс, который будет обрабатывать игровые события. Допустим, он будет называться Game;
Класс, ответственный за создание игрока и моба, класс Entity;
Класс инвентаря;
Класс предмета;
И напоследок класс действующего эффекта.
Класс Effect
Начинать нужно с того элемента, который задействует наименьшее количество упоминаний других элементов. В нашем случае это класс Effect
и у него будет два свойства name
и 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;
}
}
Данный класс ничего сам по себе не делает, но закладывает потенциал на будущее.
Теперь мы можем можем создать различные вариации эффектов:
// Кровотечение
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;
}
}
А ещё более интересным методом создания эффектов является создание на месте.
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 Item {
constructor(name) {
this.name = name;
}
get_name() {
return this.name;
}
}
Сам класс Item
нам ничего не даёт, но является отправной точкой для других классов. В качестве примера я приведу ещё несколько классов, наследующихся от 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;
}
}
Класс Weapon
имеет два свойства в дополнение к свойствам Item
. Это свойства damage и effect.
class Shield extends Item {
constructor(name, armor) {
super(name);
this.armor = armor;
}
get_shield() {
return this.armor;
}
}
Щит, позволяет пропустить некоторое количество урона мимо полосы здоровья
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));
}
}
А также тонны различных видов зелий, накладывающих соответствующие эффекты на персонажа при применении.
Класс Inventory
Простой контейнер для массива, ограничивающий входящий тип данных и размер.
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
Этот класс наверное второй по важности класс. Он отвечает за всех мобов и игрока, созданных в дальнейшем.
У него будет не особо много свойств, но они будут уместными
- максимальное здоровье max_health
- текущее здоровье health
- Щит shield
- инвентарь от Inventory
inventory
- эффекты effects
- очки действий steps
- максимум очков действий 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 = null) {
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;
}
}
Надеюсь, что комментарии излишни, но если какие-то момент не понятны, не стесняйтесь, дорогие читатели, уточнять непонятные или неверные элементы в комментариях.
Класс Game
Самый главный элемент в проекте. Он отвечает за игровой цикл, который управляет последовательностью событий. Вот что он должен содержать:
- Номер Entity
, сейчас ходящего, step_idx
- массив из Entity
, entities
- Номер игрока player_idx
- Список союзников ally_idxes
- Список противников enemy_idxes
- Количество 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, target_id = -1) {
const entity = this.entities[step_idx];
let target;
if (target_id >= 0) {
target = this.entities[target_id];
}
else {
let lowest_health;
let t_idx = -1;
if(this.ally_idxes.includes(this.step_idx)) {
lowest_health = this.entities[this.enemy_idxes[0]].health;
for (let i = 1; i < this.enemy_indexes.length; i++) {
if (this.entities[this.enemy_indexes[i]].is_alive() &&
this.entities[this.enemy_indexes[i]].health < lowest_health
) {
lowest_health = this.entities[this.enemy_idxes[i]].health;
t_idx = this.enemy_idxes[i];
}
}
}
else if (this.enemy_idxes.includes(this.step_idx)) {
lowest_health = this.entities[this.ally_idxes[0]].health;
for (let i = 1; i < this.ally_indexes.length; i++) {
if (this.get_entity(this.ally_indexes[i]).is_alive() &&
this.get_entity(this.ally_indexes[i]).health < lowest_health
) {
lowest_health = this.get_entity(this.ally_indexes[i]).health;
t_idx = this.ally_indexes[i];
}
}
}
target = this.entities[t_idx];
}
if (action === 0) {
entity.deal_damage(target, entity.get_weapon());
}
else if (action === 1) {
entity.setup_shield(entity.get_weapon().get_armor(), entity.get_weapon());
}
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;
}
}
}
А теперь немного пояснений:
В данном классе реализован выбор ходящего посредством массива самих сущностей и инкремента через step_count
. Изменение и расширение step_count позволит изменить порядок ходов до желаемого.
Метод step
выполняет просчёт действий сущности и рекомендую вызывать его через while (playing) { step(); }
, или SetInterval(()=>step(), 1000)
.
Заключение
Сегодня в этой статье я написал, так называемый игровой движок на JavaScript, хотя впереди ещё много работы, чтобы обернуть его в html.
P. S.
Мне слишком сильно хотелось высказаться, поэтому я решил написать эту статью.
UPD 20.10.2022
Я заметил, что не дописал класс Game, а также не добавил некоторые пояснения. Теперь всё исправлено.