371 lines
19 KiB
C
371 lines
19 KiB
C
|
|
#pragma once
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// Предметы и снаряжение: ЧТО боец несёт на себе.
|
|||
|
|
//
|
|||
|
|
// Разделение то же, что у оружия (sim/weapon_defs.h):
|
|||
|
|
// * ItemDef — неизменяемое описание предмета, одна запись каталога;
|
|||
|
|
// * Inventory — компонент бойца: что в руках, что надето, что в сумке.
|
|||
|
|
//
|
|||
|
|
// Снаряжение НЕ трогает правила боя. Оно даёт бойцу четыре числа —
|
|||
|
|
// броня, скорость, запас здоровья, дальность обзора, — и всё. Никакой предмет
|
|||
|
|
// не может ослабить правило «не бить своих»: у него просто нет такой ручки.
|
|||
|
|
// Это не самоограничение, а следствие устройства: FireWeapons и UpdateMelee
|
|||
|
|
// про Inventory не знают вообще, они читают уже готовые статы агента.
|
|||
|
|
//
|
|||
|
|
// Оружие живёт двумя сущностями: ItemId в инвентаре — это ЧТО взято в руки,
|
|||
|
|
// а сущность-оружие ECS хранит рабочие статы. SyncLoadout переносит одно
|
|||
|
|
// в другое каждый шаг, поэтому «поменять ствол» — это одна запись ItemId.
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
#include <cstdint>
|
|||
|
|
|
|||
|
|
#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; }
|
|||
|
|
};
|