# RPG-режим: полный алгоритм запросов (шпаргалка) Документ описывает, **какой HTTP-запрос что получает и что делает**, когда RPG включён полностью (`rpg_enabled = 1`) и в настройках активны все опции мастера: ```json { "dice": true, "narrator": true, "quests": true, "affinity": true, "choices": true, "stats": false } ``` > **Шкалы** (`lust` / `stamina` / `tension`): по умолчанию `stats: false`. Если включить чекбокс «Шкалы», после каждого хода добавляется ветка `stats_delta` (см. § «Вариант: stats включены»). Пример проходит **от создания чата до 3-го сообщения игрока**, с перекрытием основных веток (диалог / d20 / кнопка выбора / сдвиг сюжета / факты / SD). --- ## Модели и роли | Роль | Env / модель по умолчанию | Где вызывается | |------|---------------------------|----------------| | **Персонаж (чат)** | `CHAT_MODEL` → `mistralai/mistral-nemo` | `POST /chat/stream` → `stream_message()` | | **Нарратор pre/post** | `RPG_NARRATOR_MODEL` → `deepseek/deepseek-chat-v3` | Перед/после ответа персонажа | | **Сюжетная арка** | `RPG_PLOT_MODEL` → DeepSeek v3 | Opening, fallback если арки нет | | **Факты** | `RPG_FACTS_MODEL` → DeepSeek v3 | После каждого ответа персонажа | | **SD-сцена** | `SD_PROMPT_MODEL` или `SYSTEM_MODEL` | Opening + конец каждого stream | | **Появление (prose)** | `SYSTEM_MODEL` | Только при сохранении тегов персоны (не в ходе RPG) | Персонаж **не пишет** affinity/stats в БД — только читает их из `runtime_suffix`. Пишет **только narrator post** (`affinity_delta`, `stats_delta`, `scene_update`, …). --- ## Слои промпта (что видит CHAT_MODEL) ``` llm_system = static_prompt + runtime_suffix [+ context_warning] ``` | Слой | Источник | Когда меняется | |------|----------|----------------| | **static_prompt** | `get_system_prompt()` — persona + lorebook (последние 5 реплик + текущий user) + `ROLEPLAY_GUARDRAILS` | Каждый stream; в `messages` хранится **только** static (без RPG) | | **facts_block** | До 20 фактов из `facts_json` | После `extract_facts` на прошлом ходу | | **PlotArc** | `title`, `phase`, `next_beat_hint` | При генерации/сдвиге арки | | **Status quo** | `sessions.status_quo` | narrator pre/post, opening | | **Scene** | `sessions.scene_json` | narrator pre/post, opening | | **Relationship** | `affinity` + тон | narrator post (`affinity_delta`) | | **Character state** | lust/stamina/tension | Только если `stats: true` | | **Narrator directives** | narrator **pre** (текущий ход) | Эфемерно, не в БД | | **Mechanics** | d20 + outcome (текущий ход) | Только если была проверка | | **Context warning** | `~N% of CHAT_CONTEXT_MAX` | Если оценка > 85% | История в LLM: **все** `user` / `assistant` из БД + один объединённый `system`. --- ## Фаза 0: создание RPG-чата (UI → API) ### 0.1 `PATCH /sessions/{id}` **Тело (пример):** ```json { "persona_id": "card_abc", "rpg_enabled": true, "genre": "fantasy", "rpg_settings_json": "{\"dice\":true,\"narrator\":true,...}" } ``` **Действия:** запись в `sessions` — флаги RPG, жанр, настройки. Сообщений ещё нет. ### 0.2 `POST /chat/init` **Тело:** `{ "session_id", "persona_id", "first_mes_override"? }` **Действия:** 1. `get_system_prompt(persona, [], "")` → static (персона, lorebook, guardrails). 2. `upsert_static_system_message` — одна строка `role=system` в `messages`. 3. Если истории нет — `add_message(assistant, first_mes)` из карты / override. **LLM:** не вызывается. **БД после:** `messages = [system, assistant(first_mes)]`. ### 0.3 `POST /chat/opening/process` **Тело:** `{ "session_id", "persona_id", "rpg": true }` Последовательность **только на opening** (без `narrator_pre`, без stream персонажа): ```mermaid sequenceDiagram participant UI participant API as opening/process participant Plot as RPG_PLOT_MODEL participant Nar as RPG_NARRATOR_MODEL participant SD as SD_PROMPT_MODEL UI->>API: POST opening/process API->>Plot: generate_plot_arc(first_mes, persona, genre) Plot-->>API: plot_arc_json API->>API: seed rpg_quests from beats API->>Nar: narrator_post OPENING Nar-->>API: status_quo, scene, affinity_delta, outfit, choices, quests API->>API: apply_narrator_post → sessions API->>SD: generate_sd_prompt + run_sd_for_message API-->>UI: plot_arc, quests, affinity, choices, image_* ``` | Шаг | Модель | Вход | Выход в БД / UI | |-----|--------|------|-----------------| | 1 | `generate_plot_arc` | имя, description, scenario, **first_mes**, facts, genre | `plot_arc_json`; квесты из `beats[].title` | | 2 | `narrator_post(is_opening=true)` | контекст: `assistant: {first_mes}`; arc; facts | `status_quo`, `scene_json`, `outfit_json`, `affinity += delta`, квесты, **choices** (в ответ API, не в messages) | | 3 | `generate_sd_prompt` | история, outfit, **scene_json** | картинка на last assistant id | **Особенности opening для narrator post:** - Просит заполнить `scene_update` из greeting + scenario. - Просит **non-zero `affinity_delta`**, если в first_mes явный тёплый/враждебный тон. - `outfit_update` — начальная одежда из greeting (danbooru-теги). **UI после:** `reloadChatFromServer`, панель квестов, 💖 affinity, кнопки `choices`, картинка SD. --- ## Состояние сессии перед 1-м сообщением игрока | Поле | Пример | |------|--------| | `messages` | system, assistant(first_mes) | | `plot_arc_json` | сгенерированная арка | | `status_quo`, `scene_json`, `outfit_json` | из narrator opening | | `affinity` | 0 или ±1..2 после opening | | `facts_json` | `[]` | | `narrative_stats_json` | `{"lust":0,"stamina":10,"tension":0}` (если stats выкл. — всё равно дефолт в БД) | --- ## Сообщение игрока №1 — обычная реплика (без d20) **Пример:** игрок пишет: «Привет, как тебя зовут?» **UI:** `POST /chat/stream` ```json { "session_id": "...", "persona_id": "...", "message": "Привет, как тебя зовут?", "is_narrator_choice": false } ``` ### Этап A — подготовка (до stream) 1. **static_prompt** = `get_system_prompt(persona, history, message)` — lorebook по последним 5 репликам + guardrails. 2. **RPG pre (narrator):** - **Вход user:** persona, user action, Global plot (= full arc JSON), Facts, Recent (до 8 реплик). - **Модель:** `RPG_NARRATOR_MODEL`. - **Ожидание:** `needs_check: false` для чистого диалога. - **Эффекты:** - `directives` → попадут в `narrator_extra`. - `status_quo_update` → `UPDATE sessions.status_quo`. - `scene_update` (partial) → merge в `scene_json`. - **Нет** d20, **нет** bubble «Рассказчик». 3. **runtime_suffix** = `build_rpg_runtime_suffix(session)` + `narrator_extra` (directives только). 4. `upsert_static_system_message(static)` — в БД system без RPG-блоков. 5. `add_message(user, message)`. 6. **context_usage** — если > 85%, в конец system добавляется `[Context: ~N% …]`. 7. **llm_messages** = system(static+runtime) + вся история user/assistant. ### Этап B — SSE stream | Событие SSE | Когда | |-------------|--------| | `{ "chunk": "..." }` | Поток `CHAT_MODEL` — ответ персонажа | | `{ "done": true, ... }` | После сохранения assistant в БД | **Персонаж:** один запрос stream, читает affinity/scene/status/arc/facts/directives, **не меняет** счётчики. ### Этап C — post-process (после ответа персонажа, тот же HTTP stream) Порядок внутри `generate()`: | # | Действие | Модель | Условие | |---|----------|--------|---------| | C1 | `generate_plot_arc` | PLOT | Только если `plot_arc_json` пуст (редко после opening) | | C2 | `should_advance_arc(user_message)` | код | Ключевые слова: отдых → `event_driven:rest`, путь → `travel`, помощь → `help_request` | | C3 | `pop_matching_beats` + injection | — | Если trig совпал с beat; choices из beat | | C4 | `advance_phase` | — | Если beats пусты — фаза opening→hook→… | | C5 | `extract_facts` | FACTS | Последние 10 реплик; merge до 80, в промпт 20 | | C6 | **`narrator_post`** | NARRATOR | Контекст: последние 8 реплик **включая новый ответ** | | C7 | **`apply_narrator_post`** | — | status_quo, affinity_delta, scene, outfit, quests, stats_delta* | | C8 | `generate_sd_prompt` + Comfy | SD | outfit + scene_json + последние 6 реплик | | C9 | SSE `done` | — | choices, affinity, quests, image_*, debug | \* `stats_delta` только при `rpg_settings.stats === true`. **Для реплики №1 (диалог):** narrator pre → no check; post → facts, возможно choices, affinity ±, scene/status. --- ## Сообщение игрока №2 — действие с проверкой d20 **Пример:** «Пытаюсь перепрыгнуть через пропасть» Тот же `POST /chat/stream`. Отличия в **этапе A**: ```mermaid flowchart TD pre1[narrator_pre без броска] need{needs_check AND dice?} roll[d20 1-20] pre2[narrator_pre с roll+outcome] res[resolution_text + action_resolutions] chat[CHAT_MODEL stream] post[narrator_post + facts + SD] pre1 --> need need -->|да| roll --> pre2 --> res need -->|нет| chat res --> chat chat --> post ``` | Шаг | Что происходит | |-----|----------------| | pre (фаза 1) | `needs_check: true` | | Бросок | `random 1..20` → outcome: crit fail / fail / success / crit success | | pre2 | Тот же user + `Roll d20=N`, `Outcome=...` → **`resolution_text` обязателен** | | UI | SSE **`{ "narrator": { roll, outcome, text } }`** **до** chunks — bubble «📖 Рассказчик» | | runtime | + `--- Mechanics ---` (d20, outcome, «не противоречь») | | БД | `action_resolutions` — intent, roll, outcome, resolution_text | | Персонаж | Должен согласовать ответ с resolution + mechanics | Post-process (C) — **тот же**, что на ходу 1. Facts могут записать «игрок не перепрыгнул», quest может обновиться. --- ## Сообщение игрока №3 — кнопка выбора + сдвиг арки **Пример:** игрок жмёт choice «Отправиться в лес» (label уходит как user message) или пишет: «Идём дальше в лес» **UI:** `sendMessage(label, true)` → `is_narrator_choice: true` | Отличие | Поведение | |---------|-----------| | Текст в БД | `"[Player chose: Отправиться в лес]"` вместо сырого label | | pre/post | Как обычный ход; narrator видит метку выбора | | **should_advance_arc** | Если в тексте есть «идем дальше», «в путь», … → `event_driven:travel` | | beats | `pop_matching_beats(arc, trig)` — до 1 beat; **injection** в debug; **choices** из beat + post | | phase | Если beats опустели → `advance_phase` (например hook → complication) | Остальной pipeline = stream №1 или №2 (в зависимости от needs_check). --- ## Сводная таблица: кто что обновляет (ход 1–3) | Данные | Opening | narrator pre | CHAT_MODEL | narrator post | extract_facts | arc engine | |--------|---------|--------------|------------|---------------|---------------|------------| | messages | first_mes | — | user + assistant | — | — | — | | plot_arc_json | create | — | read | read | — | advance/beats | | status_quo | post | pre/post | read | post | — | — | | scene_json | post | pre/post | read | post | — | — | | outfit_json | post | — | read | post | — | — | | affinity | post | — | read | delta | — | — | | facts_json | — | read | read | post.facts* | merge | — | | rpg_quests | seed | — | — | post | — | — | | action_resolutions | — | d20 path | — | — | — | — | | narrative_stats | default | — | read** | stats_delta** | — | — | \* facts из post в JSON схеме narrator — сейчас основной путь facts = `extract_facts`. \** только при `stats: true`. --- ## Варианты настроек (перекрытие) ### `narrator: false` - Нет narrator pre/post, нет directives/mechanics bubble. - `runtime_suffix` = только `build_rpg_runtime_suffix` (facts, arc, status, scene, affinity). - Post-process: нет C6–C7; facts/arc/SD могут остаться (arc/SD/facts всё ещё в коде stream). ### `dice: false` - `needs_check` от pre **игнорируется** — всегда ветка без броска (как диалог). - pre2 и mechanics не вызываются. ### `affinity: false` - Нет блока Relationship в runtime; `affinity_delta` не применяется. ### `quests: false` - Opening не сидирует квесты; post не вызывает `upsert_quest`. ### `choices: false` - UI не рисует кнопки; post/beat choices не добавляются в SSE. ### `stats: true` - В runtime: блок lust/stamina/tension (0–10). - post: `stats_delta` каждый ключ −2..+2, clamp 0–10. - Дефолт stamina = 10. ### `is_narrator_choice: true` - User content = `[Player chose: …]`. ### Контекст > 85% - В system добавляется одна строка-предупреждение (не в БД). --- ## Вспомогательные запросы (не в примере 1–3, но в RPG) | Запрос | Когда | |--------|-------| | `GET /chat/system/{id}` | Панель system blob: static, runtime-превью, scene, stats, **context_usage** | | `GET /chat/history/{id}` | Перезагрузка чата | | `POST /chat/rpg/bootstrap` | RPG на **старом** чате: arc + **narrator_post opening** + apply (как opening, без нового first_mes) | | `POST /chat/` (не stream) | Упрощённый ход: static + runtime, один ответ, без narrator pre/post | --- ## Пример хронологии (3 сообщения, все опции ON) | # | HTTP | LLM-вызовы | Заметки | |---|------|------------|---------| | 0 | PATCH session | — | rpg_enabled | | 0 | POST init | — | system + first_mes | | 0 | POST opening/process | Plot, Narrator post, SD | arc, scene, affinity, quests, choices | | 1 | POST stream | Pre (no check), **Chat**, Facts, Post, SD | Диалог; +facts; choices в done | | 2 | POST stream | Pre (check), **Pre2+d20**, **Chat**, Facts, Post, SD | Bubble рассказчика; action_resolutions | | 3 | POST stream | Pre, Chat, Facts, Post, SD; arc travel? | `[Player chose:…]` или текст; beat injection | **Порядок SSE на ходу 2:** 1. `narrator` (d20) 2. `chunk` × N 3. `image_generating` (если SD) 4. `done` (choices, affinity, quests, debug) --- ## Файлы для углубления | Тема | Файл | |------|------| | Stream + RPG | `routers/chat.py` → `chat_stream` | | Opening | `services/opening.py` | | Narrator JSON | `services/rpg_narrator.py` | | Runtime blocks | `routers/chat.py` → `build_rpg_runtime_suffix`, `services/rpg_state.py` | | Persist post | `services/rpg_state.py` → `apply_narrator_post` | | Arc / beats | `services/rpg_plot.py` | | Facts | `services/rpg_facts.py` | | UI stream | `static/js/chat.js` → `sendMessage`, `consumeStream` | | New chat | `static/js/newChatWizard.js` | | Context % | `services/context_budget.py` | --- *Обновлено под текущую реализацию RP UX (anti-OOC, scene_json, context blob, rpFormat, bootstrap narrator).*