#include "render/sprites.h" #include "core/math.h" namespace { // Занятая часть строки y для ромба 32x16. void DiamondRow(int y, int& x0, int& x1) { const int k = (y < TILE_HALF_H) ? y : (TILE_H - 1 - y); const int width = 4 * (k + 1); x0 = TILE_HALF_W - width / 2; x1 = x0 + width; } // Обводка по силуэту: любой непрозрачный пиксель, у которого есть прозрачный // 4-сосед (или край спрайта), перекрашивается в outline. void OutlineSilhouette(Sprite& s, uint32_t outline) { 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 border = x == 0 || y == 0 || x == s.w - 1 || y == s.h - 1 || AlphaOf(copy.Get(x - 1, y)) == 0 || AlphaOf(copy.Get(x + 1, y)) == 0 || AlphaOf(copy.Get(x, y - 1)) == 0 || AlphaOf(copy.Get(x, y + 1)) == 0; if (border) s.Set(x, y, outline); } } } Sprite MakeFloor(uint32_t base) { Sprite s; s.Init(TILE_W, TILE_H, 0); s.ax = TILE_HALF_W; s.ay = TILE_HALF_H; const uint32_t edge = ShadeColor(base, 0.78f); const uint32_t fleck = ShadeColor(base, 1.18f); for (int y = 0; y < TILE_H; ++y) { int x0 = 0, x1 = 0; DiamondRow(y, x0, x1); for (int x = x0; x < x1; ++x) { uint32_t c = base; if (x < x0 + 2 || x >= x1 - 2) c = edge; // рант тайла else if (((x * 7 + y * 13) % 23) == 0) c = fleck; // «металлическая» рябь s.Set(x, y, c); } } return s; } Sprite MakeWall(uint32_t top) { Sprite s; s.Init(TILE_W, WALL_H, 0); s.ax = TILE_HALF_W; s.ay = TILE_HALF_H + TILE_H; // низ блока садится на ромб пола const uint32_t left = ShadeColor(top, 0.62f); const uint32_t right = ShadeColor(top, 0.80f); // верхняя грань for (int y = 0; y < TILE_H; ++y) { int x0 = 0, x1 = 0; DiamondRow(y, x0, x1); for (int x = x0; x < x1; ++x) s.Set(x, y, top); } // вертикальные грани: от нижней кромки ромба вниз на высоту тайла for (int x = 0; x < TILE_W; ++x) { int bottom = -1; for (int y = 0; y < TILE_H; ++y) if (AlphaOf(s.Get(x, y)) != 0) bottom = y; if (bottom < 0) continue; const uint32_t side = (x < TILE_HALF_W) ? left : right; for (int y = bottom + 1; y <= bottom + TILE_H && y < WALL_H; ++y) s.Set(x, y, side); } OutlineSilhouette(s, ShadeColor(top, 0.35f)); return s; } // Капсула с обводкой и бликом. 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; } // Боец ближнего боя: та же капсула, но шире в плечах и с клинком у бедра. // На 480x270 класс обязан читаться силуэтом — цвет уже занят номером агента. Sprite MakeMeleeCapsule(int w, int h, float radius, uint32_t body) { Sprite s = MakeCapsule(w, h, radius, body); const uint32_t steel = RGBA(196, 206, 222); const uint32_t edge = RGBA(240, 246, 255); // Клинок: короткая вертикаль вдоль правого края силуэта, с бликом. const int bx = w - 2; for (int y = 2; y < h - 4; ++y) { s.Set(bx, y, steel); if (y < h / 2) s.Set(bx, y, edge); } // Гарда, чтобы клинок не читался как случайная полоска. s.Set(bx - 1, h - 5, steel); s.Set(bx, h - 5, steel); return s; } // Стрелок с пистолетом: та же капсула, но уже в плечах и с коротким стволом // у пояса. Класс оружия обязан читаться силуэтом — цвет уже занят номером. Sprite MakePistolCapsule(int w, int h, float radius, uint32_t body) { Sprite s = MakeCapsule(w, h, radius, body); const uint32_t steel = RGBA(150, 156, 168); const uint32_t dark = RGBA(70, 74, 84); // Коротышка-ствол: две пиксельных полоски у бедра, с рукоятью вниз. const int bx = w - 2; const int by = h - 7; s.Set(bx, by, steel); s.Set(bx - 1, by, steel); s.Set(bx, by + 1, dark); s.Set(bx, by + 2, dark); return s; } } // namespace uint32_t ShadeColor(uint32_t c, float k) { const float r = Clampf(float(c & 0xFF) * k, 0.0f, 255.0f); const float g = Clampf(float((c >> 8) & 0xFF) * k, 0.0f, 255.0f); const float b = Clampf(float((c >> 16) & 0xFF) * k, 0.0f, 255.0f); return RGBA(uint8_t(r), uint8_t(g), uint8_t(b), AlphaOf(c)); } void SpriteSet::Generate() { floorA = MakeFloor(RGBA(50, 54, 62)); floorB = MakeFloor(RGBA(42, 46, 53)); wall = MakeWall(RGBA(78, 84, 96)); // 5 различимых цветов — по одному на агента static const uint32_t agentCols[NUM_AGENTS] = { RGBA(80, 200, 220), RGBA(232, 176, 72), RGBA(116, 204, 116), RGBA(212, 116, 200), RGBA(116, 148, 240), }; for (int i = 0; i < NUM_AGENTS; ++i) { agent[i] = MakeCapsule(10, 14, 4.0f, agentCols[i]); melee[i] = MakeMeleeCapsule(12, 15, 4.6f, agentCols[i]); pistol[i] = MakePistolCapsule(9, 13, 3.6f, agentCols[i]); } target = MakeCapsule(12, 16, 5.0f, RGBA(120, 44, 44)); }