161 lines
9.1 KiB
C
161 lines
9.1 KiB
C
|
|
#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"
|
|||
|
|
|
|||
|
|
// Сами типы (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};
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
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);
|