CyberMatris port for Miyoo Mini Plus
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

#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());
}