squad-proto/src/sim/weapon_defs.h

208 lines
8.1 KiB
C
Raw Normal View History

Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (raylib только окно, ввод и финальный блит). Ядро (спек, раздел 6): - LaneClear: проверка линии огня по своим, стенам и дальности; - решатель огневых позиций с гистерезисом и коммитом; - резервирование линий огня и расступание из чужих секторов; - пули-снаряды, дружественный урон физически возможен. Хоррор-слой (render-only): свет и туман войны с памятью карты, светящиеся трассеры, кровь, виньетка, зерно, напряжение. Ближний бой: сектор удара, три фазы, связка из трёх ударов, своя дисциплина «не бить сквозь своего». Проверка: squad_proto.exe --accept прогоняет критерии приёмки раздела 11 плюс блок M7 по ближнему бою — все PASS, FRIENDLY_HITS = 0 за 3 минуты боя. Сборка: cmake -S . -B build && cmake --build build --config Release raylib 6.0 подтягивается через FetchContent. Документация — CLAUDE.md как оглавление, содержание в docs/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
#pragma once
// -----------------------------------------------------------------------------
// Каталог оружия: ЧТО такое ствол или клинок, в отрыве от того, кто его держит.
//
// Оружие — отдельная сущность ECS с компонентом WeaponItem. Боец хранит только
// ссылку на неё (Agent::weaponEntity) и рабочее состояние выстрела (патроны,
// откат, увод ствола). Поэтому «дать бойцу пистолет» — это подменить компонент
// на его сущности-оружии, а не править ни агента, ни правила боя.
//
// Где живут числа. Общее правило прототипа — все числа в tuning.h. Здесь оно
// соблюдено с одной оговоркой, записанной в docs/08-decisions.md: КАРАБИН
// (оружие по умолчанию, то самое, на котором меряются критерии приёмки) читает
// свои параметры из g_tune, поэтому все прежние ручки оверлея и --set работают
// как работали. Остальные стволы — записи каталога: у второго ствола не может
// быть «того же самого» weaponRange, иначе он не второй ствол.
//
// Визуальные параметры атаки лежат ЗДЕСЬ ЖЕ, но в нейтральном виде: длины,
// радиусы в пикселях и байты RGB. Ни одного типа рендера в sim/ не появилось —
// цвет собирает render/fx.cpp. Так у пистолета своя вспышка и свой трассер,
// и при этом правило «sim не знает про рендер» не нарушено.
// -----------------------------------------------------------------------------
#include <cstdint>
#include "core/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 = 1, // короткоствол: ближе, быстрее, мельче магазин
COUNT = 2,
};
// --- ствол -------------------------------------------------------------------
struct FirearmDef
{
const char* name = "rifle";
// баллистика и урон
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;
// визуал атаки (нейтральные числа: пиксели, секунды, байты 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";
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; // рывок в момент удара
};
// --- компонент сущности-оружия ------------------------------------------------
struct WeaponItem
{
WeaponClass cls = WeaponClass::RANGED;
FirearmId gunId = FirearmId::RIFLE;
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 FirearmDefOf(FirearmId id)
{
return (id == FirearmId::PISTOL) ? PistolDef() : RifleDef();
}
inline const char* FirearmName(FirearmId id)
{
return (id == FirearmId::PISTOL) ? "PISTOL" : "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;
return d;
}