Лут перестал быть мусором; 19 новых вещей (M35)

ЧТО ДЕЛАЛО ЛУТ МУСОРОМ. Три вещи, и все три чинятся в броске:

1. Оружие могло выкатиться БЕЗ ЕДИНОГО боевого аффикса. Редкость обещала
   силу, а рулон целиком уходил в снабжение - и "реликт" оказывался хуже
   обычного карабина. Теперь первым всегда идёт ключевой тег вида: у
   оружия боевой, у брони защитный. Замер: 935 небанальных стволов, ноль
   без боевых аффиксов.

2. Тир брался равномерно до потолка глубины: на восьмидесятом этаже
   половина аффиксов была первого-второго тира, то есть глубина меняла
   число строк в описании, а не их силу. Теперь из двух бросков берётся
   больший - средний тир на дне поднялся с 5.0 до 6.5.

3. Все находки одного этажа имели один бюджет и различались только
   рулонами. Теперь уровень предмета гуляет -2..+3 от глубины: плюс три
   сверху дают находку, опережающую глубину, а это лучшая причина
   спускаться.

КОНТЕНТ. 19 новых предметов: шесть стволов (гвоздомёт, пулемёт, плазма,
рельса, огнемёт, кислота), четыре клинка (стилет с парированием 0.52,
молот на 430 урона и парированием 0.02, цепной, крио), четыре брони и
пять карманных. У каждой вещи свой минус, ни одна не доминирует старые;
клинки уложены в одну ось "вес против парирования".

ИНСТРУМЕНТЫ СЧИТАЛИ ТРЕТЬ ИГРЫ. Список стволов в tools.cpp был
захардкожен шестью значениями: --tool weapons и --tool ttk молчали про
новое оружие ровно так же, как про новых тварей. Теперь там весь арсенал.

Каталог проекта пополнен всеми новыми вещами по тирам и глубинам (3..31),
чтобы они реально падали, а не лежали в перечислении.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
z.kirill 2026-08-16 19:47:13 +03:00
parent f5240111d5
commit f7ce189352
5 changed files with 140 additions and 10 deletions

View File

@ -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

View File

@ -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;
}
}
// -------------------------------------------------------------------------
// Мастерская. Проверяются ПРАВИЛА операций, а не то, что они «есть»:
// каждая обязана менять предмет ровно так, как обещает подпись под ней, и

View File

@ -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};
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]));
// Урон в секунду с учётом перезарядки: очередь кончается, и магазин, которого

View File

@ -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);

View File

@ -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},