#include "engine/tileset.h" #include #include "engine/math.h" #include "engine/pixel.h" #include "engine/projection.h" #include "engine/sfa_library.h" namespace { Sprite MakeFloor(uint32_t base) { const int tw = g_proj.tileW; const int th = g_proj.tileH; Sprite s; s.Init(tw, th, 0); s.ax = tw / 2; s.ay = th / 2; const uint32_t edge = ShadeColor(base, 0.78f); const uint32_t fleck = ShadeColor(base, 1.18f); for (int y = 0; y < th; ++y) { int x0 = 0, x1 = 0; g_proj.TileRow(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; } // Рисунок ПОВЕРХ готовой плиты. Отдельно от MakeFloor намеренно: тайлы могут // приехать из SpriteForge, и варианты обязаны получаться из них тем же кодом — // иначе половина пола была бы нарисованной, а половина нет. // // Пишет только по непрозрачным пикселям, поэтому силуэт ромба не меняется: // сетка пола остаётся ровно там же, где была. void AddFloorDetail(Sprite& s, int variant, uint32_t base) { if (variant <= 0) return; // ровная плита const uint32_t crack = ShadeColor(base, 0.42f); const uint32_t deep = ShadeColor(base, 0.30f); const uint32_t lip = ShadeColor(base, 1.28f); auto paint = [&](int x, int y, uint32_t c) { if (AlphaOf(s.Get(x, y)) != 0) s.Set(x, y, c); }; // Размеры берутся у самой плиты, а не у констант тайла: рисунок обязан // ложиться на ту плиту, которую выдала проекция, — ромб или квадрат. const int tw = s.w; const int th = s.h; switch (variant) { case 1: { // Трещина: ломаная от левого края плиты к правому. Ход задаётся // хешем от x, а не генератором, — плита обязана быть одинаковой // в каждом прогоне. int y = th / 2; for (int x = 3; x < tw - 3; ++x) { const float r = Hash01(Hash2(0xC7A1u, uint32_t(x))); y += (r < 0.33f) ? -1 : (r > 0.66f ? 1 : 0); y = std::max(1, std::min(th - 2, y)); paint(x, y, crack); if ((x & 3) == 0) paint(x, y + 1, deep); } break; } case 2: { // Решётка стока: в изометрии слаты идут вдоль оси сетки, поэтому // шаг по x вдвое больше шага по y — иначе решётка «встаёт» из пола. // При виде сверху наклонять нечего, слаты горизонтальные. // // Контраст НАМЕРЕННО низкий. Пол — фон: на нём читаются кровь, // трассеры и твари, и решётка, нарисованная в полную силу, спорит // со всеми тремя. Она должна угадываться, а не бросаться в глаза. const uint32_t slat = ShadeColor(base, 0.74f); const uint32_t gap = ShadeColor(base, 0.90f); const bool slanted = (g_proj.mode == ViewMode::ISO_2_1); // Доли, а не пиксели: на плите любого размера решётка занимает ту // же часть плиты. Для тайла 32x16 числа совпадают с прежними — // рисунок пола не должен «поехать» от одного лишь переезда кода. const int step = std::max(2, th / 8); const int margin = std::max(1, tw * 9 / 32); for (int i = -2; i <= 2; ++i) { const int y0 = th / 2 + i * step; for (int x = margin; x < tw - margin; ++x) { const int y = y0 + (slanted ? (x - tw / 2) / 4 : 0); paint(x, y, (i & 1) ? gap : slat); } } break; } default: { // Труха и щебень: тёмные кляксы плюс редкие светлые крупинки. for (int y = 0; y < th; ++y) for (int x = 0; x < tw; ++x) { const float r = Hash01(Hash2(uint32_t(x) * 73u + 0x5B3Du, uint32_t(y))); if (r < 0.10f) paint(x, y, deep); else if (r < 0.14f) paint(x, y, crack); else if (r > 0.965f) paint(x, y, lip); } break; } } } Sprite MakeWall(uint32_t top) { const int tw = g_proj.tileW; const int th = g_proj.tileH; Sprite s; s.Init(tw, g_proj.wallH, 0); s.ax = tw / 2; s.ay = g_proj.WallAnchorY(); // в изометрии низ блока садится на ромб пола const uint32_t left = ShadeColor(top, 0.62f); const uint32_t right = ShadeColor(top, 0.80f); // верхняя грань — по форме плиты for (int y = 0; y < th; ++y) { int x0 = 0, x1 = 0; g_proj.TileRow(y, x0, x1); for (int x = x0; x < x1; ++x) s.Set(x, y, top); } // Вертикальные грани: от нижней кромки крыши вниз на высоту тайла. При // виде сверху их нет вообще — блок, показанный сверху И сбоку сразу, это // две проекции в одном спрайте. if (g_proj.WallHasSides()) { for (int x = 0; x < tw; ++x) { int bottom = -1; for (int y = 0; y < th; ++y) if (AlphaOf(s.Get(x, y)) != 0) bottom = y; if (bottom < 0) continue; const uint32_t side = (x < tw / 2) ? left : right; for (int y = bottom + 1; y <= bottom + th && y < g_proj.wallH; ++y) s.Set(x, y, side); } } OutlineSilhouette(s, ShadeColor(top, 0.35f)); return s; } // ----------------------------------------------------------------------------- // Темы. Множители по каналам, а не отдельные наборы спрайтов: пока тайлсет // один, тема обязана менять МАТЕРИАЛ, оставляя рисунок — трещины, решётку, // ранты — на месте. Игрок читает планировку по рисунку, и перерисовывать его // на каждую тему значило бы учить его читать пол заново в каждой комнате. // // Базовая тема (0) — множители 1.0: она и есть то, что нарисовал Generate() // или привезла SpriteForge, и трогать её перекраской нельзя. // ----------------------------------------------------------------------------- struct ThemeTint { const char* name; float r, g, b; }; const ThemeTint THEMES[THEME_COUNT] = { {"STEEL", 1.00f, 1.00f, 1.00f}, // сталь: базовый набор как есть {"RUST", 1.28f, 0.94f, 0.66f}, // ржавчина: тёплая, выцветшая {"FLESH", 1.18f, 0.70f, 0.74f}, // плоть: розово-багровая {"FROST", 0.78f, 0.96f, 1.32f}, // наледь: холодная и светлая }; Sprite TintSprite(const Sprite& src, const ThemeTint& t) { Sprite out = src; for (uint32_t& px : out.px) { if (AlphaOf(px) == 0) continue; // силуэт не трогаем const float r = Clampf(float(px & 0xFFu) * t.r, 0.0f, 255.0f); const float g = Clampf(float((px >> 8) & 0xFFu) * t.g, 0.0f, 255.0f); const float b = Clampf(float((px >> 16) & 0xFFu) * t.b, 0.0f, 255.0f); px = RGBA(uint8_t(r), uint8_t(g), uint8_t(b), AlphaOf(px)); } return out; } } // namespace const char* ThemeName(uint8_t theme) { return (theme < THEME_COUNT) ? THEMES[theme].name : "?"; } void TileSet::Generate() { // Шахматка x рисунок: два тона плиты, на каждом — все варианты детали. const uint32_t floorTint[2] = {RGBA(42, 46, 53), RGBA(50, 54, 62)}; for (int p = 0; p < 2; ++p) for (int v = 0; v < FLOOR_VARIANTS; ++v) { floor[0][p][v] = MakeFloor(floorTint[p]); AddFloorDetail(floor[0][p][v], v, floorTint[p]); } wall[0][0] = MakeWall(RGBA(78, 84, 96)); wall[0][1] = wall[0][0]; BuildThemes(); } void TileSet::BuildThemes() { for (int t = 1; t < THEME_COUNT; ++t) { for (int p = 0; p < 2; ++p) for (int v = 0; v < FLOOR_VARIANTS; ++v) floor[t][p][v] = TintSprite(floor[0][p][v], THEMES[t]); for (int i = 0; i < 2; ++i) wall[t][i] = TintSprite(wall[0][i], THEMES[t]); } } bool TileSet::LoadFromForge(const SpriteLibrary& lib) { if (!lib.IsOpen()) return false; // Библиотека нарисована в изометрии: ромб 32x16 и блок с гранями. Положить // её в вид сверху нельзя — это не «неточно», это другой ракурс. Отказ здесь // оставляет процедурный набор целиком, ровно как при любой другой осечке. if (g_proj.mode != ViewMode::ISO_2_1) return false; // Холст тайла канонический — TILE_W x TILE_H с ромбом на своём месте. // SpriteForge обрезает кадр по силуэту (32x16 приходит как 30x16), а // ShadeQuad из Lighting::QuadFor считается в координатах спрайта: съехавший // на пиксель холст увёл бы свет пола относительно света стен. const FramePlacement floorPlace{TILE_W, TILE_H, TILE_HALF_W, TILE_HALF_H, -1, -1}; // У стены pivot кадра — нижняя строка блока, она садится на дно холста. // Привязка же спрайта остаётся в центре крыши: низ блока ложится на ромб // пола ровно так же, как у MakeWall. const FramePlacement wallPlace{TILE_W, WALL_H, TILE_HALF_W, WALL_H - 1, TILE_HALF_W, TILE_HALF_H + TILE_H}; // Явное приведение: sf_asset_id — обычный enum, и MSVC считает его знаковым, // хотя хеши ассетов доходят до 32 бит (C4245 на SF_ASSET_WALL_BLOCK). const uint32_t floorIds[FLOOR_VARIANTS] = { uint32_t(SF_ASSET_FLOOR_CLEAN), uint32_t(SF_ASSET_FLOOR_CRACKED), uint32_t(SF_ASSET_FLOOR_GRATE), uint32_t(SF_ASSET_FLOOR_BIOMASS), }; Sprite newFloor[2][FLOOR_VARIANTS]; for (int v = 0; v < FLOOR_VARIANTS; ++v) { Sprite tile; if (!lib.Read(floorIds[v], 0, floorPlace, tile)) return false; newFloor[0][v] = tile; newFloor[1][v] = std::move(tile); } Sprite newWall[2]; if (!lib.Read(uint32_t(SF_ASSET_WALL_INDUSTRIAL), 0, wallPlace, newWall[0])) return false; if (!lib.Read(uint32_t(SF_ASSET_WALL_INFESTED), 0, wallPlace, newWall[1])) return false; // Всё прочитано — только теперь подменяем набор и пересобираем темы. for (int p = 0; p < 2; ++p) for (int v = 0; v < FLOOR_VARIANTS; ++v) floor[0][p][v] = std::move(newFloor[p][v]); wall[0][0] = std::move(newWall[0]); wall[0][1] = std::move(newWall[1]); BuildThemes(); return true; }