--WALL. Новый прогон отвечает на вопрос, появившийся вместе с чекпоинтами через пятьдесят этажей: растёт ли достижимая глубина от вылазки к вылазке. Отряд из одного, трёх и пяти, одетый по глубине, ставится в скопление тварей и живёт полторы минуты без управления. Первая стена: одиночка D7, трое D30, пятеро D30 - петля работает. И он же нашёл настоящую ошибку: PlaceSquad воскрешал ВСЕ пять слотов, в том числе незанятые. Строка писалась, когда отряд всегда был впятером; после появления ростера одиночка выходил на этаж с четырьмя невидимыми телохранителями. Из-за них прежний замер "одиночка выжил на первом этаже, 100 -> 41" был боем впятером. ЛИЧИНКИ. С исправленным замером выяснилось, что одиночка с ножом проигрывает двум бредущим: 210 здоровья против ножа это девять секунд работы под ответным уроном. Первые этажи населены личинками (55 здоровья, 7 урона, медленные), бредущий уехал на третий этаж, бегун на четвёртый. Новый ВИД, а не ослабленный бредущий с множителем: глубина в этой игре меняет, кто выходит, а не числа у одного и того же. ПАРИРОВАНИЕ. Отдельная характеристика клинка, и чем оружие легче, тем она выше: нож 34%, сабля 20%, тесак 8%. Не выведена из веса намеренно - её двигает аффикс PARRY, и лёгкий тесак обязан быть возможен как находка. Отбить можно, только если удары СОПРИКОСНУЛИСЬ: боец сам должен быть в замахе или в ударе. Иначе клинок стал бы щитом, работающим, пока стоишь. Удавшийся отбив снимает урон, бьёт тварь ударом бойца целиком и запускает повторную атаку с начала фазы, НЕ сбрасывая откат. Последнее и есть суть размена: сбрасывай отбив откат, выгоднее было бы подставляться под удары. Плевок отбивается тоже: летит назад и несёт урон бойца, а не твари. Это его удар, просто по летящей цели. Проверка меряет размен целиком, включая обратную половину правила: "стоя столбом не отбивает". Без неё условие про соприкосновение существовало бы только в комментарии. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
477 lines
18 KiB
C++
477 lines
18 KiB
C++
#pragma once
|
||
#include "sim/element.h"
|
||
// -----------------------------------------------------------------------------
|
||
// Каталог оружия: ЧТО такое ствол или клинок, в отрыве от того, кто его держит.
|
||
//
|
||
// Оружие — отдельная сущность ECS с компонентом WeaponItem. Боец хранит только
|
||
// ссылку на неё (Agent::weaponEntity) и рабочее состояние выстрела (патроны,
|
||
// откат, увод ствола). Поэтому «дать бойцу пистолет» — это подменить компонент
|
||
// на его сущности-оружии, а не править ни агента, ни правила боя.
|
||
//
|
||
// Где живут числа. Общее правило прототипа — все числа в tuning.h. Здесь оно
|
||
// соблюдено с одной оговоркой, записанной в docs/08-decisions.md: КАРАБИН
|
||
// (оружие по умолчанию, то самое, на котором меряются критерии приёмки) читает
|
||
// свои параметры из g_tune, поэтому все прежние ручки оверлея и --set работают
|
||
// как работали. Остальные стволы — записи каталога: у второго ствола не может
|
||
// быть «того же самого» weaponRange, иначе он не второй ствол.
|
||
//
|
||
// Визуальные параметры атаки лежат ЗДЕСЬ ЖЕ, но в нейтральном виде: длины,
|
||
// радиусы в пикселях и байты RGB. Ни одного типа рендера в sim/ не появилось —
|
||
// цвет собирает render/fx.cpp. Так у пистолета своя вспышка и свой трассер,
|
||
// и при этом правило «sim не знает про рендер» не нарушено.
|
||
// -----------------------------------------------------------------------------
|
||
#include <cstdint>
|
||
|
||
#include "engine/math.h"
|
||
#include "tuning.h"
|
||
|
||
enum class WeaponClass : uint8_t
|
||
{
|
||
RANGED = 0, // ствол: линия огня, снаряд, правило 6.3
|
||
MELEE = 1, // клинок: сектор замаха, то же правило в угловой форме
|
||
};
|
||
|
||
inline const char* WeaponClassName(WeaponClass c)
|
||
{
|
||
return (c == WeaponClass::MELEE) ? "MELEE" : "RANGED";
|
||
}
|
||
|
||
enum class FirearmId : uint8_t
|
||
{
|
||
RIFLE = 0, // штатный карабин отряда: числа из tuning.h
|
||
PISTOL, // короткоствол: ближе, быстрее, мельче магазин
|
||
SHOTGUN, // картечь: залп дробин, в упор рвёт, дальше двух шагов бесполезен
|
||
SMG, // шквал: очередь дешёвыми патронами, ствол уводит адски
|
||
MAGNUM, // один тяжёлый выстрел: медленно, больно, ошибок не прощает
|
||
BOW, // тихий составной лук: редкий тяжёлый снаряд без дульной вспышки
|
||
COUNT,
|
||
};
|
||
|
||
enum class BladeId : uint8_t
|
||
{
|
||
SABER = 0, // штатный клинок: числа из tuning.h
|
||
KNIFE, // нож: узкая дуга, зато почти без замаха
|
||
CLEAVER, // тесак: широкий, тяжёлый, сносит с ног
|
||
COUNT,
|
||
};
|
||
|
||
// --- ствол -------------------------------------------------------------------
|
||
|
||
struct FirearmDef
|
||
{
|
||
const char* name = "rifle";
|
||
|
||
// Из чего состоит удар. По умолчанию чистая кинетика: карабин, на котором
|
||
// меряется приёмка, обязан остаться ровно таким, каким был.
|
||
ElementMix elem;
|
||
|
||
// баллистика и урон
|
||
float range = 7.2f;
|
||
float fireInterval = 0.35f;
|
||
float damage = 12.0f;
|
||
float laneHalfWidth = 0.10f;
|
||
float muzzleOffset = 0.30f;
|
||
float bulletSpeed = 22.0f;
|
||
float bulletRadius = 0.04f;
|
||
float knockback = 0.35f; // импульс мишени от попадания
|
||
|
||
// разброс: стоя, на полном ходу, и увод ствола за выстрел
|
||
float spreadStandDeg = 0.9f;
|
||
float spreadMoveDeg = 4.2f;
|
||
float spreadRecoilDeg = 0.55f;
|
||
float spreadRecoilMax = 3.0f;
|
||
float recoilDecay = 3.2f; // 1/сек
|
||
|
||
// перезарядка
|
||
int magazine = 24;
|
||
float reloadTime = 1.9f;
|
||
|
||
// Сколько снарядов уходит за один спуск. Дробины летят в ОДНОМ уже
|
||
// проверенном конусе разброса — правило 6.3 от залпа не слабеет.
|
||
int pellets = 1;
|
||
|
||
// визуал атаки (нейтральные числа: пиксели, секунды, байты RGB)
|
||
float tracerLength = 1.30f;
|
||
float muzzleRadiusPx = 5.0f;
|
||
float muzzleTime = 0.07f;
|
||
float muzzleFlamePx = 0.35f; // длина языка пламени, юнитов мира
|
||
float flashPower = 0.55f; // как источник света
|
||
float flashRadius = 4.0f;
|
||
float shakePerShot = 0.55f;
|
||
uint8_t tracerR = 255, tracerG = 240, tracerB = 205;
|
||
uint8_t muzzleR = 255, muzzleG = 225, muzzleB = 150;
|
||
};
|
||
|
||
// --- клинок ------------------------------------------------------------------
|
||
|
||
struct BladeDef
|
||
{
|
||
const char* name = "blade";
|
||
|
||
ElementMix elem;
|
||
|
||
float reach = 1.40f;
|
||
float arcDeg = 100.0f;
|
||
float windup = 0.17f;
|
||
float strike = 0.13f;
|
||
float recover = 0.24f;
|
||
float interval = 0.60f;
|
||
float damage = 130.0f;
|
||
float knockback = 2.4f; // импульс мишени, юнитов/сек
|
||
float lungeSpeed = 3.4f; // рывок в момент удара
|
||
|
||
// Шанс отбить чужой удар. ЧЕМ ЛЕГЧЕ ОРУЖИЕ, ТЕМ ВЫШЕ: нож успевает встать
|
||
// на пути, тесаком парировать нечем — им можно только бить первым.
|
||
//
|
||
// Это отдельная характеристика, а не производная от веса: её меняют
|
||
// аффиксы (sim/affix.h), и «лёгкий тесак» должен быть возможен как
|
||
// находка, а не запрещён формулой.
|
||
float parry = 0.0f;
|
||
};
|
||
|
||
// --- компонент сущности-оружия ------------------------------------------------
|
||
|
||
struct WeaponItem
|
||
{
|
||
WeaponClass cls = WeaponClass::RANGED;
|
||
FirearmId gunId = FirearmId::RIFLE;
|
||
BladeId bladeId = BladeId::SABER;
|
||
FirearmDef gun{};
|
||
BladeDef blade{};
|
||
};
|
||
|
||
// --- каталог ------------------------------------------------------------------
|
||
|
||
// Карабин: параметры берутся из tuning, чтобы прежние ручки и --set остались
|
||
// рабочими (см. шапку файла).
|
||
inline FirearmDef RifleDef()
|
||
{
|
||
FirearmDef d;
|
||
d.name = "rifle";
|
||
d.range = g_tune.weaponRange;
|
||
d.fireInterval = g_tune.fireInterval;
|
||
d.damage = g_tune.damage;
|
||
d.laneHalfWidth = g_tune.laneHalfWidth;
|
||
d.muzzleOffset = g_tune.muzzleOffset;
|
||
d.bulletSpeed = g_tune.bulletSpeed;
|
||
d.bulletRadius = g_tune.bulletRadius;
|
||
d.knockback = g_tune.bulletKnock;
|
||
|
||
d.spreadStandDeg = g_tune.spreadStandDeg;
|
||
d.spreadMoveDeg = g_tune.spreadMoveDeg;
|
||
d.spreadRecoilDeg = g_tune.spreadRecoilDeg;
|
||
d.spreadRecoilMax = g_tune.spreadRecoilMax;
|
||
d.recoilDecay = g_tune.spreadRecoilDecay;
|
||
|
||
d.magazine = int(g_tune.magazineRifle);
|
||
d.reloadTime = g_tune.reloadRifle;
|
||
|
||
d.tracerLength = g_tune.tracerLength;
|
||
d.muzzleRadiusPx = g_tune.muzzleRadiusPx;
|
||
d.muzzleTime = g_tune.muzzleTime;
|
||
d.flashPower = g_tune.flashPower;
|
||
d.flashRadius = g_tune.flashRadius;
|
||
d.shakePerShot = g_tune.shakePerShot;
|
||
return d;
|
||
}
|
||
|
||
// Пистолет: бьёт ближе и слабее, но чаще; на ходу «гуляет» заметно сильнее
|
||
// карабина, зато перезаряжается вдвое быстрее. Вспышка мельче и холоднее,
|
||
// трассер короче — ствол должен читаться по картинке, а не только по цифрам.
|
||
inline FirearmDef PistolDef()
|
||
{
|
||
FirearmDef d;
|
||
d.name = "pistol";
|
||
d.range = 4.6f;
|
||
d.fireInterval = 0.22f;
|
||
d.damage = 8.0f;
|
||
d.laneHalfWidth = 0.10f;
|
||
d.muzzleOffset = 0.24f;
|
||
d.bulletSpeed = 17.0f;
|
||
d.bulletRadius = 0.04f;
|
||
d.knockback = 0.22f;
|
||
|
||
d.spreadStandDeg = 1.6f;
|
||
d.spreadMoveDeg = 6.5f;
|
||
d.spreadRecoilDeg = 0.9f;
|
||
d.spreadRecoilMax = 4.5f;
|
||
d.recoilDecay = 4.5f;
|
||
|
||
d.magazine = 7;
|
||
d.reloadTime = 1.15f;
|
||
|
||
d.tracerLength = 0.85f;
|
||
d.muzzleRadiusPx = 3.5f;
|
||
d.muzzleTime = 0.055f;
|
||
d.muzzleFlamePx = 0.26f;
|
||
d.flashPower = 0.42f;
|
||
d.flashRadius = 3.2f;
|
||
d.shakePerShot = 0.34f;
|
||
d.tracerR = 255; d.tracerG = 214; d.tracerB = 170;
|
||
d.muzzleR = 255; d.muzzleG = 205; d.muzzleB = 130;
|
||
return d;
|
||
}
|
||
|
||
// Картечь: один спуск — залп дробин в общем конусе. Дальность позорная, зато
|
||
// в упор снимает столько же, сколько очередь карабина. Конус широкий всегда,
|
||
// поэтому проверка «свой на линии» отсекает выстрел куда чаще, чем у ствола —
|
||
// это не баг картечи, это её цена.
|
||
inline FirearmDef ShotgunDef()
|
||
{
|
||
FirearmDef d;
|
||
d.name = "shotgun";
|
||
d.range = 3.4f;
|
||
d.fireInterval = 0.85f;
|
||
d.damage = 26.0f; // на дробину
|
||
d.laneHalfWidth = 0.10f;
|
||
d.muzzleOffset = 0.32f;
|
||
d.bulletSpeed = 19.0f;
|
||
d.bulletRadius = 0.05f;
|
||
d.knockback = 0.55f;
|
||
|
||
d.spreadStandDeg = 7.5f;
|
||
d.spreadMoveDeg = 13.0f;
|
||
d.spreadRecoilDeg = 1.4f;
|
||
d.spreadRecoilMax = 5.0f;
|
||
d.recoilDecay = 5.0f;
|
||
|
||
d.magazine = 5;
|
||
d.reloadTime = 2.6f;
|
||
d.pellets = 6;
|
||
|
||
d.tracerLength = 0.55f;
|
||
d.muzzleRadiusPx = 7.5f;
|
||
d.muzzleTime = 0.10f;
|
||
d.muzzleFlamePx = 0.50f;
|
||
d.flashPower = 0.85f;
|
||
d.flashRadius = 5.2f;
|
||
d.shakePerShot = 1.55f;
|
||
d.tracerR = 255; d.tracerG = 190; d.tracerB = 130;
|
||
d.muzzleR = 255; d.muzzleG = 196; d.muzzleB = 110;
|
||
|
||
// раскалённая дробь: стихии есть только у НАХОДИМОГО оружия. Карабин, пистолет и
|
||
// сабля остаются чистой кинетикой — на них меряется приёмка.
|
||
d.elem = ElementMix::Of(Element::INC, 0.55f);
|
||
return d;
|
||
}
|
||
|
||
// Шквал: патроны дешёвые, урон мизерный, но ствол не молчит. Увод копится
|
||
// быстрее, чем оседает, поэтому длинная очередь сама себя разбрасывает.
|
||
inline FirearmDef SmgDef()
|
||
{
|
||
FirearmDef d;
|
||
d.name = "smg";
|
||
d.range = 5.4f;
|
||
d.fireInterval = 0.09f;
|
||
d.damage = 6.0f;
|
||
d.laneHalfWidth = 0.10f;
|
||
d.muzzleOffset = 0.28f;
|
||
d.bulletSpeed = 20.0f;
|
||
d.bulletRadius = 0.035f;
|
||
d.knockback = 0.16f;
|
||
|
||
d.spreadStandDeg = 1.9f;
|
||
d.spreadMoveDeg = 7.0f;
|
||
d.spreadRecoilDeg = 0.7f;
|
||
d.spreadRecoilMax = 7.0f;
|
||
d.recoilDecay = 5.5f;
|
||
|
||
d.magazine = 40;
|
||
d.reloadTime = 2.2f;
|
||
|
||
d.tracerLength = 0.95f;
|
||
d.muzzleRadiusPx = 3.8f;
|
||
d.muzzleTime = 0.05f;
|
||
d.muzzleFlamePx = 0.22f;
|
||
d.flashPower = 0.38f;
|
||
d.flashRadius = 3.0f;
|
||
d.shakePerShot = 0.22f;
|
||
d.tracerR = 220; d.tracerG = 240; d.tracerB = 255;
|
||
d.muzzleR = 210; d.muzzleG = 230; d.muzzleB = 255;
|
||
|
||
// электризованный боеприпас: стихии есть только у НАХОДИМОГО оружия. Карабин, пистолет и
|
||
// сабля остаются чистой кинетикой — на них меряется приёмка.
|
||
d.elem = ElementMix::Of(Element::ARC, 0.50f);
|
||
return d;
|
||
}
|
||
|
||
// Тяжёлый револьвер: бьёт дальше карабина и валит с ног, но между выстрелами
|
||
// целая секунда и в барабане шесть патронов. Оружие для того, кто попадает.
|
||
inline FirearmDef MagnumDef()
|
||
{
|
||
FirearmDef d;
|
||
d.name = "magnum";
|
||
d.range = 8.6f;
|
||
d.fireInterval = 0.95f;
|
||
d.damage = 78.0f;
|
||
d.laneHalfWidth = 0.10f;
|
||
d.muzzleOffset = 0.30f;
|
||
d.bulletSpeed = 30.0f;
|
||
d.bulletRadius = 0.05f;
|
||
d.knockback = 1.30f;
|
||
|
||
d.spreadStandDeg = 0.6f;
|
||
d.spreadMoveDeg = 8.0f; // с рук на бегу — не стрелять
|
||
d.spreadRecoilDeg = 2.2f;
|
||
d.spreadRecoilMax = 6.0f;
|
||
d.recoilDecay = 3.0f;
|
||
|
||
d.magazine = 6;
|
||
d.reloadTime = 3.0f;
|
||
|
||
d.tracerLength = 1.85f;
|
||
d.muzzleRadiusPx = 6.5f;
|
||
d.muzzleTime = 0.09f;
|
||
d.muzzleFlamePx = 0.45f;
|
||
d.flashPower = 0.80f;
|
||
d.flashRadius = 5.0f;
|
||
d.shakePerShot = 1.90f;
|
||
d.tracerR = 255; d.tracerG = 246; d.tracerB = 226;
|
||
d.muzzleR = 255; d.muzzleG = 232; d.muzzleB = 176;
|
||
|
||
// пробивает то, что защищено: стихии есть только у НАХОДИМОГО оружия. Карабин, пистолет и
|
||
// сабля остаются чистой кинетикой — на них меряется приёмка.
|
||
d.elem = ElementMix::Of(Element::VOID, 0.45f);
|
||
return d;
|
||
}
|
||
|
||
inline FirearmDef BowDef()
|
||
{
|
||
FirearmDef d;
|
||
d.name = "bow";
|
||
d.range = 8.0f;
|
||
d.fireInterval = 0.82f;
|
||
d.damage = 54.0f;
|
||
d.laneHalfWidth = 0.08f;
|
||
d.muzzleOffset = 0.34f;
|
||
d.bulletSpeed = 15.0f;
|
||
d.bulletRadius = 0.035f;
|
||
d.knockback = 0.72f;
|
||
d.spreadStandDeg = 0.7f;
|
||
d.spreadMoveDeg = 7.5f;
|
||
d.spreadRecoilDeg = 0.0f;
|
||
d.spreadRecoilMax = 0.0f;
|
||
d.recoilDecay = 8.0f;
|
||
d.magazine = 1;
|
||
d.reloadTime = 0.72f;
|
||
d.tracerLength = 0.38f;
|
||
d.muzzleRadiusPx = 0.0f;
|
||
d.muzzleTime = 0.0f;
|
||
d.muzzleFlamePx = 0.0f;
|
||
d.flashPower = 0.0f;
|
||
d.flashRadius = 0.0f;
|
||
d.shakePerShot = 0.18f;
|
||
d.tracerR = 178; d.tracerG = 205; d.tracerB = 180;
|
||
d.muzzleR = 0; d.muzzleG = 0; d.muzzleB = 0;
|
||
|
||
// отравленный болт: стихии есть только у НАХОДИМОГО оружия. Карабин, пистолет и
|
||
// сабля остаются чистой кинетикой — на них меряется приёмка.
|
||
d.elem = ElementMix::Of(Element::TOX, 0.75f);
|
||
return d;
|
||
}
|
||
|
||
inline FirearmDef FirearmDefOf(FirearmId id)
|
||
{
|
||
switch (id)
|
||
{
|
||
case FirearmId::PISTOL: return PistolDef();
|
||
case FirearmId::SHOTGUN: return ShotgunDef();
|
||
case FirearmId::SMG: return SmgDef();
|
||
case FirearmId::MAGNUM: return MagnumDef();
|
||
case FirearmId::BOW: return BowDef();
|
||
default: return RifleDef();
|
||
}
|
||
}
|
||
|
||
inline const char* FirearmName(FirearmId id)
|
||
{
|
||
switch (id)
|
||
{
|
||
case FirearmId::PISTOL: return "PISTOL";
|
||
case FirearmId::SHOTGUN: return "SHOTGUN";
|
||
case FirearmId::SMG: return "SMG";
|
||
case FirearmId::MAGNUM: return "MAGNUM";
|
||
case FirearmId::BOW: return "BOW";
|
||
default: return "RIFLE";
|
||
}
|
||
}
|
||
|
||
// Штатный клинок: числа целиком в tuning, ручки оверлея крутят именно его.
|
||
inline BladeDef DefaultBladeDef()
|
||
{
|
||
BladeDef d;
|
||
d.reach = g_tune.meleeReach;
|
||
d.arcDeg = g_tune.meleeArcDeg;
|
||
d.windup = g_tune.meleeWindup;
|
||
d.strike = g_tune.meleeStrike;
|
||
d.recover = g_tune.meleeRecover;
|
||
d.interval = g_tune.meleeInterval;
|
||
d.damage = g_tune.meleeDamage;
|
||
d.knockback = g_tune.meleeKnockback;
|
||
d.lungeSpeed = g_tune.meleeLungeSpeed;
|
||
d.parry = 0.20f; // сабля: середина между ножом и тесаком
|
||
return d;
|
||
}
|
||
|
||
// Нож: узкая дуга и почти нет замаха. В тесноте это решает — сектор шириной
|
||
// в ладонь почти всегда чист, тогда как тесак некуда занести.
|
||
inline BladeDef KnifeDef()
|
||
{
|
||
BladeDef d;
|
||
d.name = "knife";
|
||
d.reach = 1.00f;
|
||
d.arcDeg = 55.0f;
|
||
d.windup = 0.08f;
|
||
d.strike = 0.09f;
|
||
d.recover = 0.13f;
|
||
d.interval = 0.34f;
|
||
d.damage = 62.0f;
|
||
d.parry = 0.34f; // нож: почти всегда успевает подставиться
|
||
d.knockback = 0.9f;
|
||
d.lungeSpeed = 4.2f;
|
||
return d;
|
||
}
|
||
|
||
// Тесак: широкий замах, тяжёлый урон, долгое восстановление. Своих распугивает
|
||
// сам по себе — резерв сектора у него больше всех.
|
||
inline BladeDef CleaverDef()
|
||
{
|
||
BladeDef d;
|
||
d.name = "cleaver";
|
||
d.reach = 1.65f;
|
||
d.arcDeg = 135.0f;
|
||
d.windup = 0.30f;
|
||
d.strike = 0.18f;
|
||
d.recover = 0.42f;
|
||
d.interval = 0.95f;
|
||
d.damage = 240.0f;
|
||
d.parry = 0.08f; // тесак: им бьют, а не защищаются
|
||
d.knockback = 4.6f;
|
||
d.lungeSpeed = 2.8f;
|
||
|
||
// холодная сталь тесака: стихии есть только у НАХОДИМОГО оружия. Карабин, пистолет и
|
||
// сабля остаются чистой кинетикой — на них меряется приёмка.
|
||
d.elem = ElementMix::Of(Element::CRY, 0.55f);
|
||
return d;
|
||
}
|
||
|
||
inline BladeDef BladeDefOf(BladeId id)
|
||
{
|
||
switch (id)
|
||
{
|
||
case BladeId::KNIFE: return KnifeDef();
|
||
case BladeId::CLEAVER: return CleaverDef();
|
||
default: return DefaultBladeDef();
|
||
}
|
||
}
|
||
|
||
inline const char* BladeName(BladeId id)
|
||
{
|
||
switch (id)
|
||
{
|
||
case BladeId::KNIFE: return "KNIFE";
|
||
case BladeId::CLEAVER: return "CLEAVER";
|
||
default: return "SABER";
|
||
}
|
||
}
|