Перейти к содержанию

Развилка и память выбора

Нелинейная история: «выбери сторону», «кого спасти», «как ответить». Выбор должен закрыть текущий квест и повлиять на дальнейшие.

InkQuest строит ветвление без специальных полей. Хватает трёх примитивов: условие optionals, опциональные задачи и scoreboard-теги. Вместе они образуют полноценный state-machine.


Зачем нужен этот паттерн

  • Нелинейный сюжет без новой системы. Никаких «диалоговых деревьев» — всё стандартными JSON.
  • Память выбора переживает релог, рестарт, перезагрузку датапаков. Теги хранятся в данных игрока, не зависят от состояния квеста.
  • Статистика «куда пошли игроки». Достаточно посчитать игроков с тем или иным тегом.

Паттерн делится на две части: развилка внутри одного квеста (выбор закрывает квест) и память выбора для следующих квестов (тег открывает или скрывает контент).


Шаг 1. Развилка внутри этапа

Этап содержит обязательную задачу с условием optionals и несколько опциональных задач. Игрок выполняет одну опциональную — обязательная закрывается сама, этап считается пройденным.

{
  "version": 3,
  "variant": 0,
  "title": "Разговор у костра",
  "description": "Старик ждёт, что я отвечу. Каждое слово — поворот.",
  "tasks": {
    "speak": {
      "title": "Сделать выбор",
      "description": "Один ответ закроет разговор.",
      "condition": {
        "success": {
          "type": "optionals",
          "status": "success",
          "min": 1
        }
      }
    },
    "answer_truth": {
      "title": "Сказать правду",
      "buttons": ["success"],
      "on": {
        "success": { "tags": ["story.ch1.path_truth"] }
      }
    },
    "answer_lie": {
      "title": "Соврать",
      "buttons": ["success"],
      "on": {
        "success": { "tags": ["story.ch1.path_lie"] }
      }
    },
    "stay_silent": {
      "title": "Промолчать",
      "buttons": ["success"],
      "on": {
        "success": { "tags": ["story.ch1.path_silence"] }
      }
    }
  },
  "stages": [
    ["speak", "answer_truth", "answer_lie", "stay_silent"]
  ]
}

Как это работает

  • Обязательная speak — это «детектор выбора». Условие optionals смотрит на optional-задачи активного этапа, кроме самой себя — то есть автоматически на все три варианта. min: 1 означает «достаточно одной с успехом».
  • Опциональные answer_truth/lie/silence — это сами варианты выбора. У каждой кнопка success в книге; клик — задача завершается, ставится свой тег.
  • Как только игрок жмёт любой ответ — на следующем тике условие speak становится истинным, она завершается, этап заканчивается, квест уходит в success.

Кнопки buttons: ["success"] — самый простой способ дать игроку явный выбор. Если выбор должен происходить через действие в мире (нажал на табличку, поговорил с NPC) — замени buttons на condition.success с predicate.

Что будет с невыбранными опциональными

Останутся в статусе active навсегда (этап уже закрыт, в новый их никто не пустит). Игрок их не увидит ни в HUD, ни в книге — туда попадают только задачи активного этапа. Закрывать их искусственно через /quest complete ... skip не нужно — это штатное поведение системы.


Шаг 2. Проверка развилки

  1. Выдай квест: /quest give @s story:ch1_fireside.
  2. Открой книгу — увидишь обязательную speak (без кнопок) и три опциональных с кнопками Success.
  3. Нажми одну — квест уйдёт в «Завершённые → success».
  4. Проверь, что тег появился: /tag @s list — должен быть, например, story.ch1.path_truth.

Шаг 3. Память выбора в следующих квестах

Теперь выбор существует только как тег у игрока. Чтобы он повлиял на дальнейший сюжет — используй require.tags в зависимых квестах.

{
  "version": 3,
  "variant": 0,
  "title": "Возвращение к старику",
  "description": "Прошло несколько дней. Костёр всё ещё горит.",
  "after": [["story:ch1_fireside"]],
  "require": {
    "tags": ["story.ch1.path_truth"]
  },
  "tasks": {
    "talk_again": { "title": "Поговорить со стариком" }
  },
  "stages": [["talk_again"]]
}

Этот квест выдастся только тем, кто в первом квесте выбрал ветку правды. Для остальных создавай отдельные квесты-продолжения с другими require.tags. В data/story/quests/ch1_lie_branch.json:

"require": { "tags": ["story.ch1.path_lie"] }

В data/story/quests/ch1_silence_branch.json:

"require": { "tags": ["story.ch1.path_silence"] }

После завершения первого квеста InkQuest посмотрит на теги игрока и выдаст ровно тот вариант продолжения, который ему подходит.


Важный момент про момент проверки

require оценивается в момент завершения зависимости из after — то есть в тот тик, когда игрок успешно прошёл предыдущий квест.

Что это значит на практике:

  • Если ты ставишь тег в on.success финальной задачи (как в шаге 1) — порядок гарантирован: тег появляется ДО проверки require зависимых квестов. ✅
  • Если игрок получит тег позже (через ивент мира, через другой квест, через tag @s add) — новый квест не разблокируется автоматически. Это намеренно: атомарность ветвления.

Практический вывод: всегда ставь теги ветвления внутри хука on.success той задачи, что является последней обязательной в зависимости. Тогда тег попадёт в скорборд в том же тике, что и завершение квеста.


Гигиена тегов

Теги — глобальный пул для игрока. Если два разных квеста используют тег secret_choice — они конфликтуют. Префиксуй теги пространством имён, как пути функций:

Плохо Хорошо
truth_chosen story.ch1.path_truth
helped_npc npc.merchant.helped
boss_killed world.boss.lich_killed

Точка как разделитель работает в командах: @a[tag=story.ch1.path_truth]. Никаких особенностей нет, это просто строка.


Расширения

Развилка через действие в мире (без кнопок)

Если выбор должен быть «органичным» — игрок ударил по табличке, выпил зелье, надел шлем — замени buttons на условие predicate:

"answer_truth": {
  "title": "Сказать правду",
  "condition": {
    "success": { "type": "predicate", "predicate": "story:answered_truth" }
  },
  "on": {
    "success": { "tags": ["story.ch1.path_truth"] }
  }
}

data/story/predicates/answered_truth.json — проверка scoreboard-флага игрока, который ставит командный блок у нужной таблички или NPC:

{
  "condition": "minecraft:entity_scores",
  "entity": "this",
  "scores": {
    "story.ch1.answered_truth": { "min": 1 }
  }
}

Заранее заведи objective: scoreboard objectives add story.ch1.answered_truth dummy. Командный блок у нужной таблички: scoreboard players set @p story.ch1.answered_truth 1.

Множественный выбор (count > 1)

Если игрок должен сделать несколько выборов перед закрытием этапа — измени count:

"speak": {
  "title": "Сделать три выбора",
  "condition": {
    "success": { "type": "optionals", "status": "success", "min": 3 }
  }
}

В HUD появится прогресс-бар «0/3», «1/3» и т.д.

Развилка с провалом

Один из ответов может быть «неправильным» — он провалит квест:

"answer_lie": {
  "title": "Соврать",
  "buttons": ["failure"],
  "on": {
    "failure": { "tags": ["story.ch1.path_lie"] }
  }
}

Теперь кнопка под «Соврать» — Failure. Клик → задача завершается failure → квест немедленно уйдёт в failure (поскольку условие optionals ловит любой терминальный статус по умолчанию, но если хотим именно провал — добавь "status": "failure" в условии speak). См. Задача с двумя исходами про эту логику.

Статистика выборов

Хочешь знать, сколько игроков пошли по каждой ветке? Подсчитай теги:

execute store result score #stats global_path_truth if entity @a[tag=story.ch1.path_truth]
execute store result score #stats global_path_lie if entity @a[tag=story.ch1.path_lie]
execute store result score #stats global_path_silence if entity @a[tag=story.ch1.path_silence]

И выведи на scoreboard sidebar:

scoreboard objectives setdisplay sidebar global

«Невыбранное» как явная ветка

Если хочешь обработать сценарий «игрок не выбрал вообще» (например, прошло время) — добавь condition.failure к speak:

"speak": {
  "condition": {
    "success": { "type": "optionals", "status": "success", "min": 1 },
    "failure": { "type": "score", "objective": "ch1_timer", "from": 6000, "to": 0 }
  },
  "on": {
    "failure": { "tags": ["story.ch1.path_default"] }
  }
}

5 минут на размышление, иначе «по умолчанию». Это уже комбинация с Задача с двумя исходами.


Подводные камни

  • Теги не сбрасываются автоматически. Если ты перезапускаешь сюжет («новая игра+»), теги старого прохождения остаются. Убирай их явно: tag @s remove story.ch1.path_truth в стартовой функции.
  • /quest drop не удаляет теги. Они живут отдельно от данных квеста. Если игроку нужно «переиграть выбор» — сначала сними тег, потом перевыдай квест (или сделай его repeatable).
  • Развилки в InkQuest — не отдельная фича, а композиция. Если что-то не работает — ищи ошибку в формате задач/условий/тегов, а не в системе ветвления.
  • Невыбранное — не «потеряно», а «не пройдено». Файлы квеста остаются нетронутыми, опциональные просто остаются с пустым статусом. Это удобно для статистики, но если тебе важно явно «закрыть» их — добавь обязательной задаче хук on.success, который проставит тег default для невыбранных путей.

См. также