squad-proto/src/render/sprites.cpp
z.kirill e3294cffd4 Личинки, парирование и инструмент --wall (M30)
--WALL. Новый прогон отвечает на вопрос, появившийся вместе с чекпоинтами
через пятьдесят этажей: растёт ли достижимая глубина от вылазки к вылазке.
Отряд из одного, трёх и пяти, одетый по глубине, ставится в скопление
тварей и живёт полторы минуты без управления. Первая стена: одиночка D7,
трое D30, пятеро D30 - петля работает.

И он же нашёл настоящую ошибку: PlaceSquad воскрешал ВСЕ пять слотов, в
том числе незанятые. Строка писалась, когда отряд всегда был впятером;
после появления ростера одиночка выходил на этаж с четырьмя невидимыми
телохранителями. Из-за них прежний замер "одиночка выжил на первом этаже,
100 -> 41" был боем впятером.

ЛИЧИНКИ. С исправленным замером выяснилось, что одиночка с ножом
проигрывает двум бредущим: 210 здоровья против ножа это девять секунд
работы под ответным уроном. Первые этажи населены личинками (55 здоровья,
7 урона, медленные), бредущий уехал на третий этаж, бегун на четвёртый.
Новый ВИД, а не ослабленный бредущий с множителем: глубина в этой игре
меняет, кто выходит, а не числа у одного и того же.

ПАРИРОВАНИЕ. Отдельная характеристика клинка, и чем оружие легче, тем она
выше: нож 34%, сабля 20%, тесак 8%. Не выведена из веса намеренно - её
двигает аффикс PARRY, и лёгкий тесак обязан быть возможен как находка.

Отбить можно, только если удары СОПРИКОСНУЛИСЬ: боец сам должен быть в
замахе или в ударе. Иначе клинок стал бы щитом, работающим, пока стоишь.

Удавшийся отбив снимает урон, бьёт тварь ударом бойца целиком и запускает
повторную атаку с начала фазы, НЕ сбрасывая откат. Последнее и есть суть
размена: сбрасывай отбив откат, выгоднее было бы подставляться под удары.

Плевок отбивается тоже: летит назад и несёт урон бойца, а не твари. Это
его удар, просто по летящей цели.

Проверка меряет размен целиком, включая обратную половину правила:
"стоя столбом не отбивает". Без неё условие про соприкосновение
существовало бы только в комментарии.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 05:04:35 +03:00

782 lines
30 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.

#include "render/sprites.h"
#include <utility>
#include "engine/hash.h"
#include "engine/math.h"
#include "engine/sfa_library.h"
#include "monster_sprites.h"
namespace
{
// DiamondRowN / DiamondRow / OutlineSilhouette / ShadeColor переехали в
// engine/pixel.h: ими рисует и движок (тайлы пола и стен), и игра. Две копии
// наклона ромба означали бы обстановку в одной изометрии, а пол — в другой.
// Капсула с обводкой и бликом.
Sprite MakeCapsule(int w, int h, float radius, uint32_t body)
{
Sprite s;
s.Init(w, h, 0);
s.ax = w / 2;
s.ay = h - 2; // «ноги» чуть выше нижнего края
const uint32_t outline = ShadeColor(body, 0.35f);
const uint32_t hilite = ShadeColor(body, 1.35f);
const uint32_t shadow = ShadeColor(body, 0.72f);
const float cx = (w - 1) * 0.5f;
const Vec2 a{cx, radius - 0.5f};
const Vec2 b{cx, h - radius - 0.5f};
for (int y = 0; y < h; ++y)
{
for (int x = 0; x < w; ++x)
{
const float d = DistancePointToSegment({float(x), float(y)}, a, b);
if (d > radius) continue;
if (d > radius - 1.0f) { s.Set(x, y, outline); continue; }
uint32_t c = body;
if (float(x) < cx - 0.5f && y < h / 2) c = hilite;
else if (float(x) > cx + 1.0f) c = shadow;
s.Set(x, y, c);
}
}
return s;
}
// --- оружие в руках ----------------------------------------------------------
//
// Ствол и клинок дорисовываются ПОВЕРХ капсулы по правому краю силуэта: на
// 14 пикселях иначе не видно ничего. Разница между длинным и тяжёлым стволом —
// два пикселя толщины и один длины, и этого достаточно: игрок различает их
// не разглядывая, а по общему пятну.
const uint32_t kSteel = RGBA(168, 176, 190);
const uint32_t kSteelDark = RGBA(74, 78, 88);
const uint32_t kEdge = RGBA(238, 244, 255);
void DrawBarrel(Sprite& s, int len, int thick, bool heavy)
{
const int bx = s.w - 2;
const int by = s.h - 7; // на уровне пояса
for (int t = 0; t < thick; ++t)
for (int i = 0; i < len; ++i)
s.Set(bx - i, by + t, (t == 0) ? kSteel : kSteelDark);
// Дуло у тяжёлого ствола шире самого ствола: пятно на конце и есть то,
// по чему дробовик отличается от карабина.
if (heavy)
{
s.Set(bx + 1, by, kSteel);
s.Set(bx + 1, by + 1, kSteel);
s.Set(bx + 1, by - 1, kSteelDark);
}
// Рукоять вниз — иначе ствол читается как случайная полоска.
s.Set(bx - len + 1, by + thick, kSteelDark);
}
void DrawBlade(Sprite& s, int len, bool guard)
{
const int bx = s.w - 2;
const int y0 = std::max(1, s.h - 4 - len);
for (int y = y0; y < s.h - 4; ++y) s.Set(bx, y, (y < s.h / 2) ? kEdge : kSteel);
if (guard)
{
s.Set(bx - 1, s.h - 5, kSteel);
s.Set(bx, s.h - 5, kSteel);
}
}
Sprite MakeBody(BodyLook look, uint32_t body)
{
switch (look)
{
case BodyLook::GUN_SHORT:
{
Sprite s = MakeCapsule(9, 13, 3.6f, body);
DrawBarrel(s, 2, 1, false);
return s;
}
case BodyLook::GUN_HEAVY:
{
Sprite s = MakeCapsule(12, 15, 4.8f, body);
DrawBarrel(s, 4, 2, true);
return s;
}
case BodyLook::BLADE_LONG:
{
Sprite s = MakeCapsule(12, 15, 4.6f, body);
DrawBlade(s, 9, true);
return s;
}
case BodyLook::BLADE_SHORT:
{
Sprite s = MakeCapsule(10, 14, 4.0f, body);
DrawBlade(s, 4, false);
return s;
}
case BodyLook::GUN_LONG:
default:
{
Sprite s = MakeCapsule(10, 14, 4.0f, body);
DrawBarrel(s, 4, 1, false);
return s;
}
}
}
// Павший: та же капсула, положенная набок и обесцвеченная. Он остаётся на полу
// до конца вылазки — отряд обязан видеть, где и кого потерял.
Sprite MakeCorpse(uint32_t body)
{
const Sprite up = MakeCapsule(10, 14, 4.0f, ShadeColor(body, 0.45f));
Sprite s;
s.Init(up.h, up.w + 2, 0);
s.ax = s.w / 2;
s.ay = s.h - 2;
for (int y = 0; y < up.h; ++y)
for (int x = 0; x < up.w; ++x)
{
const uint32_t c = up.Get(x, y);
if (AlphaOf(c) == 0) continue;
s.Set(y, s.h - 3 - x, c); // поворот на 90 градусов
}
return s;
}
// --- твари -------------------------------------------------------------------
//
// Не капсулы. Тварь на 480x270 занимает 14-24 пикселя, и опознаётся она ПЯТНОМ:
// сначала силуэт, потом цвет, детали — никогда. Поэтому каждый вид строится как
// объединение эллипсов (голова, корпус, конечности), а объём даёт не «блик
// слева», а свет по КРАЮ силуэта: подсвечена верхне-левая кромка, затенена
// нижне-правая. Тот же приём, что у стены, — и твари садятся в один свет с ней.
// Эллипс силуэта. Отрицательный ry допустим только по недосмотру, поэтому он
// зажат: вырожденный эллипс молча съел бы часть тела.
struct Blob
{
float cx, cy, rx, ry;
};
bool InsideBlob(const Blob& b, float x, float y)
{
const float dx = (x - b.cx) / std::max(0.5f, b.rx);
const float dy = (y - b.cy) / std::max(0.5f, b.ry);
return dx * dx + dy * dy <= 1.0f;
}
void FillBlobs(Sprite& s, const Blob* blobs, int count, uint32_t body)
{
for (int y = 0; y < s.h; ++y)
for (int x = 0; x < s.w; ++x)
for (int i = 0; i < count; ++i)
if (InsideBlob(blobs[i], float(x), float(y)))
{
s.Set(x, y, body);
break;
}
}
// Объём по краю силуэта: свет падает сверху-слева. Проверяется НЕ сосед, а
// пиксель через два, — иначе на теле шириной 8 подсветка съедает его целиком.
void ShadeSilhouette(Sprite& s, uint32_t body)
{
const uint32_t hilite = ShadeColor(body, 1.32f);
const uint32_t shadow = ShadeColor(body, 0.66f);
const uint32_t deep = ShadeColor(body, 0.48f);
const Sprite copy = s;
for (int y = 0; y < s.h; ++y)
for (int x = 0; x < s.w; ++x)
{
if (AlphaOf(copy.Get(x, y)) == 0) continue;
const bool litEdge = AlphaOf(copy.Get(x - 2, y - 1)) == 0;
const bool darkEdge = AlphaOf(copy.Get(x + 2, y + 1)) == 0;
if (litEdge && !darkEdge) s.Set(x, y, hilite);
else if (darkEdge && !litEdge) s.Set(x, y, shadow);
else if (darkEdge) s.Set(x, y, deep);
}
}
// Рваный низ: несколько столбцов силуэта укорачиваются. Дешёвый приём, но
// именно он отличает бредущее тело от аккуратной капсулы отряда.
void TatterBottom(Sprite& s, uint32_t seed, int maxCut)
{
for (int x = 0; x < s.w; ++x)
{
const int cut = int(Hash01(Hash2(seed, uint32_t(x))) * float(maxCut + 1));
for (int i = 0; i < cut; ++i) s.Set(x, s.h - 1 - i, 0);
}
}
// Пара глаз. Единственная деталь, которую видно на всех размерах, — и она же
// говорит, куда тварь смотрит.
void DrawEyes(Sprite& s, int x0, int x1, int y, uint32_t col)
{
s.Set(x0, y, col);
s.Set(x1, y, col);
}
Sprite MakeShambler(uint32_t body)
{
// Сутулый: голова свешена вперёд и НИЖЕ плеч, руки висят. Читается как
// «идёт, но еле-еле» — ровно то, что он и делает.
Sprite s;
s.Init(13, 17, 0);
s.ax = s.w / 2;
s.ay = s.h - 2;
const Blob parts[] = {
{6.5f, 10.0f, 4.2f, 6.0f}, // корпус
{8.0f, 4.5f, 3.0f, 2.8f}, // голова, свешена вправо-вперёд
{2.5f, 10.5f, 1.8f, 4.5f}, // левая рука
{10.5f, 11.5f, 1.6f, 4.0f}, // правая рука, длиннее
{5.0f, 15.0f, 2.4f, 2.4f}, // ноги
};
FillBlobs(s, parts, int(sizeof(parts) / sizeof(parts[0])), body);
ShadeSilhouette(s, body);
// Провал на месте лица: у бредущего его нет, и это страшнее любых глаз.
const uint32_t maw = ShadeColor(body, 0.20f);
for (int x = 7; x < 10; ++x) s.Set(x, 5, maw);
s.Set(8, 6, maw);
TatterBottom(s, 0x5A17u, 2);
OutlineSilhouette(s, ShadeColor(body, 0.28f));
return s;
}
Sprite MakeRusher(uint32_t body)
{
// Узкий, высокий, наклонён вперёд: в толпе бредущих его видно по одному
// силуэту, и наклон говорит, что он сейчас прыгнет.
Sprite s;
s.Init(11, 18, 0);
s.ax = s.w / 2;
s.ay = s.h - 2;
const Blob parts[] = {
{5.0f, 10.5f, 2.6f, 6.0f}, // тонкий корпус
{7.0f, 3.8f, 2.4f, 3.2f}, // вытянутая вперёд башка
{8.6f, 8.0f, 1.4f, 3.6f}, // передняя лапа вынесена
{2.0f, 11.0f, 1.3f, 3.4f}, // задняя
{4.5f, 15.5f, 2.2f, 2.2f},
};
FillBlobs(s, parts, int(sizeof(parts) / sizeof(parts[0])), body);
ShadeSilhouette(s, body);
// Глаза горят: он единственный, кто добежит, и знать это надо заранее.
DrawEyes(s, 6, 8, 3, ShadeColor(body, 1.85f));
TatterBottom(s, 0x9E37u, 3);
OutlineSilhouette(s, ShadeColor(body, 0.30f));
return s;
}
Sprite MakeSpitter(uint32_t body)
{
// Приземистый мешок на коротких ногах с распахнутой пастью: единственный,
// кто бьёт издалека, и по картинке это понятно до первого плевка.
Sprite s;
s.Init(16, 14, 0);
s.ax = s.w / 2;
s.ay = s.h - 2;
const Blob parts[] = {
{8.0f, 8.0f, 6.6f, 4.8f}, // раздутый корпус
{8.0f, 3.6f, 3.4f, 2.6f}, // приплюснутая голова
{3.0f, 12.0f, 1.8f, 1.8f}, // ноги-обрубки
{13.0f, 12.0f, 1.8f, 1.8f},
};
FillBlobs(s, parts, int(sizeof(parts) / sizeof(parts[0])), body);
ShadeSilhouette(s, body);
// Пасть — тёмный провал во всю голову. Это его «ствол».
const uint32_t maw = ShadeColor(body, 0.18f);
const uint32_t bile = ShadeColor(body, 1.70f);
for (int x = 5; x < 12; ++x) s.Set(x, 4, maw);
for (int x = 6; x < 11; ++x) s.Set(x, 5, maw);
s.Set(7, 3, bile);
s.Set(10, 3, bile);
// Желчные пузыри на боках: намёк на то, чем именно он плюётся.
for (int y = 7; y < 11; ++y)
for (int x = 2; x < s.w - 2; ++x)
{
if (AlphaOf(s.Get(x, y)) == 0) continue;
if (((x * 3 + y * 7) % 13) == 0) s.Set(x, y, bile);
}
OutlineSilhouette(s, ShadeColor(body, 0.30f));
return s;
}
Sprite MakeBrute(uint32_t body)
{
// Гора мяса: плечи ШИРЕ таза, голова утоплена между ними, кулаки до пола.
// Он должен читаться как «этого вдвоём», причём с одного взгляда.
Sprite s;
s.Init(22, 25, 0);
s.ax = s.w / 2;
s.ay = s.h - 2;
const Blob parts[] = {
{11.0f, 9.0f, 9.0f, 6.0f}, // плечевой пояс
{11.0f, 16.0f, 5.5f, 6.0f}, // туловище
{11.0f, 4.5f, 3.0f, 2.8f}, // утопленная голова
{ 2.5f, 17.0f, 2.6f, 5.5f}, // левая рука до пола
{19.5f, 17.0f, 2.6f, 5.5f}, // правая
{ 7.0f, 23.0f, 3.0f, 2.2f}, // ноги
{15.0f, 23.0f, 3.0f, 2.2f},
};
FillBlobs(s, parts, int(sizeof(parts) / sizeof(parts[0])), body);
ShadeSilhouette(s, body);
const uint32_t bone = ShadeColor(body, 1.55f);
const uint32_t dark = ShadeColor(body, 0.34f);
// Костяные наросты на плечах — то, чем он держит удар.
for (int i = 0; i < 4; ++i)
{
s.Set(3 + i, 5 + i, bone);
s.Set(s.w - 4 - i, 5 + i, bone);
}
// Кулаки: то, чем он бьёт, обязано быть видно отдельно от рук.
for (int y = s.h - 6; y < s.h - 2; ++y)
for (int x = 1; x < 4; ++x)
{
if (AlphaOf(s.Get(x, y)) != 0) s.Set(x, y, bone);
if (AlphaOf(s.Get(s.w - 1 - x, y)) != 0) s.Set(s.w - 1 - x, y, bone);
}
DrawEyes(s, 10, 12, 4, dark);
OutlineSilhouette(s, ShadeColor(body, 0.25f));
return s;
}
Sprite MakeLurker(uint32_t body)
{
// Хват: низкий, паучий, на длинных лапах. Раньше он был просто капсулой —
// и единственная тварь, которая вырывает бойца из строя, выглядела как
// мишень. Силуэт должен кричать «не подходи один».
Sprite s;
s.Init(17, 15, 0);
s.ax = s.w / 2;
s.ay = s.h - 2;
const Blob parts[] = {
{8.5f, 9.0f, 4.0f, 3.6f}, // низкое тело
{8.5f, 5.5f, 2.4f, 2.2f}, // маленькая голова
{2.0f, 7.0f, 1.3f, 4.6f}, // лапы: вынесены далеко вбок
{15.0f, 7.0f, 1.3f, 4.6f},
{4.0f, 12.5f, 1.4f, 2.2f},
{13.0f, 12.5f, 1.4f, 2.2f},
};
FillBlobs(s, parts, int(sizeof(parts) / sizeof(parts[0])), body);
ShadeSilhouette(s, body);
// Когти на концах передних лап — то, чем он держит.
const uint32_t claw = ShadeColor(body, 1.75f);
s.Set(1, 2, claw); s.Set(2, 3, claw);
s.Set(s.w - 2, 2, claw); s.Set(s.w - 3, 3, claw);
DrawEyes(s, 7, 10, 5, ShadeColor(body, 1.9f));
OutlineSilhouette(s, ShadeColor(body, 0.26f));
return s;
}
// --- разрушаемая обстановка --------------------------------------------------
//
// Не капсулы. Хлам обязан читаться как ЧАСТЬ УРОВНЯ, а не как тварь, которую
// забыли покрасить: коробка стоит в той же изометрии 2:1, что пол и стены, и
// светится тем же ShadeQuad. Правило чтения — то же, что у тварей: силуэт несёт
// вид, цвет — материал.
//
// Привязка у всех считается одинаково: ay = h - dh/2, то есть центр НИЖНЕГО
// ромба (основания). Ровно так же посажена стена в MakeWall — иначе обстановка
// поехала бы относительно пола на половину тайла.
// Изометрическая коробка: верхний ромб dw x dh + вертикальные грани высотой tall.
Sprite MakeBox(int dw, int dh, int tall, uint32_t top)
{
Sprite s;
s.Init(dw, dh + tall, 0);
s.ax = dw / 2;
s.ay = (dh + tall) - dh / 2;
const uint32_t left = ShadeColor(top, 0.60f);
const uint32_t right = ShadeColor(top, 0.80f);
for (int y = 0; y < dh; ++y)
{
int x0 = 0, x1 = 0;
DiamondRowN(y, dw, dh, x0, x1);
for (int x = x0; x < x1; ++x) s.Set(x, y, top);
}
for (int x = 0; x < dw; ++x)
{
int bottom = -1;
for (int y = 0; y < dh; ++y)
if (AlphaOf(s.Get(x, y)) != 0) bottom = y;
if (bottom < 0) continue;
const uint32_t side = (x < dw / 2) ? left : right;
for (int y = bottom + 1; y <= bottom + tall && y < s.h; ++y) s.Set(x, y, side);
}
return s;
}
Sprite MakeCrate(uint32_t body)
{
Sprite s = MakeBox(20, 10, 11, body);
const uint32_t plank = ShadeColor(body, 1.22f);
const uint32_t dark = ShadeColor(body, 0.42f);
const uint32_t band = ShadeColor(body, 0.52f);
// Доски: горизонтальный шов посередине граней и косой крест на них.
// Без него коробка читается как кубик, а не как заколоченный ящик.
for (int x = 0; x < s.w; ++x)
{
if (AlphaOf(s.Get(x, s.h - 6)) == 0) continue;
s.Set(x, s.h - 6, band);
}
for (int i = 0; i < 7; ++i)
{
s.Set(3 + i, s.h - 9 + i, plank);
s.Set(s.w - 4 - i, s.h - 9 + i, plank);
}
// Светлая грань верхней крышки — единственное, что даёт объём на 20 пикселях.
for (int y = 1; y < 4; ++y)
{
int x0 = 0, x1 = 0;
DiamondRowN(y, 20, 10, x0, x1);
for (int x = x0 + 1; x < x1 - 1; ++x) s.Set(x, y, plank);
}
OutlineSilhouette(s, dark);
return s;
}
Sprite MakeBarrel(uint32_t body)
{
// Бочка — цилиндр: верх эллипс, бока прямые. Ромб тут врёт, поэтому
// силуэт строится отдельно, а не через MakeBox.
const int w = 15, capH = 7, tall = 13;
Sprite s;
s.Init(w, capH + tall, 0);
s.ax = w / 2;
s.ay = s.h - capH / 2;
const uint32_t lid = ShadeColor(body, 1.25f);
const uint32_t left = ShadeColor(body, 0.95f);
const uint32_t right = ShadeColor(body, 0.62f);
const uint32_t hoop = ShadeColor(body, 1.45f);
const uint32_t rust = ShadeColor(body, 0.70f);
const float cx = (w - 1) * 0.5f;
const float rx = cx, ry = (capH - 1) * 0.5f;
// Крышка: эллипс rx x ry.
for (int y = 0; y < capH; ++y)
for (int x = 0; x < w; ++x)
{
const float dx = (float(x) - cx) / rx;
const float dy = (float(y) - ry) / std::max(0.5f, ry);
if (dx * dx + dy * dy > 1.0f) continue;
s.Set(x, y, (y < capH / 2) ? lid : body);
}
// Бока: каждый столбец крышки продлевается вниз, к низу чуть сужаясь —
// клёпаный бок, а не труба.
for (int x = 0; x < w; ++x)
{
int bottom = -1;
for (int y = 0; y < capH; ++y)
if (AlphaOf(s.Get(x, y)) != 0) bottom = y;
if (bottom < 0) continue;
const float t = std::fabs(float(x) - cx) / rx;
const int shrink = (t > 0.86f) ? 2 : 0; // нижняя кромка скруглена
const uint32_t side = (float(x) < cx - 0.5f) ? left : right;
for (int y = bottom + 1; y <= bottom + tall - shrink && y < s.h; ++y)
s.Set(x, y, side);
}
// Два обруча: то, по чему бочка отличается от тумбы одним взглядом.
for (int x = 0; x < w; ++x)
for (const int hy : {capH + 3, capH + 8})
{
if (hy >= s.h || AlphaOf(s.Get(x, hy)) == 0) continue;
s.Set(x, hy, hoop);
}
// Ржавые потёки: детерминированная рябь, как «металлическая» на полу.
for (int y = capH; y < s.h; ++y)
for (int x = 0; x < w; ++x)
{
if (AlphaOf(s.Get(x, y)) == 0) continue;
if (((x * 5 + y * 11) % 17) == 0) s.Set(x, y, rust);
}
OutlineSilhouette(s, ShadeColor(body, 0.38f));
return s;
}
Sprite MakeLocker(uint32_t body)
{
// Узкий и высокий: в ряду хлама шкафчик опознаётся по одному силуэту.
Sprite s = MakeBox(14, 7, 21, body);
const uint32_t door = ShadeColor(body, 0.88f);
const uint32_t seam = ShadeColor(body, 0.45f);
const uint32_t latch = RGBA(196, 178, 120);
const uint32_t vent = ShadeColor(body, 0.55f);
// Дверцы: шов по центру правой грани и жалюзи сверху.
const int seamX = s.w / 2;
for (int y = s.h - 21; y < s.h - 2; ++y) s.Set(seamX, y, seam);
for (int i = 0; i < 3; ++i)
{
const int y = s.h - 19 + i * 2;
for (int x = seamX + 2; x < s.w - 2; ++x) s.Set(x, y, vent);
}
for (int y = 0; y < s.h; ++y)
if (AlphaOf(s.Get(seamX - 3, y)) != 0 && y > s.h - 20) s.Set(seamX - 3, y, door);
s.Set(seamX + 1, s.h - 11, latch);
s.Set(seamX + 1, s.h - 10, latch);
OutlineSilhouette(s, ShadeColor(body, 0.35f));
return s;
}
// --- иконки предметов --------------------------------------------------------
//
// 14x14, по одной форме на ВИД предмета. Читать нужно вид, а не модель: имя
// пишется рядом текстом, и дублировать его пиктограммой незачем.
Sprite MakeIcon(ItemId id)
{
const ItemDef def = ItemDefOf(id);
Sprite s;
s.Init(14, 14, 0);
s.ax = 7;
s.ay = 7;
if (def.kind == ItemKind::NONE) return s;
const uint32_t c = RGBA(def.r, def.g, def.b);
const uint32_t dark = ShadeColor(c, 0.45f);
const uint32_t hot = ShadeColor(c, 1.35f);
switch (def.kind)
{
case ItemKind::FIREARM:
for (int x = 1; x < 12; ++x) s.Set(x, 6, c);
for (int x = 1; x < 12; ++x) s.Set(x, 7, dark);
for (int y = 7; y < 11; ++y) s.Set(3, y, dark);
s.Set(11, 5, hot);
s.Set(12, 6, hot);
break;
case ItemKind::BLADE:
for (int i = 0; i < 10; ++i) s.Set(2 + i, 11 - i, (i > 4) ? hot : c);
s.Set(3, 10, dark);
s.Set(4, 11, dark);
s.Set(2, 9, dark);
break;
case ItemKind::ARMOR:
for (int y = 2; y < 12; ++y)
{
const int inset = (y < 9) ? 2 : (2 + (y - 8));
for (int x = inset; x < 14 - inset; ++x)
s.Set(x, y, (y == 2 || x == inset || x == 13 - inset) ? dark : c);
}
break;
case ItemKind::TRINKET:
for (int y = 0; y < 14; ++y)
for (int x = 0; x < 14; ++x)
{
const float dx = float(x) - 6.5f, dy = float(y) - 6.5f;
const float d = std::sqrt(dx * dx + dy * dy);
if (d > 5.5f) continue;
s.Set(x, y, (d > 4.5f) ? dark : c);
}
s.Set(5, 4, hot);
s.Set(4, 5, hot);
break;
case ItemKind::CONSUMABLE:
for (int i = 3; i < 11; ++i)
{
s.Set(i, 6, c);
s.Set(i, 7, c);
s.Set(6, i, c);
s.Set(7, i, c);
}
s.Set(6, 6, hot);
s.Set(7, 7, hot);
break;
default: break;
}
return s;
}
} // namespace
uint32_t SlotColor(int slot)
{
// 5 различимых цветов — по одному на слот отряда. Номер бойца читается
// только по цвету, поэтому таблица одна на всю игру.
static const uint32_t cols[NUM_AGENTS] = {
RGBA(80, 200, 220),
RGBA(232, 176, 72),
RGBA(116, 204, 116),
RGBA(212, 116, 200),
RGBA(116, 148, 240),
};
return cols[(slot >= 0 && slot < NUM_AGENTS) ? slot : 0];
}
BodyLook LookOf(ItemId weapon)
{
switch (weapon)
{
case ItemId::PISTOL: return BodyLook::GUN_SHORT;
case ItemId::SHOTGUN:
case ItemId::MAGNUM: return BodyLook::GUN_HEAVY;
case ItemId::BOW: return BodyLook::GUN_LONG;
case ItemId::SABER:
case ItemId::CLEAVER: return BodyLook::BLADE_LONG;
case ItemId::KNIFE: return BodyLook::BLADE_SHORT;
default: return BodyLook::GUN_LONG;
}
}
void SpriteSet::Generate()
{
tiles.Generate(); // пол и стены со всеми темами — движок
for (int i = 0; i < NUM_AGENTS; ++i)
{
const uint32_t col = SlotColor(i);
for (int look = 0; look < int(BodyLook::COUNT); ++look)
body[look][i] = MakeBody(BodyLook(look), col);
corpse[i] = MakeCorpse(col);
}
for (int k = 0; k < int(EnemyKind::COUNT); ++k)
{
const EnemyDef def = EnemyDefOf(EnemyKind(k));
const uint32_t col = RGBA(def.r, def.g, def.b);
switch (EnemyKind(k))
{
// Личинка рисуется тем же силуэтом, что и бредущий, но мельче:
// отдельная форма — работа художника, а не программиста.
case EnemyKind::LARVA: enemy[k] = MakeShambler(col); break;
case EnemyKind::SHAMBLER: enemy[k] = MakeShambler(col); break;
case EnemyKind::RUSHER: enemy[k] = MakeRusher(col); break;
case EnemyKind::SPITTER: enemy[k] = MakeSpitter(col); break;
case EnemyKind::BRUTE: enemy[k] = MakeBrute(col); break;
case EnemyKind::LURKER: enemy[k] = MakeLurker(col); break;
// Обстановка: не тварь и выглядеть как тварь не должна.
case EnemyKind::CRATE: enemy[k] = MakeCrate(col); break;
case EnemyKind::BARREL: enemy[k] = MakeBarrel(col); break;
case EnemyKind::LOCKER: enemy[k] = MakeLocker(col); break;
// Мишень приёмки: вид не менялся ни на пиксель.
default: enemy[k] = MakeCapsule(12, 16, 5.0f, col); break;
}
}
target = enemy[int(EnemyKind::DUMMY)];
for (int i = 0; i < int(ItemId::COUNT); ++i) icon[i] = MakeIcon(ItemId(i));
}
bool SpriteSet::LoadTiles(const SpriteLibrary& lib)
{
if (!lib.IsOpen()) return false;
// Пол и стены читает движок (engine/tileset.cpp) — но НЕ здесь, а последним
// шагом: этот вызов сразу подменяет набор, а весь метод обязан оставаться
// «всё или ничего». Сначала проверяем то, что грузится с этой стороны.
struct PropRow { EnemyKind kind; uint32_t asset; };
static const PropRow propRows[] = {
{EnemyKind::CRATE, uint32_t(SF_ASSET_PROP_CRATE)},
{EnemyKind::BARREL, uint32_t(SF_ASSET_PROP_BARREL)},
{EnemyKind::LOCKER, uint32_t(SF_ASSET_PROP_LOCKER)},
};
Sprite newProps[3];
for (int i = 0; i < 3; ++i)
if (!lib.Read(propRows[i].asset, 0, newProps[i])) return false;
struct IconRow { ItemId item; uint32_t asset; };
#define SF_ICON(item, asset) {ItemId::item, uint32_t(SF_ASSET_ITEM_ICON_##asset)}
static const IconRow iconRows[] = {
SF_ICON(RIFLE, RIFLE), SF_ICON(PISTOL, PISTOL), SF_ICON(SHOTGUN, SHOTGUN),
SF_ICON(SMG, SMG), SF_ICON(MAGNUM, MAGNUM), SF_ICON(BOW, BOW),
SF_ICON(SABER, SABER), SF_ICON(KNIFE, KNIFE), SF_ICON(CLEAVER, CLEAVER),
SF_ICON(JACKET, JACKET), SF_ICON(VEST, VEST), SF_ICON(PLATE, PLATE),
SF_ICON(SHROUD, SHROUD), SF_ICON(LANTERN, LANTERN), SF_ICON(CHARM, CHARM),
SF_ICON(SPURS, SPURS), SF_ICON(BANDAGE, BANDAGE), SF_ICON(STIMPACK, STIMPACK),
};
#undef SF_ICON
Sprite newIcons[int(ItemId::COUNT)];
for (int i = 0; i < int(ItemId::COUNT); ++i) newIcons[i] = icon[i];
for (const IconRow& row : iconRows)
if (!lib.Read(uint32_t(row.asset), 0, newIcons[int(row.item)])) return false;
// Тайлы последними: они подменяются внутри TileSet вместе с пересборкой
// всех тем, и откатывать это было бы некуда. К этой строке всё остальное
// уже прочитано, поэтому единственная возможная осечка — сами тайлы.
if (!tiles.LoadFromForge(lib)) return false;
for (int i = 0; i < 3; ++i) enemy[int(propRows[i].kind)] = std::move(newProps[i]);
for (int i = 0; i < int(ItemId::COUNT); ++i) icon[i] = std::move(newIcons[i]);
return true;
}
bool SpriteSet::LoadEnemies(const SpriteLibrary& lib)
{
if (!lib.IsOpen()) return false;
// Порядок — порядок EnemyKind. DUMMY и обстановка сюда не входят: мишень
// приёмки не двигается по определению, а ящик не ходит.
struct Row { EnemyKind kind; };
static const Row rows[] = {
{EnemyKind::SHAMBLER}, {EnemyKind::RUSHER}, {EnemyKind::SPITTER},
{EnemyKind::BRUTE}, {EnemyKind::LURKER},
};
// Сначала проверяем ВСЕ, потом записываем: полусобранный набор — это одна
// анимированная тварь среди четырёх статичных, что хуже пяти статичных.
for (const Row& r : rows)
for (AnimKind anim : {AnimKind::WALK, AnimKind::ATTACK, AnimKind::HURT,
AnimKind::DIE})
if (lib.AnimationLength(MonsterAsset(r.kind, anim),
uint32_t(SF_ANIM_DEFAULT)) <= 0.0f) return false;
for (const Row& r : rows)
enemyWalk[int(r.kind)] = MonsterAsset(r.kind, AnimKind::WALK);
return true;
}