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