Закоулки мозга

Собственный движок для текстовых игр-историй Собственный движок для текстовых игр-историй Описание мира игры FATE Описание мира игры FATE

Доски в trello: Механика и система

Минимальный продукт - предустановленный персонаж, две локации, один конфликт. Игрок может переходить между локациями, выбирать действия и вступить в конфликт.

Инвентарь?

Можно использовать для начала редакцию FATE Accelerated Edition, она предусматривает меньшее количество сущностей (навыков, например, нет), поэтому должна быть немного проще в написании.

Для Javascript нужна типизация (https://www.sitepoint.com/writing-better-javascript-with-flow/, https://codeburst.io/getting-started-with-flow-and-nodejs-b8442d3d2e57) и документация https://medium.com/@kevinast/integrate-gitbook-jsdoc-974be8df6fb3

Итого план работ такой:

  1. Создать объект Персонажа
  2. Загрузить описания Локаций и граф переходов

ADL и концепция “всё - это объекты”

Вообще, ADL стоит изучить подробнее, похоже, что многие из нужных мне концепций в нем реализованы. Большей частью, конечно, там говорится о парсинге ввода с клавиатуры и о глаголах/объектах/прилагательных, которых у меня в движке не будет.

Главная особенность ADL в том, что всё в игре представлено в виде изоморфных Объектов. Т.е., как они сами пишут, игрок может подобрать объект, переместить его в другую локацию, там бросить и войти в него как в локацию. Каждый объект может быть контейнером для других объектов и сам содержаться в контейнере. Для каждого объекта определены процедуры PRE-ACTION и ACTION, которые выполняются перед и в момент совершения действия над этим объектом, именно эти процедуры определяют, что можно сделать с объектом, а что нельзя.

Ну, в случае interactive fiction, где игрок просто по различным условиям переходит от одного фрагмента текста к другому, для Локаций нужна другая управляющая структура, их в виде объектов представлять никак нельзя.

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

Тогда получается, что переходы между локациями не нужно формулировать, как действия игрока, т.е. не рассматривать их в массиве Actions. Это должен быть обособленный массив Movements. При этом некоторые передвижения должны быть доступны безусловно, некоторые открываются на основании условия, а некоторые требуют броска преодоления и их дальнейшая судьба зависит от этого броска.

Обдумать!!! Локации и сцены

[@Ford2016]: Отличное предложение использовать уточняющие пассажи, т.е. ответвления от основной ветки сюжета, описывающие дополнительную деталь локации и тут же возвращающие игрока в основную ветку. Их можно использовать для описания деталей мира, не влияющих на основной сюжет.

В общем, прихожу к тому, что игроку периодически будут показываться относительно длинные куски текста, описывающие текущую Сцену. Сцена может состоять из нескольких отдельных Локаций, внутри Сцены перемещение по Локациям выполняется простыми командами “идти туда, идти сюда”. Такие перемещения должны быть всегда доступны и не добавляют ничего нового к описанию сцены. А может быть одной Локацией. В общем, даже если это одно и то же физическое место, но посещенное в разное “сюжетное” время, то это разные Сцены, у них разное описание и разные возможности для взаимодействия.

Ну, блин, этак я от первоначальной идеи движка уйду куда-то в сторону, и это не очень хорошо. Давай-ка к первоначальной концепции.

Процессы, за которые отвечает движок

  • Формирование полного описания, выводимого пользователю - нужно собрать условные описания из локации, описание завершенного пользователем действия, описание реакции системы на это действие, если таковое имеется (отдельно от описания новой локации).
  • Формирование полного набора изменений состояния игры по результатам действия игрока
  • Формирование полного набора изменений состояния игры по результатам действия игры

Вероятно, на основе набора изменений состояния игры и будет формироваться итоговое описание хода. Скорее всего. Наверное.

Формирование описания, выводимого пользователю

Самая простая ситуация - обычное описание локации, переход в которую произошел в результате действия без описания.

В целом, я бы рассмотрел вариант какого-то обхода объектов, хранящих либо фрагменты описаний, либо какие-то условия для их отображения. В языке ADL для этого используется специальный демон Looker, который выполняет сбор описаний и их представление игроку в начале каждого хода.

Сущности игры, с которыми работает движок:

  • Локация
  • Аспект
  • Действие
  • Ситуация
  • Эффект
  • Персонаж
  • Последствие
  • Мир
  • Игрок
  • Стресс-трек
  • Условие(?)

Локация

Локация - это верхнеуровневый объект игры, содержащий в себе любые другие объекты (NPC, действия, Аспекты и т.п.)

Что-то как-то резко усложняются правила перехода между локациями, придется как-то хранить данные об условиях разных переходов (видимо, в самой Локации), а также о том, какие варианты уже были испробованы, и что изменилось в результате.

Вычитал, что в старых текстовых квестах для каждой Комнаты (Локации) хранилось два описания - длинное, которое выводилось при первом входе в комнату, и короткое, которое выводилось при возвращении в уже пройденную локацию. Мне реально придется для каждой локации создавать несколько описаний, зависящих от состояния мира.

Структура Локации

{
	"id": "01",
	"description": "Lorem ipsum...",
	"actions": [{"id":"action01-01"}, {"id":"action01-02"}],
	"movements": [{"id": "movementTo", "condition": "some condition", "destination": "newLocationId", "outcome":"generic outcome"}],
	"aspects": [{
		"id":"aspect01-01",
		"visibility":"(visible|hidden|gone)",
		"description": "to be added to location description"
	}],
	"npcs":[{"npc objects???"}]
}

LocationsPlugin

Отвечает за отображение описания локации и переход игрока между локациями. Его первоочередная задача - получить полный список всех локаций (из БД, json-файла, интернета, неважно).

Метод update:

  • [x] Принимает на вход номер текущей локации (она будет храниться в gameState).
  • [x] Находит в своем массиве указанную локацию и извлекает ее описание + базовый список доступных действий.
  • [ ] Проверить соблюдение дополнительных условий для отображения (сокрытия) описаний или действий, изменить набор описаний и действий. Это выполняется на основании данных в worldState?
  • [x] Вернуть итоговый объект Location в ядро движка для его дальнейшей обработки.

Эффект

Аспект

Некоторая характеристика практически любой сущности в игре, например, персонажа или локации. Каждый аспект может быть вызван (invoke) в определенной ситуации за определенную цену (1 очко судьбы) для получения какого-либо бонуса. Также аспект может повлиять на ход событий, усложняя жизнь персонажей (compel - момент определяется движком игры). Игрок может отказаться от усложнения, заплатив 1 очко судьбы, а может принять его и получить очко судьбы таким образом.

Трюки (stunts) можно тоже представить в виде Аспектов, применимых к действиям (action).

Что нужно для моделирования Аспектов?

Условия для применения Аспектов

Возможно, стоит учитывать различные условия для применения Аспекта в виде массива.

Тогда первой характеристикой будет активная фаза игры (phase), в которой возможен вызов Аспекта. Возможные варианты (выбрать только один):

  • game - Аспект игры в целом. В оригинальной механике доступен всем игрокам всегда, в движке, возможно, стоит его ограничить какими-то дополнительными условиями.
  • location - использование Аспекта в конкретной локации для поиска имеющихся / создания новых Аспектов Локации; вероятно, имеет смысл показывать не всегда, а с установленной вероятностью, либо в тех местах, где от обнаружения Аспектов зависят ветки сюжета.
  • situation - временный Аспект, может быть прикреплен к локации или сцене, а также к сопернику, если против него было удачно создано преимущество
  • attack - Аспект может быть использован при атаке в бою, требуется выполнение дополнительных условий
  • defense - Аспект может быть использован при защите в бою, требуется выполнение дополнительных условий
  • overcome - Аспект может быть использован при преодолении препятствий
  • advantage - Аспект может быть использован при создании преимущества
  • npc - Аспект может быть использован при общении / взаимодействии с NPC

Второй характеристикой (возможно, необязательной, актуальной только для четырех действий) будет skill - навык, при использовании которого можно вызвать Аспект.

Третьей характеристикой можно сделать вероятность или частоту применения Аспекта (насколько часто отображать вариант с Аспектом в возможных вариантах выбора для персонажа).

Четвертая характеристика - лимит использований. fate - ограничен очками судьбы, 0 - не ограничен, positive number + scale - количество использований (positive number) в течение определенного сегмента игры (scale): боя, ситуации, сцены, сценария и т.п.

Условием возникновения также может быть определенное состояние мира игры / любой другой сущности. Т.е., нужно дополнительное поле для хранения таких инициирующих состояний.

Эффект от применения Аспекта. Актуально только для compel, потому что для положительного применения игрок сам должен решить, каким будет эффект. Надо обдумать, каким образом задавать то, на что повлияет Аспект.

Объект aspectUsageConditions:

{
  "phase": "(game | location | attack | defense | overcome | advantage | npc)",
  "skill": [],
  "probability": "(high | medium | low | always)",
  "useLimit": "(fate | 0 | positive number + scale)",
  "condition": "entityId has aspectId | entityId has conditionId",
  "compelEffect": ""
}

Структура объекта Аспекта

{
  "title": "Unfamous girl with sword",
  "description": "To be included in Location description",
  "appliesTo": {
    "entity": "(character | game | location | situation | npc | action)",
    "conditions": ["male", "female", "..."]
  },
  "invoke": ["aspectUsageConditions", "aspectUsageConditions"],
  "compel": ["aspectUsageConditions", "aspectUsageConditions"],
  "passiveEffect": ["effect", "effect"]
}

Действие

На выбор игроку всегда будут предложены действия. Так что у каждого действия должно быть описание (callToAction), требование к уровню навыка (если это действие, требующее броска кубика). Не очень пока понятно, каким образом предлагать действия с разными навыками в бою… ведь возможны всякие варианты вроде “переместиться”, “атаковать мечом”, “атаковать из лука”, “атаковать магией” и т.п.

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

Структура действия

{
	"id":"action01-01",
	"type": "(move|invoke|overcome|attack|defend|advantage|search)",
	"description":"Some detailed dexcription for this action",
	"condition":"some condition to evaluate, shows action availability",
	"callToAction":"Short, to be shown on a button",
	"dice": {
		"opposition": {"skill": "Athletics", "level": "+2"},
		"failure": {
			"description":"Lorem ipsum",
			"outcome": {"expression that describes world/location/player change"},
			"nextAction": {"actionId"}
		},
		"tie": { "" },
		"succeed": { "" },
		"style": { "" },
	},
	"destination":"move actions only"
}

Обработка действия движком выполняется следующим образом:

    // Validate action
    if (action.validate(this)) {
        val changes = action.act(this)
        // Validate changes (if they are applicable), remove invalid ones
        // Choose and apply reaction (dice rolls, surprises, enemy actions)
        // Generate description object
        // Apply state changes (via repositories)
    } else {
        // Generate error description object
    }

    // Make ActionResultListener render the description
    presentDescription(descriptionObject)

Наверное, в gameState придется еще и текущее действие сохранять, для того, чтобы движок мог обработать корректно последовательности действий, связанные с бросками кубиков, получением сюрпризов и т.п.

Реально action.act(this) может возвращать список строк с изменениями в объекты-состояния. На основании этого списка строк можно попробовать придумывать реакцию системы на событие. И заодно изолировать выполнение действия от движка???

Например, для передвижения action.act может возвращать строку gameState.currentLocation = moveToId. А уж потом специальный объект движка распихает это по репозиториям. Потому что, возможно, часть этих изменений может оказаться невалидной в текущем состоянии игры, поэтому нужно будет как-то реагировать и обрабатывать только корректные последствия. Плюс к этому, такая реализация позволит тестировать логику движка отдельно от логики выполнения разных действий. Последовательность событий в списке может также иметь значение, это нужно учесть при генерации описания.

Метаязык

Нужен для описания доступности и результатов Действий, условий применимости Аспектов, может быть, чего-нибудь ещё. Парсер метаязыка явно должен иметь доступ к объектам worldState, playerState, locationState. Видимо, придется какую-то рекурсивную процедуру оценки писать, чтобы разбирать сложные логические выражения.

Может быть, представлять условия в виде JSON-структуры. Вложенные объекты condition, каждый из которых можно оценить.

{
  "and": [],
  "or": [],
  "condition": "[!](world|player|npc|location|limbo|game).FIELD_NAME (is|has|eq|gt|lt) SOME_VALUE"
}

Пример такой структуры:

{
  "and": [
    {
      "condition": "world.timeOfDay eq MORNING"
    },
    {
      "condition": "game.phase eq combat"
    }
  ]
}

Тогда парсер сможет разбирать это выражение (а как хранить?) и затем оценивать (ему будет нужен доступ к переменным, хранящим состояние?). Допустим, объединим разбор и оценку в одну сущность, чтобы не заморачиваться с хранением и передачей, так что достаточно будет просто вызвать этот парсер с объектами-состояниями и строкой условия, которую нужно будет разобрать и оценить. Сначала сделать простые случаи, завязанные на наличие / отсуствие поля в worldState, остальные фичи добавлять по мере необходимости.

Состояние игры (мира, локации, игрока)

Вообще-то состояние должно быть персистентным, т.е. сохраняться в ПЗУ при каждом изменении, чтобы в любой момент можно было его восстановить даже после падения приложения. Но пока у меня это все будет в оперативной памяти.

Соответственно, работу с объектами-состояниями так-то тоже придется делать подключаемой как плагин, потому что можно же хранить в файловой системе (разными способами), а можно в БД (разных типов / видов). Это через объекты-репозитории (их будет нужно несколько, как минимум для объектов-состояний и для Локаций ) надо будет сделать, похоже. Т.е. на уровне ядра объявить интерфейс репозитория, а его реализацию оставить на откуп клиенту. А в самом ядре замутить только какой-то тестовый механизм для сохранения (в оперативной памяти). Предполагаю, что это будут карты Map, ключи - идентификаторы сущностей (локаций, например), а содержимое - снова карты, хранящие конкретные свойства.