Лут перестал быть мусором; 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:
parent
f5240111d5
commit
f7ce189352
@ -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 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 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
|
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
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Мастерская. Проверяются ПРАВИЛА операций, а не то, что они «есть»:
|
// Мастерская. Проверяются ПРАВИЛА операций, а не то, что они «есть»:
|
||||||
// каждая обязана менять предмет ровно так, как обещает подпись под ней, и
|
// каждая обязана менять предмет ровно так, как обещает подпись под ней, и
|
||||||
|
|||||||
@ -50,8 +50,12 @@ const EnemyKind KINDS[] = {EnemyKind::LARVA, EnemyKind::SHAMBLER, EnemyKind::R
|
|||||||
EnemyKind::VOIDLING};
|
EnemyKind::VOIDLING};
|
||||||
constexpr int KIND_COUNT = int(sizeof(KINDS) / sizeof(KINDS[0]));
|
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]));
|
constexpr int GUN_COUNT = int(sizeof(GUNS) / sizeof(GUNS[0]));
|
||||||
|
|
||||||
// Урон в секунду с учётом перезарядки: очередь кончается, и магазин, которого
|
// Урон в секунду с учётом перезарядки: очередь кончается, и магазин, которого
|
||||||
|
|||||||
@ -34,6 +34,28 @@ const AffixTag ARMOR_TAGS[] = {AffixTag::ARMOR, AffixTag::HEALTH, AffixTag::SPEE
|
|||||||
const AffixTag TRINKET_TAGS[] = {AffixTag::SIGHT, AffixTag::LIGHT, AffixTag::HEALTH,
|
const AffixTag TRINKET_TAGS[] = {AffixTag::SIGHT, AffixTag::LIGHT, AffixTag::HEALTH,
|
||||||
AffixTag::SPEED, AffixTag::CARRY, AffixTag::TRIGGER};
|
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
|
} // namespace
|
||||||
|
|
||||||
const char* AffixName(AffixTag t)
|
const char* AffixName(AffixTag t)
|
||||||
@ -239,10 +261,13 @@ ItemInstance RollItem(ItemId base, int depth, uint32_t& rng)
|
|||||||
int want = AffixCountFor(it.rarity, rng);
|
int want = AffixCountFor(it.rarity, rng);
|
||||||
if (want <= 0) return it;
|
if (want <= 0) return it;
|
||||||
|
|
||||||
// Уровень предмета — это глубина, на которой он найден. Отсюда и бюджет:
|
// Уровень предмета гуляет вокруг глубины: −2..+3. Без разброса все находки
|
||||||
// одна и та же куртка внизу держит больше аффиксов, чем наверху.
|
// одного этажа имеют один и тот же бюджет и различаются только рулонами —
|
||||||
float budget = ItemBudget(depth);
|
// то есть «ещё одна такая же». Плюс три сверху дают редкую находку, которая
|
||||||
const int tierCap = MaxAffixTier(depth);
|
// ощущается опережающей глубину, а это лучшая причина спускаться.
|
||||||
|
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;
|
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 > poolCount) want = poolCount; // один тег один раз (правило 2)
|
||||||
if (want > MAX_AFFIXES) want = MAX_AFFIXES;
|
if (want > MAX_AFFIXES) want = MAX_AFFIXES;
|
||||||
|
|
||||||
for (int i = 0; i < want; ++i)
|
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;
|
while (tier > 1 && AffixCost(tier) > budget) --tier;
|
||||||
if (AffixCost(tier) > budget) break;
|
if (AffixCost(tier) > budget) break;
|
||||||
budget -= AffixCost(tier);
|
budget -= AffixCost(tier);
|
||||||
|
|||||||
@ -52,6 +52,16 @@ const LootRow LOOT_ROWS[] = {
|
|||||||
{"magnum", ItemId::MAGNUM}, {"bow", ItemId::BOW},
|
{"magnum", ItemId::MAGNUM}, {"bow", ItemId::BOW},
|
||||||
{"saber", ItemId::SABER}, {"knife", ItemId::KNIFE},
|
{"saber", ItemId::SABER}, {"knife", ItemId::KNIFE},
|
||||||
{"cleaver", ItemId::CLEAVER},
|
{"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},
|
{"jacket", ItemId::JACKET}, {"vest", ItemId::VEST},
|
||||||
{"plate", ItemId::PLATE}, {"shroud", ItemId::SHROUD},
|
{"plate", ItemId::PLATE}, {"shroud", ItemId::SHROUD},
|
||||||
{"lantern", ItemId::LANTERN}, {"charm", ItemId::CHARM},
|
{"lantern", ItemId::LANTERN}, {"charm", ItemId::CHARM},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user