Godot
July 18, 2023

Простая утилити для упрощения поиска приоритетных целей

Bounce у Runemaster оказался очень сложным скиллом с точки зрения логики

При каждом попадании нужно искать новую цель, куда отскочить:

  • Исключая, очевидно, себя и текущего противника
  • Исключая повторные отскоки по уже задетым целям (если нет перка, который это отключает)
  • Отдавая приоритет Pulse (если есть такой перк)
  • Разрешая повторные отскоки, если камень проскочил по всем мобам, но отскоки ещё остались

Также при каждом отскоке нужно учитывать

  • Если отскок был последним, то деспавним снаряд, а затем:
    • Если попадание убило юнита, то заспавнить камень на полу
    • Если не убило, навесить статус застрявшего камня на время
      • Если юнит умрёт до окончания таймера, заспавнить камень сразу
      • Если нет, заспавнить по окончанию
  • Если отскок был не последним, то:
    • Если есть рядом стоящие доступные цели, отскакиваем дальше
    • Если нет, идём по ветке "Если отскок был последним"
  • Если снаряд ударился в стенку или слишком долго летел, он деспавнится
    • В таком случае статус на врага накладывать не нужно,

Чтобы упростить как минимум поиск приоритетных целей, пришёл к небольшой вспомогательной штуке

Какую проблему решаем?

Если реализовывать поиск приоритетных юнитов втупую, то получится многословная простыня из вложенных условий:

DPipe.new([
  Units.get_all(),
  DCondition.new({
    If = DState.get_value(should_strike_twice),
    Then = [
      filter_already_stroke,
      DCondition.new({
        If = DListMapper.has_items(),
        Then = DMapper.get_current(),
        Else = Units.get_all()
      })
    ],
    Else = Units.get_all()
  })
])

И с каждым уровнем приоритетов вложенность будет расти.
Попробуйте добавить критерий "отдавать приоритет Pulse, если есть такой перк"

Это не говоря уж о том, что сама структура неудобна к прочтению

В итоге я написал такую штуку:

PrioritizedFilter.new([
  filter_already_stroke,
  filter_all
])

Под капотом PrioritizedFilter проходится по каждому из фильтров и возвращает первый отфильтрованный список, в котором есть хотя бы один подходящий результат

То есть "либо первый (если есть), либо второй (если есть), либо "

А для включения/выключения приоритетов (см. пункт про перки), можно просто использовать условие в описании самого фильтра, которое вернёт пустой массив, когда приоритетную очередь нужно не учитывать

Конечная композиция выглядит так:

DPipe.new([
  Units.get_all(),
  # Исключаем игрока, союзных юнитов и только что атакованную цель
  filter_valid_targets,
  # В ограниченном радиусе
  filter_nearby,
  # Ещё не задетые (если есть), либо все
  PrioritizedFilter.new([
    DCondition.new({
      If = DState.get_value(should_strike_twice),
      Then = filter_all,
      Else = filter_not_already_struck
    }),
    filter_all
  ]),
  # Камень Пульса (если есть), либо все
  PrioritzedFilter.new([
    DCondition.new({
      If = DState.get_value(should_prioritize_pulse),
      Then = filter_pulse_stones,
      Else = filter_empty
    }),
    filter_all
  ])
])

В будущем для лучшего QoL сюда можно будет легко добавить отсеивание неуязвимых противников, брать в приоритет почти добитых мобов и так далее

Также в очередной раз похвалю себя за свой декларативный движок и скажу, что условия можно менять находу не только через описания условий внутри скилла, но и снаружи, если положить PrioritizedFilter в переменную

# Внутри скилла
var filter = PrioritizedFilter.new([
  filter_all
])

# Внутри перка, который добавляет "приоритет мобам с меткой"
filter.children.push_front([
  filter_with_mark
])

Такие дела


Кстати говоря, баг с тем, что иногда дюпаются камни от Bounce, я, кажется, полностью пофиксил! Нашёл аж 5 причин, почему это происходило. Самый упоротый баг за всю жизнь, пожалуй

Надеюсь, шестой не появится, тьфу-тьфу ¯\_(ツ)_/¯