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