#include "render/sprites.h" #include #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::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; }