#include "sim/affix.h" #include #include namespace { // Свой генератор, а не : броски обязаны быть одинаковыми на всех // платформах и сборках. `std::uniform_real_distribution` этого не обещает, и // «тот же сид — тот же уровень» на нём разъезжается между компиляторами. uint32_t NextRand(uint32_t& s) { s = s * 1664525u + 1013904223u; return s; } float Frand(uint32_t& s) { return float(NextRand(s) & 0xFFFFFF) / float(0x1000000); } int RandRange(uint32_t& s, int lo, int hi) // [lo, hi] { if (hi <= lo) return lo; return lo + int(NextRand(s) % uint32_t(hi - lo + 1)); } // Теги, которые ложатся на оружие, броню и карман. Списком, потому что это // каталог содержимого, а не правило: «что бывает на куртке» меняют, глядя на // таблицу, а не выводя из вида предмета. const AffixTag WEAPON_TAGS[] = {AffixTag::DAMAGE, AffixTag::PEN, AffixTag::CONVERT, AffixTag::AMMO, AffixTag::SPEED}; const AffixTag ARMOR_TAGS[] = {AffixTag::ARMOR, AffixTag::HEALTH, AffixTag::SPEED, AffixTag::CARRY, AffixTag::AMMO}; const AffixTag TRINKET_TAGS[] = {AffixTag::SIGHT, AffixTag::LIGHT, AffixTag::HEALTH, AffixTag::SPEED, AffixTag::CARRY}; } // namespace const char* AffixName(AffixTag t) { switch (t) { case AffixTag::DAMAGE: return "DAMAGE"; case AffixTag::PEN: return "PIERCE"; case AffixTag::CONVERT: return "CONVERT"; case AffixTag::ARMOR: return "ARMOR"; case AffixTag::HEALTH: return "HEALTH"; case AffixTag::SPEED: return "SPEED"; case AffixTag::SIGHT: return "SIGHT"; case AffixTag::AMMO: return "ROUNDS"; case AffixTag::CARRY: return "CARRY"; case AffixTag::LIGHT: return "LIGHT"; default: return "-"; } } void AffixLabel(const Affix& a, char* out, int cap) { if (!out || cap <= 0) return; out[0] = '\0'; switch (a.tag) { // Проценты — там, где число и есть доля. Смешивать «+14%» и «+14» в // одном списке нельзя: игрок читает подсказку целиком и сравнивает // строки между собой. case AffixTag::DAMAGE: case AffixTag::PEN: case AffixTag::ARMOR: case AffixTag::SPEED: case AffixTag::SIGHT: std::snprintf(out, size_t(cap), "%s +%d%% T%d", AffixName(a.tag), int(a.value * 100.0f + 0.5f), int(a.tier)); break; case AffixTag::CONVERT: std::snprintf(out, size_t(cap), "%d%% DAMAGE AS %s T%d", int(a.value * 100.0f + 0.5f), ElementName(Element(a.param)), int(a.tier)); break; case AffixTag::CARRY: std::snprintf(out, size_t(cap), "CARRY +%.1f T%d", double(a.value), int(a.tier)); break; default: std::snprintf(out, size_t(cap), "%s +%d T%d", AffixName(a.tag), int(a.value + 0.5f), int(a.tier)); break; } } bool AffixFitsKind(AffixTag t, ItemKind k) { auto has = [&](const AffixTag* list, int n) { for (int i = 0; i < n; ++i) if (list[i] == t) return true; return false; }; switch (k) { case ItemKind::FIREARM: case ItemKind::BLADE: return has(WEAPON_TAGS, int(sizeof(WEAPON_TAGS) / sizeof(AffixTag))); case ItemKind::ARMOR: return has(ARMOR_TAGS, int(sizeof(ARMOR_TAGS) / sizeof(AffixTag))); case ItemKind::TRINKET: return has(TRINKET_TAGS, int(sizeof(TRINKET_TAGS) / sizeof(AffixTag))); // Расходники не катаются. Бинт с аффиксами — это не решение, а шум: // его применяют автоматически и выбрасывают. default: return false; } } void AffixMods::Add(const Affix& a) { switch (a.tag) { case AffixTag::DAMAGE: damageMul += a.value; break; case AffixTag::PEN: pen += a.value; break; case AffixTag::ARMOR: armor += a.value; break; case AffixTag::HEALTH: hp += a.value; break; case AffixTag::SPEED: speedMul += a.value; break; case AffixTag::SIGHT: sightMul += a.value; break; case AffixTag::AMMO: ammo += a.value; break; case AffixTag::CARRY: carry += a.value; break; case AffixTag::LIGHT: light += a.value; break; case AffixTag::CONVERT: { // Конверсии складываются между собой, а кинетика — это остаток. // Складывать смеси иначе (перемножением долей) значило бы, что два // аффикса по 20% дают 36%, и игрок никогда не сойдётся с подсказкой. const int e = int(a.param); if (e <= 0 || e >= ELEMENT_COUNT) break; convert.share[e] += a.value; float sum = 0.0f; for (int i = 1; i < ELEMENT_COUNT; ++i) sum += convert.share[i]; if (sum > 0.85f) // кинетика не исчезает никогда (sim/element.h) { const float k = 0.85f / sum; for (int i = 1; i < ELEMENT_COUNT; ++i) convert.share[i] *= k; sum = 0.85f; } convert.share[int(Element::KIN)] = 1.0f - sum; break; } default: break; } } void AffixMods::Add(const ItemInstance& it) { for (int i = 0; i < it.Count(); ++i) Add(it.affix[i]); } ElementMix MergeConversion(const ElementMix& base, const ElementMix& convert) { // Доля, которую забрала конверсия, вычитается из КИНЕТИКИ оружия, а не из // его собственной стихии: дробовик с жаром, на который выкатили холод, // остаётся зажигательным — просто теперь ещё и холодным. float taken = 0.0f; for (int e = 1; e < ELEMENT_COUNT; ++e) taken += convert.share[e]; if (taken <= 0.0f) return base; ElementMix out = base; const float kin = base.share[int(Element::KIN)]; const float use = (taken < kin) ? taken : kin; // кинетики меньше, чем забирают const float k = (taken > 0.0f) ? use / taken : 0.0f; for (int e = 1; e < ELEMENT_COUNT; ++e) out.share[e] += convert.share[e] * k; out.share[int(Element::KIN)] = kin - use; return out; } Rarity RollRarity(int depth, uint32_t& rng) { // Шанс «не обычного» растёт с глубиной и упирается в 0.88. Потолок нужен: // без него на дне обычных вещей не остаётся вовсе, и редкость перестаёт // что-либо значить — сравнивать станет не с чем. // // Наклон 0.011 подобран замером (`--tool drops`), а не взят из документа: // в docs/22-loot.md §1 формула и таблица противоречили друг другу. Формула // (0.02 + 0.0075·d) давала на первом этаже 2.7% небанальных находок, а // таблица рядом — 22%. Выбрана формула, потому что первый этаж обязан быть // скучным по замыслу игры, а наклон поднят так, чтобы сойтись с таблицей на // дне: 12% обычных на сотом этаже. const float d = float(depth > 0 ? depth : 1); const float pRare = std::min(0.88f, 0.02f + 0.011f * d); if (Frand(rng) >= pRare) return Rarity::COMMON; // Внутри «не обычного» — своя лестница, тоже по глубине: реликты внизу // становятся обычным делом, наверху их нет вовсе. const float r = Frand(rng); const float pRelic = std::min(0.30f, 0.004f * d); const float pProto = std::min(0.45f, 0.06f + 0.006f * d); if (r < pRelic) return Rarity::RELIC; if (r < pRelic + pProto) return Rarity::PROTOTYPE; return Rarity::MARKED; } int AffixCountFor(Rarity r, uint32_t& rng) { switch (r) { case Rarity::MARKED: return RandRange(rng, 1, 2); case Rarity::PROTOTYPE: return RandRange(rng, 3, 4); case Rarity::RELIC: return RandRange(rng, 5, 6); default: return 0; } } ItemInstance RollItem(ItemId base, int depth, uint32_t& rng) { ItemInstance it(base); if (base == ItemId::NONE) return it; const ItemDef def = ItemDefOf(base); it.rarity = AffixFitsKind(AffixTag::DAMAGE, def.kind) || AffixFitsKind(AffixTag::ARMOR, def.kind) || AffixFitsKind(AffixTag::SIGHT, def.kind) ? RollRarity(depth, rng) : Rarity::COMMON; int want = AffixCountFor(it.rarity, rng); if (want <= 0) return it; // Уровень предмета — это глубина, на которой он найден. Отсюда и бюджет: // одна и та же куртка внизу держит больше аффиксов, чем наверху. float budget = ItemBudget(depth); const int tierCap = MaxAffixTier(depth); // Список тегов, доступных этому виду. Порядок перемешивается броском, а не // берётся как есть: иначе первый тег в таблице оказывался бы на каждом // предмете в игре. AffixTag pool[int(AffixTag::COUNT)]; int poolCount = 0; for (int t = 1; t < int(AffixTag::COUNT); ++t) if (AffixFitsKind(AffixTag(t), def.kind)) pool[poolCount++] = AffixTag(t); if (poolCount == 0) return it; for (int i = poolCount - 1; i > 0; --i) { const int j = RandRange(rng, 0, i); const AffixTag tmp = pool[i]; pool[i] = pool[j]; pool[j] = tmp; } if (want > poolCount) want = poolCount; // один тег один раз (правило 2) if (want > MAX_AFFIXES) want = MAX_AFFIXES; for (int i = 0; i < want; ++i) { // Тир — случайный до потолка глубины, но не дороже остатка бюджета. // Именно так бюджет и ограничивает: не «нельзя шесть высших», а // «на шестой уже не хватило». int tier = RandRange(rng, 1, tierCap); while (tier > 1 && AffixCost(tier) > budget) --tier; if (AffixCost(tier) > budget) break; budget -= AffixCost(tier); const AffixRange base_ = AffixBase(pool[i]); const float scale = std::pow(float(tier), 1.35f); Affix a; a.tag = pool[i]; a.tier = uint8_t(tier); a.value = (base_.lo + (base_.hi - base_.lo) * Frand(rng)) * scale; // Стихия конверсии — любая, кроме кинетики: «перевести урон в // кинетику» не значит ничего. if (a.tag == AffixTag::CONVERT) a.param = uint8_t(RandRange(rng, 1, ELEMENT_COUNT - 1)); it.affix[it.count++] = a; } // Аффиксов могло влезть меньше, чем обещала редкость. Понижаем её до // фактического содержимого: «реликт» с двумя аффиксами — это обман, // причём тот самый, который игрок замечает первым. if (it.count <= 0) it.rarity = Rarity::COMMON; else if (it.count <= 2) it.rarity = Rarity::MARKED; else if (it.count <= 4) it.rarity = Rarity::PROTOTYPE; else it.rarity = Rarity::RELIC; return it; }