# Психика NPC и режиссёр рейда — `$.alive`

Порт из audm-neko: `game/alive2d/npc/alive_psyche.gd` (786 строк) и
`game/alive2d/director/alive_raid_director.gd` (149). Оригинал почти не трогает
Godot — психика это числа и события, поэтому перенесена целиком.

```js
const mind = $.alive.psyche('veteran', { aggression: 0.8, skill: 0.7 });
mind.onHurt(0.4, true);          // ранение
mind.onAllyDown(true);           // лидер отряда погиб
mind.tick(dt, { leaderNear: true, underFire: true });
mind.aimErrorMult();             // множители для боя
mind.isBroken();                 // срыв?

const dir = $.alive.director({ seed: 1, lootValue: () => $.inv.value() });
dir.tick(dt, { playerHp: 0.8, enemies: [{ id: 'a', alive: true, kit: {} }] });
dir.phase();                     // 'spare' | 'even' | 'press'
```

---

## 1. Три слоя — как в оригинале

* **Статика** — психотип (`$.alive.types()` — 44 акцентуации по Личко/Леонгарду),
  14 черт, OCEAN (невротизм и экстраверсия выводятся из черт, если не заданы),
  тренированность, роль в отряде.
* **Динамика** — страх, пульс, подавление, усталость, ярость, стресс, голод,
  холод, глухота.
* **Триггеры** — ранение, смерть союзника, пролёт пули, взрыв, окружение,
  убийство: спайк и затухание **по чертам**, а не константой.

## 2. Черты и акцентуация

Черты: `aggression`, `caution`, `curiosity`, `greed`, `jumpiness`, `patience`,
`morale` (−1…1), `skill`, `sociability`, `honesty`, `empathy`, `fear`, `madness`,
`talkativeness`. Значения по умолчанию — `TRAIT_DEFAULTS`.

Психотип даёт **поправки поверх черт**: `fg` — множитель страха, `rg` — ярости,
`fr` — склонность к ступору, `br` — веса срывов. Так ветеран держит позицию,
новичок паникует, эпилептоид свирепеет от раны, астеник застывает, а эмпат
медлит в упор.

```js
$.alive.types();                       // 44 психотипа
$.alive.typeBias('berserker');         // { fg, rg, fr, br }
$.alive.traits();                      // список черт
```

## 3. Роль в отряде

Лидером становится явная метка, либо психотип из `LEADER_TYPES` (офицер,
ветеран) при `skill >= 0.65` и морали не ниже −0.1. Ведомый (`follower`) без
лидера рядом **сыпется** (затухание страха становится отрицательным), с лидером
— остывает быстрее (Маршалл). `leadership()` — качество лидера.

## 4. Эффекты (Гроссман)

| Метод | Смысл |
|---|---|
| `overload()` | доля пути от порога туннельного зрения до предела пульса |
| `aimErrorMult()` | ошибка прицела: дрожь и «мимо» при высоком пульсе и страхе |
| `tremorMult()` | микродрожь руки (усиливается холодом) |
| `reactionMult()` | **время** реакции (>1 — медленнее соображает) |
| `reloadMult()` | **время** перезарядки (>1 — мелкая моторика села) |
| `visionConeMult()` | обзор: пульс выше 140 сужает картинку |
| `hearingMult()` | слух: отчуждение под пульсом и глухота после взрыва |
| `freezeChance(dist)` | шанс ступора лицом к лицу |
| `killingHesitation(dist)` | барьер убийства: заминка перед выстрелом в упор |

Пульс: покой 62, предел 200, порог туннельного зрения 140. Вверх разгоняется
быстро, вниз остывает медленно (тахикардия боя).

## 5. Срывы (Darkest Dungeon)

На пороге стресса (0.72) проверка с шансом по психотипу даёт **панику, ярость,
ступор, сдачу или молитву**, плюс «взгляд на 1000 ярдов» (CSR) у вымотанных и
травмированных. Длительность у каждого своя, после срыва — откат 16 с и
опустошение.

```js
mind.breakChance();       // шанс на текущем стрессе
mind.breakWeights();      // веса видов (лидер не бежит, вымотанный смотрит в пустоту)
mind.checkBreak();        // проверить и, может быть, сорваться
mind.forceBreak('panic'); // принудительно (скрипты, тесты)
mind.breakLeft; mind.breakTitle(); mind.breakCooldown;
```

## 6. Моральная травма (Шэй, Litz)

`onKill` и `onAllyDown` копят `moralInjury`. Внутри рейда она не спадает,
между рейдами живёт через `carryMoralInjury(value)`. Даже полностью
травмированный держит 30% характера (`TRAUMA_FLOOR`) — иначе «не лутает и не
прячется» становится не травмой, а выключенным NPC.

| Метод | Смысл |
|---|---|
| `anhedonia()` | интерес к добыче гаснет |
| `greedMult()` | множитель жадности (не ниже пола) |
| `cautionMult()` | множитель осторожности (утрата самосохранения) |
| `selfPreservationLoss()` | насколько перестал себя беречь |
| `carryMoralInjury(v)` | принять травму прошлых рейдов |

Первое убийство переживается фазами: **эйфория → отрицание → раскаяние**, и
эмпата в упор может вырвать (`vomit`).

## 7. Режиссёр рейда

Фазы: `spare` (щадит) → `even` (ровно) → `press` (давит). Решение
пересматривается раз в 4 с: пока рейд молод и рюкзак пуст — щадит; как только
рюкзак набит (900) или рейд долгий (>300 с) и рюкзак приличный (450) — давит.

`knobsFor(phase)` — настройки давления для игры: `mercy` (пощада), `let_win`
(шанс дать выиграть), `focus` (фокус на игроке).

В фазе `press` режиссёр посылает «загонщика» к **самому слабо вооружённому**
бойцу, который ни с кем не дерётся, — и **не добивает** игрока: при здоровье
ниже 35% охота не начинается. У бойца появляется назначение
(`objectiveFor(id)` → `{ pos, score: 0.62 }`) со сроком жизни 28 с.

## 8. Связь с узлами

```js
$.alive.attach('#npc', 'rookie');
$.alive.of('#npc');                      // психика узла
$.alive.tick(dt);                        // один тик на все привязанные
```

`tick` ставит узлу `fear`, `pulse`, `stress`, `broken` — игра читает их для
анимации и интерфейса.

## 9. Ограничения (честно)

* **тело не переносилось**: `alive_npc_core.gd` (1 289 строк), `alive_mover.gd`,
  `alive_nav_graph.gd` — это движение и навигация за игроком; портирована
  только «психика внутри»;
* **`$.alive.tick` не управляет поведением**: срыв — только решение; что делает
  сломанный боец (бежит, застывает, сдаётся) — на игре;
* **зрение и слух не считаются**: `alive_senses.gd` не портирован, множители
  `visionConeMult`/`hearingMult` отданы наружу;
* **панель и метр навыка** (`alive_panel.gd`, `alive_skill_meter.gd`) не
  портированы — это интерфейс;
* **память между рейдами — только значения**: `alive_memory.gd` (записи NPC,
  остывание травмы за сутки) не портирован, `carryMoralInjury` принимает число;
* **`hunger`/`cold` — числа**: `$.world.time` к ним не привязан, ночь ставится
  вручную (`mind.night(true)`).

## 10. Проверка

```bash
# OCEAN, роли, спайки, пульс, срывы, травма, фазы режиссёра
build/_deps/quickjs-build/qjs tests/js/alive_test.mjs
```
