diff --git a/projects/descent/catalog/entities.txt b/projects/descent/catalog/entities.txt index d823d30..8745757 100644 --- a/projects/descent/catalog/entities.txt +++ b/projects/descent/catalog/entities.txt @@ -79,3 +79,31 @@ entity bow class=loot tier=3 depth=10 radius=0.26 color=154,190,164 s entity plate class=loot tier=3 depth=9 radius=0.30 color=140,146,156 sprite=item_icon_plate name=PLATE entity shroud class=loot tier=3 depth=11 radius=0.26 color=88,84,100 sprite=item_icon_shroud name=SHROUD entity charm class=loot tier=3 depth=8 radius=0.22 color=196,130,190 sprite=item_icon_charm name=CHARM + +# --- оружие и снаряжение актов II-VI ------------------------------------------- +# Тир — «с какой глубины вещь становится находкой». Роли разные, и каждая вещь +# чем-то хуже соседней: гвоздомёт дешёвый и слабый, пулемёт тяжёлый, рельса +# бьёт далеко и редко (docs/13-combat.md). +entity nailer class=loot tier=1 depth=3 radius=0.24 color=170,170,150 sprite=- name=NAILER +entity recon class=loot tier=1 depth=4 radius=0.26 color=120,140,120 sprite=- name=RECON +entity harness class=loot tier=1 depth=5 radius=0.22 color=150,140,120 sprite=- name=HARNESS + +entity acid class=loot tier=2 depth=7 radius=0.26 color=120,170,90 sprite=- name=ACID +entity stiletto class=loot tier=2 depth=7 radius=0.22 color=200,200,220 sprite=- name=STILETTO +entity weave class=loot tier=2 depth=8 radius=0.26 color=110,120,140 sprite=- name=WEAVE +entity clamp class=loot tier=2 depth=8 radius=0.22 color=170,150,110 sprite=- name=CLAMP + +entity plasma class=loot tier=3 depth=11 radius=0.26 color=150,190,230 sprite=- name=PLASMA +entity ripper class=loot tier=3 depth=12 radius=0.26 color=160,190,120 sprite=- name=RIPPER +entity helm class=loot tier=3 depth=12 radius=0.28 color=130,130,140 sprite=- name=HELM +entity adrenal class=loot tier=3 depth=13 radius=0.22 color=210,140,120 sprite=- name=ADRENAL +entity thermal class=loot tier=3 depth=14 radius=0.22 color=200,120,90 sprite=- name=THERMAL + +entity flamer class=loot tier=4 depth=17 radius=0.28 color=230,140,60 sprite=- name=FLAMER +entity cryoedge class=loot tier=4 depth=18 radius=0.26 color=140,200,230 sprite=- name=CRYOEDGE +entity pauldron class=loot tier=4 depth=19 radius=0.24 color=140,140,150 sprite=- name=PAULDRON +entity carapace class=loot tier=4 depth=20 radius=0.30 color=120,110,90 sprite=- name=CARAPACE + +entity hmg class=loot tier=5 depth=24 radius=0.30 color=90,100,110 sprite=- name=HMG +entity maul class=loot tier=5 depth=26 radius=0.30 color=150,130,110 sprite=- name=MAUL +entity rail class=loot tier=6 depth=31 radius=0.28 color=180,180,210 sprite=- name=RAIL diff --git a/src/debug/harness.cpp b/src/debug/harness.cpp index 53a107e..bde3d51 100644 --- a/src/debug/harness.cpp +++ b/src/debug/harness.cpp @@ -1679,6 +1679,53 @@ int RunDescend(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t } } + // ------------------------------------------------------------------------- + // Качество находок. Проверяется не «падает ли лут», а то, из-за чего он + // ощущался мусором: у оружия могло не оказаться НИ ОДНОГО боевого аффикса, + // и «реликт» выходил хуже обычного карабина. + // ------------------------------------------------------------------------- + { + uint32_t rng = 4242u; + int rolled = 0, empty = 0, tierSum = 0, affixSum = 0; + + for (int i = 0; i < 2000; ++i) + { + const ItemInstance it = RollItem(ItemId::RIFLE, 40, rng); + if (it.rarity == Rarity::COMMON) continue; + ++rolled; + + bool core = false; + for (int a = 0; a < it.Count(); ++a) + { + const AffixTag t = it.affix[a].tag; + if (t == AffixTag::DAMAGE || t == AffixTag::PEN || t == AffixTag::CONVERT || + t == AffixTag::TRIGGER || t == AffixTag::PARRY) + core = true; + tierSum += it.affix[a].tier; + ++affixSum; + } + if (!core) ++empty; + } + + const float avgTier = (affixSum > 0) ? float(tierSum) / float(affixSum) : 0.0f; + std::printf("\nнаходки (карабин, D40): выкатано %d, без боевых аффиксов %d," + " средний тир %.2f при потолке %d\n", + rolled, empty, double(avgTier), MaxAffixTier(40)); + + if (empty > 0) + { + std::printf(" [FAIL] %d стволов без боевых аффиксов: редкость обещает силу," + " а даёт строки про переноску\n", empty); + ++bad; + } + if (avgTier < float(MaxAffixTier(40)) * 0.5f) + { + std::printf(" [FAIL] средний тир %.2f при потолке %d — глубина не чувствуется\n", + double(avgTier), MaxAffixTier(40)); + ++bad; + } + } + // ------------------------------------------------------------------------- // Мастерская. Проверяются ПРАВИЛА операций, а не то, что они «есть»: // каждая обязана менять предмет ровно так, как обещает подпись под ней, и diff --git a/src/debug/tools.cpp b/src/debug/tools.cpp index f184aa8..6ac54c9 100644 --- a/src/debug/tools.cpp +++ b/src/debug/tools.cpp @@ -50,8 +50,12 @@ const EnemyKind KINDS[] = {EnemyKind::LARVA, EnemyKind::SHAMBLER, EnemyKind::R EnemyKind::VOIDLING}; constexpr int KIND_COUNT = int(sizeof(KINDS) / sizeof(KINDS[0])); -const FirearmId GUNS[] = {FirearmId::RIFLE, FirearmId::PISTOL, FirearmId::SHOTGUN, - FirearmId::SMG, FirearmId::MAGNUM, FirearmId::BOW}; +// ВЕСЬ арсенал. Список был захардкожен шестью стволами: инструменты считали +// баланс по трети оружия и молчали про остальное. +const FirearmId GUNS[] = {FirearmId::RIFLE, FirearmId::PISTOL, FirearmId::SHOTGUN, + FirearmId::SMG, FirearmId::MAGNUM, FirearmId::BOW, + FirearmId::NAILER, FirearmId::HMG, FirearmId::PLASMA, + FirearmId::RAIL, FirearmId::FLAMER, FirearmId::ACID}; constexpr int GUN_COUNT = int(sizeof(GUNS) / sizeof(GUNS[0])); // Урон в секунду с учётом перезарядки: очередь кончается, и магазин, которого diff --git a/src/sim/affix.cpp b/src/sim/affix.cpp index a75ce67..b5630ad 100644 --- a/src/sim/affix.cpp +++ b/src/sim/affix.cpp @@ -34,6 +34,28 @@ const AffixTag ARMOR_TAGS[] = {AffixTag::ARMOR, AffixTag::HEALTH, AffixTag::SPEE const AffixTag TRINKET_TAGS[] = {AffixTag::SIGHT, AffixTag::LIGHT, AffixTag::HEALTH, AffixTag::SPEED, AffixTag::CARRY, AffixTag::TRIGGER}; +// Какой тег делает вещь ЭТОГО вида осмысленной. Оружие без единого боевого +// аффикса — это не «редкая находка», это шесть строк про переноску на стволе. +// +// Именно из-за этого лут ощущался мусором: редкость обещала силу, а рулон мог +// целиком уйти в снабжение, и «реликт» оказывался хуже обычного карабина. +bool IsCoreTag(AffixTag t, ItemKind k) +{ + switch (k) + { + case ItemKind::FIREARM: + case ItemKind::BLADE: + return t == AffixTag::DAMAGE || t == AffixTag::PEN || t == AffixTag::CONVERT || + t == AffixTag::TRIGGER || t == AffixTag::PARRY; + case ItemKind::ARMOR: + return t == AffixTag::ARMOR || t == AffixTag::HEALTH; + case ItemKind::TRINKET: + return t == AffixTag::SIGHT || t == AffixTag::LIGHT || t == AffixTag::TRIGGER; + default: + return false; + } +} + } // namespace const char* AffixName(AffixTag t) @@ -239,10 +261,13 @@ ItemInstance RollItem(ItemId base, int depth, uint32_t& rng) int want = AffixCountFor(it.rarity, rng); if (want <= 0) return it; - // Уровень предмета — это глубина, на которой он найден. Отсюда и бюджет: - // одна и та же куртка внизу держит больше аффиксов, чем наверху. - float budget = ItemBudget(depth); - const int tierCap = MaxAffixTier(depth); + // Уровень предмета гуляет вокруг глубины: −2..+3. Без разброса все находки + // одного этажа имеют один и тот же бюджет и различаются только рулонами — + // то есть «ещё одна такая же». Плюс три сверху дают редкую находку, которая + // ощущается опережающей глубину, а это лучшая причина спускаться. + const int ilvl = std::max(1, depth + RandRange(rng, -2, 3)); + float budget = ItemBudget(ilvl); + const int tierCap = MaxAffixTier(ilvl); // Список тегов, доступных этому виду. Порядок перемешивается броском, а не // берётся как есть: иначе первый тег в таблице оказывался бы на каждом @@ -261,15 +286,31 @@ ItemInstance RollItem(ItemId base, int depth, uint32_t& rng) pool[j] = tmp; } + // ПЕРВЫМ идёт ключевой тег вида: у оружия боевой, у брони защитный. + // Перемешивание оставлено — оно решает, КАКОЙ именно ключевой выпадет и в + // каком порядке пойдут остальные, — но вещь без своего главного свойства + // больше не рождается. Это и была главная беда лута: редкость обещала + // силу, а рулон мог целиком уйти в снабжение. + for (int i = 0; i < poolCount; ++i) + { + if (!IsCoreTag(pool[i], def.kind)) continue; + const AffixTag core = pool[i]; + pool[i] = pool[0]; + pool[0] = core; + break; + } + 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); + // Тир тянется К ПОТОЛКУ ГЛУБИНЫ, а не берётся равномерно: из двух + // бросков выбирается больший. Равномерный бросок означал, что на + // восьмидесятом этаже половина аффиксов — первого-второго тира, то + // есть глубина меняла только ЧИСЛО строк в описании, а не их силу. + // Отсюда и ощущение, что лут внизу такой же, как наверху. + int tier = std::max(RandRange(rng, 1, tierCap), RandRange(rng, 1, tierCap)); while (tier > 1 && AffixCost(tier) > budget) --tier; if (AffixCost(tier) > budget) break; budget -= AffixCost(tier); diff --git a/src/sim/spawn_catalog.cpp b/src/sim/spawn_catalog.cpp index 1d9feaa..49edb38 100644 --- a/src/sim/spawn_catalog.cpp +++ b/src/sim/spawn_catalog.cpp @@ -52,6 +52,16 @@ const LootRow LOOT_ROWS[] = { {"magnum", ItemId::MAGNUM}, {"bow", ItemId::BOW}, {"saber", ItemId::SABER}, {"knife", ItemId::KNIFE}, {"cleaver", ItemId::CLEAVER}, + {"nailer", ItemId::NAILER}, {"hmg", ItemId::HMG}, + {"plasma", ItemId::PLASMA}, {"rail", ItemId::RAIL}, + {"flamer", ItemId::FLAMER}, {"acid", ItemId::ACID}, + {"stiletto", ItemId::STILETTO}, {"maul", ItemId::MAUL}, + {"ripper", ItemId::RIPPER}, {"cryoedge", ItemId::CRYOEDGE}, + {"recon", ItemId::RECON}, {"helm", ItemId::HELM}, + {"carapace", ItemId::CARAPACE}, {"weave", ItemId::WEAVE}, + {"thermal", ItemId::THERMAL}, {"pauldron", ItemId::PAULDRON}, + {"clamp", ItemId::CLAMP}, {"adrenal", ItemId::ADRENAL}, + {"harness", ItemId::HARNESS}, {"jacket", ItemId::JACKET}, {"vest", ItemId::VEST}, {"plate", ItemId::PLATE}, {"shroud", ItemId::SHROUD}, {"lantern", ItemId::LANTERN}, {"charm", ItemId::CHARM},