293 lines
12 KiB
C++
293 lines
12 KiB
C++
|
|
#include "engine/tileset.h"
|
|||
|
|
|
|||
|
|
#include <utility>
|
|||
|
|
|
|||
|
|
#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;
|
|||
|
|
}
|