#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, // тихий составной лук: редкий тяжёлый снаряд без дульной вспышки NAILER, // гвоздомёт: дешевле некуда, бьёт часто и почти никак HMG, // пулемёт: полтораста патронов и шесть секунд беспомощности PLASMA, // резак: два с половиной шага дальности, зато почти чистая дуга RAIL, // рельса: три выстрела на всю комнату, между ними целая вечность FLAMER, // огнемёт: конус жара в упор, свои под него лезут постоянно ACID, // распылитель: медленные кислотные капли, разъедают жирное COUNT, }; enum class BladeId : uint8_t { SABER = 0, // штатный клинок: числа из tuning.h KNIFE, // нож: узкая дуга, зато почти без замаха CLEAVER, // тесак: широкий, тяжёлый, сносит с ног STILETTO, // стилет: держит чужой клинок лучше всех, а бьёт слабее всех MAUL, // молот: самый тяжёлый удар в игре и никакой защиты RIPPER, // цепной: мелкие частые укусы, кислота CRYOEDGE, // крио-клинок: сабля, отдавшая часть урона за холод 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; } // ----------------------------------------------------------------------------- // Вторая шестёрка стволов. Правило то же, что и у первой: НИ ОДИН не имеет права // быть «карабином, но лучше». У каждого своя роль, и у каждой роли — своя цена, // записанная числами, а не обещанная в комментарии. // // Чем платят, по каналам: // * дальностью (`range`) — резак и огнемёт стоят там же, где клинок; // * простоем (`reloadTime`) — пулемёт и рельса молчат секундами; // * разбросом на ходу (`spreadMoveDeg`) — тяжёлое с рук на бегу не работает; // * весом (`ItemDef::weight`, sim/items.h) — пулемёт съедает норму переноски // отряда целиком, и это его главный минус, а не масса ради массы; // * составом удара — стихия сильнее у того, у кого хуже баллистика. // // ЧЕГО ЗДЕСЬ НЕТ И ПОЧЕМУ. Раскрутки (ствол разгоняется и начинает бить точнее) // и сквозного пробития (пуля идёт дальше сквозь тело) в правилах боя нет: // у выстрела есть откат, конус и снаряд, и всё. Заводить поля, которых никто не // читает, значило бы положить в каталог две мёртвые ветки, поэтому обе роли // отыграны тем, что работает: у пулемёта — магазином и простоем, у рельсы — // пустотой (VOID и означает «защита не работает») и скоростью снаряда. // ----------------------------------------------------------------------------- // Гвоздомёт: строительный инструмент, которым стреляют от бедности. Самый // дешёвый выстрел в игре — и самый бесполезный по отдельности: пять единиц // урона броня съедает почти целиком. Берут его не за урон, а за то, что он // перезаряжается за секунду с четвертью и не уводит ствол, то есть работает // там, где отряд отступает и стрелять надо ВСЁ ВРЕМЯ. inline FirearmDef NailerDef() { FirearmDef d; d.name = "nailer"; d.range = 4.0f; // короче пистолета: гвоздь и не должен лететь d.fireInterval = 0.12f; d.damage = 4.5f; d.laneHalfWidth = 0.10f; d.muzzleOffset = 0.24f; d.bulletSpeed = 13.0f; // медленный снаряд: по бегуну мимо d.bulletRadius = 0.035f; d.knockback = 0.10f; // тварь даже не качнётся d.spreadStandDeg = 2.6f; // гвоздь летит криво сам по себе d.spreadMoveDeg = 6.0f; d.spreadRecoilDeg = 0.25f; d.spreadRecoilMax = 2.0f; d.recoilDecay = 6.0f; // увод оседает быстрее, чем копится d.magazine = 60; d.reloadTime = 1.25f; // быстрее всех: обойма гвоздей ставится одним движением d.tracerLength = 0.45f; d.muzzleRadiusPx = 2.5f; d.muzzleTime = 0.04f; d.muzzleFlamePx = 0.10f; d.flashPower = 0.22f; d.flashRadius = 2.0f; d.shakePerShot = 0.12f; d.tracerR = 198; d.tracerG = 192; d.tracerB = 176; d.muzzleR = 210; d.muzzleG = 198; d.muzzleB = 168; // Ржавчина на гвозде — доля кислоты крошечная: это не стихийное оружие, а // кинетика, которой немного повезло с материалом. d.elem = ElementMix::Of(Element::TOX, 0.20f); return d; } // Тяжёлый пулемёт: полтораста патронов подряд — и шесть секунд, за которые // отряд успевает потерять бойца. Второй ствол в игре, у которого простой опаснее // самого боя (первый — рельса). // // Раскрутки в правилах боя нет (см. шапку блока), и подделывать её нечем: увод // ствола умеет только РАСТИ. Поэтому цена входа отыграна тем, что есть, — // магазином, простоем и весом 7.5 в sim/items.h, ровно равным норме переноски // отряда: пулемётчик один занимает всю сумку, и любая находка уже замедляет. inline FirearmDef HmgDef() { FirearmDef d; d.name = "hmg"; d.range = 6.4f; // КОРОЧЕ карабина: длинный ствол не даёт дальности сам по себе d.fireInterval = 0.10f; d.damage = 9.0f; d.laneHalfWidth = 0.10f; d.muzzleOffset = 0.34f; d.bulletSpeed = 24.0f; d.bulletRadius = 0.045f; d.knockback = 0.30f; d.spreadStandDeg = 1.4f; d.spreadMoveDeg = 15.0f; // на ходу это стрельба в белый свет d.spreadRecoilDeg = 0.35f; d.spreadRecoilMax = 5.5f; d.recoilDecay = 2.4f; // увод оседает медленнее, чем у любого другого ствола d.magazine = 150; d.reloadTime = 6.2f; // самое дорогое молчание в каталоге d.tracerLength = 1.45f; d.muzzleRadiusPx = 6.0f; d.muzzleTime = 0.06f; d.muzzleFlamePx = 0.40f; d.flashPower = 0.62f; d.flashRadius = 4.4f; d.shakePerShot = 0.30f; d.tracerR = 255; d.tracerG = 226; d.tracerB = 176; d.muzzleR = 255; d.muzzleG = 214; d.muzzleB = 140; // Обеднённый сердечник: доля радиации небольшая, но очередь кладёт её // стаками — единственный ствол, который набивает статус САМ, без сборки. d.elem = ElementMix::Of(Element::RAD, 0.30f); return d; } // Плазменный резак: горная техника, приспособленная под бой. Дальность 2.6 — // меньше, чем у дробовика, то есть стрелок с резаком стоит ровно там же, где // боец с клинком, и получает всё, что прилетает в первую линию. Взамен почти // чистая дуга: против того, кто держит кинетику, это единственный ответ // с ЭТОГО расстояния. inline FirearmDef PlasmaDef() { FirearmDef d; d.name = "plasma"; d.range = 2.6f; // короче всех в игре d.fireInterval = 0.34f; d.damage = 30.0f; d.laneHalfWidth = 0.10f; d.muzzleOffset = 0.28f; d.bulletSpeed = 26.0f; d.bulletRadius = 0.055f; d.knockback = 0.15f; // струя не толкает: тварь дойдёт вплотную и останется d.spreadStandDeg = 0.5f; // луч точный — толку от этого на 2.6 немного d.spreadMoveDeg = 5.0f; d.spreadRecoilDeg = 0.0f; // отдачи нет вовсе d.spreadRecoilMax = 0.0f; d.recoilDecay = 8.0f; d.magazine = 14; d.reloadTime = 2.9f; d.tracerLength = 0.60f; d.muzzleRadiusPx = 5.5f; d.muzzleTime = 0.08f; d.muzzleFlamePx = 0.30f; d.flashPower = 0.90f; // светит сильнее, чем стреляет: резак виден издалека d.flashRadius = 5.0f; d.shakePerShot = 0.25f; d.tracerR = 150; d.tracerG = 230; d.tracerB = 255; d.muzzleR = 190; d.muzzleG = 240; d.muzzleB = 255; d.elem = ElementMix::Of(Element::ARC, 0.90f); return d; } // Рельса: три выстрела и почти пять секунд на перезарядку. Бьёт дальше всего в // игре (11.5 против 8.6 у магнума), и это её единственное безусловное // достоинство — всё остальное отнято. // // «Пробивает» здесь означает НЕ сквозное попадание (такого в правилах боя нет), // а пустоту: VOID срезает сопротивления, то есть защита цели перестаёт работать. // Заводить сквозной снаряд ради одного ствола значило бы менять правило 6.3 — // пуля, летящая ДАЛЬШЕ цели, перестаёт быть проверенной на своих. inline FirearmDef RailDef() { FirearmDef d; d.name = "rail"; d.range = 11.5f; d.fireInterval = 1.70f; // между выстрелами тварь проходит три шага d.damage = 170.0f; d.laneHalfWidth = 0.08f; d.muzzleOffset = 0.36f; d.bulletSpeed = 48.0f; // быстрее всех: на такой дальности иначе не попасть d.bulletRadius = 0.045f; d.knockback = 2.20f; d.spreadStandDeg = 0.20f; // со станка — точнее некуда d.spreadMoveDeg = 14.0f; // с рук на ходу — выброшенный выстрел из трёх d.spreadRecoilDeg = 3.00f; d.spreadRecoilMax = 7.00f; d.recoilDecay = 2.00f; // второй выстрел подряд уже не точный d.magazine = 3; d.reloadTime = 4.60f; d.tracerLength = 3.20f; // след через полкомнаты d.muzzleRadiusPx = 5.0f; d.muzzleTime = 0.11f; d.muzzleFlamePx = 0.20f; d.flashPower = 0.95f; d.flashRadius = 6.0f; d.shakePerShot = 2.20f; // трясёт сильнее магнума d.tracerR = 214; d.tracerG = 208; d.tracerB = 255; d.muzzleR = 200; d.muzzleG = 196; d.muzzleB = 255; d.elem = ElementMix::Of(Element::VOID, 0.70f); return d; } // Огнемёт: конус в упор. Конус — это `pellets` в широком разбросе, то есть тот // же механизм, что у картечи, и та же цена, только больше: пятнадцать градусов // проверяются правилом 6.3 на своих КАЖДЫЙ спуск, и в свалке огнемётчик молчит // чаще, чем стреляет. Это не недоработка, а весь смысл вещи: жар требует места. // // Урон за язык мизерный, ставка целиком на горение, а горение считается от // МАКСИМАЛЬНОГО здоровья цели (docs/21-damage.md) — значит по мелочи огнемёт // перерасход, а по громиле единственный смысл. inline FirearmDef FlamerDef() { FirearmDef d; d.name = "flamer"; d.range = 2.4f; d.fireInterval = 0.16f; d.damage = 7.0f; // на язык пламени d.laneHalfWidth = 0.10f; d.muzzleOffset = 0.30f; d.bulletSpeed = 9.0f; // языки ползут: по бегуну на отходе мимо d.bulletRadius = 0.07f; d.knockback = 0.05f; d.spreadStandDeg = 15.0f; // это и есть конус d.spreadMoveDeg = 19.0f; d.spreadRecoilDeg = 0.0f; d.spreadRecoilMax = 0.0f; d.recoilDecay = 8.0f; d.magazine = 100; d.reloadTime = 3.6f; d.pellets = 3; d.tracerLength = 0.30f; d.muzzleRadiusPx = 8.0f; d.muzzleTime = 0.09f; d.muzzleFlamePx = 0.75f; // самый длинный язык в игре d.flashPower = 0.75f; d.flashRadius = 4.8f; d.shakePerShot = 0.10f; d.tracerR = 255; d.tracerG = 168; d.tracerB = 70; d.muzzleR = 255; d.muzzleG = 150; d.muzzleB = 60; d.elem = ElementMix::Of(Element::INC, 0.95f); return d; } // Кислотный распылитель: две медленные капли за спуск. Урон за каплю смешной, // зато почти всё в нём — кислота, а кислота работает по тому, по чему не // работает ничего: по жирному и бронированному. По бегуну и по всему, что // движется поперёк, капли просто не попадают — скорость снаряда 11. inline FirearmDef AcidDef() { FirearmDef d; d.name = "acid"; d.range = 5.0f; d.fireInterval = 0.40f; d.damage = 16.0f; // на каплю d.laneHalfWidth = 0.10f; d.muzzleOffset = 0.28f; d.bulletSpeed = 11.0f; d.bulletRadius = 0.06f; d.knockback = 0.10f; d.spreadStandDeg = 6.0f; d.spreadMoveDeg = 11.0f; d.spreadRecoilDeg = 0.30f; d.spreadRecoilMax = 3.5f; d.recoilDecay = 4.0f; d.magazine = 18; d.reloadTime = 2.9f; d.pellets = 2; d.tracerLength = 0.35f; d.muzzleRadiusPx = 4.0f; d.muzzleTime = 0.05f; d.muzzleFlamePx = 0.0f; // не порох: вспышки нет d.flashPower = 0.20f; d.flashRadius = 2.4f; d.shakePerShot = 0.14f; d.tracerR = 176; d.tracerG = 226; d.tracerB = 110; d.muzzleR = 150; d.muzzleG = 210; d.muzzleB = 96; d.elem = ElementMix::Of(Element::TOX, 0.85f); 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(); case FirearmId::NAILER: return NailerDef(); case FirearmId::HMG: return HmgDef(); case FirearmId::PLASMA: return PlasmaDef(); case FirearmId::RAIL: return RailDef(); case FirearmId::FLAMER: return FlamerDef(); case FirearmId::ACID: return AcidDef(); 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"; case FirearmId::NAILER: return "NAILER"; case FirearmId::HMG: return "HMG"; case FirearmId::PLASMA: return "PLASMA"; case FirearmId::RAIL: return "RAIL"; case FirearmId::FLAMER: return "FLAMER"; case FirearmId::ACID: return "ACID"; 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; } // ----------------------------------------------------------------------------- // Вторая четвёрка клинков. Ось, вдоль которой они разложены, одна и та же: // ВЕС ПРОТИВ ПАРИРОВАНИЯ (docs/11-melee.md). Стилет 0.3 кг держит чужой удар в // половине случаев, молот 5.5 кг не держит его никогда — и это ровно тот размен, // из-за которого «взять потяжелее» перестаёт быть очевидным решением. // // Соотношение с уже имеющимися: нож 0.4 -> 0.34, сабля 1.2 -> 0.20, // тесак 2.2 -> 0.08. Новые садятся между ними, а не поверх. // // Второй общий размен — УРОН ПРОТИВ СТИХИИ: у крио-клинка и цепного урон // заметно ниже сабельного, и разницу они отдали за состав удара. Клинок с // полным уроном И полной стихией был бы просто лучшей саблей. // ----------------------------------------------------------------------------- // Стилет: фехтовальная игла с гардой. Парирование 0.52 — потолок каталога, и // это единственное, ради чего его берут. // // Цена честная и вся сразу: самый слабый удар в игре (44 против 62 у ножа), // досягаемость 0.85 — короче не бывает, значит бить приходится ВНУТРИ круга // твари, — и отброс почти нулевой, то есть отбитая тварь остаётся стоять // вплотную и бьёт снова. Стилетом выживают, а не убивают. inline BladeDef StilettoDef() { BladeDef d; d.name = "stiletto"; d.reach = 0.85f; d.arcDeg = 42.0f; // уже ножа: в тесноте сектор почти всегда чист d.windup = 0.06f; d.strike = 0.07f; d.recover = 0.11f; d.interval = 0.28f; d.damage = 44.0f; d.parry = 0.52f; d.knockback = 0.35f; d.lungeSpeed = 4.8f; // Игла ищет щель в защите — доля пустоты маленькая, но именно она делает // стилет пригодным против того, кто равномерно защищён. d.elem = ElementMix::Of(Element::VOID, 0.30f); return d; } // Молот: 430 за удар — больше, чем у чего угодно в игре, и отброс 7.5, который // выносит тварь из свалки целиком. // // Платит всем остальным. Замах 0.46 — вдвое длиннее тесачного, и на нём боец // вкопан; восстановление 0.62 — почти две трети секунды открытым. Парирование // 0.02 означает, что размен «удар на удар» молот проигрывает всегда, поэтому // бить он обязан ПЕРВЫМ. И удар у него ЧИСТО КИНЕТИЧЕСКИЙ: там, где цель режет // кинетику бронёй или сопротивлением, четыреста тридцать превращаются в сотню, // а стихии, чтобы обойти это, у молота нет. inline BladeDef MaulDef() { BladeDef d; d.name = "maul"; d.reach = 1.55f; d.arcDeg = 105.0f; d.windup = 0.46f; d.strike = 0.20f; d.recover = 0.62f; d.interval = 1.45f; d.damage = 430.0f; d.parry = 0.02f; d.knockback = 7.50f; d.lungeSpeed = 1.90f; // рывка почти нет: с молотом не бегают return d; } // Цепной меч: зубья вместо лезвия, укус вместо удара. Бьёт чаще всех (0.24 с // между началами замахов) и слабее почти всех — 42 за укус. // // Из этого следует его минус, и он не в цифрах, а в арифметике урона: сорок два // умножаются на сопротивление ДО того, как что-нибудь наберётся, поэтому против // защищённой цели цепь стачивается в ноль. Плюс парирование 0.10: цепь идёт // сама по себе и подставить её под чужой клинок нельзя — она уводит удар в // сторону вместе с рукой. inline BladeDef RipperDef() { BladeDef d; d.name = "ripper"; d.reach = 1.25f; d.arcDeg = 70.0f; d.windup = 0.09f; d.strike = 0.10f; d.recover = 0.15f; d.interval = 0.24f; d.damage = 42.0f; d.parry = 0.10f; d.knockback = 0.50f; d.lungeSpeed = 3.40f; // Смазка на зубьях: частый удар набивает стаки кислоты быстрее всего в игре, // и в этом весь смысл вещи — не в уроне, а в скорости накопления статуса. d.elem = ElementMix::Of(Element::TOX, 0.70f); return d; } // Крио-клинок: та же сабля, отдавшая четверть урона за холод. 104 против 130 — // разница и есть цена стихии, и по цели, которая холод держит, это просто // плохая сабля, без единого преимущества. // // Тонкий сплав легче стали (1.1 против 1.2), поэтому парирование чуть выше // сабельного — правило «легче значит лучше держит» одно на весь каталог. inline BladeDef CryoedgeDef() { BladeDef d; d.name = "cryoedge"; d.reach = 1.45f; d.arcDeg = 92.0f; d.windup = 0.19f; d.strike = 0.13f; d.recover = 0.26f; d.interval = 0.66f; // медленнее сабли: сплав тяжело разгонять d.damage = 104.0f; d.parry = 0.24f; d.knockback = 1.80f; d.lungeSpeed = 3.20f; d.elem = ElementMix::Of(Element::CRY, 0.85f); return d; } inline BladeDef BladeDefOf(BladeId id) { switch (id) { case BladeId::KNIFE: return KnifeDef(); case BladeId::CLEAVER: return CleaverDef(); case BladeId::STILETTO: return StilettoDef(); case BladeId::MAUL: return MaulDef(); case BladeId::RIPPER: return RipperDef(); case BladeId::CRYOEDGE: return CryoedgeDef(); default: return DefaultBladeDef(); } } inline const char* BladeName(BladeId id) { switch (id) { case BladeId::KNIFE: return "KNIFE"; case BladeId::CLEAVER: return "CLEAVER"; case BladeId::STILETTO: return "STILETTO"; case BladeId::MAUL: return "MAUL"; case BladeId::RIPPER: return "RIPPER"; case BladeId::CRYOEDGE: return "CRYOEDGE"; default: return "SABER"; } }