#pragma once // ----------------------------------------------------------------------------- // Предметы и снаряжение: ЧТО боец несёт на себе. // // Разделение то же, что у оружия (sim/weapon_defs.h): // * ItemDef — неизменяемое описание предмета, одна запись каталога; // * Inventory — компонент бойца: что в руках, что надето, что в сумке. // // Снаряжение НЕ трогает правила боя. Оно даёт бойцу четыре числа — // броня, скорость, запас здоровья, дальность обзора, — и всё. Никакой предмет // не может ослабить правило «не бить своих»: у него просто нет такой ручки. // Это не самоограничение, а следствие устройства: FireWeapons и UpdateMelee // про Inventory не знают вообще, они читают уже готовые статы агента. // // Оружие живёт двумя сущностями: ItemId в инвентаре — это ЧТО взято в руки, // а сущность-оружие ECS хранит рабочие статы. SyncLoadout переносит одно // в другое каждый шаг, поэтому «поменять ствол» — это одна запись ItemId. // ----------------------------------------------------------------------------- #include #include "sim/weapon_defs.h" // Сколько патронов в найденной пачке. Не «магазин»: магазины у стволов разные, // а пачка — это просто запас, из которого магазин потом набивается. constexpr int AMMO_BOX_ROUNDS = 40; enum class ItemKind : uint8_t { NONE = 0, FIREARM, // в руки, класс RANGED BLADE, // в руки, класс MELEE ARMOR, // на корпус TRINKET, // в карман: свет, чутьё, живучесть CONSUMABLE, // расходник }; enum class ItemId : uint8_t { NONE = 0, // --- стволы --- RIFLE, PISTOL, SHOTGUN, SMG, MAGNUM, BOW, // --- клинки --- SABER, KNIFE, CLEAVER, // --- броня --- JACKET, // куртка: почти ничего, зато не мешает VEST, // жилет: ровный размен брони на скорость PLATE, // плита: держит удар, но боец становится обозом SHROUD, // саван: брони нет, зато тише и быстрее // --- карман --- LANTERN, // фонарь: видно дальше, и тебя тоже CHARM, // оберег: запас здоровья SPURS, // шпоры: скорость // --- расходники --- BANDAGE, STIMPACK, AMMO, // пачка патронов: уходит в подсумок, когда стрелять нечем COUNT, }; struct ItemDef { const char* name = "-"; ItemKind kind = ItemKind::NONE; // Что взять в руки, если kind — оружие. FirearmId gun = FirearmId::RIFLE; BladeId blade = BladeId::SABER; // Модификаторы бойца. Складываются по всем надетым предметам. float armor = 0.0f; // доля поглощённого урона, суммарно жёстко режется float speedMul = 1.0f; // множитель скорости хода float hpBonus = 0.0f; float sightMul = 1.0f; // множитель радиуса обзора (render-only) float heal = 0.0f; // расходник: сколько здоровья возвращает int rounds = 0; // расходник: сколько патронов добавляет в запас // Сколько предмет весит. Единица условная, но соотношения — нет: плита // тяжелее всего, что можно взять в руки, а карманное почти ничего не весит. // // Вес — это то, чем «унести всё» перестало быть бесплатным. До него решение // на выходе сводилось к счёту свободных ячеек, а шесть бинтов и шесть плит // стоили одинаково (docs/17-extraction.md). // // По умолчанию НОЛЬ, а не единица: ItemId::NONE — это пустая ячейка и // «брони нет», и весить пустота не должна. С единицей боец без снаряжения // таскал бы три килограмма ничего. float weight = 0.0f; // Цвет иконки в инвентаре. Нейтральные байты RGB — в sim/ типов рендера нет. uint8_t r = 200, g = 200, b = 200; }; // Броня складывается, но потолок жёсткий: полностью неуязвимых в отряде нет. constexpr float ARMOR_CAP = 0.72f; // ----------------------------------------------------------------------------- // Переноска. Порогов ДВА, и это главное решение всей вехи. // // Один жёсткий предел означал бы «сумка кончилась» — то же самое, что счёт // ячеек, только считать труднее. Настоящий выбор возникает там, где лишнее // взять МОЖНО, но за это платят: до нормы отряд идёт как шёл, дальше — тем // медленнее, чем больше нагрёб. А наверх идти через весь этаж, который уже // растревожен. // // Верхний предел всё-таки есть: без него отряд уносил бы этаж целиком со // скоростью ноль, и решение снова исчезло бы — только с другой стороны. // // Числа выставлены по каталогу лута, а не на глаз. Средняя находка первых // тиров весит около 1.1, поздних — около 1.7, значит полная сумка из шести // вещей тянет на 7–10, плюс ствол в руках. // // Отсюда пороги. Норма 7.5 — это ствол и три-четыре находки: отряд, который // взял нужное и ушёл, идёт как шёл. Полная сумка (6.6–10 плюс ствол) неизбежно // оказывается за нормой — так и задумано: «унести всё» обязано стоить скорости. // Предел 14 оставляет место найденной плите (6), но с ней в сумке больше почти // ничего не влезет — и это ровно тот выбор, ради которого вес заводился. // // Норму 6 пришлось поднять: прогон показал, что «карабин, сабля, фонарь и // жилет» весят 6.9, то есть под неё не проходил даже обычный набор. Число // противоречило собственному замыслу, и заметила это проверка, а не глаз. // // Сверяться с прогоном тут ОБЯЗАТЕЛЬНО, но не по столбцу «груз»: `--descend` // кладёт в сумки бинты, чтобы проверить перенос между этажами, и по ним вес // выглядит втрое меньше настоящего. Правило проверяется отдельно — там же, // в прогоне, куском на выдуманном инвентаре. // ----------------------------------------------------------------------------- constexpr float CARRY_LIMIT = 7.5f; // до этого веса скорость не страдает constexpr float CARRY_MAX = 14.0f; // выше не поднимет: руки заняты constexpr float CARRY_SLOW = 0.55f; // во сколько раз медленнее на пределе // Множитель скорости от нагрузки. Между нормой и пределом падает линейно: // нелинейная кривая тут была бы честнее физически и хуже играбельно — игрок // должен чувствовать «каждая лишняя вещь стоит столько-то». // bonus — прибавка к норме от улучшений профиля (sim/profile.h). Предел // двигается вместе с нормой: улучшение обязано увеличивать И то, сколько можно // нести свободно, И то, сколько вообще можно поднять, — иначе прокачка // переноски упиралась бы в невидимую стену. inline float CarryMul(float load, float bonus = 0.0f) { const float limit = CARRY_LIMIT + bonus; const float top = CARRY_MAX + bonus; if (load <= limit) return 1.0f; if (load >= top) return CARRY_SLOW; return 1.0f + (CARRY_SLOW - 1.0f) * (load - limit) / (top - limit); } inline ItemDef ItemDefOf(ItemId id) { ItemDef d; switch (id) { // --- стволы --- case ItemId::RIFLE: d.name = "CARBINE"; d.kind = ItemKind::FIREARM; d.gun = FirearmId::RIFLE; d.weight = 2.2f; d.r = 208; d.g = 196; d.b = 150; break; case ItemId::PISTOL: d.name = "PISTOL"; d.kind = ItemKind::FIREARM; d.gun = FirearmId::PISTOL; d.weight = 0.7f; d.r = 190; d.g = 180; d.b = 168; break; case ItemId::SHOTGUN: d.name = "SHOTGUN"; d.kind = ItemKind::FIREARM; d.gun = FirearmId::SHOTGUN; d.weight = 2.8f; d.r = 214; d.g = 158; d.b = 104; break; case ItemId::SMG: d.name = "SMG"; d.kind = ItemKind::FIREARM; d.gun = FirearmId::SMG; d.weight = 1.6f; d.r = 176; d.g = 200; d.b = 220; break; case ItemId::MAGNUM: d.name = "MAGNUM"; d.kind = ItemKind::FIREARM; d.gun = FirearmId::MAGNUM; d.weight = 1.5f; d.r = 226; d.g = 210; d.b = 176; break; case ItemId::BOW: d.name = "BOW"; d.kind = ItemKind::FIREARM; d.gun = FirearmId::BOW; d.weight = 1.4f; d.r = 154; d.g = 190; d.b = 164; break; // --- клинки --- case ItemId::SABER: d.name = "SABER"; d.kind = ItemKind::BLADE; d.blade = BladeId::SABER; d.weight = 1.2f; d.r = 200; d.g = 212; d.b = 228; break; case ItemId::KNIFE: d.name = "KNIFE"; d.kind = ItemKind::BLADE; d.blade = BladeId::KNIFE; d.speedMul = 1.06f; d.weight = 0.4f; d.r = 214; d.g = 222; d.b = 232; break; case ItemId::CLEAVER: d.name = "CLEAVER"; d.kind = ItemKind::BLADE; d.blade = BladeId::CLEAVER; d.speedMul = 0.94f; d.weight = 2.2f; d.r = 196; d.g = 174; d.b = 160; break; // --- броня --- case ItemId::JACKET: d.name = "JACKET"; d.kind = ItemKind::ARMOR; d.armor = 0.10f; d.speedMul = 1.00f; d.weight = 1.2f; d.r = 122; d.g = 112; d.b = 96; break; case ItemId::VEST: d.name = "VEST"; d.kind = ItemKind::ARMOR; d.armor = 0.28f; d.speedMul = 0.95f; d.hpBonus = 10.0f; d.weight = 3.0f; d.r = 108; d.g = 118; d.b = 108; break; case ItemId::PLATE: d.name = "PLATE"; d.kind = ItemKind::ARMOR; d.armor = 0.48f; d.speedMul = 0.82f; d.hpBonus = 25.0f; d.weight = 6.0f; d.r = 140; d.g = 146; d.b = 156; break; case ItemId::SHROUD: d.name = "SHROUD"; d.kind = ItemKind::ARMOR; d.armor = 0.04f; d.speedMul = 1.12f; d.sightMul = 0.92f; d.weight = 0.8f; d.r = 88; d.g = 84; d.b = 100; break; // --- карман --- case ItemId::LANTERN: d.name = "LANTERN"; d.kind = ItemKind::TRINKET; d.sightMul = 1.35f; d.weight = 0.5f; d.r = 240; d.g = 206; d.b = 130; break; case ItemId::CHARM: d.name = "CHARM"; d.kind = ItemKind::TRINKET; d.hpBonus = 30.0f; d.weight = 0.3f; d.r = 196; d.g = 130; d.b = 190; break; case ItemId::SPURS: d.name = "SPURS"; d.kind = ItemKind::TRINKET; d.speedMul = 1.14f; d.weight = 0.3f; d.r = 198; d.g = 184; d.b = 120; break; // --- расходники --- case ItemId::BANDAGE: d.name = "BANDAGE"; d.kind = ItemKind::CONSUMABLE; d.heal = 25.0f; d.weight = 0.2f; d.r = 216; d.g = 208; d.b = 196; break; case ItemId::STIMPACK: d.name = "STIM"; d.kind = ItemKind::CONSUMABLE; d.heal = 60.0f; d.weight = 0.25f; d.r = 130; d.g = 220; d.b = 170; break; case ItemId::AMMO: d.name = "AMMO"; d.kind = ItemKind::CONSUMABLE; d.rounds = AMMO_BOX_ROUNDS; d.weight = 1.0f; d.r = 200; d.g = 176; d.b = 96; break; default: break; } return d; } inline const char* ItemName(ItemId id) { return ItemDefOf(id).name; } inline bool IsWeaponItem(ItemId id) { const ItemKind k = ItemDefOf(id).kind; return k == ItemKind::FIREARM || k == ItemKind::BLADE; } // --- инвентарь ---------------------------------------------------------------- // ----------------------------------------------------------------------------- // Сумка — ОДНА НА ОТРЯД, а не шесть ячеек на каждого. // // Раньше каждый боец таскал своё, и это порождало ровно одну проблему, которую // нечем было решить: пачка патронов, поднятая бойцом с клинком, доставалась // только ему и лежала мёртвым грузом до конца забега. Перекладывать между // бойцами игра не умела, и научить её значило бы завести целый обмен. // // Отряд ходит одной кучей и дерётся как одно целое — пусть и несёт как одно // целое. Личным остаётся НАДЕТОЕ: руки, броня, карман. Их не разделить, они и // есть «кто этот боец». // ----------------------------------------------------------------------------- constexpr int SQUAD_BAG_SLOTS = 16; struct SquadBag { ItemId slots[SQUAD_BAG_SLOTS] = {}; float Load() const { float w = 0.0f; for (int i = 0; i < SQUAD_BAG_SLOTS; ++i) w += ItemDefOf(slots[i]).weight; return w; } int Count() const { int n = 0; for (int i = 0; i < SQUAD_BAG_SLOTS; ++i) if (slots[i] != ItemId::NONE) ++n; return n; } int FreeSlot() const { for (int i = 0; i < SQUAD_BAG_SLOTS; ++i) if (slots[i] == ItemId::NONE) return i; return -1; } int Find(ItemKind kind) const { for (int i = 0; i < SQUAD_BAG_SLOTS; ++i) if (slots[i] != ItemId::NONE && ItemDefOf(slots[i]).kind == kind) return i; return -1; } // Влезет ли ещё одна вещь. Ячейки — это «сколько разных вещей», вес — // «сколько они тянут» на КАЖДОГО, и одно другое не заменяет. // // carriers — сколько живых бойцов делят ношу. Меньше живых — тяжелее всем, // и это не побочный эффект: потерять бойца значит взвалить его долю на // остальных, и обратно наверх они пойдут медленнее. bool CanTake(ItemId id, int carriers, float weaponLoad, float bonus = 0.0f) const { if (FreeSlot() < 0) return false; const int n = (carriers > 0) ? carriers : 1; return weaponLoad + (Load() + ItemDefOf(id).weight) / float(n) <= CARRY_MAX + bonus; } bool Take(ItemId id, int carriers, float weaponLoad, float bonus = 0.0f) { if (!CanTake(id, carriers, weaponLoad, bonus)) return false; const int slot = FreeSlot(); if (slot < 0) return false; slots[slot] = id; return true; } void Clear() { for (int i = 0; i < SQUAD_BAG_SLOTS; ++i) slots[i] = ItemId::NONE; } }; // Компонент ECS на сущности бойца: только НАДЕТОЕ. struct Inventory { ItemId weapon = ItemId::RIFLE; // в руках ItemId armor = ItemId::NONE; ItemId trinket = ItemId::NONE; // Суммарные модификаторы надетого. Оружие в руках тоже считается: тесак // весит, нож — нет. float Armor() const { const float a = ItemDefOf(armor).armor + ItemDefOf(trinket).armor; return (a < ARMOR_CAP) ? a : ARMOR_CAP; } float SpeedMul() const { return ItemDefOf(armor).speedMul * ItemDefOf(trinket).speedMul * ItemDefOf(weapon).speedMul; } float HpBonus() const { return ItemDefOf(armor).hpBonus + ItemDefOf(trinket).hpBonus; } float SightMul() const { return ItemDefOf(armor).sightMul * ItemDefOf(trinket).sightMul; } // Что боец несёт САМ — ствол в руках. Доля общей сумки прибавляется снаружи // (SyncLoadout): инвентарь про отряд не знает. // // Надетое сюда НЕ входит, и это не поблажка. За броню уже заплачено // множителем скорости (плита — 0.82), и считать её вес вторично значило бы // наказывать за один выбор дважды. Зато плита, НАЙДЕННАЯ и лежащая в // сумке, весит полной мерой: тащить её наверх — это и есть выбор. float Load() const { return ItemDefOf(weapon).weight; } };