squad-proto/src/sim/affix.h
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

170 lines
9.8 KiB
C++
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.

#pragma once
// -----------------------------------------------------------------------------
// Редкости и аффиксы: то, что превращает находку в РЕШЕНИЕ.
//
// До этой вехи предмет был строкой каталога: «карабин» на первом этаже и
// «карабин» на сотом — одна и та же вещь. Идти за ней в темноту было незачем,
// а весь спуск держался на количестве находок, а не на их качестве.
//
// Аффикс = тег + тир + рулон (docs/22-loot.md §3). Тег говорит, ЧТО он даёт;
// тир — насколько сильным он бывает на этой глубине; рулон — конкретное число.
//
// ТРИ ПРАВИЛА, КОТОРЫЕ ЗДЕСЬ ЗАЩИЩАЮТСЯ АРИФМЕТИКОЙ, А НЕ СПИСКАМИ ЗАПРЕТОВ:
//
// 1. БЮДЖЕТ. У предмета есть бюджет по уровню, аффикс стоит по тиру. Шесть
// высших аффиксов не влезают никуда, кроме предмета сотого уровня, — и
// это единственный барьер, который не приходится подпирать частными
// исключениями. Список исключений разрастается и однажды противоречит
// сам себе.
// 2. ОДИН ТЕГ ОДИН РАЗ. Иначе «+урон» шесть раз становится единственной
// правильной сборкой, а весь остальной список — мусором.
// 3. КИНЕТИКА НЕ ИСЧЕЗАЕТ. Конверсий не больше 0.85 в сумме: оружие без
// физического урона перестало бы работать по целям без сопротивлений
// вовсе (sim/element.h).
//
// Аффиксы НЕ заводят новых каналов влияния. Каждый тег кладётся в то, что уже
// есть: урон оружия, сопротивления, стихии, статы бойца, ресурсы вылазки. Это
// не экономия, а то же правило, что и у снаряжения: у предмета нет ручки к
// правилу «не бить своих», и появиться она не может (sim/items.h).
// -----------------------------------------------------------------------------
#include <cstdint>
#include "sim/element.h"
#include "sim/items.h"
#include "sim/trigger.h"
// Сами типы (Rarity, AffixTag, Affix, ItemInstance) объявлены в sim/items.h:
// экземпляр предмета — это то, что боец несёт, и место ему там. Здесь — вся
// арифметика вокруг них.
inline const char* RarityName(Rarity r)
{
switch (r)
{
case Rarity::MARKED: return "MARKED";
case Rarity::PROTOTYPE: return "PROTOTYPE";
case Rarity::RELIC: return "RELIC";
default: return "COMMON";
}
}
// Цвет рамки и подписи. Байты RGB: в sim/ типов рендера нет.
struct RarityColor { uint8_t r, g, b; };
inline RarityColor RarityColorOf(Rarity r)
{
switch (r)
{
case Rarity::MARKED: return {120, 190, 235};
case Rarity::PROTOTYPE: return {170, 140, 240};
case Rarity::RELIC: return {235, 165, 70};
default: return {190, 190, 190};
}
}
// Доступный тир на глубине и стоимость аффикса в бюджете предмета.
constexpr int MAX_AFFIX_TIER = 9;
inline int MaxAffixTier(int depth)
{
const int t = 1 + (depth > 0 ? depth : 1) / 12;
return (t < MAX_AFFIX_TIER) ? t : MAX_AFFIX_TIER;
}
// Цена тира и бюджет предмета. Числа НЕ те, что стояли в docs/22-loot.md §3:
// там было cost = 6 + 4·T при бюджете 40 + 3·ilvl, и утверждалось, что на
// предмете сотого уровня это даёт «шесть аффиксов девятого тира и ни одним
// больше». На деле давало восемь (340 / 42 = 8.1), а предмет-максимум
// собирался уже к семидесятому этажу.
//
// Пересчитано так, чтобы обещание выполнялось арифметически:
//
// 6 · cost(9) = 294 <= B(100) = 295 < 7 · cost(9) = 343
//
// То есть предельная вещь существует ровно на дне и ровно одна. Проверяется
// инструментом `--tool affix`, а не глазами.
inline float AffixCost(int tier) { return 4.0f + 5.0f * float(tier); }
inline float ItemBudget(int ilvl) { return 45.0f + 2.5f * float(ilvl); }
// Диапазон рулона тега на первом тире. Дальше растёт как T^1.35 — так высокий
// тир заметно лучше низкого, но не превращает предмет в другую игру.
struct AffixRange { float lo, hi; };
inline AffixRange AffixBase(AffixTag t)
{
switch (t)
{
case AffixTag::DAMAGE: return {0.05f, 0.09f};
case AffixTag::PEN: return {0.03f, 0.06f};
case AffixTag::CONVERT: return {0.08f, 0.14f};
case AffixTag::ARMOR: return {0.02f, 0.04f};
case AffixTag::HEALTH: return {4.0f, 8.0f};
case AffixTag::SPEED: return {0.02f, 0.035f};
case AffixTag::SIGHT: return {0.03f, 0.05f};
case AffixTag::AMMO: return {8.0f, 14.0f};
case AffixTag::CARRY: return {0.30f, 0.55f};
case AffixTag::LIGHT: return {18.0f, 30.0f};
// Триггер — это ШАНС сработать. Дорогой аффикс с маленькими числами:
// «изредка поджигает» меняет бой сильнее, чем «+7% урона».
case AffixTag::TRIGGER: return {0.06f, 0.10f};
default: return {0.0f, 0.0f};
}
}
const char* AffixName(AffixTag t);
// Строка для подсказки: «DAMAGE +14%». Пишет в буфер, потому что подсказки
// собираются на стеке и аллокаций в UI за кадр быть не должно.
void AffixLabel(const Affix& a, char* out, int cap);
// Какие теги вообще ложатся на этот вид предмета. Каталог, а не «на что
// похоже»: конверсия на куртке — это урон, которого у куртки нет.
bool AffixFitsKind(AffixTag t, ItemKind k);
// --- накопленные модификаторы -------------------------------------------------
// Сумма аффиксов надетого. Считается в SyncLoadout один раз за шаг, дальше все
// читают готовые числа: лазать по аффиксам из правил боя нельзя, там про
// предметы не знают вообще.
struct AffixMods
{
float damageMul = 1.0f; // множитель урона оружия
float pen = 0.0f; // пробитие сопротивлений
ElementMix convert; // во что переведена часть урона
float armor = 0.0f;
float hp = 0.0f;
float speedMul = 1.0f;
float sightMul = 1.0f;
float ammo = 0.0f;
float carry = 0.0f;
float light = 0.0f;
// Сила по видам триггеров. Ноль — такого триггера на бойце нет
// (sim/trigger.h). Массивом, а не флагами: два предмета с одним видом
// складываются, и это законно — сильнее, но не чаще, откат общий.
float trigger[TRIGGER_COUNT] = {};
void Add(const Affix& a);
void Add(const ItemInstance& it);
};
// Смешать собственную стихию оружия с конверсией аффиксов.
//
// Конверсия ЗАМЕНЯЕТ часть смеси, а не добавляется поверх: у оружия смесь одна
// и сумма долей всегда единица (sim/element.h). Иначе «дробовик с жаром плюс
// 20% холода» давал бы 120% урона, и стихии стали бы каналом наращивания
// чисел — ровно тем, чем они не должны быть.
ElementMix MergeConversion(const ElementMix& base, const ElementMix& convert);
// --- бросок предмета ----------------------------------------------------------
// Сид передаётся снаружи: генерация уровня детерминирована по сиду, и находки
// обязаны быть частью этой же детерминированности. Иначе «тот же уровень» — это
// уже не тот же уровень, и сравнить два прогона нельзя.
ItemInstance RollItem(ItemId base, int depth, uint32_t& rng);
// Редкость по глубине. Отдельно от RollItem, потому что её проверяют
// инструментом на десятках тысяч бросков (`--tool drops`).
Rarity RollRarity(int depth, uint32_t& rng);
// Сколько аффиксов у этой редкости (нижняя и верхняя граница).
int AffixCountFor(Rarity r, uint32_t& rng);