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

Задача с двумя исходами

«Доставь пакет — или умри в попытке». Одна задача описывает, что должно случиться, и что её провалит. Что сработает первым — то и засчитается.

В JSON это выражается одновременным заполнением condition.success и condition.failure. Провал одной обязательной задачи мгновенно завершает весь квест статусом failure — это мощный нарративный приём, которым авторы часто не пользуются просто потому, что не задумываются.


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

  • Драматическое напряжение. Игрок знает, что может проиграть — и это меняет восприятие любой механики.
  • Сюжетные развилки через провал. На on.failure можно повесить открытие тёмной ветки истории, потерю NPC, отрезание награды — а на on.success светлой.
  • Миссии на время и эскорт. Канонические RPG-шаблоны делаются ровно так.

Шаг 1. Создаём квест с двумя условиями

data/story/quests/delivery.json:

{
  "version": 3,
  "variant": 0,
  "title": "Доставка пакета",
  "description": "Курьер должен дойти до условленного места, не потеряв груз.",
  "tasks": {
    "deliver_package": {
      "title": "Доставить пакет",
      "condition": {
        "success": { "type": "predicate", "predicate": "story:reached_destination" },
        "failure": { "type": "predicate", "predicate": "story:package_destroyed" }
      }
    }
  },
  "stages": [["deliver_package"]]
}

Каждый тик InkQuest проверяет оба условия. Первое сработавшее победит: - Если reached_destination — задача завершается success, квест получит success. - Если package_destroyed — задача завершается failure, квест немедленно получит failure и закроется.


Шаг 2. Заводим scoreboard-флаги, которые проверяют предикаты

Самый прозрачный способ описать «событие в мире» для условия — через scoreboard-флаг игрока. Командные блоки в нужных местах мира ставят флаг, а предикат проверяет его наличие.

data/story/functions/init_flags.mcfunction (вызови один раз через #minecraft:load или из стартовой функции):

scoreboard objectives add story.delivery_arrived dummy
scoreboard objectives add story.delivery_lost dummy

В мире:

  • В сундуке-цели поставь командный блок: scoreboard players set @p story.delivery_arrived 1 (срабатывает, когда игрок открывает сундук — например, по сигналу с trapped chest).
  • К предмету «пакет» привяжи detection: при выпадении из инвентаря или при смерти — scoreboard players set @p story.delivery_lost 1.

Шаг 3. Описываем предикаты

data/story/predicates/reached_destination.json:

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

data/story/predicates/package_destroyed.json:

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

entity_scores проверяет, что у игрока ("entity": "this") одноимённый score >= 1. Поднимать флаг → условие сработает на следующем тике.


Шаг 4. Добавляем реакции на оба исхода

Хуки on.success и on.failure — место для нарративной реакции. Дополни задачу в delivery.json:

"deliver_package": {
  "title": "Доставить пакет",
  "condition": {
    "success": { "type": "predicate", "predicate": "story:reached_destination" },
    "failure": { "type": "predicate", "predicate": "story:package_destroyed" }
  },
  "on": {
    "success": {
      "functions": ["story:delivery/reward"],
      "tags": ["story.delivery_done"]
    },
    "failure": {
      "functions": ["story:delivery/aftermath"],
      "tags": ["story.delivery_failed"]
    }
  }
}

Функции:

data/story/functions/delivery/reward.mcfunction:

give @s minecraft:emerald 16
tellraw @s {"text":"Заказчик доволен. Серебро в кармане.","color":"gold"}

data/story/functions/delivery/aftermath.mcfunction:

tellraw @s {"text":"Пакет погиб вместе с надеждой на оплату.","color":"dark_red"}

Теги — это «память» исхода: последующие квесты могут открыться только для тех, кто доставил (или, наоборот, провалил) задание. См. Развилка и память выбора.


Проверка

  1. Выдай квест: /quest give @s story:delivery.
  2. Вручную подними флаг провала: /scoreboard players set @s story.delivery_lost 1.
  3. В книге квест должен сразу уйти в «Завершённые → failure» (а не остаться активным). ✅
  4. Сбрось: /scoreboard players reset @s story.delivery_lost и перевыдай квест (после /quest drop).
  5. Подними флаг успеха: /scoreboard players set @s story.delivery_arrived 1 — квест должен закрыться success.

Если оба предиката сработали в один тик — success имеет приоритет (он проверяется первым). Но в реальной игре такое почти не случается.


Шаблон A.1 — миссия на время

Один из самых частых случаев: «успей за N минут, иначе провал».

data/story/quests/mansion.json:

{
  "version": 3,
  "variant": 0,
  "title": "Спасти горящую усадьбу",
  "description": "Огонь распространяется. У тебя одна минута.",
  "tasks": {
    "extinguish": {
      "title": "Потушить очаги пожара",
      "condition": {
        "success": { "type": "predicate", "predicate": "story:all_fires_out" },
        "failure": {
          "type": "score",
          "objective": "mansion_timer",
          "from": 1200,
          "to": 0
        }
      },
      "on": {
        "tick": {
          "functions": ["story:mansion/tick_timer"]
        }
      }
    }
  },
  "stages": [["extinguish"]]
}

data/story/functions/mansion/tick_timer.mcfunction:

scoreboard players remove @s mansion_timer 1

data/story/predicates/all_fires_out.json — предикат проверяет, что в мире не осталось ни одного блока огня в зоне усадьбы. В ванильных предикатах нет прямой проверки «нет такого блока», но можно проверить флаг, который ставит тиковая функция-наблюдатель:

{
  "condition": "minecraft:value_check",
  "value": {
    "type": "minecraft:score",
    "target": { "type": "minecraft:fixed", "name": "MANSION" },
    "score": "mansion_fires_left",
    "scale": 1.0
  },
  "range": 0
}

Этот предикат сравнивает score MANSION mansion_fires_left с нулём. Тиковая функция-наблюдатель пересчитывает огни:

data/story/functions/mansion/count_fires.mcfunction (зарегистрируй в #minecraft:tick):

execute store result score MANSION mansion_fires_left if blocks 100 64 100 110 70 110 100 64 100 all

(Команда упрощённая; реальная проверка зон сложнее — суть в том, что value_check сравнивает с фиксированным числом.)

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

  • from: 1200 — при загрузке задачи в scoreboard запишется 1200 (минута игрового времени = 1200 тиков).
  • to: 0 — условие сработает, когда счёт опустится до 0. Поскольку from > to, условие читается как нисходящее (см. Условия выполнения задачи).
  • В HUD игрок видит прогресс-бар, убывающий каждый тик.
  • Если успел потушить — success. Если не успел — таймер обнулился, failure.

Подводные камни таймера:

  • Офлайн = пауза. Когда игрок выходит, on.tick не работает — таймер замораживается. Это можно подать как фичу («часы идут, пока ты в игре»), но игроки иногда воспринимают это как чит. Если для тебя критично «жёсткое» время — используй on.pinned_tick (тикает только пока квест закреплён) или ванильный minecraft:tick с тегом игрока.
  • Не клади тяжёлые функции в on.tick. Эта функция вызывается каждый тик у каждого игрока с активной задачей. scoreboard remove дешёвый, а вот summon или fill в on.tick — катастрофа.
  • При смене этапа таймер сбрасывается заново. Поле from сработает при каждой загрузке задачи (при reset: true, что дефолт): если задача попадает в активный этап повторно (после unload), счёт будет переписан. Для одноразовых таймеров это нормально; для накопительных поставь reset: false и инициализируй счёт в on.load-функции с проверкой execute unless score ....

Шаблон A.2 — эскорт NPC

«Защити NPC, пока он идёт до точки. Если умрёт — миссия провалена.»

data/story/quests/escort.json:

{
  "version": 3,
  "variant": 0,
  "title": "Сопровождение торговца",
  "description": "Проведи торговца до таверны живым.",
  "tasks": {
    "escort": {
      "title": "Сопроводить торговца к таверне",
      "condition": {
        "success": { "type": "predicate", "predicate": "story:merchant_at_tavern" },
        "failure": { "type": "predicate", "predicate": "story:merchant_dead" }
      },
      "on": {
        "failure": {
          "functions": ["story:escort/aftermath_dead_merchant"]
        }
      }
    }
  },
  "stages": [["escort"]]
}

NPC живёт на сервере как обычный mob с тегом — например, tag MERCHANT. Заранее заведи общий scoreboard:

data/story/functions/escort/init.mcfunction:

scoreboard objectives add merchant_arrived dummy
scoreboard objectives add merchant_dead dummy
scoreboard players set GLOBAL merchant_arrived 0
scoreboard players set GLOBAL merchant_dead 0

В таверне поставь повторяющийся командный блок:

execute if entity @e[tag=MERCHANT,distance=..3] run scoreboard players set GLOBAL merchant_arrived 1

Тиковая функция-наблюдатель ставит флаг смерти:

data/story/functions/escort/watch_merchant.mcfunction#minecraft:tick):

execute unless entity @e[tag=MERCHANT] run scoreboard players set GLOBAL merchant_dead 1

data/story/predicates/merchant_at_tavern.json:

{
  "condition": "minecraft:value_check",
  "value": {
    "type": "minecraft:score",
    "target": { "type": "minecraft:fixed", "name": "GLOBAL" },
    "score": "merchant_arrived",
    "scale": 1.0
  },
  "range": { "min": 1 }
}

data/story/predicates/merchant_dead.json:

{
  "condition": "minecraft:value_check",
  "value": {
    "type": "minecraft:score",
    "target": { "type": "minecraft:fixed", "name": "GLOBAL" },
    "score": "merchant_dead",
    "scale": 1.0
  },
  "range": { "min": 1 }
}

data/story/functions/escort/aftermath_dead_merchant.mcfunction:

tellraw @s {"text":"Торговец мёртв. Возвращайся ни с чем.","color":"dark_red"}
tag @s add story.escort_failed

Подводные камни эскорта:

  • NPC должен быть в forced-loaded chunks — иначе при выходе игрока из зоны прогрузки NPC «исчезнет», и тиковая функция увидит «нет сущности» = ошибочный провал. Используй forceload add на зону маршрута.
  • Флаг — глобальный, провал прилетит всем. Если эскорт коллективный, это нужно. Если у каждого игрока должен быть собственный эскорт — заводи отдельные NPC и отдельные scoreboard-флаги на игрока (например, через scoreboard players set @s merchant_arrived 1 и entity_scores в предикате).

Расширения

Условие провала через failure: optionals

Если квест должен провалиться, когда optional-задача этапа провалена — используй optionals-условие. Предполагается, что protect_ally — единственная optional в этапе:

"final_battle": {
  "title": "Сразить дракона",
  "condition": {
    "success": { "type": "predicate", "predicate": "story:dragon_dead" },
    "failure": {
      "type": "optionals",
      "status": "failure",
      "min": 1
    }
  }
}

data/story/predicates/dragon_dead.json:

{
  "condition": "minecraft:value_check",
  "value": {
    "type": "minecraft:score",
    "target": { "type": "minecraft:fixed", "name": "GLOBAL" },
    "score": "story.dragon_killed",
    "scale": 1.0
  },
  "range": { "min": 1 }
}

Флаг ставит командный блок при смерти дракона (например, через execute as @e[type=ender_dragon] store success score GLOBAL story.dragon_killed run … или из advancement-rewards). Если protect_ally провалена — final_battle мгновенно провалится тоже, и квест уйдёт в failure.

Цепочка из провалов

on.failure может выдать другой квест:

data/story/functions/delivery/open_dark_branch.mcfunction:

quest give @s story:dark_branch

И повесь её на on.failure исходной задачи. Так из провала первой главы открывается тёмная ветка — пока остальные игроки идут по светлой.

Тихая отмена через condition.failure

Если задача должна молча сняться без записи в «завершённые» — используй condition.failure с on.failure без видимых эффектов. Квест уйдёт в архив со статусом failure, но игрок ничего не заметит — кроме того, что зависимые квесты (с after: [["story:that_quest"]]) больше не откроются.


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

  • failure обязательной задачи = failure квеста, немедленно. Это не «глобальная проверка раз в N тиков», а атомарная операция. Опциональные задачи в этот момент тоже завершатся (как unload), даже если их условие ещё не сработало.
  • condition.success и condition.failure проверяются одновременно каждый тик. Если оба сработают в один тик — побеждает success. Если важно гарантировать failure — убедись, что условие успеха недостижимо одновременно с условием провала (например, через предикаты, проверяющие взаимоисключающие состояния мира).
  • on.failure сработает только один раз. Это терминальный хук. Если хочешь логировать каждую попытку — используй on.tick, а не повторный failure.
  • Опциональные задачи с condition.failure не валят квест. Только провал обязательной (первой в этапе) задачи завершает квест. Провал опциональной просто помечает её как failure и не влияет на исход. Это удобно: можно дать игроку «штрафную» опциональную задачу, которая отрезает дополнительную награду, но не убивает основную линию.

См. также