squad-proto/src/sim/affix.h

170 lines
9.8 KiB
C
Raw Normal View History

Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
#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"
Триггеры: четыре условных эффекта на аффиксах (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 07:32:12 +07:00
#include "sim/trigger.h"
Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
// Сами типы (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};
Триггеры: четыре условных эффекта на аффиксах (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 07:32:12 +07:00
// Триггер — это ШАНС сработать. Дорогой аффикс с маленькими числами:
// «изредка поджигает» меняет бой сильнее, чем «+7% урона».
case AffixTag::TRIGGER: return {0.06f, 0.10f};
Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
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;
Триггеры: четыре условных эффекта на аффиксах (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 07:32:12 +07:00
// Сила по видам триггеров. Ноль — такого триггера на бойце нет
// (sim/trigger.h). Массивом, а не флагами: два предмета с одним видом
// складываются, и это законно — сильнее, но не чаще, откат общий.
float trigger[TRIGGER_COUNT] = {};
Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
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);