Прежние пять операций были СЛЕПЫМИ: перековка бросала всё заново,
прививка ставила случайный тег, отсечение снимало последний аффикс,
возвышение поднимало случайный. Игрок не принимал ни одного решения - жал
кнопку и смотрел, что выпало. Это не крафт, это второй бросок предмета за
материалы.
Теперь половина операций направленные: игрок показывает пальцем, какой
аффикс трогать, и платит за это вдвое - вот это уже решение
("перебросить всё дёшево или снять один дорого").
Два новых глагола:
ЗАКАЛКА двигает рулон выбранного аффикса к максимуму его тира на треть
остатка. Дёшево, понятно, повторяемо - то самое "доработать почти
хорошую вещь". Тир не трогает: за ступень платят возвышением.
ОТПЕЧАТОК переносит лучший аффикс с ДРУГОЙ вещи, сжигая её. Именно он
превращает хлам из схрона в ресурс, а не в мусор, который лежит.
Правило "один тег один раз" им не обходится, бюджет проверяется.
В мастерской: клик по вещи кладёт её на верстак, повторный клик по той же
назначает донором (помечается кровью - он сгорит, и видеть это надо ДО
нажатия). Строки аффиксов стали кнопками: по ним выбирается цель.
Проверка меряет НАПРАВЛЕННОСТЬ, а не факт срабатывания:
направленность: отсечение по цели, возвышение по цели,
закалка двигает рулон, отпечаток перенёс и сжёг донора
Отсечение пробуется на СРЕДНЕМ аффиксе из трёх: операция, которая
"работает", снимая последний, прошла бы и слепую версию.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
910 lines
41 KiB
C++
910 lines
41 KiB
C++
#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";
|
||
}
|
||
}
|