You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
845 lines
33 KiB
845 lines
33 KiB
#include "Renderer.hpp" |
|
#include "Font.hpp" |
|
#include <cmath> |
|
#include <cstdlib> |
|
#include <sstream> |
|
#include <iomanip> |
|
#include <algorithm> |
|
|
|
// Playboard grid rendering layout variables |
|
static constexpr int CELL_SIZE = 25; |
|
static constexpr int BOARD_X = 275; |
|
static constexpr int BOARD_Y = 50; |
|
static constexpr int BOARD_W = 10 * CELL_SIZE; |
|
static constexpr int BOARD_H = 20 * CELL_SIZE; |
|
|
|
// Sleek neon color constants |
|
static const SDL_Color COLOR_CYAN = {0, 240, 255, 255}; |
|
static const SDL_Color COLOR_PINK = {255, 0, 150, 255}; |
|
static const SDL_Color COLOR_WHITE = {255, 255, 255, 255}; |
|
static const SDL_Color COLOR_ORANGE = {255, 140, 0, 255}; |
|
static const SDL_Color COLOR_RED = {245, 0, 50, 255}; |
|
|
|
// Helper function to draw a single pixel with optional alpha blending onto an RGBA32 surface |
|
static inline void drawPixelSoftware(SDL_Surface* surface, int x, int y, SDL_Color color, bool blend) { |
|
if (x < 0 || x >= surface->w || y < 0 || y >= surface->h) return; |
|
|
|
uint32_t* pixels = static_cast<uint32_t*>(surface->pixels); |
|
int pitch = surface->pitch / 4; // 32-bit words per row |
|
uint32_t& dstPixel = pixels[y * pitch + x]; |
|
|
|
if (!blend || color.a == 255) { |
|
dstPixel = SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a); |
|
} else if (color.a > 0) { |
|
Uint8 dstR, dstG, dstB, dstA; |
|
SDL_GetRGBA(dstPixel, surface->format, &dstR, &dstG, &dstB, &dstA); |
|
|
|
float alpha = color.a / 255.0f; |
|
float invAlpha = 1.0f - alpha; |
|
|
|
Uint8 outR = static_cast<Uint8>(color.r * alpha + dstR * invAlpha); |
|
Uint8 outG = static_cast<Uint8>(color.g * alpha + dstG * invAlpha); |
|
Uint8 outB = static_cast<Uint8>(color.b * alpha + dstB * invAlpha); |
|
Uint8 outA = static_cast<Uint8>(color.a + dstA * invAlpha); |
|
|
|
dstPixel = SDL_MapRGBA(surface->format, outR, outG, outB, outA); |
|
} |
|
} |
|
|
|
// Helper function to fill a rectangle with optional alpha blending on an RGBA32 surface |
|
static void fillRectSoftware(SDL_Surface* surface, const SDL_Rect* rect, SDL_Color color, bool blend) { |
|
SDL_Rect clippedRect; |
|
SDL_Rect surfaceRect = {0, 0, surface->w, surface->h}; |
|
if (!SDL_IntersectRect(rect, &surfaceRect, &clippedRect)) return; |
|
|
|
uint32_t* pixels = static_cast<uint32_t*>(surface->pixels); |
|
int pitch = surface->pitch / 4; |
|
|
|
if (!blend || color.a == 255) { |
|
uint32_t mappedColor = SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a); |
|
for (int y = clippedRect.y; y < clippedRect.y + clippedRect.h; ++y) { |
|
uint32_t* row = pixels + y * pitch; |
|
std::fill(row + clippedRect.x, row + clippedRect.x + clippedRect.w, mappedColor); |
|
} |
|
} else if (color.a > 0) { |
|
float alpha = color.a / 255.0f; |
|
float invAlpha = 1.0f - alpha; |
|
|
|
for (int y = clippedRect.y; y < clippedRect.y + clippedRect.h; ++y) { |
|
uint32_t* row = pixels + y * pitch; |
|
for (int x = clippedRect.x; x < clippedRect.x + clippedRect.w; ++x) { |
|
uint32_t& dstPixel = row[x]; |
|
Uint8 dstR, dstG, dstB, dstA; |
|
SDL_GetRGBA(dstPixel, surface->format, &dstR, &dstG, &dstB, &dstA); |
|
|
|
Uint8 outR = static_cast<Uint8>(color.r * alpha + dstR * invAlpha); |
|
Uint8 outG = static_cast<Uint8>(color.g * alpha + dstG * invAlpha); |
|
Uint8 outB = static_cast<Uint8>(color.b * alpha + dstB * invAlpha); |
|
Uint8 outA = static_cast<Uint8>(color.a + dstA * invAlpha); |
|
|
|
dstPixel = SDL_MapRGBA(surface->format, outR, outG, outB, outA); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Helper function to draw a rectangle border with optional alpha blending on an RGBA32 surface |
|
static void drawRectSoftware(SDL_Surface* surface, const SDL_Rect* rect, SDL_Color color, bool blend) { |
|
// Top border |
|
SDL_Rect top = {rect->x, rect->y, rect->w, 1}; |
|
fillRectSoftware(surface, &top, color, blend); |
|
// Bottom border |
|
SDL_Rect bottom = {rect->x, rect->y + rect->h - 1, rect->w, 1}; |
|
fillRectSoftware(surface, &bottom, color, blend); |
|
// Left border |
|
SDL_Rect left = {rect->x, rect->y, 1, rect->h}; |
|
fillRectSoftware(surface, &left, color, blend); |
|
// Right border |
|
SDL_Rect right = {rect->x + rect->w - 1, rect->y, 1, rect->h}; |
|
fillRectSoftware(surface, &right, color, blend); |
|
} |
|
|
|
// Helper function to draw a line with optional alpha blending on an RGBA32 surface using Bresenham's algorithm |
|
static void drawLineSoftware(SDL_Surface* surface, int x1, int y1, int x2, int y2, SDL_Color color, bool blend) { |
|
// Handle trivial horizontal/vertical cases with fast fillRectSoftware |
|
if (x1 == x2) { |
|
int minY = std::min(y1, y2); |
|
int maxY = std::max(y1, y2); |
|
SDL_Rect rect = {x1, minY, 1, maxY - minY + 1}; |
|
fillRectSoftware(surface, &rect, color, blend); |
|
return; |
|
} |
|
if (y1 == y2) { |
|
int minX = std::min(x1, x2); |
|
int maxX = std::max(x1, x2); |
|
SDL_Rect rect = {minX, y1, maxX - minX + 1, 1}; |
|
fillRectSoftware(surface, &rect, color, blend); |
|
return; |
|
} |
|
|
|
int dx = std::abs(x2 - x1); |
|
int dy = std::abs(y2 - y1); |
|
int sx = (x1 < x2) ? 1 : -1; |
|
int sy = (y1 < y2) ? 1 : -1; |
|
int err = dx - dy; |
|
|
|
while (true) { |
|
drawPixelSoftware(surface, x1, y1, color, blend); |
|
if (x1 == x2 && y1 == y2) break; |
|
int e2 = 2 * err; |
|
if (e2 > -dy) { |
|
err -= dy; |
|
x1 += sx; |
|
} |
|
if (e2 < dx) { |
|
err += dx; |
|
y1 += sy; |
|
} |
|
} |
|
} |
|
|
|
Renderer::Renderer() { |
|
initStars(); |
|
} |
|
|
|
Renderer::~Renderer() { |
|
if (mFontTexture != nullptr) { |
|
SDL_DestroyTexture(mFontTexture); |
|
} |
|
if (mFontSurface != nullptr) { |
|
SDL_FreeSurface(mFontSurface); |
|
} |
|
if (mBackbufferSurface != nullptr) { |
|
SDL_FreeSurface(mBackbufferSurface); |
|
} |
|
if (mScaledSurface != nullptr) { |
|
SDL_FreeSurface(mScaledSurface); |
|
} |
|
if (mBackbufferTexture != nullptr) { |
|
SDL_DestroyTexture(mBackbufferTexture); |
|
} |
|
} |
|
|
|
void Renderer::initStars() { |
|
mStars.clear(); |
|
for (int i = 0; i < 60; ++i) { |
|
Star s; |
|
s.x = static_cast<float>(std::rand() % 800); |
|
s.y = static_cast<float>(std::rand() % 600); |
|
s.speed = 10.0f + static_cast<float>(std::rand() % 30); |
|
s.size = 1.0f + static_cast<float>(std::rand() % 3); |
|
s.alpha = 50.0f + static_cast<float>(std::rand() % 150); |
|
mStars.push_back(s); |
|
} |
|
} |
|
|
|
bool Renderer::init(SDL_Renderer* renderer, int targetW, int targetH) { |
|
mRenderer = renderer; |
|
mTargetW = targetW; |
|
mTargetH = targetH; |
|
buildFontTexture(); |
|
if (mFontSurface == nullptr) { |
|
return false; |
|
} |
|
|
|
// Create software backbuffer surface of 800x600 in RGBA32 format |
|
mBackbufferSurface = SDL_CreateRGBSurfaceWithFormat(0, 800, 600, 32, SDL_PIXELFORMAT_RGBA32); |
|
if (mBackbufferSurface == nullptr) { |
|
return false; |
|
} |
|
|
|
#ifdef MIYOO_BUILD |
|
// On Miyoo, the maximum streaming texture size supported by the hardware/driver is 640x480. |
|
// So we always scale the 800x600 backbuffer to a 640x480 surface/texture in RAM, |
|
// and let the GPU/hardware scale the 640x480 texture to the actual window resolution (e.g., 752x560 on V4). |
|
mScaledSurface = SDL_CreateRGBSurfaceWithFormat(0, 640, 480, 32, SDL_PIXELFORMAT_RGBA32); |
|
if (mScaledSurface == nullptr) { |
|
return false; |
|
} |
|
mBackbufferTexture = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, 640, 480); |
|
#else |
|
mBackbufferTexture = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, mTargetW, mTargetH); |
|
#endif |
|
|
|
if (mBackbufferTexture == nullptr) { |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
void Renderer::buildFontTexture() { |
|
// Lay out the 95-char font atlas in two rows to stay within the |
|
// 640px texture width limit imposed by the Miyoo MI_GFX driver. |
|
// Row 0: chars 0..47 (ASCII 32..79), Row 1: chars 48..94 (ASCII 80..126) |
|
static constexpr int CHARS_PER_ROW = 48; |
|
int atlasW = CHARS_PER_ROW * 8; // 384 pixels — fits in 640 |
|
int atlasH = 16; // 2 rows of 8 pixels each |
|
|
|
SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormat( |
|
0, atlasW, atlasH, 32, SDL_PIXELFORMAT_RGBA32); |
|
if (surface == nullptr) return; |
|
|
|
SDL_LockSurface(surface); |
|
uint32_t* pixels = (uint32_t*)surface->pixels; |
|
int pitch = atlasW; // pixels per row (stride = atlasW * 4 bytes, but we use uint32*) |
|
|
|
for (int c = 0; c < 95; ++c) { |
|
int row = c / CHARS_PER_ROW; |
|
int col = c % CHARS_PER_ROW; |
|
for (int r = 0; r < 8; ++r) { |
|
uint8_t byte = Font::BITMAP[c][r]; |
|
for (int bit = 0; bit < 8; ++bit) { |
|
bool on = (byte >> (7 - bit)) & 1; |
|
int px = col * 8 + bit; |
|
int py = row * 8 + r; |
|
pixels[py * pitch + px] = on ? 0xFFFFFFFF : 0x00000000; |
|
} |
|
} |
|
} |
|
|
|
SDL_UnlockSurface(surface); |
|
mFontSurface = surface; |
|
mFontTexture = SDL_CreateTextureFromSurface(mRenderer, mFontSurface); |
|
|
|
if (mFontTexture != nullptr) { |
|
SDL_SetTextureBlendMode(mFontTexture, SDL_BLENDMODE_BLEND); |
|
} |
|
} |
|
|
|
void Renderer::update(float dt) { |
|
mTime += dt; |
|
|
|
// 1. Update Parallax Background Stars |
|
for (auto& s : mStars) { |
|
s.y += s.speed * dt; |
|
if (s.y > 600.0f) { |
|
s.y = 0.0f; |
|
s.x = static_cast<float>(std::rand() % 800); |
|
} |
|
} |
|
|
|
// 2. Update Physics Particles (gravitational debris) |
|
for (size_t i = 0; i < mParticles.size();) { |
|
Particle& p = mParticles[i]; |
|
p.life -= p.decay * dt; |
|
|
|
if (p.life <= 0.0f) { |
|
// Remove dead particles |
|
mParticles[i] = mParticles.back(); |
|
mParticles.pop_back(); |
|
} else { |
|
// Kinematic updates with gravity pulling down |
|
p.x += p.vx * dt; |
|
p.y += p.vy * dt; |
|
p.vy += 320.0f * dt; // gravity |
|
i++; |
|
} |
|
} |
|
} |
|
|
|
SDL_Color Renderer::getPieceColor(PieceType type) { |
|
switch (type) { |
|
case PieceType::I: return {0, 240, 240, 255}; // Vibrant Cyan |
|
case PieceType::O: return {240, 240, 0, 255}; // Vibrant Yellow |
|
case PieceType::T: return {170, 0, 245, 255}; // Neon Purple |
|
case PieceType::S: return {0, 240, 0, 255}; // Neon Green |
|
case PieceType::Z: return {245, 0, 0, 255}; // Neon Red |
|
case PieceType::J: return {0, 100, 245, 255}; // Deep Blue |
|
case PieceType::L: return {245, 130, 0, 255}; // Bright Orange |
|
default: return {0, 0, 0, 0}; |
|
} |
|
} |
|
|
|
void Renderer::drawBlock(int gridX, int gridY, PieceType type, bool isGhost, float alphaOverride) { |
|
if (type == PieceType::NONE) return; |
|
|
|
int rx = BOARD_X + gridX * CELL_SIZE; |
|
int ry = BOARD_Y + gridY * CELL_SIZE; |
|
|
|
SDL_Color color = getPieceColor(type); |
|
|
|
if (isGhost) { |
|
// Draw pulsing outline for ghost pieces |
|
float pulse = 0.5f + 0.3f * std::sin(mTime * 10.0f); |
|
SDL_Color pulseCol = {color.r, color.g, color.b, static_cast<Uint8>(pulse * 255.0f * alphaOverride)}; |
|
SDL_Rect rect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2}; |
|
drawRectSoftware(mBackbufferSurface, &rect, pulseCol, true); |
|
return; |
|
} |
|
|
|
// Neon beveled crystal shading style |
|
// 1. Draw solid crystal core |
|
SDL_Color coreCol = {color.r, color.g, color.b, static_cast<Uint8>(255 * alphaOverride)}; |
|
SDL_Rect coreRect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2}; |
|
fillRectSoftware(mBackbufferSurface, &coreRect, coreCol, true); |
|
|
|
// 2. Draw glossy 3D inner reflection core |
|
SDL_Color lighterColor = { |
|
static_cast<Uint8>(std::min(color.r + 60, 255)), |
|
static_cast<Uint8>(std::min(color.g + 60, 255)), |
|
static_cast<Uint8>(std::min(color.b + 60, 255)), |
|
255 |
|
}; |
|
SDL_Color reflectionCol = {lighterColor.r, lighterColor.g, lighterColor.b, static_cast<Uint8>(200 * alphaOverride)}; |
|
SDL_Rect innerRect = {rx + 3, ry + 3, CELL_SIZE - 6, CELL_SIZE - 6}; |
|
fillRectSoftware(mBackbufferSurface, &innerRect, reflectionCol, true); |
|
|
|
// 3. Highlight top-left border (specular light sheen) |
|
SDL_Color highlightCol = {255, 255, 255, static_cast<Uint8>(180 * alphaOverride)}; |
|
drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + CELL_SIZE - 2, ry + 1, highlightCol, true); |
|
drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + 1, ry + CELL_SIZE - 2, highlightCol, true); |
|
|
|
// 4. Shade bottom-right border (3D shadow depth) |
|
SDL_Color shadowCol = {0, 0, 0, static_cast<Uint8>(130 * alphaOverride)}; |
|
drawLineSoftware(mBackbufferSurface, rx + 1, ry + CELL_SIZE - 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); |
|
drawLineSoftware(mBackbufferSurface, rx + CELL_SIZE - 1, ry + 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); |
|
} |
|
|
|
void Renderer::drawGrid(const Game& game) { |
|
// Subtle dark blue/gray playboard background |
|
SDL_Color boardCol = {15, 17, 24, 255}; |
|
SDL_Rect boardBG = {BOARD_X, BOARD_Y, BOARD_W, BOARD_H}; |
|
fillRectSoftware(mBackbufferSurface, &boardBG, boardCol, true); |
|
|
|
// Draw grid lines |
|
SDL_Color gridLineCol = {30, 34, 46, 120}; |
|
for (int x = 1; x < 10; ++x) { |
|
drawLineSoftware(mBackbufferSurface, BOARD_X + x * CELL_SIZE, BOARD_Y, BOARD_X + x * CELL_SIZE, BOARD_Y + BOARD_H, gridLineCol, true); |
|
} |
|
for (int y = 1; y < 20; ++y) { |
|
drawLineSoftware(mBackbufferSurface, BOARD_X, BOARD_Y + y * CELL_SIZE, BOARD_X + BOARD_W, BOARD_Y + y * CELL_SIZE, gridLineCol, true); |
|
} |
|
|
|
// Draw locked blocks on the board |
|
for (int y = 0; y < 20; ++y) { |
|
for (int x = 0; x < 10; ++x) { |
|
PieceType cell = game.getCell(x, y); |
|
if (cell != PieceType::NONE) { |
|
// If a line is cleared, skip drawing it here so it flashes white under the particles |
|
if (std::find(game.mClearedLinesThisTick.begin(), game.mClearedLinesThisTick.end(), y) != game.mClearedLinesThisTick.end()) { |
|
continue; |
|
} |
|
drawBlock(x, y, cell); |
|
} |
|
} |
|
} |
|
|
|
// Draw dynamic glowing borders around the play board (cyberpunk look) |
|
float pulse = 0.8f + 0.2f * std::sin(mTime * 4.0f); |
|
SDL_Color borderCol = {0, 150, 255, static_cast<Uint8>(255 * pulse)}; |
|
|
|
// Outer border outline layers |
|
for (int i = 0; i < 3; ++i) { |
|
SDL_Rect border = {BOARD_X - i, BOARD_Y - i, BOARD_W + i * 2, BOARD_H + i * 2}; |
|
drawRectSoftware(mBackbufferSurface, &border, borderCol, true); |
|
} |
|
} |
|
|
|
void Renderer::drawText(const std::string& text, int x, int y, int scale, SDL_Color color, bool center, bool glow) { |
|
if (mFontSurface == nullptr) return; |
|
|
|
// Font atlas layout: 48 chars per row, 2 rows of 8px each. |
|
static constexpr int CHARS_PER_ROW = 48; |
|
|
|
int totalWidth = text.length() * 8 * scale; |
|
int startX = center ? x - totalWidth / 2 : x; |
|
|
|
SDL_SetSurfaceBlendMode(mFontSurface, SDL_BLENDMODE_BLEND); |
|
|
|
auto charSrc = [](char c) -> SDL_Rect { |
|
int idx = (c >= 32 && c <= 126) ? (c - 32) : 0; |
|
int row = idx / CHARS_PER_ROW; |
|
int col = idx % CHARS_PER_ROW; |
|
return SDL_Rect{ col * 8, row * 8, 8, 8 }; |
|
}; |
|
|
|
// Optional black text outline to simulate glow/shadow |
|
if (glow) { |
|
SDL_SetSurfaceColorMod(mFontSurface, 0, 0, 0); |
|
SDL_SetSurfaceAlphaMod(mFontSurface, 180); |
|
for (int dx = -1; dx <= 1; ++dx) { |
|
for (int dy = -1; dy <= 1; ++dy) { |
|
if (dx == 0 && dy == 0) continue; |
|
int curX = startX; |
|
for (char c : text) { |
|
if (c >= 32 && c <= 126) { |
|
SDL_Rect src = charSrc(c); |
|
SDL_Rect dest = { curX + dx * scale, y + dy * scale, 8 * scale, 8 * scale }; |
|
SDL_BlitScaled(mFontSurface, &src, mBackbufferSurface, &dest); |
|
} |
|
curX += 8 * scale; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Main text render |
|
SDL_SetSurfaceColorMod(mFontSurface, color.r, color.g, color.b); |
|
SDL_SetSurfaceAlphaMod(mFontSurface, color.a); |
|
|
|
int curX = startX; |
|
for (char c : text) { |
|
if (c >= 32 && c <= 126) { |
|
SDL_Rect src = charSrc(c); |
|
SDL_Rect dest = { curX, y, 8 * scale, 8 * scale }; |
|
SDL_BlitScaled(mFontSurface, &src, mBackbufferSurface, &dest); |
|
} |
|
curX += 8 * scale; |
|
} |
|
} |
|
|
|
void Renderer::drawUI(const Game& game) { |
|
// 1. Title |
|
float titlePulse = std::sin(mTime * 3.0f); |
|
SDL_Color dynamicColor = (titlePulse >= 0.0f) ? COLOR_CYAN : COLOR_PINK; |
|
drawText("CYBERMATRIS", 400, 15, 3, dynamicColor, true, true); |
|
|
|
// 2. Hold Box Panel (Left Side) |
|
drawText("HOLD", 140, 50, 2, COLOR_CYAN, true, true); |
|
SDL_Color panelBorderCol = {0, 150, 255, 120}; |
|
SDL_Rect holdRect = {40, 70, 200, 110}; |
|
drawRectSoftware(mBackbufferSurface, &holdRect, panelBorderCol, true); |
|
|
|
PieceType holdPiece = game.getHoldPieceType(); |
|
if (holdPiece != PieceType::NONE) { |
|
int typeIdx = static_cast<int>(holdPiece); |
|
SDL_Color pColor = getPieceColor(holdPiece); |
|
|
|
// Compute offsets to center pieces perfectly |
|
int offX = -12; |
|
int offY = -12; |
|
|
|
for (int i = 0; i < 4; ++i) { |
|
Point p = TETROMINO_CELLS[typeIdx][0][i]; |
|
int rx = 40 + 100 + offX + p.x * CELL_SIZE; |
|
int ry = 70 + 55 + offY + p.y * CELL_SIZE; |
|
|
|
// Draw beveled block |
|
SDL_Color blockCol = {pColor.r, pColor.g, pColor.b, 255}; |
|
SDL_Rect bRect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2}; |
|
fillRectSoftware(mBackbufferSurface, &bRect, blockCol, true); |
|
|
|
SDL_Color highlightCol = {255, 255, 255, 160}; |
|
drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + CELL_SIZE - 2, ry + 1, highlightCol, true); |
|
drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + 1, ry + CELL_SIZE - 2, highlightCol, true); |
|
|
|
SDL_Color shadowCol = {0, 0, 0, 110}; |
|
drawLineSoftware(mBackbufferSurface, rx + 1, ry + CELL_SIZE - 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); |
|
drawLineSoftware(mBackbufferSurface, rx + CELL_SIZE - 1, ry + 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); |
|
} |
|
} |
|
|
|
// 3. Stats Dashboard Panel (Left Side, below Hold) |
|
drawText("STATS", 140, 205, 2, COLOR_CYAN, true, true); |
|
SDL_Rect statsRect = {40, 225, 200, 325}; |
|
drawRectSoftware(mBackbufferSurface, &statsRect, panelBorderCol, true); |
|
|
|
// Render text metrics inside dashboard |
|
drawText("SCORE", 140, 245, 1, COLOR_CYAN, true, false); |
|
std::stringstream ssScore; |
|
ssScore << std::setw(6) << std::setfill('0') << game.getScore(); |
|
drawText(ssScore.str(), 140, 260, 2, COLOR_ORANGE, true, true); |
|
|
|
drawText("LEVEL", 140, 305, 1, COLOR_CYAN, true, false); |
|
drawText(std::to_string(game.getLevel()), 140, 320, 2, COLOR_WHITE, true, true); |
|
|
|
drawText("LINES", 140, 365, 1, COLOR_CYAN, true, false); |
|
drawText(std::to_string(game.getLinesCleared()), 140, 380, 2, COLOR_WHITE, true, true); |
|
|
|
// Show Combo notifier if positive |
|
if (game.getCombo() > 0) { |
|
float comboPulse = std::sin(mTime * 15.0f) * 0.5f + 0.5f; |
|
SDL_Color cColor = {255, static_cast<Uint8>(100 + 155 * comboPulse), 0, 255}; |
|
|
|
std::string comboStr = "COMBO X" + std::to_string(game.getCombo() + 1); |
|
drawText(comboStr, 140, 440, 2, cColor, true, true); |
|
drawText("Pulsing!", 140, 470, 1, COLOR_PINK, true, false); |
|
} else { |
|
drawText("CHIPTUNE", 140, 445, 1, COLOR_PINK, true, false); |
|
drawText("SYNTH ACTIVE", 140, 465, 1, COLOR_CYAN, true, false); |
|
drawText("4-CH MONSTER", 140, 485, 1, COLOR_WHITE, true, false); |
|
} |
|
|
|
// 4. Next Pieces Queue Box (Right Side) |
|
drawText("NEXT", 660, 50, 2, COLOR_CYAN, true, true); |
|
SDL_Rect nextRect = {560, 70, 200, 305}; |
|
drawRectSoftware(mBackbufferSurface, &nextRect, panelBorderCol, true); |
|
|
|
// Draw next 3 pieces |
|
auto nextQueue = game.getNextQueue(); |
|
for (int n = 0; n < 3; ++n) { |
|
if (n >= (int)nextQueue.size()) break; |
|
PieceType nPiece = nextQueue[n]; |
|
|
|
int typeIdx = static_cast<int>(nPiece); |
|
SDL_Color pColor = getPieceColor(nPiece); |
|
|
|
int offX = -12; |
|
int offY = -12; |
|
|
|
// Render mini blocks for each next queue element in slots |
|
for (int i = 0; i < 4; ++i) { |
|
Point p = TETROMINO_CELLS[typeIdx][0][i]; |
|
int rx = 560 + 100 + offX + p.x * CELL_SIZE; |
|
int ry = 70 + 60 + n * 95 + offY + p.y * CELL_SIZE; |
|
|
|
SDL_Color blockCol = {pColor.r, pColor.g, pColor.b, 255}; |
|
SDL_Rect bRect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2}; |
|
fillRectSoftware(mBackbufferSurface, &bRect, blockCol, true); |
|
|
|
SDL_Color highlightCol = {255, 255, 255, 160}; |
|
drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + CELL_SIZE - 2, ry + 1, highlightCol, true); |
|
drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + 1, ry + CELL_SIZE - 2, highlightCol, true); |
|
|
|
SDL_Color shadowCol = {0, 0, 0, 110}; |
|
drawLineSoftware(mBackbufferSurface, rx + 1, ry + CELL_SIZE - 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); |
|
drawLineSoftware(mBackbufferSurface, rx + CELL_SIZE - 1, ry + 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); |
|
} |
|
} |
|
} |
|
|
|
void Renderer::drawVisualizer(Synth& synth, int x, int y, int w, int h) { |
|
// 1. Draw outer pane container |
|
SDL_Color containerCol = {0, 150, 255, 120}; |
|
SDL_Rect container = {x, y, w, h}; |
|
drawRectSoftware(mBackbufferSurface, &container, containerCol, true); |
|
|
|
// Subtle Visualizer label |
|
drawText("AUDIO OSCILLOSCOPE", x + w / 2, y + 10, 1, {0, 240, 255, 255}, true, false); |
|
|
|
// Fetch the live stereo buffer copies |
|
auto buffer = synth.getVisualizerBuffer(); |
|
if (buffer.empty()) return; |
|
|
|
int halfH = h / 2; |
|
int centerY = y + halfH + 10; |
|
int dataW = w - 20; |
|
int dataX = x + 10; |
|
|
|
// 2. Draw 12 spectrum equalizer bar segments in background |
|
int numBars = 12; |
|
int barW = (dataW - (numBars - 1) * 3) / numBars; |
|
int samplesPerBar = buffer.size() / numBars; |
|
|
|
for (int b = 0; b < numBars; ++b) { |
|
// Calculate dynamic energy (Root Mean Square average amplitude) of segment |
|
float rmsSum = 0.0f; |
|
for (int s = 0; s < samplesPerBar; ++s) { |
|
float val = buffer[b * samplesPerBar + s]; |
|
rmsSum += val * val; |
|
} |
|
float rms = std::sqrt(rmsSum / samplesPerBar); |
|
|
|
// Smooth scaling factor |
|
float barHeight = rms * (h - 60) * 2.8f; |
|
barHeight = std::clamp(barHeight, 2.0f, static_cast<float>(h - 50)); |
|
|
|
// Pulsing color cycle: fades from hot purple to high green |
|
SDL_Color barCol = { |
|
static_cast<Uint8>(std::min(rms * 900.0f, 255.0f)), |
|
static_cast<Uint8>(180 - std::min(rms * 300.0f, 180.0f)), |
|
245, |
|
70 |
|
}; |
|
|
|
SDL_Rect barRect = { |
|
dataX + b * (barW + 3), |
|
y + h - 15 - static_cast<int>(barHeight), |
|
barW, |
|
static_cast<int>(barHeight) |
|
}; |
|
fillRectSoftware(mBackbufferSurface, &barRect, barCol, true); |
|
} |
|
|
|
// 3. Draw neon green oscilloscope line connecting points |
|
SDL_Color waveCol = {0, 255, 120, 255}; |
|
|
|
int prevX = dataX; |
|
int prevY = centerY; |
|
int step = buffer.size() / dataW; |
|
if (step <= 0) step = 1; |
|
|
|
for (int i = 0; i < dataW; ++i) { |
|
int idx = i * step; |
|
if (idx >= (int)buffer.size()) break; |
|
|
|
float sample = buffer[idx]; |
|
int px = dataX + i; |
|
|
|
// Amplify wave height |
|
int py = centerY + static_cast<int>(sample * halfH * 1.5f); |
|
py = std::clamp(py, y + 25, y + h - 10); |
|
|
|
if (i > 0) { |
|
drawLineSoftware(mBackbufferSurface, prevX, prevY, px, py, waveCol, true); |
|
} |
|
prevX = px; |
|
prevY = py; |
|
} |
|
} |
|
|
|
void Renderer::spawnLineClearParticles(const Game& game) { |
|
// When lines clear, shatter blocks into glowing spark bursts! |
|
|
|
for (int y : game.mClearedLinesThisTick) { |
|
// For every block across the line |
|
for (int x = 0; x < 10; ++x) { |
|
PieceType pType = game.getCell(x, y); |
|
if (pType == PieceType::NONE) pType = PieceType::I; // fallback |
|
|
|
SDL_Color color = getPieceColor(pType); |
|
int rx = BOARD_X + x * CELL_SIZE + CELL_SIZE / 2; |
|
int ry = BOARD_Y + y * CELL_SIZE + CELL_SIZE / 2; |
|
|
|
// Spawn 5 explosive sparks per block |
|
for (int p = 0; p < 5; ++p) { |
|
Particle part; |
|
part.x = static_cast<float>(rx); |
|
part.y = static_cast<float>(ry); |
|
|
|
// Random explosive velocity vectors |
|
float angle = static_cast<float>(std::rand() % 360) * (3.14159f / 180.0f); |
|
float speed = 50.0f + static_cast<float>(std::rand() % 220); |
|
part.vx = std::cos(angle) * speed; |
|
part.vy = std::sin(angle) * speed - 50.0f; // slight upwards blast bias |
|
|
|
part.life = 1.0f; |
|
part.decay = 1.2f + static_cast<float>(std::rand() % 10) * 0.1f; // decay ~0.8s |
|
part.size = 2.0f + static_cast<float>(std::rand() % 4); |
|
part.color = color; |
|
|
|
mParticles.push_back(part); |
|
} |
|
} |
|
} |
|
} |
|
|
|
void Renderer::spawnLandDustParticles(int gridX, int gridY, PieceType type) { |
|
if (type == PieceType::NONE) return; |
|
|
|
SDL_Color color = {230, 235, 255, 200}; // light grayish white puff |
|
|
|
int rx = BOARD_X + gridX * CELL_SIZE + CELL_SIZE / 2; |
|
int ry = BOARD_Y + (gridY + 1) * CELL_SIZE; // spawn right at contact base |
|
|
|
// Spawn 8 tiny dust puff particles drifting left and right |
|
for (int i = 0; i < 8; ++i) { |
|
Particle p; |
|
p.x = static_cast<float>(rx + (std::rand() % CELL_SIZE) - CELL_SIZE / 2); |
|
p.y = static_cast<float>(ry - 2); |
|
|
|
p.vx = static_cast<float>((std::rand() % 100) - 50); // drift sideways |
|
p.vy = -10.0f - static_cast<float>(std::rand() % 30); // slight upwards float |
|
|
|
p.life = 1.0f; |
|
p.decay = 2.0f + static_cast<float>(std::rand() % 10) * 0.2f; // quick decay ~0.4s |
|
p.size = 2.0f + static_cast<float>(std::rand() % 3); |
|
p.color = color; |
|
|
|
mParticles.push_back(p); |
|
} |
|
} |
|
|
|
void Renderer::render(const Game& game, Synth& synth) { |
|
if (mBackbufferSurface == nullptr) return; |
|
|
|
// 1. Clear Screen with deep cosmic space black |
|
SDL_Color clearCol = {10, 11, 16, 255}; |
|
SDL_Rect bgRect = {0, 0, 800, 600}; |
|
fillRectSoftware(mBackbufferSurface, &bgRect, clearCol, false); |
|
|
|
// 2. Render drifting parallax background stars |
|
for (const auto& s : mStars) { |
|
SDL_Color starCol = {255, 255, 255, static_cast<Uint8>(s.alpha)}; |
|
SDL_Rect starRect = { |
|
static_cast<int>(s.x), |
|
static_cast<int>(s.y), |
|
static_cast<int>(s.size), |
|
static_cast<int>(s.size) |
|
}; |
|
fillRectSoftware(mBackbufferSurface, &starRect, starCol, true); |
|
} |
|
|
|
// Apply decayed screenshake camera offsets if any |
|
int camDX = 0; |
|
int camDY = 0; |
|
mShakeIntensity = game.getScreenShakeIntensity(); |
|
|
|
if (mShakeIntensity > 0.01f) { |
|
camDX = static_cast<int>(((float)std::rand() / RAND_MAX * 2.0f - 1.0f) * mShakeIntensity * 15.0f); |
|
camDY = static_cast<int>(((float)std::rand() / RAND_MAX * 2.0f - 1.0f) * mShakeIntensity * 15.0f); |
|
} |
|
|
|
// 3. Render the dynamic cyberpunk grid and locked board blocks |
|
drawGrid(game); |
|
|
|
// 4. Render Active Tetromino (glowing crystal style) |
|
PieceType activeType = game.getActivePieceType(); |
|
if (activeType != PieceType::NONE) { |
|
// A. Draw Ghost projection piece first (pulsing neon wireframe) |
|
auto ghostCells = game.getGhostCells(); |
|
for (const auto& p : ghostCells) { |
|
drawBlock(p.x, p.y, activeType, true); |
|
} |
|
|
|
// B. Draw Active Piece blocks |
|
auto activeCells = game.getActiveCells(); |
|
for (const auto& p : activeCells) { |
|
drawBlock(p.x, p.y, activeType); |
|
} |
|
} |
|
|
|
// 5. Render sidebars, score gauge, next and hold compartments |
|
drawUI(game); |
|
|
|
// 6. Render real-time audio visualizer oscilloscope panel |
|
drawVisualizer(synth, 560, 390, 200, 160); |
|
|
|
// 7. Render physics particles (debris sparks) |
|
for (const auto& p : mParticles) { |
|
SDL_Color pCol = {p.color.r, p.color.g, p.color.b, static_cast<Uint8>(p.life * 255.0f)}; |
|
SDL_Rect pRect = { |
|
static_cast<int>(p.x - p.size / 2), |
|
static_cast<int>(p.y - p.size / 2), |
|
static_cast<int>(p.size), |
|
static_cast<int>(p.size) |
|
}; |
|
fillRectSoftware(mBackbufferSurface, &pRect, pCol, true); |
|
} |
|
|
|
// 8. Render Game Over / Start Banner overlay |
|
if (game.getState() == GameState::GAME_OVER) { |
|
// Translucent overlay shroud |
|
SDL_Color overlayCol = {10, 10, 15, 200}; |
|
SDL_Rect overlay = {0, 0, 800, 600}; |
|
fillRectSoftware(mBackbufferSurface, &overlay, overlayCol, true); |
|
|
|
float flash = std::sin(mTime * 6.0f) * 0.5f + 0.5f; |
|
SDL_Color flashRed = {255, static_cast<Uint8>(flash * 100), static_cast<Uint8>(flash * 50), 255}; |
|
|
|
drawText("GAME OVER", 400, 220, 4, flashRed, true, true); |
|
|
|
std::stringstream ssFinalScore; |
|
ssFinalScore << "FINAL SCORE " << game.getScore(); |
|
drawText(ssFinalScore.str(), 400, 280, 2, COLOR_CYAN, true, false); |
|
|
|
drawText("PRESS R TO RESTART", 400, 350, 2, COLOR_WHITE, true, true); |
|
drawText("OR ESC TO QUIT", 400, 390, 1, {180, 180, 200, 255}, true, false); |
|
} else if (game.getState() == GameState::START) { |
|
SDL_Color overlayCol = {8, 9, 13, 230}; |
|
SDL_Rect overlay = {0, 0, 800, 600}; |
|
fillRectSoftware(mBackbufferSurface, &overlay, overlayCol, true); |
|
|
|
// Blinking start cue |
|
float blink = std::sin(mTime * 5.0f) * 0.5f + 0.5f; |
|
SDL_Color cueColor = {static_cast<Uint8>(180 + 75 * blink), static_cast<Uint8>(200 + 55 * blink), 255, 255}; |
|
|
|
drawText("CYBERMATRIS", 400, 180, 5, COLOR_PINK, true, true); |
|
drawText("RETROWAVE PROCEDURAL TETRIS", 400, 240, 1, COLOR_CYAN, true, false); |
|
|
|
// Game features overview |
|
drawText("PROCEDURAL CHIPTUNE MUSIC", 400, 300, 1, COLOR_WHITE, true, false); |
|
drawText("NEON PARTICLE SHATTER EFFECTS", 400, 320, 1, COLOR_WHITE, true, false); |
|
drawText("REAL-TIME AUDIO OSCILLOSCOPE", 400, 340, 1, COLOR_WHITE, true, false); |
|
|
|
drawText("PRESS ENTER TO START", 400, 420, 2, cueColor, true, true); |
|
|
|
// Controls list |
|
drawText("CONTROLS", 400, 475, 1, COLOR_CYAN, true, false); |
|
drawText("LEFT RIGHT MOVEMENT DOWN SOFT DROP SPACE HARD DROP", 400, 495, 1, {170, 180, 200, 255}, true, false); |
|
drawText("UP X ROTATE CLOCKWISE Z ROTATE COUNTER C HOLD PIECE", 400, 515, 1, {170, 180, 200, 255}, true, false); |
|
drawText("P PAUSE GAME R RESTART GAME", 400, 535, 1, {170, 180, 200, 255}, true, false); |
|
|
|
drawText("CREATED BY MATTEO BENEDETTO (ENNE2)", 400, 575, 1, {255, 200, 0, 255}, true, false); |
|
} else if (game.getState() == GameState::PAUSED) { |
|
SDL_Color overlayCol = {10, 11, 16, 150}; |
|
SDL_Rect overlay = {0, 0, 800, 600}; |
|
fillRectSoftware(mBackbufferSurface, &overlay, overlayCol, true); |
|
|
|
float flash = std::sin(mTime * 4.0f) * 0.5f + 0.5f; |
|
SDL_Color flashCyan = {static_cast<Uint8>(flash * 100), 220, 255, 255}; |
|
|
|
drawText("PAUSED", 400, 260, 4, flashCyan, true, true); |
|
drawText("PRESS P TO RESUME", 400, 310, 2, COLOR_WHITE, true, false); |
|
} |
|
|
|
// 9. Upload backbuffer surface to streaming texture on GPU |
|
#ifdef MIYOO_BUILD |
|
// Scale the 800x600 software backbuffer to target screen size in software RAM |
|
SDL_BlitScaled(mBackbufferSurface, nullptr, mScaledSurface, nullptr); |
|
SDL_UpdateTexture(mBackbufferTexture, nullptr, mScaledSurface->pixels, mScaledSurface->pitch); |
|
#else |
|
SDL_UpdateTexture(mBackbufferTexture, nullptr, mBackbufferSurface->pixels, mBackbufferSurface->pitch); |
|
#endif |
|
|
|
// Clear the hardware renderer (Miyoo requires this before rendering copy) |
|
SDL_RenderClear(mRenderer); |
|
|
|
// Draw the texture containing our fully rendered software scene |
|
// If screenshake is active, apply the shake offset directly to the destination rectangle! |
|
#ifdef MIYOO_BUILD |
|
SDL_Rect destRect = { |
|
static_cast<int>(camDX * static_cast<float>(mTargetW) / 800.0f), |
|
static_cast<int>(camDY * static_cast<float>(mTargetH) / 600.0f), |
|
mTargetW, |
|
mTargetH |
|
}; |
|
#else |
|
SDL_Rect destRect = { |
|
camDX, |
|
camDY, |
|
mTargetW, |
|
mTargetH |
|
}; |
|
#endif |
|
SDL_RenderCopy(mRenderer, mBackbufferTexture, nullptr, &destRect); |
|
|
|
// Present the frame! |
|
SDL_RenderPresent(mRenderer); |
|
} |
|
|
|
void Renderer::takeScreenshot(const std::string& filename) { |
|
if (mBackbufferSurface == nullptr) return; |
|
SDL_SaveBMP(mBackbufferSurface, filename.c_str()); |
|
}
|
|
|