squad-proto/src/sim/weapon_defs.h
z.kirill 92a8864726 Крафт стал выбором: закалка, отпечаток, направленные операции (M33)
Прежние пять операций были СЛЕПЫМИ: перековка бросала всё заново,
прививка ставила случайный тег, отсечение снимало последний аффикс,
возвышение поднимало случайный. Игрок не принимал ни одного решения - жал
кнопку и смотрел, что выпало. Это не крафт, это второй бросок предмета за
материалы.

Теперь половина операций направленные: игрок показывает пальцем, какой
аффикс трогать, и платит за это вдвое - вот это уже решение
("перебросить всё дёшево или снять один дорого").

Два новых глагола:
  ЗАКАЛКА двигает рулон выбранного аффикса к максимуму его тира на треть
  остатка. Дёшево, понятно, повторяемо - то самое "доработать почти
  хорошую вещь". Тир не трогает: за ступень платят возвышением.

  ОТПЕЧАТОК переносит лучший аффикс с ДРУГОЙ вещи, сжигая её. Именно он
  превращает хлам из схрона в ресурс, а не в мусор, который лежит.
  Правило "один тег один раз" им не обходится, бюджет проверяется.

В мастерской: клик по вещи кладёт её на верстак, повторный клик по той же
назначает донором (помечается кровью - он сгорит, и видеть это надо ДО
нажатия). Строки аффиксов стали кнопками: по ним выбирается цель.

Проверка меряет НАПРАВЛЕННОСТЬ, а не факт срабатывания:

  направленность: отсечение по цели, возвышение по цели,
                  закалка двигает рулон, отпечаток перенёс и сжёг донора

Отсечение пробуется на СРЕДНЕМ аффиксе из трёх: операция, которая
"работает", снимая последний, прошла бы и слепую версию.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 19:37:07 +03:00

910 lines
41 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, // тихий составной лук: редкий тяжёлый снаряд без дульной вспышки
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";
}
}