#pragma once // ----------------------------------------------------------------------------- // Редкости и аффиксы: то, что превращает находку в РЕШЕНИЕ. // // До этой вехи предмет был строкой каталога: «карабин» на первом этаже и // «карабин» на сотом — одна и та же вещь. Идти за ней в темноту было незачем, // а весь спуск держался на количестве находок, а не на их качестве. // // Аффикс = тег + тир + рулон (docs/22-loot.md §3). Тег говорит, ЧТО он даёт; // тир — насколько сильным он бывает на этой глубине; рулон — конкретное число. // // ТРИ ПРАВИЛА, КОТОРЫЕ ЗДЕСЬ ЗАЩИЩАЮТСЯ АРИФМЕТИКОЙ, А НЕ СПИСКАМИ ЗАПРЕТОВ: // // 1. БЮДЖЕТ. У предмета есть бюджет по уровню, аффикс стоит по тиру. Шесть // высших аффиксов не влезают никуда, кроме предмета сотого уровня, — и // это единственный барьер, который не приходится подпирать частными // исключениями. Список исключений разрастается и однажды противоречит // сам себе. // 2. ОДИН ТЕГ ОДИН РАЗ. Иначе «+урон» шесть раз становится единственной // правильной сборкой, а весь остальной список — мусором. // 3. КИНЕТИКА НЕ ИСЧЕЗАЕТ. Конверсий не больше 0.85 в сумме: оружие без // физического урона перестало бы работать по целям без сопротивлений // вовсе (sim/element.h). // // Аффиксы НЕ заводят новых каналов влияния. Каждый тег кладётся в то, что уже // есть: урон оружия, сопротивления, стихии, статы бойца, ресурсы вылазки. Это // не экономия, а то же правило, что и у снаряжения: у предмета нет ручки к // правилу «не бить своих», и появиться она не может (sim/items.h). // ----------------------------------------------------------------------------- #include #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);