# 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. **Linear story (pre-stream):** `reconcile_story_arc` → один active-квест = текущий `steps[i]`:
   - `format_step_guidance_for_character` — цель шага в system CHAT.
   - При первом входе в шаг: `injection` как мягкая подсказка (`format_step_hint_for_character`).
   - Если арка завершена и игрок выбрал «новую арку» → `roll_next_arc`.
4. **runtime_suffix** = `build_rpg_runtime_suffix(session)` + `narrator_extra` (directives + step guidance/hint).
5. `upsert_static_system_message(static)` — в БД system без RPG-блоков.
6. `add_message(user, message)`.
7. **context_usage** — если > 85%, в конец system добавляется `[Context: ~N% …]`.
8. **llm_messages** = system(static+runtime) + история; язык сессии (`infer_rp_language`) в narrator/plot/injection.

### Этап 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 | **`narrator_post`** | NARRATOR | Контекст: шаг N/M, completion_criteria, последние 8 реплик |
| C3 | **`apply_narrator_post_with_story`** | — | facts, affinity, scene; `step_complete` → advance step, sync quest |
| C4 | step choices / new arc | — | choices из нового шага; при `arc_completed` — «Начать новую арку» |
| C5 | `extract_facts` | FACTS | Последние 10 реплик |
| C6 | SD + SSE `done` | — | **всегда** `quests` + `story_arc` в payload (live UI) |
| 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).*
