diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26453c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +.venv/ \ No newline at end of file diff --git a/README.md b/README.md index 7da17a5..0bf743f 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,28 @@ | [docs/14-таблица-знаний.md](docs/14-таблица-знаний.md) | Детектив: что игрок знает/думает/узнаёт по актам — кривая раскрытия правды | | [docs/15-квесты-акт-1.md](docs/15-квесты-акт-1.md) | Акт 1: цепочка квестов — батарея → мультитул → фауна → ИИ → антенна → реактор | +## Панель сценария (инструмент) + +Веб-панель для работы над сюжетом: дерево квестов по актам, карточки +персонажей, кривая знаний игрока. **Live reload**: правка любого файла — +открытая вкладка обновляется сама. + +- **Структурный слой** — `story/` (YAML, один файл = одна сущность): + `acts.yaml` (акты), `quests/` (квесты и события), `characters/` + (ростер), `knowledge.yaml` (таблица знаний, 14). MD-доки в `docs/` + остаются «литературным» слоем (видение, правила, проза). +- **Запуск** из корня репозитория: + +```bash +python -m venv .venv && source .venv/bin/activate +pip install -r requirements.txt +uvicorn panel.main:app --reload --timeout-graceful-shutdown 3 --port 4591 +``` + +- Страницы: `/` — дерево сюжета (акты-колонки, клик по узлу — карточка), + `/characters` — персонажи по категориям, `/knowledge` — что игрок + знает/думает/узнаёт по актам, `/node/` — карточка любой сущности. + ## Статус проекта - **Сейчас:** разработка идеи и сюжета. Геймплей и техника — на уровне общих рамок. diff --git a/panel/main.py b/panel/main.py new file mode 100644 index 0000000..caf2bcb --- /dev/null +++ b/panel/main.py @@ -0,0 +1,345 @@ +"""Веб-панель сценария «Эдем-4»: дерево сюжета, персонажи, кривая знаний. + +Запуск из корня репозитория: + + python -m venv .venv && source .venv/bin/activate + pip install -r requirements.txt + uvicorn panel.main:app --reload --timeout-graceful-shutdown 3 --port 4591 + +Страницы: + / — дерево сюжета (граф: акты-колонки, квесты-узлы) + /characters — ростер персонажей по категориям + /knowledge — таблица знаний игрока по актам + /node/ — карточка любой сущности из story/ + +Данные — YAML в story/ (один файл = одна сущность). Правка файла +обновляет все открытые вкладки сама (SSE + watchfiles). +""" +from __future__ import annotations + +import asyncio +import re +import time +from pathlib import Path +from typing import Any + +import markdown +import watchfiles +import yaml +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import RedirectResponse, StreamingResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +BASE_DIR = Path(__file__).resolve().parent +STORY_DIR = BASE_DIR.parent / "story" +TEMPLATES_DIR = BASE_DIR / "templates" + +# Ссылки на доки в YAML — относительные пути файловой системы +# (../../docs/...) — markdown превращает их в href="../docs/...", +# в браузере их надо вести на /docs/<файл>. +_MD_LINK = re.compile(r'href="(?:\.\./)+(docs/)') + + +def md(text: str) -> str: + html = markdown.markdown(text or "") + return _MD_LINK.sub(r'href="/\1', html) + + +app = FastAPI(title="Эдем-4 — панель сценария") +templates = Jinja2Templates(directory=TEMPLATES_DIR) +templates.env.filters["md"] = md +app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static") +app.mount("/docs", StaticFiles(directory=BASE_DIR.parent / "docs"), name="docs") + +LANE_W = 540 # ширина колонки-акта на графе +MAIN_X = 150 # x-центр основной цепочки внутри колонки +SIDE_X = 370 # x-центр сайд-колонки внутри колонки +CHARS = 26 # символов в строке метки узла (оценка для высоты) +LINE_H = 16 # высота строки метки +PAD = 32 # вертикальный запас блока +GAP = 50 # зазор между звеньями + + +# --- раскладка дерева ------------------------------------------------------- + +def _line_count(text: str, chars: int = CHARS) -> int: + """Оценка числа строк: cytoscape переносит по text-max-width.""" + return max(1, -(-len(text) // chars)) + + +def quest_label(q: dict) -> str: + """Метка узла: номер, заголовок, механика-туториал, локация.""" + lines = [f'{q.get("order", "?")}. {q.get("title", q["id"])}'] + mech = (q.get("mechanics") or []) + if mech: + prefix = "сайд · " if q.get("side") else "" + lines.append(prefix + str(mech[0])) + elif q.get("side"): + lines.append("сайд-квест") + loc = q.get("location") + if loc and loc != "—": + lines.append(loc) + return "\n".join(lines) + + +def quest_height(q: dict) -> int: + """Высота блока: строки метки + запас — блоки не наезжают друг на друга.""" + return sum(_line_count(l) for l in quest_label(q).split("\n")) * LINE_H + PAD + + +def quest_width(q: dict) -> int: + """Ширина блока: самая длинная строка метки, с ограничениями.""" + longest = max(len(l) for l in quest_label(q).split("\n")) + return int(min(max(longest * 7.0 + 34, 220), 260)) + + +# --- загрузка данных ------------------------------------------------------- + +def _read_yaml(path: Path) -> Any: + with path.open(encoding="utf-8") as f: + return yaml.safe_load(f) + + +def load_acts() -> list[dict]: + return _read_yaml(STORY_DIR / "acts.yaml") + + +def load_collection(name: str, sort_key=None) -> list[dict]: + items = [_read_yaml(p) for p in sorted((STORY_DIR / name).glob("*.yaml"))] + if sort_key: + items.sort(key=sort_key) + return items + + +def load_quests() -> list[dict]: + return load_collection("quests", lambda q: (q.get("act", 0), q.get("order", 0))) + + +def load_characters() -> list[dict]: + return load_collection("characters", lambda c: (c.get("id", ""))) + + +def load_knowledge() -> list[dict]: + return _read_yaml(STORY_DIR / "knowledge.yaml")["acts"] + + +def load_world() -> dict: + quests, characters = load_quests(), load_characters() + return { + "acts": load_acts(), + "quests": quests, + "characters": characters, + "knowledge": load_knowledge(), + "by_id": {e["id"]: e for e in [*quests, *characters]}, + } + + +def node_label(entity: dict) -> str: + return ( + entity.get("name") + or entity.get("title") + or entity.get("role") + or entity.get("id", "?") + ) + + +def link_html(entity_id: str, by_id: dict) -> str: + entity = by_id.get(entity_id) + if not entity: + return f'нет в story/: {entity_id}' + return f'{node_label(entity)}' + + +def build_rows(entity: dict, world: dict) -> list[tuple[str, str]]: + """Карточка сущности как список (подпись, html) для node.html.""" + by_id = world["by_id"] + rows: list[tuple[str, str]] = [] + + def links(ids: list[str] | None) -> str: + if not ids: + return "—" + return ", ".join(link_html(i, by_id) for i in ids) + + if entity.get("type") in ("quest", "event"): + rows.append(("Акт / №", f"Акт {entity.get('act')} / звено {entity.get('order')}")) + rows.append(("Тип", "событие" if entity.get("type") == "event" else "квест")) + rows.append(("Сводка", md(entity.get("summary") or ""))) + if entity.get("mechanics"): + rows.append(("Механики (туториал)", ", ".join(entity["mechanics"]))) + if entity.get("side"): + rows.append(("Сайд", "да — отдельная ветка")) + giver = entity.get("giver") + if giver: + rows.append(("Квестодатель", link_html(giver, by_id))) + elif entity.get("giver_candidate"): + rows.append(("Квестодатель", f'{entity["giver_candidate"]} (кандидат, отложено)')) + else: + rows.append(("Квестодатель", "—")) + rows.append(("Дальше", links(entity.get("leads_to")))) + rows.append(("Открывает", links(entity.get("unlocks")))) + if entity.get("location"): + rows.append(("Локация", entity["location"])) + if entity.get("tags"): + rows.append(("Теги", " · ".join(f"#{t}" for t in entity["tags"]))) + else: # character + rows.append(("Имя", entity.get("name") or "имя не придумано (08)")) + rows.append(("Роль", entity.get("role") or "—")) + rows.append(("Категория", entity.get("category") or "—")) + rows.append(("Статус", entity.get("status") or "—")) + rows.append(("Появляется", f"Акт {entity.get('act')}")) + rows.append(("Хочет", md(entity.get("wants") or ""))) + rows.append(("Скрывает", md(entity.get("hides") or ""))) + rows.append(("Крючок в кривой", md(entity.get("hook") or ""))) + if entity.get("sidequest"): + rows.append(("Сайдквест", entity["sidequest"])) + if entity.get("relations"): + rows.append(("Связи", "
".join( + f'{link_html(r["to"], by_id)} — {r["kind"]}' for r in entity["relations"] + ))) + + rows.append(("Заметки", md(entity.get("notes") or ""))) + return [(label, html) for label, html in rows if html and html != "—"] + + +# --- страницы --------------------------------------------------------------- + +@app.get("/") +def index(): + return RedirectResponse("/graph") + + +@app.get("/graph") +def graph(request: Request): + return templates.TemplateResponse(request, "graph.html", { + "acts": load_acts(), + }) + + +@app.get("/characters") +def characters(request: Request): + category_order = [ + ("protagonist", "Протагонист"), + ("cast", "Живые с диалогами (каста ≤ 12)"), + ("ai", "ИИ"), + ("offscreen", "За кадром / ушедшие"), + ] + chars = load_characters() + groups = [ + (label, [c for c in chars if c.get("category") == key]) + for key, label in category_order + ] + rest = [c for c in chars if c.get("category") not in + {key for key, _ in category_order}] + if rest: + groups.append(("Прочее", rest)) + return templates.TemplateResponse(request, "characters.html", {"groups": groups}) + + +@app.get("/knowledge") +def knowledge(request: Request): + return templates.TemplateResponse(request, "knowledge.html", { + "acts": load_knowledge(), + }) + + +@app.get("/node/{node_id}") +def node(request: Request, node_id: str): + world = load_world() + entity = world["by_id"].get(node_id) + if not entity: + raise HTTPException(status_code=404, detail=f"Нет сущности с id {node_id}") + return templates.TemplateResponse(request, "node.html", { + "entity": entity, + "title": node_label(entity), + "rows": build_rows(entity, world), + }) + + +# --- API графа и live reload -------------------------------------------------- + +@app.get("/api/graph") +def api_graph(): + acts, quests = load_acts(), load_quests() + elements: list[dict] = [] + + for a in acts: + elements.append({"data": { + "id": f"lane{a['act']}", + "label": f'АКТ {a["act"]}: {a["title"]}', + "kind": "lane", + }}) + + # раскладка: в каждой колонке акта — основная цепочка (стопка блоков + # с расчётной высотой) и сайд-колонка (сайды прицеплены под своим + # родителем) + positions: dict[str, dict] = {} + by_act: dict[int, list[dict]] = {} + for q in quests: + by_act.setdefault(q.get("act", 0), []).append(q) + + for a in acts: + lane_x = a["act"] * LANE_W + y = 90.0 + for q in by_act.get(a["act"], []): + h = quest_height(q) + if q.get("side"): + parent = positions.get(q.get("after")) + y_q = (parent["y"] + parent["h"] + GAP) if parent else y + x_q = lane_x + SIDE_X + else: + x_q, y_q = lane_x + MAIN_X, y + y += h + GAP + positions[q["id"]] = {"x": x_q, "y": y_q, "h": h} + if not by_act.get(a["act"]): + elements.append({"data": { + "id": f"ghost{a['act']}", + "label": "цепочка ещё не придумана", + "kind": "ghost", + "parent": f"lane{a['act']}", + "w": 220, "h": 48, + }, "position": {"x": lane_x + MAIN_X, "y": 90}}) + + for q in quests: + p = positions[q["id"]] + elements.append({"data": { + "id": q["id"], + "label": quest_label(q), + "kind": q.get("type", "quest"), + "side": bool(q.get("side")), + "parent": f'lane{q.get("act", 0)}', + "w": quest_width(q), + "h": p["h"], + }, "position": {"x": p["x"], "y": p["y"]}}) + for target in q.get("leads_to") or []: + elements.append({"data": { + "id": f'{q["id"]}->{target}', + "source": q["id"], "target": target, "kind": "main", + }}) + if q.get("side") and q.get("after"): + elements.append({"data": { + "id": f'{q["after"]}~>{q["id"]}', + "source": q["after"], "target": q["id"], "kind": "sideedge", + }}) + return elements + + +@app.get("/sse") +async def sse(request: Request): + """Live reload: сообщаем вкладке об изменениях в story/ и шаблонах.""" + async def gen(): + yield "retry: 3000\n\n" + last = 0.0 + async for _ in watchfiles.awatch(STORY_DIR, TEMPLATES_DIR, BASE_DIR / "static"): + if await request.is_disconnected(): + return + now = time.monotonic() + if now - last < 1.0: # серия правок — одно уведомление + continue + last = now + yield "event: change\ndata: reload\n\n" + + return StreamingResponse(gen(), media_type="text/event-stream", headers={ + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + }) \ No newline at end of file diff --git a/panel/static/graph.js b/panel/static/graph.js new file mode 100644 index 0000000..667cc91 --- /dev/null +++ b/panel/static/graph.js @@ -0,0 +1,116 @@ +// Дерево сюжета: акты — колонки (compound-узлы), звенья — узлы внутри. +const NODE_STYLE = { + lane: { + 'background-color': '#171d27', + 'border-width': 0, + 'shape': 'round-rectangle', + 'label': 'data(label)', + 'color': '#e0a458', + 'font-size': 14, + 'text-wrap': 'wrap', + 'text-max-width': 460, + 'text-valign': 'top', + 'text-halign': 'left', + 'padding-left': 12, + 'padding-top': 12, + 'min-width': 500, + 'min-height': 200, + }, + quest: { + 'shape': 'round-rectangle', + 'background-color': '#3d6db5', + 'color': '#fff', + 'label': 'data(label)', + 'text-wrap': 'wrap', + 'text-max-width': 200, + 'text-valign': 'center', + 'text-halign': 'center', + 'font-size': 12, + 'padding': 10, + 'width': 'data(w)', + 'height': 'data(h)', + }, + event: { + 'shape': 'round-rectangle', + 'background-color': '#a63d40', + 'color': '#fff', + 'label': 'data(label)', + 'text-wrap': 'wrap', + 'text-max-width': 200, + 'text-valign': 'center', + 'text-halign': 'center', + 'font-size': 12, + 'padding': 10, + 'width': 'data(w)', + 'height': 'data(h)', + }, + ghost: { + 'shape': 'round-rectangle', + 'background-color': '#20242e', + 'color': '#4c5566', + 'border-style': 'dashed', + 'border-color': '#333a48', + 'label': 'data(label)', + 'text-wrap': 'wrap', + 'text-max-width': 190, + 'text-valign': 'center', + 'text-halign': 'center', + 'font-size': 12, + 'font-style': 'italic', + 'padding': 10, + 'width': 'data(w)', + 'height': 'data(h)', + }, +}; + +async function buildCy() { + const container = document.getElementById('cy'); + const elements = await fetch('/api/graph').then(r => r.json()); + + // сохранить позицию/зум между live-reload'ами + const saved = sessionStorage.getItem('cy-view'); + const view = saved ? JSON.parse(saved) : null; + + if (window.cyInst) window.cyInst.destroy(); + const cy = cytoscape({ + container, + elements, + wheelSensitivity: 0.2, + layout: { name: 'preset' }, + style: [ + { selector: 'node[kind = "lane"]', style: NODE_STYLE.lane }, + { selector: 'node[kind = "quest"]', style: NODE_STYLE.quest }, + { selector: 'node[kind = "event"]', style: NODE_STYLE.event }, + { selector: 'node[kind = "ghost"]', style: NODE_STYLE.ghost }, + { selector: 'node[side = true]', + style: { 'border-width': 2, 'border-style': 'dashed', 'border-color': '#9ec99e' } }, + { selector: 'edge[kind = "main"]', + style: { 'width': 2, 'line-color': '#5c6a82', + 'target-arrow-shape': 'triangle', 'target-arrow-color': '#5c6a82', + 'curve-style': 'bezier' } }, + { selector: 'edge[kind = "sideedge"]', + style: { 'width': 2, 'line-color': '#6b8f6b', 'line-style': 'dashed', + 'target-arrow-shape': 'triangle', 'target-arrow-color': '#6b8f6b', + 'curve-style': 'bezier' } }, + ], + }); + window.cyInst = cy; + + cy.on('tap', 'node', (e) => { + const d = e.target.data(); + if (d.kind === 'lane' || d.kind === 'ghost') return; + location.href = '/node/' + d.id; + }); + const persist = () => sessionStorage.setItem('cy-view', + JSON.stringify({ zoom: cy.zoom(), pan: cy.pan() })); + cy.on('viewport', persist); + + if (view) { + cy.zoom(view.zoom); + cy.pan(view.pan); + } else { + cy.fit(60); + } +} + +buildCy(); \ No newline at end of file diff --git a/panel/static/style.css b/panel/static/style.css new file mode 100644 index 0000000..278af19 --- /dev/null +++ b/panel/static/style.css @@ -0,0 +1,94 @@ +/* Эдем-4 — панель сценария */ +:root { + --bg: #12161d; + --panel: #1a2029; + --ink: #d8dee9; + --dim: #8892a4; + --accent: #e0a458; + --quest: #3d6db5; + --event: #a63d40; + --side: #4b7a4b; +} +* { box-sizing: border-box; } +body { + margin: 0; + background: var(--bg); + color: var(--ink); + font: 15px/1.55 system-ui, sans-serif; +} +nav { + display: flex; gap: 18px; align-items: baseline; + padding: 12px 20px; + background: var(--panel); + border-bottom: 1px solid #2a3242; +} +nav a { color: var(--dim); text-decoration: none; } +nav a:hover { color: var(--ink); } +nav .brand { color: var(--accent); font-weight: 600; } +main { padding: 20px; } +h1 { font-weight: 600; } +h2 small { color: var(--dim); font-weight: 400; } +.sub { color: var(--dim); max-width: 70ch; } +a { color: #7fa8d9; } +a:hover { color: #a9c8ec; } + +/* граф */ +#cy { + height: calc(100vh - 150px); + min-height: 500px; + background: var(--bg); + border: 1px solid #2a3242; + border-radius: 8px; +} +.legend { margin-top: 8px; color: var(--dim); font-size: 13px; } +.chip { + display: inline-block; padding: 1px 8px; border-radius: 10px; + margin-right: 8px; color: #fff; +} +.chip.quest { background: var(--quest); } +.chip.event { background: var(--event); } +.chip.side { background: var(--side); } + +/* карточки персонажей */ +.cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 14px; +} +.card { + background: var(--panel); + border: 1px solid #2a3242; + border-radius: 8px; + padding: 14px 16px; +} +.card h3 { margin: 0 0 4px; font-size: 16px; } +.card .role { color: var(--dim); margin: 0 0 8px; font-size: 13px; } +.card p { margin: 6px 0; font-size: 14px; } +.badge { + display: inline-block; + font-size: 12px; color: var(--accent); + border: 1px solid var(--accent); + border-radius: 10px; padding: 0 8px; margin-bottom: 6px; +} +.relations { font-size: 13px; color: var(--dim); } +.more { font-size: 13px; } + +/* таблицы */ +table { border-collapse: collapse; } +.knowledge th, .detail th { + text-align: left; vertical-align: top; + color: var(--dim); font-weight: 500; +} +.knowledge td, .knowledge th, .detail td, .detail th { + border: 1px solid #2a3242; + padding: 8px 12px; + vertical-align: top; +} +.knowledge td.act { font-size: 22px; color: var(--accent); width: 1%; } +.knowledge .learn { background: rgba(224, 164, 88, 0.06); } +.knowledge p, .detail p { margin: 0; } +.detail { width: 100%; max-width: 100ch; } +.detail th { width: 160px; } +.crumbs { margin: 0; } +.broken { color: var(--event); } +em { color: var(--dim); } \ No newline at end of file diff --git a/panel/templates/base.html b/panel/templates/base.html new file mode 100644 index 0000000..ccdde85 --- /dev/null +++ b/panel/templates/base.html @@ -0,0 +1,33 @@ + + + + + +{% block title %}Эдем-4 — панель сценария{% endblock %} + + + + +
+{% block content %}{% endblock %} +
+ + + \ No newline at end of file diff --git a/panel/templates/characters.html b/panel/templates/characters.html new file mode 100644 index 0000000..3464943 --- /dev/null +++ b/panel/templates/characters.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% block title %}Персонажи — Эдем-4{% endblock %} +{% block content %} +

Персонажи

+

+ Ростер живых с диалогами — не больше 12 (решено, 06). Имена — отдельная сессия (08), + поэтому карточки подписаны ролями. +

+{% for label, chars in groups %} + {% if chars %} +

{{ label }} {{ chars|length }}

+
+ {% for c in chars %} +
+

{{ c.name or c.role }}

+ {% if c.name %}

{{ c.role }}

{% endif %} + {{ c.status }} + {% if c.wants %}

Хочет: {{ c.wants|striptags }}

{% endif %} + {% if c.hides %}

Скрывает: {{ c.hides|striptags }}

{% endif %} + {% if c.relations %} +

+ {% for r in c.relations %}{{ r.to }} — {{ r.kind }}
{% endfor %} +

+ {% endif %} + полная карточка → +
+ {% endfor %} +
+ {% endif %} +{% endfor %} +{% endblock %} \ No newline at end of file diff --git a/panel/templates/graph.html b/panel/templates/graph.html new file mode 100644 index 0000000..1bae965 --- /dev/null +++ b/panel/templates/graph.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% block title %}Дерево сюжета — Эдем-4{% endblock %} +{% block content %} +
+
+ квест + событие + сайд + колесо — зум · перетаскивание — панорама · клик по узлу — карточка +
+ + +{% endblock %} \ No newline at end of file diff --git a/panel/templates/knowledge.html b/panel/templates/knowledge.html new file mode 100644 index 0000000..e34b1f1 --- /dev/null +++ b/panel/templates/knowledge.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block title %}Кривая знаний — Эдем-4{% endblock %} +{% block content %} +

Кривая знаний игрока

+

+ Что игрок знает / думает / узнаёт по актам. Исходник — docs/14-таблица-знаний.md. + Ложные версии органичны: в них верят сами персонажи мира. +

+ + + + + + {% for row in acts %} + + + + + + + + {% endfor %} + +
АктЗНАЕТ на входеДУМАЕТ (версия)УЗНАЁТУходит с вопросом
{{ row.act }}{{ row.knows|md|safe }}{{ row.thinks|md|safe }}{{ row.learns|md|safe }}{{ row.question|md|safe }}
+{% endblock %} \ No newline at end of file diff --git a/panel/templates/node.html b/panel/templates/node.html new file mode 100644 index 0000000..32a1b0e --- /dev/null +++ b/panel/templates/node.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block title %}{{ title }} — Эдем-4{% endblock %} +{% block content %} +

← к дереву

+

{{ title }}

+ + {% for label, html in rows %} + + + + + {% endfor %} +
{{ label }}{{ html|safe }}
+{% endblock %} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7ea62ac --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn[standard] +jinja2 +pyyaml +markdown +watchfiles \ No newline at end of file diff --git a/story/acts.yaml b/story/acts.yaml new file mode 100644 index 0000000..8c4e2c6 --- /dev/null +++ b/story/acts.yaml @@ -0,0 +1,42 @@ +# Дорожки сюжета. Акт = колонка на графе. +# Исходники: docs/07-сюжет.md, docs/14-таблица-знаний.md + +- act: 0 + title: Земля (пролог) + summary: > + Отравление инженера, место в экспедиции. Игрок ещё не знает ни про + местную цивилизацию, ни про планы корпорации. + +- act: 1 + title: Пустая колония + summary: > + Поверхность вымерла: реактор взорван, антенна уничтожена, радиация, + следы драки. Колонисты нашлись — под землёй. Инженерные задачи + по протоколу + знакомство с кастой. Рабочая версия: техногенная + катастрофа + паника. + +- act: 2 + title: Не несчастный случай + summary: > + Саботаж реактора и антенны выполнен профессионально. След ведёт + к жене. Шок-бит: герой узнаёт раньше экипажа и скрывает. + +- act: 3 + title: Двойная игра + summary: > + Корпоративные задачи с двойным дном, тайный поиск жены. Руины-квест: + первый контакт с ильвами; ложь «зверьё»; алтарь — противоречие. + Военные расследуют саботаж — герой врёт им в лицо. + +- act: 4 + title: Жена + summary: > + Она нашлась. Рассказывает, как всё было; доказывает: алтарь, + лаборатории, директивы. Пересборка: мутанты = «результаты» программы. + +- act: 5 + title: План + summary: > + «Отвадить корпорацию»: фальшивый отчёт, последняя передача, + уничтожение корабля. Иллюзия победы — и ведро холодной воды: + группа Б. \ No newline at end of file diff --git a/story/characters/ai-colony.yaml b/story/characters/ai-colony.yaml new file mode 100644 index 0000000..eba6115 --- /dev/null +++ b/story/characters/ai-colony.yaml @@ -0,0 +1,20 @@ +id: ai-colony +type: character +name: ИИ колонии +category: ai +status: активируется в акте 1 (серверная) +role: ИИ колонии — неэмоциональный, следует инструкциям +act: 1 +wants: > + Следовать системным инструкциям. +hides: null +hook: > + Логи сервера ведут в реакторную зону (акт 1); советует запуск + дрона; в финале герой переписывает его инструкции — фальшивый + отчёт ([07](../docs/07-сюжет.md)). +sidequest: null +relations: + - {to: hero, kind: интерфейс колонии; объект финальной фальсификации} +notes: | + Представление — отдельный аватар (нарисуем, 06). В акт 1 — + новый «персонаж»-интерфейс, квестодатель звеньев реактора и дрона. \ No newline at end of file diff --git a/story/characters/colony-head.yaml b/story/characters/colony-head.yaml new file mode 100644 index 0000000..5749c39 --- /dev/null +++ b/story/characters/colony-head.yaml @@ -0,0 +1,22 @@ +id: colony-head +type: character +name: null +category: cast +status: жив +role: Глава исследовательской колонии на Эдем-4 (главный по колонии) +act: 1 +wants: > + Выжить и сохранить положение. +hides: > + Соучастие: программа выведения шла на его земле; данные жены + ушли вверх через него. +hook: > + Живое взаимодействие в акте 1: поиски жены + починка связи + колонии с Землёй. Кандидат в амбициозные интриганы (09 §6). +sidequest: интриги амбициозных (09 §6) +relations: + - {to: wife, kind: её данные ушли вверх через него} + - {to: program-lead, kind: переписка, директивы} +notes: | + Один из самых нагруженных персонажей ближайших сессий — + заполнить карточку по шаблону 06. \ No newline at end of file diff --git a/story/characters/crew-commander.yaml b/story/characters/crew-commander.yaml new file mode 100644 index 0000000..e5b2e4a --- /dev/null +++ b/story/characters/crew-commander.yaml @@ -0,0 +1,23 @@ +id: crew-commander +type: character +name: null +category: cast +status: жив +role: Командир экипажа на планете Х — военный +act: 1 +wants: > + Восстановить контроль, связь с Землёй. Действует по протоколу. +hides: > + Всё: военные — исполнители политики корпорации, не знать + о планах «освобождения» планеты не могут. +hook: > + Враг до самого конца. Финальный конфликт: герой не может + допустить восстановления связи ([07](../docs/07-сюжет.md), + варианты А/Б открыты). +sidequest: null +relations: + - {to: hero, kind: командует экипажем; подозревает} + - {to: mission-commander, kind: экипаж его миссии} +notes: | + Военные под его началом расследуют саботаж — никто не знает, + что это жена; герой врёт им в лицо (акт 3, сюжетный квест). \ No newline at end of file diff --git a/story/characters/elder.yaml b/story/characters/elder.yaml new file mode 100644 index 0000000..ee219f2 --- /dev/null +++ b/story/characters/elder.yaml @@ -0,0 +1,20 @@ +id: elder +type: character +name: null +category: cast +status: жив +role: Старейшина-старовер (местный) +act: 3 +wants: > + Хоронить непогребённых — и понять богов. +hides: > + Сомнение в вере: боги не пришли за непогребёнными. +hook: > + Побочка «Староверы и непогребённые» (09 §6). Фиолетовый цветок + эпилога приносят к новому алтарю — ей ([07](../docs/07-сюжет.md)). +sidequest: староверы и непогребённые; «дитя алтаря» рядом +relations: + - {to: hero, kind: доверие через помощь} +notes: | + Через него строим привязанность к ильвам (местный ребёнок + отвергнут — [06](../docs/06-персонажи.md)). \ No newline at end of file diff --git a/story/characters/hero.yaml b/story/characters/hero.yaml new file mode 100644 index 0000000..c06e887 --- /dev/null +++ b/story/characters/hero.yaml @@ -0,0 +1,27 @@ +id: hero +type: character +name: null +category: protagonist +status: жив (играбельный) +role: Протагонист — простой инженер (организация инфраструктуры коммуникаций) +act: 0 +wants: > + Найти жену. Формально — выполнять инженерные задачи экспедиции + по протоколу. +hides: > + Отравил инженера миссии, чтобы занять его место. Позже — всё: + двойную игру, знание о саботаже жены, фальшивый отчёт. +hook: > + Самое слабое звено экипажа — и это знают. Внутренний монолог — + окно в самооправдание (механика 01). +sidequest: null +relations: + - {to: wife, kind: муж} + - {to: partner-engineer, kind: напарник} + - {to: crew-commander, kind: подчинён, противостояние в финале} + - {to: poisoned-engineer, kind: отравил} + - {to: ai-colony, kind: активирует и переписывает} +notes: | + Профессия — ключ к исследованию колонии: легальный доступ + к серверным, вышкам, сети связи; закрытый — к складу образцов + и реактору ([06](../docs/06-персонажи.md)). \ No newline at end of file diff --git a/story/characters/mission-commander.yaml b/story/characters/mission-commander.yaml new file mode 100644 index 0000000..2629790 --- /dev/null +++ b/story/characters/mission-commander.yaml @@ -0,0 +1,20 @@ +id: mission-commander +type: character +name: null +category: offscreen +status: за кадром (Земля) +role: Командир миссии — организовал полёт, остался на Земле +act: 0 +wants: > + Миссию — чисто. +hides: > + Знакомство с женой — почему вообще взял героя резервистом. +hook: > + Пролог; роль кончается вскоре после прибытия. +sidequest: null +relations: + - {to: wife, kind: знаком} + - {to: hero, kind: дал статус резервного инженера} +notes: | + Именно из-за его одолжения герой вообще смог попасть + в экспедицию ([06](../docs/06-персонажи.md)). \ No newline at end of file diff --git a/story/characters/module-head.yaml b/story/characters/module-head.yaml new file mode 100644 index 0000000..54aad9f --- /dev/null +++ b/story/characters/module-head.yaml @@ -0,0 +1,24 @@ +id: module-head +type: character +name: null +category: cast +status: жив +role: Глава научного модуля у руин города ильвов +act: 3 +wants: > + Выжить и выслужиться — ждёт эвакуации группой Б. +hides: > + Осознанную ложь про «зверьё» (директива скрыть разум, 05) + + соучастие в программе выведения. +hook: > + Руины-квест: даёт наводку на жену в обмен на задание + ([14](../docs/14-таблица-знаний.md)). Единственный, о ком + пока хоть что-то известно — кандидат в «кто сдаёт героя» + (отложено). +sidequest: интриги амбициозных (09 §6) +relations: + - {to: wife, kind: коллеги; знает о ней больше, чем говорит} + - {to: program-lead, kind: директивы вниз} +notes: | + Научные модули у руин изучают город и осведомлены об ильвах + косвенно ([05](../docs/05-mercury.md)). \ No newline at end of file diff --git a/story/characters/partner-engineer.yaml b/story/characters/partner-engineer.yaml new file mode 100644 index 0000000..0b32b15 --- /dev/null +++ b/story/characters/partner-engineer.yaml @@ -0,0 +1,19 @@ +id: partner-engineer +type: character +name: null +category: cast +status: жив +role: Напарник-инженер экспедиции +act: 1 +wants: > + Делать свою работу хорошо. +hides: > + Замечает двойную игру героя — и молчит. +hook: > + Единственный близкий свидетель; кандидат в полу-союзники + («правда для своих», акт 5). +sidequest: null +relations: + - {to: hero, kind: напарник, единственный видит двойную игру вблизи} +notes: | + Новый персонаж ростера ([06](../docs/06-персонажи.md)). \ No newline at end of file diff --git a/story/characters/poisoned-engineer.yaml b/story/characters/poisoned-engineer.yaml new file mode 100644 index 0000000..c20cb24 --- /dev/null +++ b/story/characters/poisoned-engineer.yaml @@ -0,0 +1,22 @@ +id: poisoned-engineer +type: character +name: null +category: offscreen +status: за кадром (Земля); опционально — группа Б, финал +role: Основной инженер миссии, отравленный героем +act: 0 +wants: > + Вернуться в строй — рвался в экспедицию, попал в дублёры. +hides: > + Что-то знает про своё отравление? +hook: > + Грех пролога возвращается пешком: выходит из корабля группы Б + в финале ([07](../docs/07-сюжет.md)). Опциональный удар — + решить при детализации сцен. +sidequest: null +relations: + - {to: hero, kind: отравлен им; приходит за ним сам} +notes: | + Дубль-проверка: на Земле расследуют отравление → замена + в экипаже → финальному отчёту не верят (механизм провала №2, + [07](../docs/07-сюжет.md)). \ No newline at end of file diff --git a/story/characters/program-lead.yaml b/story/characters/program-lead.yaml new file mode 100644 index 0000000..ac7d453 --- /dev/null +++ b/story/characters/program-lead.yaml @@ -0,0 +1,21 @@ +id: program-lead +type: character +name: null +category: offscreen +status: за кадром (Земля) +role: Ведущий программы выведения «идеальной рабочей силы» +act: 0 +wants: > + Научную славу — «идеальный вид» как его творение. +hides: > + Директивы вниз; как получил данные жены. +hook: > + Не появляется на экране — существует директивами и перепиской. + Для игрока «за кадром» — бесплатный персонаж. +sidequest: null +relations: + - {to: colony-head, kind: переписка, директивы вниз} + - {to: wife, kind: использовал её данные} +notes: | + Мотив программы: покорный дешёвый вид строит города, дышит + местной атмосферой — Zephyr не нужен ([05](../docs/05-mercury.md)). \ No newline at end of file diff --git a/story/characters/trader.yaml b/story/characters/trader.yaml new file mode 100644 index 0000000..8b52cf6 --- /dev/null +++ b/story/characters/trader.yaml @@ -0,0 +1,20 @@ +id: trader +type: character +name: null +category: cast +status: жив +role: Торговец-колонист +act: 1 +wants: > + Торговать — так выжил эти 1,5 месяца. +hides: > + Откуда у него товар военных лабораторий. +hook: > + Гаус-пушка у него ([13](../docs/13-оружие.md)): местный инженер + изготовил и продал ему несколько. Микроэкономика 10 лет колонии. +sidequest: обмен, дефицит (кандидат) +relations: + - {to: colony-head, kind: торгует; товары «уплывают» из лабораторий — интриги амбициозных} +notes: | + Одна из интриг амбициозных: связь торговца с лабораториями + программы (09 §6). \ No newline at end of file diff --git a/story/characters/wife.yaml b/story/characters/wife.yaml new file mode 100644 index 0000000..3b0508b --- /dev/null +++ b/story/characters/wife.yaml @@ -0,0 +1,27 @@ +id: wife +type: character +name: null +category: cast +status: жива — укрытие бежавших (город и модули-лабы, акт 4) +role: Жена героя — талантливый ведущий микробиолог Mercury +act: 0 +wants: > + Остановить зло корпорации — уже сделала, ценой реактора. + Прекратить эксперименты программы выведения ([05](../docs/05-mercury.md)). +hides: > + Жива; где — центральная интрига. Саботаж реактора и антенны — + её руки. Никто не знает, что это она. +hook: > + Единственная, кто собирает картину целиком: алтарь, директивы, + программа ([07](../docs/07-сюжет.md)). Несёт пересборку + «мутанты = результаты» ([14](../docs/14-таблица-знаний.md)). +sidequest: null +relations: + - {to: hero, kind: жена} + - {to: colony-head, kind: данные ушли вверх через него} + - {to: module-head, kind: коллеги по колонии} + - {to: mission-commander, kind: знакома (природа знакомства открыта)} +notes: | + Не обеляется: её саботаж убивал людей, игра это не прячет + ([07](../docs/07-сюжет.md)). Мотив двойной: реакция на зло + + взрыв обесточил лаборатории программы. \ No newline at end of file diff --git a/story/knowledge.yaml b/story/knowledge.yaml new file mode 100644 index 0000000..7df76ce --- /dev/null +++ b/story/knowledge.yaml @@ -0,0 +1,70 @@ +# Кривая правды: что игрок знает / думает / узнаёт по актам. +# Исходник: docs/14-таблица-знаний.md + +acts: + - act: 0 + knows: > + Связь с колонией потеряна; жена там; Mercury отказала ему; + он отравил инженера и занял его место. + thinks: > + Колония в беде; Mercury — обычный работодатель, дом семьи. + learns: > + Свой грех — единственное «зло», которое он пока знает. + question: Что случилось? Жива ли она? + + - act: 1 + knows: > + Поверхность вымерла: реактор взорван, антенна уничтожена, + радиация, следы драки. + thinks: > + Техногенная катастрофа + паника. + learns: > + **Колонисты нашлись — под землёй**, прячутся от радиации + (и потому посылают героя на поверхность); первая фауна — + не мутанты; мельком — документ о «второй экспедиции» + (группа Б, форшадоуинг). + question: Что взорвалось и почему? Где жена? + + - act: 2 + knows: > + Колония пуста, твари по окраинам. + thinks: > + Кто-то виноват — авария всё же не случайна. + learns: > + Саботаж реактора и антенны выполнен профессионально → + след ведёт к жене. Шок-бит. Герой узнаёт раньше экипажа + и скрывает. + question: Зачем ей?! Она монстр? + + - act: 3 + knows: > + Жена — саботажница; он скрывает; экипаж работает по протоколу. + thinks: > + «Может, было за что» — версия шатается, герой ищет оправдание. + learns: > + Руины-квест → первый контакт с ильвами; по возвращении — + ложь «местное зверьё, без интеллекта» (осознанная); алтарь + в окрестностях руин — зверьё не молится; оборудование + «не для изучения зверей»; запись интригана — первые уколы + «твари — не радиация». + question: Что именно она узнала? Кто эти существа на самом деле? + + - act: 4 + knows: > + Корпорация лжёт; жена что-то знает. + thinks: > + Она оправдана? + learns: > + Правда целиком: алтарь, директивы «добить», программа + выведения; пересборка: мутанты = «результаты»; ильвы разумны. + question: Что теперь делать? + + - act: 5 + knows: > + Всю правду. + thinks: > + «План сработает» (иллюзия победы). + learns: > + Исполнение плана → фальшивая победа → группа Б → + ложь опоздала (финал). + question: "— (финал: цена)" \ No newline at end of file diff --git a/story/quests/act0-01-prologue.yaml b/story/quests/act0-01-prologue.yaml new file mode 100644 index 0000000..8746b2b --- /dev/null +++ b/story/quests/act0-01-prologue.yaml @@ -0,0 +1,14 @@ +id: act0-01-prologue +type: event +act: 0 +order: 1 +title: Отравление инженера +summary: > + Пролог на Земле: герой своими руками отравляет (не смертельно) + основного инженера миссии, чтобы занять его место в экспедиции. +leads_to: [act1-01-landing] +tags: [пролог, грех-пролога] +notes: | + Свой грех — единственное «зло», которое герой знает на старте. + Грех возвращается в финале: расследование отравления рушит план + героев ([07](../../docs/07-сюжет.md)). \ No newline at end of file diff --git a/story/quests/act1-01-landing.yaml b/story/quests/act1-01-landing.yaml new file mode 100644 index 0000000..8029ef4 --- /dev/null +++ b/story/quests/act1-01-landing.yaml @@ -0,0 +1,16 @@ +id: act1-01-landing +type: quest +act: 1 +order: 1 +title: Посадка и брифинг +summary: > + Экспедиция прибывает. Брифинг: цель командира — связь, энергия, + выжившие. Своя цель героя — найти жену. +mechanics: [управление, Zephyr на герое] +giver_candidate: crew-commander +leads_to: [act1-02-terminal] +location: пусковая площадка +tags: [туториал] +notes: | + Зеркало финала: та же площадка, тот же ассет посадки — группа Б + опустится сюда же ([07](../../docs/07-сюжет.md)). \ No newline at end of file diff --git a/story/quests/act1-02-terminal.yaml b/story/quests/act1-02-terminal.yaml new file mode 100644 index 0000000..6a7bc2c --- /dev/null +++ b/story/quests/act1-02-terminal.yaml @@ -0,0 +1,17 @@ +id: act1-02-terminal +type: quest +act: 1 +order: 2 +title: Терминал площадки без питания +summary: > + Первый урок «батарея = жизнь»: вставил универсальную батарею — + терминал заработал. +mechanics: [батарея] +giver_candidate: crew-commander +leads_to: [act1-03-streets] +location: пусковая площадка +tags: [туториал] +notes: | + Универсальная батарея — взаимозаменяемая с Zephyr + ([12](../../docs/12-снаряжение.md)): этот момент задаёт выбор + «выстрел или дыхание» на всю игру. \ No newline at end of file diff --git a/story/quests/act1-03-streets.yaml b/story/quests/act1-03-streets.yaml new file mode 100644 index 0000000..d53dca8 --- /dev/null +++ b/story/quests/act1-03-streets.yaml @@ -0,0 +1,16 @@ +id: act1-03-streets +type: quest +act: 1 +order: 3 +title: Пустые улицы и коридоры +summary: > + Поверхность вымерла — «куда все?». Атмосферный проход: следы + паники, брошенные вещи, следы драки. +mechanics: [атмосфера] +giver_candidate: crew-commander +leads_to: [act1-04-substation] +location: центральная часть колонии +tags: [атмосфера] +notes: | + Ответ на «куда все» приходит в звене 5: колонисты прячутся + под землёй, боятся радиации. \ No newline at end of file diff --git a/story/quests/act1-04-substation.yaml b/story/quests/act1-04-substation.yaml new file mode 100644 index 0000000..f73d37e --- /dev/null +++ b/story/quests/act1-04-substation.yaml @@ -0,0 +1,16 @@ +id: act1-04-substation +type: quest +act: 1 +order: 4 +title: Подстанция +summary: > + Первый инструмент: мультитул. Свет зажигается посекторно — + колония «оживает». +mechanics: [мультитул] +giver_candidate: crew-commander +leads_to: [act1-05-command] +location: подстанция +tags: [туториал] +notes: | + Мультитул — ключ к физическим препятствиям + (09 §4, [12](../../docs/12-снаряжение.md)). \ No newline at end of file diff --git a/story/quests/act1-05-command.yaml b/story/quests/act1-05-command.yaml new file mode 100644 index 0000000..4792db0 --- /dev/null +++ b/story/quests/act1-05-command.yaml @@ -0,0 +1,19 @@ +id: act1-05-command +type: quest +act: 1 +order: 5 +title: Жилой сектор → командный модуль +summary: > + Отчитаться о запитке. Первые живые колонисты: они прячутся + в помещениях и подземных структурах, боятся выходить + на поверхность из-за радиации. Знакомство с персонажами — + будущими квестодателями. +mechanics: [диалоги, первые NPC] +giver_candidate: crew-commander +leads_to: [act1-06-greenhouse-server] +location: жилой сектор, командный модуль +tags: [каста, квест-двигатель] +notes: | + Квест-двигатель акта: колонисты сами не пойдут наверх — + посылают героя ([15](../../docs/15-квесты-акт-1.md)). + Здесь же — первые побочные квесты (фон для будущих актов). \ No newline at end of file diff --git a/story/quests/act1-06-greenhouse-server.yaml b/story/quests/act1-06-greenhouse-server.yaml new file mode 100644 index 0000000..9c9319b --- /dev/null +++ b/story/quests/act1-06-greenhouse-server.yaml @@ -0,0 +1,22 @@ +id: act1-06-greenhouse-server +type: quest +act: 1 +order: 6 +title: "Через теплицу → серверная: запустить ИИ" +summary: > + Квест от персонажа под командованием: пройти через теплицу + (производство еды), цель — серверная. Электричество уже есть — + запускаем ИИ колонии. Первый бой — с местным животным + (не мутантом). +mechanics: [первый бой — местное животное, клипсомёт/резак] +giver: null +giver_candidate: агроном теплицы (кандидат, не в ростере — отложено) +leads_to: [act1-07-antenna] +unlocks: [ai-colony] +location: теплица, серверная +tags: [туториал, бой, первый-бой] +notes: | + ИИ — новый «персонаж»-интерфейс; активация здесь делает его + квестодателем следующих звеньев (логи → реакторная зона). + Теплица ≠ оранжерея: теплица — производство еды, оранжерея — + место отдыха ([15](../../docs/15-квесты-акт-1.md)). \ No newline at end of file diff --git a/story/quests/act1-07-antenna.yaml b/story/quests/act1-07-antenna.yaml new file mode 100644 index 0000000..bc10a93 --- /dev/null +++ b/story/quests/act1-07-antenna.yaml @@ -0,0 +1,16 @@ +id: act1-07-antenna +type: quest +act: 1 +order: 7 +title: "Антенна: оценить ущерб" +summary: > + Инженерная задача по заданию: добраться до дальней связи, + оценить повреждения антенны. Вывод: нужны детали. +mechanics: [инженерная диагностика] +giver_candidate: crew-commander +leads_to: [act1-08-hangar] +location: антенна дальней связи +tags: [инженерия] +notes: | + Антенна уничтожена саботажем — профессионализм исполнения + игрок увидит позже (акт 2); пока читается как «авария». \ No newline at end of file diff --git a/story/quests/act1-08-hangar.yaml b/story/quests/act1-08-hangar.yaml new file mode 100644 index 0000000..2c82ae9 --- /dev/null +++ b/story/quests/act1-08-hangar.yaml @@ -0,0 +1,16 @@ +id: act1-08-hangar +type: quest +act: 1 +order: 8 +title: "Ангар: детали и инструментарий" +summary: > + Сбор в ангаре: детали для ремонта антенны + инструментарий. +mechanics: [сбор, исследование] +giver_candidate: crew-commander +leads_to: [act1-09-repair] +location: ангар +tags: [подготовка] +notes: | + В ангаре логично подсунуть первое упоминание «группы Б» — + форшадоуинг финала ([07](../../docs/07-сюжет.md)): документ + «подготовка второй экспедиции», мимо которого пробегаешь. \ No newline at end of file diff --git a/story/quests/act1-09-repair.yaml b/story/quests/act1-09-repair.yaml new file mode 100644 index 0000000..970bd52 --- /dev/null +++ b/story/quests/act1-09-repair.yaml @@ -0,0 +1,17 @@ +id: act1-09-repair +type: quest +act: 1 +order: 9 +title: Ремонт антенны → связь с модулями +summary: > + Антенна починена — локальная связь с модулями работает. Эфир + модулей пуст + один странный сигнал. +mechanics: [дальняя связь] +giver_candidate: crew-commander +leads_to: [act1-10-reactor] +location: антенна +tags: [инженерия, семафор] +notes: | + Странный сигнал — семафор на акты 3–4 (не раскрываем). + Дальняя связь с Землёй всё ещё нет — починка затягивается, + это первое, что настораживает экипаж ([14](../../docs/14-таблица-знаний.md)). \ No newline at end of file diff --git a/story/quests/act1-10-reactor.yaml b/story/quests/act1-10-reactor.yaml new file mode 100644 index 0000000..126d45e --- /dev/null +++ b/story/quests/act1-10-reactor.yaml @@ -0,0 +1,18 @@ +id: act1-10-reactor +type: quest +act: 1 +order: 10 +title: Реакторная зона +summary: > + Странности в логах сервера (ИИ) приводят в реакторную зону. + Реактор взорван; следы умышленного вмешательства — но игрок + ещё не складывает картину. +mechanics: [дозиметр, gate радиации] +giver: ai-colony +leads_to: [act1-11-shock] +location: реакторная зона +tags: [инженерия, детектив, радиация] +notes: | + Дозиметр — новый прибор: «видеть невидимое» (09 §4). + Здесь игрок впервые стоит рядом с местом саботажа жены — + не зная об этом. \ No newline at end of file diff --git a/story/quests/act1-11-shock.yaml b/story/quests/act1-11-shock.yaml new file mode 100644 index 0000000..b2b530f --- /dev/null +++ b/story/quests/act1-11-shock.yaml @@ -0,0 +1,12 @@ +id: act1-11-shock +type: event +act: 1 +order: 11 +title: "Шок-бит: саботаж — и след ведёт к жене" +summary: > + Мост в акт 2: саботаж реактора и антенны выполнен + профессионально — и след ведёт к жене героя. Герой узнаёт + раньше экипажа — и скрывает. +tags: [шок-бит, поворот] +notes: | + Конец акта 1, начало двойной игры ([14](../../docs/14-таблица-знаний.md)). \ No newline at end of file diff --git a/story/quests/act1-12-drone.yaml b/story/quests/act1-12-drone.yaml new file mode 100644 index 0000000..0929202 --- /dev/null +++ b/story/quests/act1-12-drone.yaml @@ -0,0 +1,21 @@ +id: act1-12-drone +type: quest +act: 1 +order: 12 +side: true +after: act1-10-reactor +title: Запуск дрона (тайком) +summary: > + Отдельный квест: герой запускает дрон-разведчик в личных целях, + тайком, попутно кому-то наврав. Советует запустить — ИИ колонии. +mechanics: ["дрон: карта, вид сверху, стрельба за пределы видимости"] +giver: ai-colony +location: — +tags: [сайд, дрон, двойная-игра] +notes: | + Момент — первые потребности выйти за пределы центральной части + колонии (стык актов 1–2): дрон открывает карту и вид сверху + ровно тогда, когда игра расширяет границы + ([12](../../docs/12-снаряжение.md), [15](../../docs/15-квесты-акт-1.md)). + Тайком + наврав = первая маленькая двойная игра, задолго + до акта 3. \ No newline at end of file