Задача с двумя исходами¶
«Доставь пакет — или умри в попытке». Одна задача описывает, что должно случиться, и что её провалит. Что сработает первым — то и засчитается.
В 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:
Теги — это «память» исхода: последующие квесты могут открыться только для тех, кто доставил (или, наоборот, провалил) задание. См. Развилка и память выбора.
Проверка¶
- Выдай квест:
/quest give @s story:delivery. - Вручную подними флаг провала:
/scoreboard players set @s story.delivery_lost 1. - В книге квест должен сразу уйти в «Завершённые → failure» (а не остаться активным). ✅
- Сбрось:
/scoreboard players reset @s story.delivery_lostи перевыдай квест (после/quest drop). - Подними флаг успеха:
/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:
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):
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:
И повесь её на 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и не влияет на исход. Это удобно: можно дать игроку «штрафную» опциональную задачу, которая отрезает дополнительную награду, но не убивает основную линию.
См. также¶
- Условия выполнения задачи — полная документация по типам условий
- Хуки жизненного цикла задачи — про
on.success,on.failure,on.tick - Статусы квестов и задач — как
failureобязательной задачи влияет на итог квеста - Развилка и память выбора — как зафиксировать исход в теге и развести сюжет