Как стать автором
Обновить

Пошаговая боевая система на примере JS

Время на прочтение7 мин
Количество просмотров11K

Возможно, вы, как программист, когда-то интересовались пошаговыми стратегиями. В этой статье я решил рассказать о собственном взгляде на эту тему, используя 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, а также не добавил некоторые пояснения. Теперь всё исправлено.

Теги:
Хабы:
Всего голосов 11: ↑8 и ↓3+8
Комментарии8

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань