squad-proto/docs/21-damage.md
z.kirill 28a9e5b6e4 Триггеры: четыре условных эффекта на аффиксах (M28)
ON_HIT поджигает цель, ON_KILL даёт рывок скорости стаками, ON_HURT
обжигает того, кто ударил, ON_DOWN даёт отряду ярость за павшего.
Отдельным тегом аффикса, то есть по тем же правилам, что и всё
остальное: платит по тиру, влезает в бюджет, катается и прививается.

СДЕЛАНО НЕ ТАК, КАК ПРЕДЛАГАЛ ДИЗАЙН. В docs/21-damage.md §4 стояло:
журнал боевых событий уже есть, вешайте триггеры на него. Звучит
правильно, и сделать так нельзя: sim/events.h это канал «симуляция ->
эффекты», односторонний по устройству, и в headless-прогонах его нет
вовсе. Начни правила боя его читать — игра станет вести себя по-разному
с окном и без, то есть --accept начнёт мерить не то, что меряет.

Триггеры срабатывают в месте урона. Опасение «два канала разойдутся»
снято иначе: второго канала никто и не завёл, есть четыре точки рядом с
тем уроном, который триггер обслуживает.

Четыре из девяти, и это не «пока»: каждый эффект обязан класть своё
число в уже существующий канал влияния. Крита в игре нет, площадного
урона нет, выдачи предметов на входе нет — заводить их ради строк
таблицы значит завести пять мёртвых веток. Причины выписаны в документе.

Три ограничителя проверяются числами в --descend:

  триггеры: аффикс доехал да, ON_HIT поджёг стаков 1, откат 0.9 с
            (за него наложено 0), ON_KILL стаков 1, скорость 1.06
  рекурсия: за один шаг стаков прибавлялось максимум 1

Первая строка про проводку: аффикс с триггером легко сделать так, что он
лежит в предмете, показывается в подсказке и не делает ничего.

Проверка сперва брала targets[0] и проходила на descent по везению: на
hollow первым в пуле стоял ящик, а обстановка не горит и убийством не
считается. Теперь цель ищется по виду и ставится в свободную клетку с
прямой видимостью — иначе замеряется планировка этажа, а не триггеры.

Шаг симуляции 3.7 мкс из 16666. --accept PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 03:32:12 +03:00

369 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 21 — Урон: стихии, статусы, реакции, триггеры
Продолжение [20-gdd.md](20-gdd.md). Здесь вся боевая арифметика.
Главное ограничение, из которого всё следует: **в игре нет ручных действий**
(спек 5). Игрок водит диск и строит отряд; ни одна механика ниже не вводит
кнопку. Стихии накладываются оружием, реакции срабатывают сами, эффекты висят
на снаряжении. Мастерство — это ЧТО ты взял вниз и КУДА повёл отряд, а не как
быстро ты нажимаешь.
## 1. Семь стихий
| Код | Имя | Что делает | Против кого |
|---|---|---|---|
| KIN | Кинетика | оглушение, отбрасывание | универсально, режется бронёй |
| INC | Жар | горение, распространение при смерти | мягкие, скопления |
| CRY | Холод | замедление, заморозка | быстрые, рывковые |
| TOX | Кислота | снятие брони, урон от макс. HP | бронированные, жирные |
| ARC | Дуга | цепь по целям, наводка | толпы, слабые |
| RAD | Радиация | взрыв при смерти, заражение | стаи, respawn-твари |
| VOID | Пустота | срезание сопротивлений, притяжение | всё, что защищено |
Стихии вводятся по актам ([20-gdd.md](20-gdd.md) §3), а не разом: игрок должен
успеть понять каждую, прежде чем получит следующую.
### 1.1. Статусы
Каждая стихия кладёт **стаки** статуса. Стак живёт `dur`, сгорает по одному.
| Статус | Стаки | Эффект за стак | Потолок |
|---|---|---|---|
| Горение (INC) | 5 | 6% урона удара в секунду, 4 с | при смерти поджигает в радиусе 1.5 |
| Оледенение (CRY) | 10 | 4% скорости, 3 с | 10 стаков → Заморозка 1.4 с, +50% получаемого KIN |
| Разъедание (TOX) | 8 | 4% брони, 5 с | +0.8% макс. HP цели в секунду |
| Разряд (ARC) | 4 | +5% получаемого крита, 3 с | 4 стака → цепь на 2 цели по 40% |
| Заражение (RAD) | 6 | +3% получаемого урона, 6 с | при смерти взрыв 60% макс. HP, R=2.2 |
| Схлопывание (VOID) | 3 | 8% всех сопротивлений, 4 с | 3 стака → притяжение к центру |
Числа подобраны под один принцип: **один стак не значит ничего, полный набор
меняет бой**. Это заставляет выбирать цель и держать её, а не поливать всех.
## 2. Формула урона
Одна формула на всё — и на пулю, и на клинок, и на тик горения.
```
┌ базовый урон источника
dmg_raw = W · (1 + Σ add) · Π mult
dmg_elem = dmg_raw · share(e) для каждой стихии e удара
mitig(e) = 1 clamp(R(e) + R_aff PEN(e), 0.60, +0.85)
dmg_final = Σ_e dmg_elem · mitig(e) · crit · vuln
```
| Символ | Что | Диапазон |
|---|---|---|
| `W` | урон оружия (каталог `sim/weapon_defs.h`) | 8 → 900 по глубине |
| `Σ add` | аддитивные проценты (аффиксы «+X% урона») | 0 → 4.5 |
| `Π mult` | множители (специализации, реакции, метки) | 1.0 → 3.0 |
| `share(e)` | доля стихии в уроне, Σ = 1 | конверсия аффиксами |
| `R(e)` | сопротивление вида (каталог) | 0.5 → 0.9 |
| `PEN(e)` | пробитие сборки | 0 → 0.75 |
| `crit` | 1.0 или `critMult` | 1.5 → 4.0 |
| `vuln` | уязвимости от статусов | 1.0 → 2.2 |
**Почему сопротивление режется до 0.60, а не глубже.** Отрицательное
сопротивление — это уязвимость, и она обязана быть ощутимой, но не абсурдной:
при 2.0 одна верная стихия обесценивала бы всю остальную сборку.
**Почему потолок +0.85, а не 1.0.** Иммунитета нет ни у кого. Тварь, которую
физически нельзя ранить, — это не сложность, а тупик: игрок без нужной стихии
просто не может играть.
### 2.1. Броня — отдельно от сопротивлений
Броня режет только KIN и работает долей с жёстким потолком `ARMOR_CAP = 0.72`
(уже в коде). TOX снимает броню стаками — это единственный способ пробить
тяжёлую цель без пробития в сборке.
### 2.2. Крит
```
critChance = clamp(0.05 + Σ aff, 0, 0.75)
critMult = 1.5 + Σ aff (без потолка)
```
Шанс с потолком, множитель без — потому что «всегда крит» убивает разброс
результата, а «очень больно раз в четыре удара» его сохраняет.
## 3. Реакции
Две РАЗНЫЕ стихии на одной цели в пределах 3 секунд дают реакцию. Реакция
срабатывает сама и снимает оба статуса.
| Реакция | Пара | Эффект |
|---|---|---|
| **Растрескивание** | INC + CRY | всплеск 250% APow в точке, +80% к следующему KIN |
| **Возгорание** | INC + TOX | облако R=3.0, 180% APow в секунду, 3 с |
| **Сверхпроводник** | ARC + CRY | 30% всех сопротивлений цели и соседей, 6 с |
| **Электролиз** | ARC + TOX | все DoT на цели ×3 на 3 с |
| **Распад** | RAD + INC | зона R=2.5 на 8 с, 60% APow в секунду, кладёт RAD |
| **Пепел** | RAD + CRY | цель не воскресает и не респавнится |
| **Резонанс** | VOID + любая | реакция повторяется вторым срабатыванием на 60% |
| **Схлоп** | VOID + ARC | притяжение всех в R=4 к цели, оглушение 1 с |
| **Осколки** | KIN + CRY | замороженная цель при смерти бьёт осколками, 120% APow |
| **Прорыв** | KIN + TOX | 40% брони цели на 5 с, игнорируя потолок |
### 3.1. Сила реакции не зависит от оружия
```
APow(d) = 4 + 0.12·d (d — глубина этажа)
reaction = k · APow(d) · (1 + reactionBonus)
```
Это ключевое решение. Реакция считается **от глубины, а не от урона оружия**
иначе на сотом этаже реакции стали бы округлением, и вся система стихий
превратилась бы в украшение. Так же поступает Genshin с Elemental Mastery, и по
той же причине.
Следствие: **реакции — это канал «знания»** из закона масштаба
([20-gdd.md](20-gdd.md) §2). Они растут сами по глубине, но только у того, кто
умеет их запускать.
### 3.2. Внутренний откат
У каждой пары свой ICD: 1.2 с на цель. Без него цепь ARC по десяти тварям
взорвала бы этаж одним нажатием, которого к тому же нет.
## 4. Триггеры: on-hit, on-kill, on-death
Сделано в M28, но **не так, как предлагал этот раздел**.
Здесь стояло: журнал боевых событий уже есть (`sim/events.h`), вешайте триггеры
на него, второй канал разойдётся с первым. Звучит правильно, и сделать так
нельзя.
`sim/events.h` — канал «симуляция → ЭФФЕКТЫ», односторонний по устройству:
симуляция в него только пишет, а в headless-прогонах указателя на журнал нет
вовсе. Начни правила боя его **читать** — и игра станет вести себя по-разному с
окном и без окна, то есть `--accept` начнёт мерить не то, что меряет сейчас.
Поэтому триггеры срабатывают **там же, где случается событие**: в месте
нанесения урона, в месте смерти цели, в месте попадания по бойцу. Журнал остался
тем, чем был. Опасение из первой редакции («два канала разойдутся») снято иначе:
второго канала событий никто и не завёл — есть четыре точки в коде, и все они
рядом с тем уроном, который триггер и обслуживает.
Сделаны четыре, и это не «пока четыре»: каждый обязан класть свой эффект в уже
существующий канал влияния, и остальные пять таких каналов не имеют.
| Триггер | Когда | Что делает | Откат |
|---|---|---|---|
| `on_hit` | попадание по твари | поджигает её (стак INC) | 0.9 с |
| `on_kill` | тварь умерла от тебя | +6% скорости, стак до 5, набор живёт 3 с | 0.5 с |
| `on_hurt` | по тебе попали | обжигает ТОГО, кто попал | 1.5 с |
| `on_down` | твой оперативник пал | +25% урона всему отряду на 10 с | 8 с |
Не сделаны и почему:
- `on_crit` — крита в игре нет;
- `on_death` (любая смерть) — некому платить: у трупа нет владельца, а «взрыв
трупа» это площадной урон, которого пока нет ни у чего;
- `on_attack`, `on_floor`, `on_extract` — эффекты вида «бесплатный выстрел» и
«+1 бинт» требуют своих каналов (боезапас в момент выстрела, выдача предметов
на входе), и заводить их ради одной строки таблицы значит завести пять мёртвых
веток. Так уже вышло с реакциями: шесть из десяти ждут систем, которых нет.
Ярость за павшего — **отрядная**, а не личная: эффект называется «отряд получает
+25% урона», и считать его на каждом бойце отдельно значило бы, что павшего
оплакивает только сосед.
### 4.1. Против взрыва комбинаций
Три ограничителя, все обязательные:
1. **ICD у каждого эффекта.** Минимум 0.5 с, у мощных — до 8 с.
2. **Бюджет строки.** Предмет несёт суммарный бюджет `B(ilvl) = 40 + 3·ilvl`;
каждый аффикс стоит по тиру ([22-loot.md](22-loot.md) §3). Шесть сильнейших
аффиксов не влезают в один предмет никогда.
3. **Запрет рекурсии.** Эффект, сработавший от триггера, не может запустить тот
же триггер: `on_kill` → взрыв → смерть → `on_kill` обрывается на первом шаге.
Без третьего пункта одна цепь ARC на скоплении из сорока тварей вешает игру —
и это не гипотеза, а то, как ломаются все игры с триггерами.
Все три проверяются в `--descend`, и проверяются ЧИСЛАМИ:
```
триггеры: аффикс доехал да, ON_HIT поджёг стаков 1, откат 0.9 с (за него наложено 0),
ON_KILL стаков 1, скорость 1.06
рекурсия: за один шаг стаков прибавлялось максимум 1
```
Первая строка — про проводку: аффикс с триггером легко сделать так, что он лежит
в предмете, показывается в подсказке и не делает ничего. Вторая — про рекурсию:
поджиг от триггера сам урона не наносит и второй раз триггер не запускает,
поэтому больше одного стака за шаг появиться не может.
Шаг симуляции после вехи — 3.7 мкс при бюджете кадра 16666.
## 5. Твари: сопротивления и роли
Сопротивление — свойство ВИДА, из каталога проекта. Роль читается по профилю
сопротивлений, и это единственная подсказка, которую игра даёт напрямую.
| Вид | Акт | KIN | INC | CRY | TOX | ARC | RAD | Роль |
|---|---|---|---|---|---|---|---|---|
| Бредущий | I | .0 | .2 | .0 | .0 | .0 | .3 | мясо |
| Бегун | I | .0 | .0 | .4 | .0 | .2 | .0 | давление |
| Плевок | II | .1 | .0 | .0 | .5 | .3 | .2 | дистанция |
| Хват | III | .3 | .3 | .1 | .0 | .0 | .0 | контроль |
| Громила | III | .5 | .2 | .3 | .4 | .0 | .1 | танк |
| Рой | IV | .2 | .5 | .0 | .0 | .4 | .4 | толпа |
| Скорлупа | IV | .7 | .3 | .4 | .5 | .3 | .2 | стена |
| Плакальщик | V | .2 | .0 | .0 | .2 | .2 | .6 | воскрешает |
| Пустотник | VI | .3 | .3 | .3 | .3 | .3 | .3 | требует VOID |
Читается так: против Скорлупы бесполезно всё, кроме TOX; Плакальщика надо жечь
радиацией, иначе он поднимет остальных; Пустотник равномерно защищён, и его
берут только срезанием сопротивлений.
**Ни у одного вида нет множителя HP от глубины.** Глубже выходят ДРУГИЕ виды —
правило из [16-descent.md](16-descent.md), и оно не отменяется до Бездны
([23-endgame.md](23-endgame.md) §2, где отменяется осознанно и с объяснением).
## 6. Коридор TTK
Чтобы бой не превращался ни в тир, ни в губку, время убийства держится в
коридоре — это проверяемое обещание, а не пожелание.
| Цель | TTK одним оперативником, с верной стихией | с неверной |
|---|---|---|
| мясо своей полосы | 0.61.2 с | 24 с |
| давление | 1.02.0 с | 48 с |
| танк | 47 с | 1540 с |
| элита (мутант) | 815 с | не убивается до отката ресурсов |
Разрыв «верная/неверная» — от ×3 до ×6. Меньше — стихии перестают значить,
больше — игра требует энциклопедии перед каждым спуском.
Проверяется инструментом `--tool ttk <глубина>`: он печатает таблицу «вид ×
сборка → секунды» и валится с кодом 1, если хоть одна пара вышла из коридора.
## 6.1. Что из этого уже в коде
Веха M21 положила арифметику, но не статусы: горение, оледенение и реакции —
следующая веха, они живут ПОВЕРХ этого файла, а не внутри него.
| Что | Где |
|---|---|
| стихии, сопротивления, формула | [src/sim/element.h](../src/sim/element.h) |
| профиль вида | `EnemyDef::resist`, [src/sim/target.cpp](../src/sim/target.cpp) |
| состав удара оружия | `FirearmDef::elem`, `BladeDef::elem` |
| единственная точка урона | `ResolveDamage` в `bullet.cpp` и в клинке |
| проверка | `squad_proto --tool ttk` |
Стихии есть только у НАХОДИМОГО оружия. Карабин, пистолет и сабля — чистая
кинетика, потому что на них меряется приёмка, и её числа обязаны остаться
теми же.
### Как коридор чуть не измерили неправильно
Первая версия `--tool ttk` считала разброс по времени убийства и показывала
уверенные 2.8x на каждой твари. Выглядело как готовая система — а на самом деле
это была разница МОЩНОСТИ стволов: магнум лучше пистолета против всех, и стихии
тут ни при чём.
С вынесенной за скобки мощностью оружия правда оказалась другой: **1.041.36x**,
то есть состав удара не значил почти ничего. Механика была декоративной ровно в
том же смысле, в каком когда-то был вес.
Чтобы разброс появился, у каждой твари должны быть И уязвимость, И сопротивление
среди тех стихий, которые вообще носит оружие. Одной уязвимости мало: если ни
один ствол не бьёт тем, что тварь держит, минимум остаётся единицей.
```
ENEMY rifle pistol shotgun smg magnum bow ELEM
SHAMBLER 7.5 10.1 8.3 7.2 3.9 6.0 1.90x
SPITTER 4.4 5.9 6.1 2.3 2.2 7.0 2.61x
BRUTE 46.5 62.4 44.2 18.0 14.4 12.1 3.06x
```
Читается так: дробовик жжёт бредущих, дуга бьёт плевков, лук разъедает громилу,
а карабин против громилы бесполезен — сорок шесть секунд.
Коридор пока 1.88.0, а не заявленные 36: чтобы дотянуть до тройки, нужны почти
чистые стихийные сборки, а они приходят с аффиксами ([22-loot.md](22-loot.md)).
Занижать цель в документе нельзя, поэтому в инструменте стоит достижимое, а в
таблице выше — целевое.
## 6.2. Статусы и реакции в коде (M22)
| Что | Где |
|---|---|
| правила статусов, потолки, реакции, числа | [src/sim/status.h](../src/sim/status.h) |
| наложение и запуск реакции | [src/sim/reactions.cpp](../src/sim/reactions.cpp) |
| тик горения и сгорание стаков | `UpdateStatuses`, зовётся из `UpdateTargets` |
| проверка | `squad_proto --tool reactions` |
Реализованы четыре реакции из десяти: SHATTER, IGNITE, SUPERCOND, DECAY.
Остальные ждут систем, которых пока нет (крит, воскрешение, притяжение), и
дописывать их «пока не работает» значило бы завести шесть мёртвых веток.
Два отступления от документа, оба сознательные:
- **кислота и дуга увеличивают получаемый урон**, а не снимают броню и не
вешают метку крита: у тварей в этой игре брони как числа нет, а крита нет
вовсе. Эффект тот же, лишней сущности не заводится;
- **SUPERCOND кладёт стаки пустоты** вместо отдельного «среза сопротивлений»:
пустота в этой игре и означает «защита не работает», и второе имя для того же
было бы просто вторым именем.
Горение считается от МАКСИМАЛЬНОГО здоровья цели, а не от урона удара. Иначе на
громиле оно не значит ничего, а мелочь дожигает само.
### Три ограничителя проверяются, а не подразумеваются
```
REACTION TARGET NEIGHBOUR STACKS
SHATTER 13.0 0.0 0
IGNITE 9.4 4.7 0
SUPERCOND 3.1 0.0 0
DECAY 6.2 3.1 0
откат: вторая пара внутри 1.2 c нанесла 0.0
рекурсия: урон от реакции наложил стаков 0
сила реакции: D1 4.1, D50 10.0, D100 16.0
```
Столбец STACKS — сколько стихий осталось после реакции. Ноль обязателен: не
погасив пару, следующий удар сложил бы её снова, и реакция стала бы постоянным
фоном вместо события.
Шаг симуляции после вехи — 2.9 мкс при бюджете кадра 16666: статусы на
шестидесяти четырёх целях ничего не стоят.
## 7. Свет как ресурс
Сделано в M23, но **не по формуле, которая стояла здесь раньше**. В ней расход
считался от времени:
```
заряд(t) = заряд₀ drain·t, drain = 1/сек <- ТАК НЕ СДЕЛАНО
```
Это таймер. Игра заявлена без таймера намеренно
([17-extraction.md](17-extraction.md)): давить обязано исчерпание, а не часы.
Фонарь, садящийся сам по себе, торопил бы игрока ровно как секундомер, только
без циферблата, — и думать над этажом стало бы дорого.
Как сделано:
```
заряд ← заряд путь · drain(глубина), drain = 1 + 0.012·глубина
```
Путь — смещение центра отряда за шаг. Стоять бесплатно, идти дорого. Полного
заряда (420) хватает примерно на четыре прямых пути до спуска, то есть на этаж
с осмотром и не больше.
Радиус не тает плавно вместе с зарядом, как предполагала старая формула:
фонарь либо светит полностью, либо не светит вовсе (обзор 35%, твари замечают в
1.35 раза дальше). Плавное угасание — это «стало чуть хуже» пять минут подряд,
и заметить момент, когда пора наверх, по нему нельзя. Обрыв заметен сразу.
Батареи — расходник (вес 0.3), встают в фонарь сами при разряде, как бинты.
Это второй после патронов ресурс, который гонит наверх, и он же — причина, по
которой ветвь Разведки в дереве узлов не «приятная», а обязательная.