squad-proto/src/sim/weapon_defs.h
z.kirill e3294cffd4 Личинки, парирование и инструмент --wall (M30)
--WALL. Новый прогон отвечает на вопрос, появившийся вместе с чекпоинтами
через пятьдесят этажей: растёт ли достижимая глубина от вылазки к вылазке.
Отряд из одного, трёх и пяти, одетый по глубине, ставится в скопление
тварей и живёт полторы минуты без управления. Первая стена: одиночка D7,
трое D30, пятеро D30 - петля работает.

И он же нашёл настоящую ошибку: PlaceSquad воскрешал ВСЕ пять слотов, в
том числе незанятые. Строка писалась, когда отряд всегда был впятером;
после появления ростера одиночка выходил на этаж с четырьмя невидимыми
телохранителями. Из-за них прежний замер "одиночка выжил на первом этаже,
100 -> 41" был боем впятером.

ЛИЧИНКИ. С исправленным замером выяснилось, что одиночка с ножом
проигрывает двум бредущим: 210 здоровья против ножа это девять секунд
работы под ответным уроном. Первые этажи населены личинками (55 здоровья,
7 урона, медленные), бредущий уехал на третий этаж, бегун на четвёртый.
Новый ВИД, а не ослабленный бредущий с множителем: глубина в этой игре
меняет, кто выходит, а не числа у одного и того же.

ПАРИРОВАНИЕ. Отдельная характеристика клинка, и чем оружие легче, тем она
выше: нож 34%, сабля 20%, тесак 8%. Не выведена из веса намеренно - её
двигает аффикс PARRY, и лёгкий тесак обязан быть возможен как находка.

Отбить можно, только если удары СОПРИКОСНУЛИСЬ: боец сам должен быть в
замахе или в ударе. Иначе клинок стал бы щитом, работающим, пока стоишь.

Удавшийся отбив снимает урон, бьёт тварь ударом бойца целиком и запускает
повторную атаку с начала фазы, НЕ сбрасывая откат. Последнее и есть суть
размена: сбрасывай отбив откат, выгоднее было бы подставляться под удары.

Плевок отбивается тоже: летит назад и несёт урон бойца, а не твари. Это
его удар, просто по летящей цели.

Проверка меряет размен целиком, включая обратную половину правила:
"стоя столбом не отбивает". Без неё условие про соприкосновение
существовало бы только в комментарии.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 05:04:35 +03:00

477 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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";
}
}