Семь новых тварей: акты II-VI перестали быть пустыми (M34)

Каталог кончался громилой на седьмом этаже: глубже бюджет опасности
выбирался теми же пятью видами, то есть спуск переставал показывать новое
ровно там, где обязан был начать.

CINDER (D9) горит сама - дробовик против неё бесполезен, первый повод
носить второй ствол. STINGER (D21) не убивает, а не даёт стоять. SWARM
(D36) толпа в одном теле, сектор 160 градусов - вбок не обойти, берётся
площадью. HUSK (D36) стена: 0.70 по кинетике, вскрывается только
кислотой. SLEEPER (D44) с радиусом агро 3.4 - мимо неё МОЖНО пройти, а
разбуженная бьёт кругом на 62 по всем, кто шёл кучей. WAILER (D53)
меняет порядок целей: пока жив, павшие рядом встают вшестеро быстрее.
VOIDLING (D73) ровные 0.45 по шести стихиям - верного ствола нет, берётся
срезанием сопротивлений.

Аура воскрешения у плакальщика РАБОТАЕТ, а не описана: список поднимающих
собирается раз в шаг, берётся наибольший множитель, а не сумма. В
песочнице приёмки список всегда пуст.

Ни одного множителя от глубины, ни одного иммунитета, ни одного числа
вне EnemyDefOf.

ИНСТРУМЕНТЫ ЗНАЛИ ПОЛОВИНУ ИГРЫ. Список KINDS в tools.cpp был захардкожен
пятью видами и не содержал даже личинки: --tool enemies и --tool ttk
молчали про остальных. Теперь там весь бестиарий, тринадцать видов.

Личинка из коридора стихий исключена намеренно и с пометкой "учебная":
она населяет первые этажи и умирает от чего угодно - в этом её работа.
Требовать от неё разброса значило бы требовать знания стихий на первом
этаже.

Проверено: --descend 80 собирает все глубины, --tool ttk в коридоре,
--accept PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
z.kirill 2026-08-16 19:41:49 +03:00
parent 92a8864726
commit f5240111d5
5 changed files with 209 additions and 17 deletions

View File

@ -30,6 +30,17 @@ entity spitter class=monster depth=3 danger=2.0 radius=0.31 color=96,1
entity lurker class=monster depth=5 danger=2.6 radius=0.30 color=88,62,118 sprite=lurker_walk name=LURKER
entity brute class=monster depth=7 danger=4.5 radius=0.46 color=142,60,78 sprite=brute_walk name=BRUTE
# Акты II-VI. До этого каталог кончался громилой на седьмом этаже, и глубже
# бюджет опасности выбирался теми же пятью видами — то есть спуск переставал
# показывать новое ровно там, где обязан был начать.
entity cinder class=monster depth=9 danger=2.8 radius=0.32 color=178,92,40 sprite=shambler_walk name=CINDER
entity stinger class=monster depth=21 danger=2.2 radius=0.26 color=104,120,132 sprite=spitter_walk name=STINGER
entity swarm class=monster depth=36 danger=1.6 radius=0.30 color=138,132,56 sprite=rusher_walk name=SWARM
entity husk class=monster depth=36 danger=7.0 radius=0.44 color=128,122,104 sprite=brute_walk name=HUSK
entity sleeper class=monster depth=44 danger=6.0 radius=0.50 color=96,88,74 sprite=brute_walk name=SLEEPER
entity wailer class=monster depth=53 danger=8.0 radius=0.33 color=158,150,176 sprite=spitter_walk name=WAILER
entity voidling class=monster depth=73 danger=11.0 radius=0.32 color=52,48,86 sprite=lurker_walk name=VOIDLING
# Мишень приёмки: не ходит и не бьёт. Опасности не несёт, поэтому и бюджета
# не тратит — ею ставят неподвижную цель, когда нужна именно цель.
entity dummy class=monster depth=1 danger=0.0 radius=0.34 color=120,44,44 sprite=- name=TARGET

View File

@ -41,8 +41,13 @@
namespace
{
const EnemyKind KINDS[] = {EnemyKind::SHAMBLER, EnemyKind::RUSHER, EnemyKind::SPITTER,
EnemyKind::LURKER, EnemyKind::BRUTE};
// ВЕСЬ бестиарий, а не пятеро. Список был захардкожен пятью видами и не знал
// даже личинки: инструменты показывали половину игры и молчали про остальное.
const EnemyKind KINDS[] = {EnemyKind::LARVA, EnemyKind::SHAMBLER, EnemyKind::RUSHER,
EnemyKind::SPITTER, EnemyKind::LURKER, EnemyKind::BRUTE,
EnemyKind::CINDER, EnemyKind::STINGER, EnemyKind::SWARM,
EnemyKind::HUSK, EnemyKind::SLEEPER, EnemyKind::WAILER,
EnemyKind::VOIDLING};
constexpr int KIND_COUNT = int(sizeof(KINDS) / sizeof(KINDS[0]));
const FirearmId GUNS[] = {FirearmId::RIFLE, FirearmId::PISTOL, FirearmId::SHOTGUN,
@ -171,10 +176,16 @@ int CmdTtk()
worst = std::max(worst, mul);
}
// Личинка из коридора исключена НАМЕРЕННО. Она населяет первые этажи и
// умирает от чего угодно — в этом её работа: показать бой до того, как
// игрок узнает про стихии. Требовать от неё разброса значило бы
// требовать знания стихий на первом этаже.
const bool tutorial = (k == EnemyKind::LARVA);
const float spread = worst / std::max(0.01f, best);
const bool off = !tutorial && (spread < SPREAD_MIN || spread > SPREAD_MAX);
std::printf(" %6.2fx%s\n", double(spread),
(spread < SPREAD_MIN || spread > SPREAD_MAX) ? " <-- вне коридора" : "");
if (spread < SPREAD_MIN || spread > SPREAD_MAX) ++bad;
off ? " <-- вне коридора" : (tutorial ? " (учебная)" : ""));
if (off) ++bad;
}
std::printf("\nсекунды на убийство одним стрелком, без промахов и движения.\n"

View File

@ -568,6 +568,14 @@ Sprite MakeLocker(uint32_t body)
//
// 14x14, по одной форме на ВИД предмета. Читать нужно вид, а не модель: имя
// пишется рядом текстом, и дублировать его пиктограммой незачем.
//
// Внутри вида форма чуть гуляет по ВЕСУ, и только по нему. Причина простая:
// стволов в каталоге стало двенадцать, и полка из двенадцати одинаковых
// пиктограмм, отличающихся лишь оттенком, читается как одна вещь в двенадцати
// цветах. Вес — единственное, что видно на 14 пикселях и что при этом значит в
// игре хоть что-то: гвоздомёт короткий и тонкий, пулемёт длинный и толстый.
// Второй характеристики сюда класть нельзя — начнётся рисование, а рисуют в
// этом проекте не здесь (docs/12-assets.md).
Sprite MakeIcon(ItemId id)
{
const ItemDef def = ItemDefOf(id);
@ -582,44 +590,65 @@ Sprite MakeIcon(ItemId id)
const uint32_t dark = ShadeColor(c, 0.45f);
const uint32_t hot = ShadeColor(c, 1.35f);
// «Насколько тяжёлое» в долях: 0 — карманная мелочь, 1 — пулемёт и панцирь.
const float heft = Clampf(def.weight / 7.5f, 0.0f, 1.0f);
switch (def.kind)
{
case ItemKind::FIREARM:
for (int x = 1; x < 12; ++x) s.Set(x, 6, c);
for (int x = 1; x < 12; ++x) s.Set(x, 7, dark);
{
// Длина ствола и вторая, тёмная линия под ним — у лёгкого её нет.
const int len = 8 + int(heft * 4.0f + 0.5f);
for (int x = 1; x < 1 + len; ++x) s.Set(x, 6, c);
if (def.weight >= 1.5f)
for (int x = 1; x < 1 + len; ++x) s.Set(x, 7, dark);
for (int y = 7; y < 11; ++y) s.Set(3, y, dark);
s.Set(11, 5, hot);
s.Set(12, 6, hot);
s.Set(len, 5, hot);
s.Set(1 + len, 6, hot);
break;
}
case ItemKind::BLADE:
for (int i = 0; i < 10; ++i) s.Set(2 + i, 11 - i, (i > 4) ? hot : c);
{
// Тот же диагональный клинок, только длиннее у тяжёлого.
const int len = 7 + int(heft * 5.0f + 0.5f);
for (int i = 0; i < len; ++i) s.Set(2 + i, 11 - i, (i > len / 2) ? hot : c);
s.Set(3, 10, dark);
s.Set(4, 11, dark);
s.Set(2, 9, dark);
break;
}
case ItemKind::ARMOR:
{
// Плечи: чем тяжелее броня, тем шире силуэт.
const int pad = (def.weight >= 3.0f) ? 1 : ((def.weight <= 1.5f) ? 3 : 2);
for (int y = 2; y < 12; ++y)
{
const int inset = (y < 9) ? 2 : (2 + (y - 8));
const int inset = (y < 9) ? pad : (pad + (y - 8));
for (int x = inset; x < 14 - inset; ++x)
s.Set(x, y, (y == 2 || x == inset || x == 13 - inset) ? dark : c);
}
break;
}
case ItemKind::TRINKET:
{
// Карманное весит доли килограмма, поэтому шкала тут своя: делить
// на вес пулемёта значило бы, что все обереги одного размера.
const float r = 4.0f + Clampf(def.weight / 2.4f, 0.0f, 1.0f) * 1.5f;
for (int y = 0; y < 14; ++y)
for (int x = 0; x < 14; ++x)
{
const float dx = float(x) - 6.5f, dy = float(y) - 6.5f;
const float d = std::sqrt(dx * dx + dy * dy);
if (d > 5.5f) continue;
s.Set(x, y, (d > 4.5f) ? dark : c);
if (d > r) continue;
s.Set(x, y, (d > r - 1.0f) ? dark : c);
}
s.Set(5, 4, hot);
s.Set(4, 5, hot);
break;
}
case ItemKind::CONSUMABLE:
for (int i = 3; i < 11; ++i)
@ -656,15 +685,37 @@ uint32_t SlotColor(int slot)
BodyLook LookOf(ItemId weapon)
{
// Силуэтов пять на весь каталог, и больше не нужно: на 14 пикселях
// различимы только «короткое», «длинное», «тяжёлое» и то же самое для
// клинка. Новое оружие раскладывается по ним, а не заводит шестую форму, —
// иначе пришлось бы рисовать, а рисуют здесь не так (docs/12-assets.md).
//
// Молчаливый default сюда класть НЕЛЬЗЯ: он даёт GUN_LONG, и клинок в руках
// выглядел бы стволом. Поэтому каждый клинок перечислен явно.
switch (weapon)
{
case ItemId::PISTOL: return BodyLook::GUN_SHORT;
case ItemId::PISTOL:
case ItemId::NAILER:
case ItemId::PLASMA: return BodyLook::GUN_SHORT;
case ItemId::SHOTGUN:
case ItemId::MAGNUM: return BodyLook::GUN_HEAVY;
case ItemId::BOW: return BodyLook::GUN_LONG;
case ItemId::MAGNUM:
case ItemId::HMG:
case ItemId::FLAMER:
case ItemId::RAIL: return BodyLook::GUN_HEAVY;
case ItemId::BOW:
case ItemId::ACID: return BodyLook::GUN_LONG;
case ItemId::SABER:
case ItemId::CLEAVER: return BodyLook::BLADE_LONG;
case ItemId::KNIFE: return BodyLook::BLADE_SHORT;
case ItemId::CLEAVER:
case ItemId::MAUL:
case ItemId::RIPPER:
case ItemId::CRYOEDGE: return BodyLook::BLADE_LONG;
case ItemId::KNIFE:
case ItemId::STILETTO: return BodyLook::BLADE_SHORT;
default: return BodyLook::GUN_LONG;
}
}

View File

@ -415,6 +415,50 @@ inline ItemDef ItemDefOf(ItemId id)
d.weight = 0.8f;
d.r = 88; d.g = 84; d.b = 100; break;
// --- броня: три размена, по одному на вещь ---------------------------
//
// Вес НАДЕТОЙ брони в `Inventory::Load` не входит (за неё уже заплачено
// множителем скорости), но входит, пока она лежит в сумке. Поэтому
// тяжёлая броня — это не «медленно», а «либо надел, либо не унёс».
// Разведрама: обзор в обмен на защиту. Брони меньше, чем у куртки,
// то есть это САМАЯ слабая броня в каталоге, и берут её не за неё.
// Минус жёсткий: в темноте (сел фонарь) обзор режется вместе со всем
// остальным, и рама превращается в полтора килограмма ничего.
case ItemId::RECON:
d.name = "RECON"; d.kind = ItemKind::ARMOR;
d.armor = 0.08f; d.speedMul = 1.06f; d.sightMul = 1.22f;
d.weight = 1.8f;
d.r = 130; d.g = 140; d.b = 120; break;
// Глухой шлем: защита в обмен на обзор. Брони больше, чем у жилета, а
// видит боец на пятую часть хуже — и это не косметика: обзор решает,
// кого отряд заметит первым (docs/17-extraction.md).
case ItemId::HELM:
d.name = "HELM"; d.kind = ItemKind::ARMOR;
d.armor = 0.36f; d.speedMul = 0.96f; d.hpBonus = 8.0f; d.sightMul = 0.78f;
d.weight = 3.6f;
d.r = 150; d.g = 150; d.b = 158; break;
// Панцирь из чужого хитина: здоровье в обмен на вес. Брони МЕНЬШЕ, чем у
// плиты, зато запас втрое больше — то есть он не «терпит удар», а даёт
// пережить лишний. Тяжелее всего, что вообще есть в игре: найденный
// панцирь в сумке — это половина нормы переноски отряда.
case ItemId::CARAPACE:
d.name = "CARAPACE"; d.kind = ItemKind::ARMOR;
d.armor = 0.34f; d.speedMul = 0.94f; d.hpBonus = 60.0f;
d.weight = 8.4f;
d.r = 120; d.g = 104; d.b = 86; break;
// Плетёнка: лёгкая броня ценой зрения. Между курткой и жилетом по
// защите, быстрее обеих — но лицо закрыто, и видит боец хуже всех,
// кроме шлема.
case ItemId::WEAVE:
d.name = "WEAVE"; d.kind = ItemKind::ARMOR;
d.armor = 0.18f; d.speedMul = 1.08f; d.sightMul = 0.88f;
d.weight = 1.4f;
d.r = 96; d.g = 100; d.b = 112; break;
// --- карман ---
case ItemId::LANTERN:
d.name = "LANTERN"; d.kind = ItemKind::TRINKET;
@ -432,6 +476,74 @@ inline ItemDef ItemDefOf(ItemId id)
d.weight = 0.3f;
d.r = 198; d.g = 184; d.b = 120; break;
// --- карман: чем он может быть и чем не может ------------------------
//
// У предмета РОВНО ЧЕТЫРЕ канала влияния — броня, скорость, здоровье,
// обзор (шапка файла), и это не недосмотр, а то, из-за чего снаряжение
// не может ослабить правило «не бить своих». Всё, что кладётся в карман,
// обязано укладываться в них.
//
// Отсюда — чего в кармане НЕТ и почему (тот же разбор, что у триггеров
// в docs/21-damage.md §4):
// * усилителя реакций — сила реакции считается от ГЛУБИНЫ, у неё
// вообще нет ручки на предмете, и заводить её ради одной вещи
// значило бы завести второй канал урона;
// * стабилизатора разброса — конус живёт в `FirearmDef`, а `Inventory`
// до оружия не дотягивается: снаряжение правил стрельбы не трогает;
// * подсумка «+патроны» — этот канал уже ЕСТЬ, но не у предмета, а у
// рулона: `AffixTag::AMMO` (sim/affix.h). Второй способ добавлять
// патроны был бы вторым именем того же самого;
// * маскировки — агро тварей читает свет отряда (`DARK_AGGRO_MUL`), а
// не карман бойца.
// Каждая из этих вещей, положенная сюда «как есть», лежала бы в сумке,
// показывалась в подсказке и не делала НИЧЕГО.
// Тепловизор: обзора меньше, чем у фонаря, и тяжелее его вдвое. Берут
// ради единственного свойства, которого у фонаря нет: тепло видно и БЕЗ
// СВЕТА. Погасший фонарь не даёт ничего (`Inventory::SightMul`), а
// тепловизор продолжает работать — пусть и по тёмным правилам.
case ItemId::THERMAL:
d.name = "THERMAL"; d.kind = ItemKind::TRINKET;
d.sightMul = 1.30f; d.speedMul = 0.94f;
d.weight = 1.0f;
d.r = 236; d.g = 140; d.b = 90; break;
// Наплечник: единственная броня, которая лезет в карман. Мало, и уходит
// в тот же потолок ARMOR_CAP — то есть на бойце в плите он не даёт
// почти ничего, и это ровно тот выбор, ради которого он и заведён:
// наплечник для лёгких, а не «плюс к тяжёлым».
case ItemId::PAULDRON:
d.name = "PAULDRON"; d.kind = ItemKind::TRINKET;
d.armor = 0.12f; d.speedMul = 0.97f;
d.weight = 1.1f;
d.r = 132; d.g = 138; d.b = 146; break;
// Зажим: здоровья почти вдвое больше оберега, и за это боец ходит
// медленнее всех в кармане. Живучесть, купленная скоростью, — самый
// опасный размен в игре с экстракшеном: наверх ещё идти.
case ItemId::CLAMP:
d.name = "CLAMP"; d.kind = ItemKind::TRINKET;
d.hpBonus = 55.0f; d.speedMul = 0.92f;
d.weight = 0.6f;
d.r = 200; d.g = 90; d.b = 90; break;
// Впрыск: быстрее шпор, но зрение сужается — туннельное. Отряд с
// впрыском доезжает куда угодно первым и замечает всё последним.
case ItemId::ADRENAL:
d.name = "ADRENAL"; d.kind = ItemKind::TRINKET;
d.speedMul = 1.22f; d.sightMul = 0.86f;
d.weight = 0.4f;
d.r = 240; d.g = 230; d.b = 120; break;
// Разгрузка: понемногу хода и обзора, ни в чём не лучшая. Цена —
// вес 2.4, в разы больше любой другой карманной вещи, то есть она
// отъедает норму переноски у ТОГО, ЧТО ОТРЯД УНЕСЁТ НАВЕРХ.
case ItemId::HARNESS:
d.name = "HARNESS"; d.kind = ItemKind::TRINKET;
d.sightMul = 1.18f; d.speedMul = 1.08f;
d.weight = 2.4f;
d.r = 176; d.g = 150; d.b = 110; break;
// --- расходники ---
case ItemId::BANDAGE:
d.name = "BANDAGE"; d.kind = ItemKind::CONSUMABLE; d.heal = 25.0f;

View File

@ -24,6 +24,13 @@ const Row ROWS[] = {
{"spitter", EnemyKind::SPITTER},
{"brute", EnemyKind::BRUTE},
{"lurker", EnemyKind::LURKER},
{"cinder", EnemyKind::CINDER},
{"stinger", EnemyKind::STINGER},
{"swarm", EnemyKind::SWARM},
{"husk", EnemyKind::HUSK},
{"sleeper", EnemyKind::SLEEPER},
{"wailer", EnemyKind::WAILER},
{"voidling", EnemyKind::VOIDLING},
{"crate", EnemyKind::CRATE},
{"barrel", EnemyKind::BARREL},
{"locker", EnemyKind::LOCKER},