commit
e0c2cd210a
13 changed files with 3125 additions and 0 deletions
@ -0,0 +1,13 @@
|
||||
# Build artifacts |
||||
*.o |
||||
cybermatris |
||||
cybermatris_miyoo |
||||
dist_miyoo/ |
||||
|
||||
# System / IDE files |
||||
.DS_Store |
||||
.vscode/ |
||||
.idea/ |
||||
|
||||
# Large screenshot BMP (keep png instead) |
||||
screenshot.bmp |
||||
@ -0,0 +1,108 @@
|
||||
#pragma once |
||||
#include <cstdint> |
||||
|
||||
// A complete 8x8 bitmap font layout for printable ASCII characters from 32 (space) to 126 (~).
|
||||
// Total: 95 characters. Each character is represented by 8 bytes.
|
||||
// Each byte represents one row of 8 pixels from left to right (MSB to LSB).
|
||||
namespace Font { |
||||
constexpr int WIDTH = 8; |
||||
constexpr int HEIGHT = 8; |
||||
|
||||
inline const uint8_t BITMAP[95][8] = { |
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 32 (space)
|
||||
{0x18, 0x3c, 0x3c, 0x18, 0x18, 0x00, 0x18, 0x00}, // 33 !
|
||||
{0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 34 "
|
||||
{0x36, 0x36, 0x7f, 0x36, 0x7f, 0x36, 0x36, 0x00}, // 35 #
|
||||
{0x1c, 0x3e, 0x61, 0x3c, 0x07, 0x83, 0x7c, 0x38}, // 36 $
|
||||
{0x63, 0x66, 0x0c, 0x18, 0x30, 0x66, 0xc6, 0x00}, // 37 %
|
||||
{0x38, 0x6c, 0x38, 0x76, 0xdc, 0xcc, 0x76, 0x00}, // 38 &
|
||||
{0x18, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00}, // 39 '
|
||||
{0x0c, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0c, 0x00}, // 40 (
|
||||
{0x30, 0x18, 0x0c, 0x0c, 0x0c, 0x18, 0x30, 0x00}, // 41 )
|
||||
{0x00, 0x66, 0x3c, 0xff, 0x3c, 0x66, 0x00, 0x00}, // 42 *
|
||||
{0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00}, // 43 +
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30}, // 44 ,
|
||||
{0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00}, // 45 -
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00}, // 46 .
|
||||
{0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0x00}, // 47 /
|
||||
{0x3e, 0x63, 0x63, 0x6b, 0x63, 0x63, 0x3e, 0x00}, // 48 0
|
||||
{0x18, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 49 1
|
||||
{0x3e, 0x63, 0x07, 0x0e, 0x1c, 0x38, 0x7f, 0x00}, // 50 2
|
||||
{0x3e, 0x63, 0x07, 0x1e, 0x07, 0x63, 0x3e, 0x00}, // 51 3
|
||||
{0x06, 0x0e, 0x1e, 0x36, 0x7f, 0x06, 0x06, 0x00}, // 52 4
|
||||
{0x7f, 0x60, 0x7e, 0x03, 0x03, 0x63, 0x3e, 0x00}, // 53 5
|
||||
{0x1e, 0x30, 0x60, 0x7e, 0x63, 0x63, 0x3e, 0x00}, // 54 6
|
||||
{0x7f, 0x63, 0x03, 0x06, 0x0c, 0x18, 0x18, 0x00}, // 55 7
|
||||
{0x3e, 0x63, 0x63, 0x3e, 0x63, 0x63, 0x3e, 0x00}, // 56 8
|
||||
{0x3e, 0x63, 0x63, 0x7f, 0x03, 0x06, 0x3c, 0x00}, // 57 9
|
||||
{0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00}, // 58 :
|
||||
{0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x30, 0x00}, // 59 ;
|
||||
{0x06, 0x0c, 0x18, 0x30, 0x18, 0x0c, 0x06, 0x00}, // 60 <
|
||||
{0x00, 0x00, 0x7e, 0x00, 0x7e, 0x00, 0x00, 0x00}, // 61 =
|
||||
{0x60, 0x30, 0x18, 0x0c, 0x18, 0x30, 0x60, 0x00}, // 62 >
|
||||
{0x3e, 0x63, 0x07, 0x0e, 0x18, 0x00, 0x18, 0x00}, // 63 ?
|
||||
{0x3e, 0x63, 0x6b, 0x6b, 0x6b, 0x3e, 0x00, 0x00}, // 64 @
|
||||
{0x18, 0x3c, 0x66, 0x66, 0x7e, 0x66, 0x66, 0x00}, // 65 A
|
||||
{0x7c, 0x66, 0x66, 0x7c, 0x66, 0x66, 0x7c, 0x00}, // 66 B
|
||||
{0x3e, 0x63, 0x60, 0x60, 0x60, 0x63, 0x3e, 0x00}, // 67 C
|
||||
{0x78, 0x6c, 0x66, 0x66, 0x66, 0x6c, 0x78, 0x00}, // 68 D
|
||||
{0x7f, 0x60, 0x60, 0x7c, 0x60, 0x60, 0x7f, 0x00}, // 69 E
|
||||
{0x7f, 0x60, 0x60, 0x7c, 0x60, 0x60, 0x60, 0x00}, // 70 F
|
||||
{0x3e, 0x63, 0x60, 0x6f, 0x63, 0x63, 0x3e, 0x00}, // 71 G
|
||||
{0x66, 0x66, 0x66, 0x7e, 0x66, 0x66, 0x66, 0x00}, // 72 H
|
||||
{0x3e, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x3e, 0x00}, // 73 I
|
||||
{0x1f, 0x06, 0x06, 0x06, 0x06, 0x66, 0x3c, 0x00}, // 74 J
|
||||
{0x66, 0x6c, 0x78, 0x70, 0x78, 0x6c, 0x66, 0x00}, // 75 K
|
||||
{0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x7f, 0x00}, // 76 L
|
||||
{0x63, 0x77, 0x7f, 0x6b, 0x63, 0x63, 0x63, 0x00}, // 77 M
|
||||
{0x63, 0x67, 0x6f, 0x7b, 0x73, 0x63, 0x63, 0x00}, // 78 N
|
||||
{0x3e, 0x63, 0x63, 0x63, 0x63, 0x63, 0x3e, 0x00}, // 79 O
|
||||
{0x7c, 0x66, 0x66, 0x7c, 0x60, 0x60, 0x60, 0x00}, // 80 P
|
||||
{0x3e, 0x63, 0x63, 0x63, 0x6b, 0x6c, 0x3e, 0x03}, // 81 Q
|
||||
{0x7c, 0x66, 0x66, 0x7c, 0x78, 0x6c, 0x66, 0x00}, // 82 R
|
||||
{0x3e, 0x63, 0x60, 0x3e, 0x03, 0x63, 0x3e, 0x00}, // 83 S
|
||||
{0x7f, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x00}, // 84 T
|
||||
{0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x3e, 0x00}, // 85 U
|
||||
{0x63, 0x63, 0x63, 0x63, 0x63, 0x36, 0x1c, 0x00}, // 86 V
|
||||
{0x63, 0x63, 0x63, 0x6b, 0x7f, 0x77, 0x63, 0x00}, // 87 W
|
||||
{0x63, 0x66, 0x3c, 0x18, 0x3c, 0x66, 0x63, 0x00}, // 88 X
|
||||
{0x63, 0x63, 0x63, 0x3e, 0x0c, 0x0c, 0x0c, 0x00}, // 89 Y
|
||||
{0x7f, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x7f, 0x00}, // 90 Z
|
||||
{0x3c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3c, 0x00}, // 91 [
|
||||
{0xc0, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x03, 0x00}, // 92 \
|
||||
{0x3c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x3c, 0x00}, // 93 ]
|
||||
{0x10, 0x38, 0x6c, 0xc6, 0x00, 0x00, 0x00, 0x00}, // 94 ^
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff}, // 95 _
|
||||
{0x30, 0x30, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // 96 `
|
||||
{0x00, 0x00, 0x3c, 0x06, 0x3e, 0x66, 0x3b, 0x00}, // 97 a
|
||||
{0x60, 0x60, 0x7c, 0x66, 0x66, 0x66, 0x7c, 0x00}, // 98 b
|
||||
{0x00, 0x00, 0x3c, 0x66, 0x60, 0x66, 0x3c, 0x00}, // 99 c
|
||||
{0x06, 0x06, 0x3e, 0x66, 0x66, 0x66, 0x3e, 0x00}, // 100 d
|
||||
{0x00, 0x00, 0x3c, 0x66, 0x7e, 0x60, 0x3c, 0x00}, // 101 e
|
||||
{0x1c, 0x36, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x00}, // 102 f
|
||||
{0x00, 0x00, 0x3e, 0x66, 0x66, 0x3e, 0x06, 0x3c}, // 103 g
|
||||
{0x60, 0x60, 0x7c, 0x66, 0x66, 0x66, 0x66, 0x00}, // 104 h
|
||||
{0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 105 i
|
||||
{0x06, 0x00, 0x1e, 0x06, 0x06, 0x06, 0x66, 0x3c}, // 106 j
|
||||
{0x60, 0x60, 0x66, 0x6c, 0x78, 0x6c, 0x66, 0x00}, // 107 k
|
||||
{0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 108 l
|
||||
{0x00, 0x00, 0x66, 0x7f, 0x5b, 0x49, 0x49, 0x00}, // 109 m
|
||||
{0x00, 0x00, 0x7c, 0x66, 0x66, 0x66, 0x66, 0x00}, // 110 n
|
||||
{0x00, 0x00, 0x3c, 0x66, 0x66, 0x66, 0x3c, 0x00}, // 111 o
|
||||
{0x00, 0x00, 0x7c, 0x66, 0x66, 0x7c, 0x60, 0x60}, // 112 p
|
||||
{0x00, 0x00, 0x3e, 0x66, 0x66, 0x3e, 0x06, 0x06}, // 113 q
|
||||
{0x00, 0x00, 0x7c, 0x66, 0x60, 0x60, 0x60, 0x00}, // 114 r
|
||||
{0x00, 0x00, 0x3e, 0x60, 0x3c, 0x06, 0x7c, 0x00}, // 115 s
|
||||
{0x30, 0x30, 0x7c, 0x30, 0x30, 0x36, 0x1c, 0x00}, // 116 t
|
||||
{0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3e, 0x00}, // 117 u
|
||||
{0x00, 0x00, 0x66, 0x66, 0x66, 0x3c, 0x18, 0x00}, // 118 v
|
||||
{0x00, 0x00, 0x63, 0x6b, 0x7f, 0x3e, 0x1c, 0x00}, // 119 w
|
||||
{0x00, 0x00, 0x66, 0x3c, 0x18, 0x3c, 0x66, 0x00}, // 120 x
|
||||
{0x00, 0x00, 0x66, 0x66, 0x66, 0x3e, 0x06, 0x3c}, // 121 y
|
||||
{0x00, 0x00, 0x7e, 0x0c, 0x18, 0x30, 0x7e, 0x00}, // 122 z
|
||||
{0x0c, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0c, 0x00}, // 123 {
|
||||
{0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // 124 |
|
||||
{0x30, 0x18, 0x18, 0x0e, 0x18, 0x18, 0x30, 0x00}, // 125 }
|
||||
{0x3b, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // 126 ~
|
||||
}; |
||||
} |
||||
@ -0,0 +1,450 @@
|
||||
#include "Game.hpp" |
||||
#include <algorithm> |
||||
#include <chrono> |
||||
#include <iostream> |
||||
|
||||
|
||||
|
||||
// SRS Wall-Kick translation vectors (x, y) for 3x3 shapes (T, J, L, S, Z)
|
||||
// Direction is either Clockwise (CW) or Counter-Clockwise (CCW).
|
||||
// Format: KICKS_3x3[CW/CCW][StartRot][TestIndex]
|
||||
static const Point KICKS_3x3_CW[4][5] = { |
||||
{ {0, 0}, {-1, 0}, {-1, 1}, {0, -2}, {-1, -2} }, // 0 -> 1
|
||||
{ {0, 0}, {1, 0}, {1, -1}, {0, 2}, {1, 2} }, // 1 -> 2
|
||||
{ {0, 0}, {1, 0}, {1, 1}, {0, -2}, {1, -2} }, // 2 -> 3
|
||||
{ {0, 0}, {-1, 0}, {-1, -1}, {0, 2}, {-1, 2} } // 3 -> 0
|
||||
}; |
||||
|
||||
static const Point KICKS_3x3_CCW[4][5] = { |
||||
{ {0, 0}, {1, 0}, {1, 1}, {0, -2}, {1, -2} }, // 0 -> 3
|
||||
{ {0, 0}, {1, 0}, {1, -1}, {0, 2}, {1, 2} }, // 1 -> 0
|
||||
{ {0, 0}, {-1, 0}, {-1, 1}, {0, -2}, {-1, -2} }, // 2 -> 1
|
||||
{ {0, 0}, {-1, 0}, {-1, -1}, {0, 2}, {-1, 2} } // 3 -> 2
|
||||
}; |
||||
|
||||
// SRS Wall-Kick translation vectors (x, y) for I piece (4x4)
|
||||
static const Point KICKS_I_CW[4][5] = { |
||||
{ {0, 0}, {-2, 0}, {1, 0}, {-2, 1}, {1, -2} }, // 0 -> 1
|
||||
{ {0, 0}, {-1, 0}, {2, 0}, {-1, -2}, {2, 1} }, // 1 -> 2
|
||||
{ {0, 0}, {2, 0}, {-1, 0}, {2, -1}, {-1, 2} }, // 2 -> 3
|
||||
{ {0, 0}, {1, 0}, {-2, 0}, {1, 2}, {-2, -1} } // 3 -> 0
|
||||
}; |
||||
|
||||
static const Point KICKS_I_CCW[4][5] = { |
||||
{ {0, 0}, {-1, 0}, {2, 0}, {-1, -2}, {2, 1} }, // 0 -> 3
|
||||
{ {0, 0}, {2, 0}, {-1, 0}, {2, -1}, {-1, 2} }, // 1 -> 0
|
||||
{ {0, 0}, {1, 0}, {-2, 0}, {1, 2}, {-2, -1} }, // 2 -> 1
|
||||
{ {0, 0}, {-2, 0}, {1, 0}, {-2, 1}, {1, -2} } // 3 -> 2
|
||||
}; |
||||
|
||||
Game::Game() { |
||||
unsigned seed = std::chrono::system_clock::now().time_since_epoch().count(); |
||||
mRng.seed(seed); |
||||
init(); |
||||
} |
||||
|
||||
void Game::init() { |
||||
mState = GameState::START; |
||||
reset(); |
||||
} |
||||
|
||||
void Game::reset() { |
||||
// Clear grid
|
||||
for (int y = 0; y < BOARD_HEIGHT; ++y) { |
||||
for (int x = 0; x < BOARD_WIDTH; ++x) { |
||||
mBoard[y][x] = PieceType::NONE; |
||||
} |
||||
} |
||||
|
||||
mScore = 0; |
||||
mLinesCleared = 0; |
||||
mLevel = 1; |
||||
mCombo = -1; |
||||
|
||||
mHoldType = PieceType::NONE; |
||||
mCanHold = true; |
||||
|
||||
mFallTimer = 0.0f; |
||||
mLockTimer = 0.0f; |
||||
mIsLocking = false; |
||||
mShakeIntensity = 0.0f; |
||||
|
||||
mBag.clear(); |
||||
mNextQueue.clear(); |
||||
|
||||
// Populate the queue with initial pieces
|
||||
fillBag(); |
||||
for (int i = 0; i < 4; ++i) { |
||||
mNextQueue.push_back(popNextPiece()); |
||||
} |
||||
|
||||
mActiveType = PieceType::NONE; |
||||
spawnPiece(); |
||||
} |
||||
|
||||
void Game::fillBag() { |
||||
std::vector<PieceType> pieces = { |
||||
PieceType::I, PieceType::O, PieceType::T, PieceType::S, PieceType::Z, PieceType::J, PieceType::L |
||||
}; |
||||
std::shuffle(pieces.begin(), pieces.end(), mRng); |
||||
mBag.insert(mBag.end(), pieces.begin(), pieces.end()); |
||||
} |
||||
|
||||
PieceType Game::popNextPiece() { |
||||
if (mBag.empty()) { |
||||
fillBag(); |
||||
} |
||||
PieceType next = mBag.back(); |
||||
mBag.pop_back(); |
||||
return next; |
||||
} |
||||
|
||||
void Game::spawnPiece() { |
||||
mActiveType = mNextQueue.front(); |
||||
mNextQueue.erase(mNextQueue.begin()); |
||||
mNextQueue.push_back(popNextPiece()); |
||||
|
||||
mActiveRot = 0; |
||||
|
||||
// Spawn at top center
|
||||
mActiveX = BOARD_WIDTH / 2 - 1; |
||||
mActiveY = 0; |
||||
|
||||
mCanHold = true; |
||||
mIsLocking = false; |
||||
mLockTimer = 0.0f; |
||||
|
||||
// Check game over right at spawn
|
||||
if (fits(mActiveType, mActiveRot, mActiveX, mActiveY)) { |
||||
std::cout << "[DEBUG] Game Over triggered on spawn of piece type " << static_cast<int>(mActiveType) << std::endl; |
||||
mState = GameState::GAME_OVER; |
||||
flagGameOverSFX = true; |
||||
} else { |
||||
std::cout << "[DEBUG] Spawned piece type " << static_cast<int>(mActiveType) << " successfully at (" << mActiveX << ", " << mActiveY << ")" << std::endl; |
||||
} |
||||
} |
||||
|
||||
std::vector<Point> Game::getActiveCells() const { |
||||
std::vector<Point> cells; |
||||
if (mActiveType == PieceType::NONE) return cells; |
||||
|
||||
int typeIdx = static_cast<int>(mActiveType); |
||||
for (int i = 0; i < 4; ++i) { |
||||
Point p = TETROMINO_CELLS[typeIdx][mActiveRot][i]; |
||||
cells.push_back({mActiveX + p.x, mActiveY + p.y}); |
||||
} |
||||
return cells; |
||||
} |
||||
|
||||
std::vector<Point> Game::getGhostCells() const { |
||||
std::vector<Point> cells = getActiveCells(); |
||||
if (cells.empty()) return cells; |
||||
|
||||
int dy = 0; |
||||
while (!checkCollision(cells, 0, dy + 1)) { |
||||
dy++; |
||||
} |
||||
|
||||
for (auto& p : cells) { |
||||
p.y += dy; |
||||
} |
||||
return cells; |
||||
} |
||||
|
||||
bool Game::checkCollision(const std::vector<Point>& cells, int dx, int dy) const { |
||||
for (const auto& p : cells) { |
||||
int nx = p.x + dx; |
||||
int ny = p.y + dy; |
||||
|
||||
// Bounds check
|
||||
if (nx < 0 || nx >= BOARD_WIDTH || ny >= BOARD_HEIGHT) { |
||||
return true; |
||||
} |
||||
|
||||
// Top cap check (allow pieces to scroll off top grid temporarily)
|
||||
if (ny < 0) continue; |
||||
|
||||
if (mBoard[ny][nx] != PieceType::NONE) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
bool Game::fits(PieceType type, int rot, int cx, int cy) const { |
||||
int typeIdx = static_cast<int>(type); |
||||
for (int i = 0; i < 4; ++i) { |
||||
Point p = TETROMINO_CELLS[typeIdx][rot][i]; |
||||
int nx = cx + p.x; |
||||
int ny = cy + p.y; |
||||
|
||||
if (nx < 0 || nx >= BOARD_WIDTH || ny >= BOARD_HEIGHT) { |
||||
return true; |
||||
} |
||||
if (ny < 0) continue; |
||||
|
||||
if (mBoard[ny][nx] != PieceType::NONE) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
bool Game::moveLeft() { |
||||
if (mState != GameState::PLAYING) return false; |
||||
std::vector<Point> cells = getActiveCells(); |
||||
if (!checkCollision(cells, -1, 0)) { |
||||
mActiveX--; |
||||
flagMoveSFX = true; |
||||
// Slide kick resets lock timer
|
||||
if (mIsLocking) { |
||||
mLockTimer = 0.0f; |
||||
} |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
bool Game::moveRight() { |
||||
if (mState != GameState::PLAYING) return false; |
||||
std::vector<Point> cells = getActiveCells(); |
||||
if (!checkCollision(cells, 1, 0)) { |
||||
mActiveX++; |
||||
flagMoveSFX = true; |
||||
// Slide kick resets lock timer
|
||||
if (mIsLocking) { |
||||
mLockTimer = 0.0f; |
||||
} |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
bool Game::rotate(int dir) { |
||||
if (mState != GameState::PLAYING || mActiveType == PieceType::NONE) return false; |
||||
if (mActiveType == PieceType::O) return false; // O piece does not rotate
|
||||
|
||||
int nextRot = (mActiveRot + dir + 4) % 4; |
||||
|
||||
// Get wall kick offset trials
|
||||
const Point(*kicks)[5] = nullptr; |
||||
if (mActiveType == PieceType::I) { |
||||
kicks = (dir == 1) ? KICKS_I_CW : KICKS_I_CCW; |
||||
} else { |
||||
kicks = (dir == 1) ? KICKS_3x3_CW : KICKS_3x3_CCW; |
||||
} |
||||
|
||||
// Try all 5 Wall-kick tests
|
||||
for (int t = 0; t < 5; ++t) { |
||||
int dx = kicks[mActiveRot][t].x; |
||||
int dy = kicks[mActiveRot][t].y; |
||||
|
||||
if (!fits(mActiveType, nextRot, mActiveX + dx, mActiveY + dy)) { |
||||
// Apply rotation and kick offset
|
||||
mActiveRot = nextRot; |
||||
mActiveX += dx; |
||||
mActiveY += dy; |
||||
flagRotateSFX = true; |
||||
|
||||
// Reset lock delay on successful rotation
|
||||
if (mIsLocking) { |
||||
mLockTimer = 0.0f; |
||||
} |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
void Game::softDrop() { |
||||
if (mState != GameState::PLAYING) return; |
||||
std::vector<Point> cells = getActiveCells(); |
||||
if (!checkCollision(cells, 0, 1)) { |
||||
mActiveY++; |
||||
mScore += 1; // Soft drop score
|
||||
mFallTimer = 0.0f; |
||||
} |
||||
} |
||||
|
||||
bool Game::hardDrop() { |
||||
if (mState != GameState::PLAYING) return false; |
||||
std::vector<Point> cells = getActiveCells(); |
||||
int dy = 0; |
||||
while (!checkCollision(cells, 0, dy + 1)) { |
||||
dy++; |
||||
} |
||||
|
||||
mActiveY += dy; |
||||
mScore += dy * 2; // Hard drop score
|
||||
|
||||
// Force instant lock
|
||||
mShakeIntensity = 0.15f + (dy * 0.015f); // hard drop screenshake!
|
||||
lockPiece(); |
||||
return true; |
||||
} |
||||
|
||||
void Game::holdPiece() { |
||||
if (mState != GameState::PLAYING || !mCanHold) return; |
||||
|
||||
flagRotateSFX = true; // cool swoosh
|
||||
PieceType temp = mHoldType; |
||||
mHoldType = mActiveType; |
||||
|
||||
if (temp == PieceType::NONE) { |
||||
spawnPiece(); |
||||
} else { |
||||
mActiveType = temp; |
||||
mActiveRot = 0; |
||||
mActiveX = BOARD_WIDTH / 2 - 1; |
||||
mActiveY = 0; |
||||
mIsLocking = false; |
||||
mLockTimer = 0.0f; |
||||
} |
||||
|
||||
mCanHold = false; |
||||
} |
||||
|
||||
float Game::getFallDelay() const { |
||||
// Standard progressive fall timing (seconds per grid step)
|
||||
switch (mLevel) { |
||||
case 1: return 0.85f; |
||||
case 2: return 0.72f; |
||||
case 3: return 0.60f; |
||||
case 4: return 0.48f; |
||||
case 5: return 0.38f; |
||||
case 6: return 0.30f; |
||||
case 7: return 0.22f; |
||||
case 8: return 0.16f; |
||||
case 9: return 0.11f; |
||||
case 10: return 0.08f; |
||||
default: return 0.06f; // level 11+
|
||||
} |
||||
} |
||||
|
||||
bool Game::update(float dt) { |
||||
if (mState != GameState::PLAYING) return false; |
||||
|
||||
if (mShakeIntensity > 0.0f) { |
||||
mShakeIntensity -= dt * 0.8f; // Decay over time
|
||||
if (mShakeIntensity < 0.0f) mShakeIntensity = 0.0f; |
||||
} |
||||
|
||||
mClearedLinesThisTick.clear(); |
||||
|
||||
std::vector<Point> cells = getActiveCells(); |
||||
bool onGround = checkCollision(cells, 0, 1); |
||||
|
||||
if (onGround) { |
||||
if (!mIsLocking) { |
||||
mIsLocking = true; |
||||
mLockTimer = 0.0f; |
||||
} |
||||
|
||||
mLockTimer += dt; |
||||
// Lock delay threshold of 0.45 seconds
|
||||
if (mLockTimer >= 0.45f) { |
||||
lockPiece(); |
||||
return true; |
||||
} |
||||
} else { |
||||
mIsLocking = false; |
||||
mFallTimer += dt; |
||||
|
||||
float fallDelay = getFallDelay(); |
||||
if (mFallTimer >= fallDelay) { |
||||
mFallTimer -= fallDelay; |
||||
mActiveY++; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
void Game::lockPiece() { |
||||
std::vector<Point> cells = getActiveCells(); |
||||
if (cells.empty()) return; |
||||
|
||||
mLastLockedCells = cells; |
||||
|
||||
for (const auto& p : cells) { |
||||
if (p.y >= 0 && p.y < BOARD_HEIGHT && p.x >= 0 && p.x < BOARD_WIDTH) { |
||||
mBoard[p.y][p.x] = mActiveType; |
||||
} |
||||
} |
||||
|
||||
flagLandSFX = true; |
||||
clearLines(); |
||||
spawnPiece(); |
||||
} |
||||
|
||||
void Game::clearLines() { |
||||
std::vector<int> fullLines; |
||||
|
||||
for (int y = 0; y < BOARD_HEIGHT; ++y) { |
||||
bool full = true; |
||||
for (int x = 0; x < BOARD_WIDTH; ++x) { |
||||
if (mBoard[y][x] == PieceType::NONE) { |
||||
full = false; |
||||
break; |
||||
} |
||||
} |
||||
if (full) { |
||||
fullLines.push_back(y); |
||||
} |
||||
} |
||||
|
||||
if (fullLines.empty()) { |
||||
mCombo = -1; // reset combo
|
||||
return; |
||||
} |
||||
|
||||
mClearedLinesThisTick = fullLines; // save for particle animations
|
||||
mCombo++; |
||||
|
||||
// Scoring guideline
|
||||
int baseScore = 0; |
||||
int cleared = fullLines.size(); |
||||
if (cleared == 1) { |
||||
baseScore = 100; |
||||
flagLineClearSFX = true; |
||||
mShakeIntensity = std::max(mShakeIntensity, 0.08f); |
||||
} else if (cleared == 2) { |
||||
baseScore = 300; |
||||
flagLineClearSFX = true; |
||||
mShakeIntensity = std::max(mShakeIntensity, 0.15f); |
||||
} else if (cleared == 3) { |
||||
baseScore = 500; |
||||
flagLineClearSFX = true; |
||||
mShakeIntensity = std::max(mShakeIntensity, 0.22f); |
||||
} else if (cleared >= 4) { |
||||
baseScore = 800; |
||||
flagTetrisClearSFX = true; // Boom! Tetris!
|
||||
mShakeIntensity = std::max(mShakeIntensity, 0.40f); // Screen Rumble!
|
||||
} |
||||
|
||||
mScore += baseScore * mLevel; |
||||
if (mCombo > 0) { |
||||
mScore += 50 * mCombo * mLevel; |
||||
} |
||||
|
||||
// Erase cleared lines and shift elements down
|
||||
for (int lineY : fullLines) { |
||||
for (int currY = lineY; currY > 0; --currY) { |
||||
for (int x = 0; x < BOARD_WIDTH; ++x) { |
||||
mBoard[currY][x] = mBoard[currY - 1][x]; |
||||
} |
||||
} |
||||
// Top line cleared
|
||||
for (int x = 0; x < BOARD_WIDTH; ++x) { |
||||
mBoard[0][x] = PieceType::NONE; |
||||
} |
||||
} |
||||
|
||||
mLinesCleared += cleared; |
||||
|
||||
// Level up logic (every 10 lines)
|
||||
int nextLevel = (mLinesCleared / 10) + 1; |
||||
if (nextLevel > mLevel) { |
||||
mLevel = nextLevel; |
||||
flagLevelUpSFX = true; |
||||
} |
||||
} |
||||
@ -0,0 +1,189 @@
|
||||
#pragma once |
||||
#include <vector> |
||||
#include <random> |
||||
|
||||
enum class PieceType { |
||||
I = 0, O, T, S, Z, J, L, NONE |
||||
}; |
||||
|
||||
struct Point { |
||||
int x; |
||||
int y; |
||||
}; |
||||
|
||||
// Coordinates for all 7 Tetromino types in their 4 rotation states (0, 1, 2, 3)
|
||||
// Pivot anchor is (0,0). Screen space has Y-down, X-right.
|
||||
inline const Point TETROMINO_CELLS[7][4][4] = { |
||||
// 0: I Piece
|
||||
{ |
||||
{ {-1, 0}, {0, 0}, {1, 0}, {2, 0} }, // rot 0
|
||||
{ {1, -1}, {1, 0}, {1, 1}, {1, 2} }, // rot 1
|
||||
{ {-1, 1}, {0, 1}, {1, 1}, {2, 1} }, // rot 2
|
||||
{ {0, -1}, {0, 0}, {0, 1}, {0, 2} } // rot 3
|
||||
}, |
||||
// 1: O Piece
|
||||
{ |
||||
{ {0, 0}, {1, 0}, {0, 1}, {1, 1} }, // rot 0
|
||||
{ {0, 0}, {1, 0}, {0, 1}, {1, 1} }, // rot 1
|
||||
{ {0, 0}, {1, 0}, {0, 1}, {1, 1} }, // rot 2
|
||||
{ {0, 0}, {1, 0}, {0, 1}, {1, 1} } // rot 3
|
||||
}, |
||||
// 2: T Piece
|
||||
{ |
||||
{ {-1, 0}, {0, 0}, {1, 0}, {0, 1} }, // rot 0
|
||||
{ {0, -1}, {0, 0}, {0, 1}, {-1, 0} }, // rot 1
|
||||
{ {-1, 0}, {0, 0}, {1, 0}, {0, -1} }, // rot 2
|
||||
{ {0, -1}, {0, 0}, {0, 1}, {1, 0} } // rot 3
|
||||
}, |
||||
// 3: S Piece
|
||||
{ |
||||
{ {0, 0}, {1, 0}, {-1, 1}, {0, 1} }, // rot 0
|
||||
{ {0, -1}, {0, 0}, {1, 0}, {1, 1} }, // rot 1
|
||||
{ {0, -1}, {1, -1}, {-1, 0}, {0, 0} }, // rot 2
|
||||
{ {-1, -1}, {-1, 0}, {0, 0}, {0, 1} } // rot 3
|
||||
}, |
||||
// 4: Z Piece
|
||||
{ |
||||
{ {-1, 0}, {0, 0}, {0, 1}, {1, 1} }, // rot 0
|
||||
{ {1, -1}, {1, 0}, {0, 0}, {0, 1} }, // rot 1
|
||||
{ {-1, -1}, {0, -1}, {0, 0}, {1, 0} }, // rot 2
|
||||
{ {0, -1}, {0, 0}, {-1, 0}, {-1, 1} } // rot 3
|
||||
}, |
||||
// 5: J Piece
|
||||
{ |
||||
{ {-1, 0}, {0, 0}, {1, 0}, {-1, 1} }, // rot 0
|
||||
{ {0, -1}, {0, 0}, {0, 1}, {-1, -1} }, // rot 1
|
||||
{ {-1, 0}, {0, 0}, {1, 0}, {1, -1} }, // rot 2
|
||||
{ {0, -1}, {0, 0}, {0, 1}, {1, 1} } // rot 3
|
||||
}, |
||||
// 6: L Piece
|
||||
{ |
||||
{ {-1, 0}, {0, 0}, {1, 0}, {1, 1} }, // rot 0
|
||||
{ {0, -1}, {0, 0}, {0, 1}, {-1, 1} }, // rot 1
|
||||
{ {-1, 0}, {0, 0}, {1, 0}, {-1, -1} }, // rot 2
|
||||
{ {0, -1}, {0, 0}, {0, 1}, {1, -1} } // rot 3
|
||||
} |
||||
}; |
||||
|
||||
enum class GameState { |
||||
START, |
||||
PLAYING, |
||||
PAUSED, |
||||
GAME_OVER |
||||
}; |
||||
|
||||
class Game { |
||||
public: |
||||
static constexpr int BOARD_WIDTH = 10; |
||||
static constexpr int BOARD_HEIGHT = 20; |
||||
|
||||
Game(); |
||||
~Game() = default; |
||||
|
||||
void init(); |
||||
void reset(); |
||||
|
||||
// Core game loop update (handles gravity, lock delay, game states)
|
||||
// Returns true if lines were cleared in this frame
|
||||
bool update(float dt); |
||||
|
||||
// Gameplay commands (inputs)
|
||||
bool moveLeft(); |
||||
bool moveRight(); |
||||
bool rotate(int dir); // dir = 1 (CW), -1 (CCW)
|
||||
void softDrop(); |
||||
bool hardDrop(); // Returns true if piece locked
|
||||
void holdPiece(); |
||||
|
||||
// Query states
|
||||
GameState getState() const { return mState; } |
||||
void setState(GameState state) { mState = state; } |
||||
|
||||
int getScore() const { return mScore; } |
||||
int getLinesCleared() const { return mLinesCleared; } |
||||
int getLevel() const { return mLevel; } |
||||
int getCombo() const { return mCombo; } |
||||
|
||||
PieceType getCell(int x, int y) const { |
||||
if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) return PieceType::NONE; |
||||
return mBoard[y][x]; |
||||
} |
||||
|
||||
PieceType getActivePieceType() const { return mActiveType; } |
||||
int getActiveX() const { return mActiveX; } |
||||
int getActiveY() const { return mActiveY; } |
||||
int getActiveRotation() const { return mActiveRot; } |
||||
|
||||
// Relative cell coordinates of current falling block
|
||||
std::vector<Point> getActiveCells() const; |
||||
|
||||
// Absolute cell coordinates of where active piece would land (ghost projection)
|
||||
std::vector<Point> getGhostCells() const; |
||||
|
||||
PieceType getHoldPieceType() const { return mHoldType; } |
||||
bool canHold() const { return mCanHold; } |
||||
|
||||
std::vector<PieceType> getNextQueue() const { return mNextQueue; } |
||||
|
||||
// Inter-thread game sound effects triggers
|
||||
bool flagMoveSFX = false; |
||||
bool flagRotateSFX = false; |
||||
bool flagLandSFX = false; |
||||
bool flagLineClearSFX = false; |
||||
bool flagTetrisClearSFX = false; |
||||
bool flagLevelUpSFX = false; |
||||
bool flagGameOverSFX = false; |
||||
|
||||
// Screen shake parameters
|
||||
float getScreenShakeIntensity() const { return mShakeIntensity; } |
||||
void resetScreenShake() { mShakeIntensity = 0.0f; } |
||||
|
||||
// List of lines cleared on the current tick (for particle animations)
|
||||
std::vector<int> mClearedLinesThisTick; |
||||
std::vector<Point> mLastLockedCells; |
||||
|
||||
private: |
||||
GameState mState = GameState::START; |
||||
|
||||
// The grid: index y=0 is top, y=19 is bottom
|
||||
PieceType mBoard[BOARD_HEIGHT][BOARD_WIDTH]; |
||||
|
||||
// Falling piece details
|
||||
PieceType mActiveType = PieceType::NONE; |
||||
int mActiveX = 0; |
||||
int mActiveY = 0; |
||||
int mActiveRot = 0; |
||||
|
||||
// Hold slot details
|
||||
PieceType mHoldType = PieceType::NONE; |
||||
bool mCanHold = true; |
||||
|
||||
// 7-piece bag randomizer
|
||||
std::vector<PieceType> mNextQueue; |
||||
std::vector<PieceType> mBag; |
||||
std::mt19937 mRng; |
||||
|
||||
// Score metrics
|
||||
int mScore = 0; |
||||
int mLinesCleared = 0; |
||||
int mLevel = 1; |
||||
int mCombo = -1; |
||||
|
||||
// Fall & Lock delay timers
|
||||
float mFallTimer = 0.0f; |
||||
float mLockTimer = 0.0f; |
||||
bool mIsLocking = false; |
||||
float mShakeIntensity = 0.0f; |
||||
|
||||
float getFallDelay() const; |
||||
|
||||
void spawnPiece(); |
||||
void fillBag(); |
||||
PieceType popNextPiece(); |
||||
|
||||
bool checkCollision(const std::vector<Point>& cells, int dx, int dy) const; |
||||
bool fits(PieceType type, int rot, int cx, int cy) const; |
||||
|
||||
void lockPiece(); |
||||
void clearLines(); |
||||
}; |
||||
@ -0,0 +1,28 @@
|
||||
CXX = g++
|
||||
CXXFLAGS = -std=c++17 -Wall -Wextra -O3 `sdl2-config --cflags`
|
||||
LDFLAGS = `sdl2-config --libs`
|
||||
|
||||
TARGET = cybermatris
|
||||
OBJS = main.o Game.o Synth.o Renderer.o
|
||||
|
||||
all: $(TARGET) |
||||
|
||||
$(TARGET): $(OBJS) |
||||
$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS) $(LDFLAGS)
|
||||
|
||||
main.o: main.cpp Game.hpp Synth.hpp Renderer.hpp Font.hpp |
||||
$(CXX) $(CXXFLAGS) -c main.cpp
|
||||
|
||||
Game.o: Game.cpp Game.hpp |
||||
$(CXX) $(CXXFLAGS) -c Game.cpp
|
||||
|
||||
Synth.o: Synth.cpp Synth.hpp |
||||
$(CXX) $(CXXFLAGS) -c Synth.cpp
|
||||
|
||||
Renderer.o: Renderer.cpp Renderer.hpp Game.hpp Synth.hpp Font.hpp |
||||
$(CXX) $(CXXFLAGS) -c Renderer.cpp
|
||||
|
||||
clean: |
||||
rm -f $(OBJS) $(TARGET)
|
||||
|
||||
.PHONY: all clean |
||||
@ -0,0 +1,109 @@
|
||||
# ============================================================
|
||||
# Makefile.miyoo - Cross-compile CyberMatris per Miyoo Mini Plus
|
||||
# Toolchain: mini_toolchain-v1.0 (SigmaStar ARM)
|
||||
# SDL2: sdl2-miyoo (build custom con backend MI GFX/AO)
|
||||
#
|
||||
# Usage: make -f Makefile.miyoo
|
||||
# Deploy: make -f Makefile.miyoo deploy (richiede SSH su 10.0.0.199)
|
||||
# ============================================================
|
||||
|
||||
TOOLCHAIN_ROOT = /home/enne2/dev/mini_toolchain-v1.0/mini
|
||||
SDL2_ROOT = /home/enne2/dev/sdl2-miyoo
|
||||
|
||||
CROSS = $(TOOLCHAIN_ROOT)/bin/arm-linux-gnueabihf-
|
||||
CXX = $(CROSS)g++
|
||||
STRIP = $(CROSS)strip
|
||||
|
||||
SDL2_INC = $(SDL2_ROOT)/sdl2/include
|
||||
SDL2_LIB = $(SDL2_ROOT)/sdl2/build/.libs
|
||||
SYSROOT = $(TOOLCHAIN_ROOT)/arm-buildroot-linux-gnueabihf/sysroot
|
||||
|
||||
CXXFLAGS = -std=c++17 -O2 -Wall -Wextra \
|
||||
-I$(SDL2_INC) \
|
||||
-I$(SDL2_INC)/SDL2 \
|
||||
-DSDL_MAIN_HANDLED \
|
||||
-DMIYOO_BUILD
|
||||
|
||||
# Linka SDL2 dinamicamente (il .so Miyoo custom include il backend MI_GFX/MI_AO)
|
||||
# Le libmi_*.so sono proprietarie SigmaStar: esistono SOLO sul device in /config/lib/
|
||||
# Vengono risolte a runtime tramite LD_LIBRARY_PATH=.:/config/lib
|
||||
# Usiamo --allow-shlib-undefined perché il linker host non le vede ma sul device ci sono.
|
||||
LDFLAGS = -L$(SDL2_LIB) \
|
||||
-lSDL2 \
|
||||
-lpthread -lm -ldl -lrt \
|
||||
-Wl,-rpath,'$$ORIGIN' \
|
||||
-Wl,--allow-shlib-undefined
|
||||
|
||||
TARGET = cybermatris_miyoo
|
||||
OBJS = main.o Game.o Synth.o Renderer.o
|
||||
|
||||
DEPLOY_IP = 10.0.0.199
|
||||
DEPLOY_USER = root
|
||||
DEPLOY_PASS =
|
||||
DEPLOY_DIR = /mnt/SDCARD/Roms/PORTS/Games/CyberMatris
|
||||
|
||||
# Runtime libs da copiare nella stessa cartella del binario
|
||||
RUNTIME_LIBS = \
|
||||
$(SDL2_ROOT)/sdl2/build/.libs/libSDL2-2.0.so.0 \
|
||||
$(SDL2_ROOT)/sdl2/build/.libs/libSDL2-2.0.so.0.18.2 \
|
||||
$(SDL2_ROOT)/sdl2/build/.libs/libEGL.so \
|
||||
$(SDL2_ROOT)/sdl2/build/.libs/libGLESv2.so \
|
||||
$(SYSROOT)/usr/lib/libjson-c.so.5 \
|
||||
$(SYSROOT)/usr/lib/libjson-c.so.5.1.0
|
||||
|
||||
.PHONY: all clean deploy |
||||
|
||||
all: $(TARGET) |
||||
|
||||
$(TARGET): $(OBJS) |
||||
$(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
$(STRIP) $@
|
||||
@echo ""
|
||||
@echo ">>> Build completata: $(TARGET)"
|
||||
@echo ">>> Dimensione: $$(du -sh $(TARGET) | cut -f1)"
|
||||
|
||||
main.o: main.cpp Game.hpp Synth.hpp Renderer.hpp Font.hpp |
||||
$(CXX) $(CXXFLAGS) -c main.cpp
|
||||
|
||||
Game.o: Game.cpp Game.hpp Synth.hpp |
||||
$(CXX) $(CXXFLAGS) -c Game.cpp
|
||||
|
||||
Synth.o: Synth.cpp Synth.hpp |
||||
$(CXX) $(CXXFLAGS) -c Synth.cpp
|
||||
|
||||
Renderer.o: Renderer.cpp Renderer.hpp Game.hpp Synth.hpp Font.hpp |
||||
$(CXX) $(CXXFLAGS) -c Renderer.cpp
|
||||
|
||||
clean: |
||||
rm -f $(OBJS) $(TARGET)
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Crea il pacchetto di deploy (cartella con binario + libs + launcher)
|
||||
# -------------------------------------------------------
|
||||
package: $(TARGET) |
||||
@mkdir -p dist_miyoo
|
||||
@cp $(TARGET) dist_miyoo/
|
||||
@for lib in $(RUNTIME_LIBS); do \
|
||||
if [ -f "$$lib" ]; then cp "$$lib" dist_miyoo/; echo " Copiata: $$(basename $$lib)"; \
|
||||
else echo " WARN: $$lib non trovata, skip"; fi; \
|
||||
done
|
||||
@cp launch_miyoo.sh dist_miyoo/ 2>/dev/null || true
|
||||
@echo ""
|
||||
@echo ">>> Pacchetto pronto in ./dist_miyoo/"
|
||||
@ls -lh dist_miyoo/
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Deploy via SSH + sshpass (password vuota)
|
||||
# -------------------------------------------------------
|
||||
deploy: package |
||||
@echo ">>> Deploy su $(DEPLOY_USER)@$(DEPLOY_IP):$(DEPLOY_DIR)"
|
||||
sshpass -p '$(DEPLOY_PASS)' ssh -o StrictHostKeyChecking=no \
|
||||
$(DEPLOY_USER)@$(DEPLOY_IP) "mkdir -p $(DEPLOY_DIR)"
|
||||
sshpass -p '$(DEPLOY_PASS)' scp -o StrictHostKeyChecking=no \
|
||||
dist_miyoo/* $(DEPLOY_USER)@$(DEPLOY_IP):$(DEPLOY_DIR)/
|
||||
@echo ">>> Deploy completato!"
|
||||
@echo ""
|
||||
@echo ">>> Per lanciare sul device:"
|
||||
@echo " ssh root@$(DEPLOY_IP)"
|
||||
@echo " kill -STOP \`pidof MainUI\`"
|
||||
@echo " cd $(DEPLOY_DIR) && LD_LIBRARY_PATH=.:/config/lib ./cybermatris_miyoo"
|
||||
@ -0,0 +1,845 @@
|
||||
#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()); |
||||
} |
||||
@ -0,0 +1,74 @@
|
||||
#pragma once |
||||
#include <SDL2/SDL.h> |
||||
#include <vector> |
||||
#include <string> |
||||
#include "Game.hpp" |
||||
#include "Synth.hpp" |
||||
|
||||
struct Particle { |
||||
float x = 0.0f; |
||||
float y = 0.0f; |
||||
float vx = 0.0f; |
||||
float vy = 0.0f; |
||||
float life = 1.0f; // 1.0 down to 0.0
|
||||
float decay = 1.0f; // decay rate per second
|
||||
float size = 4.0f; |
||||
SDL_Color color = {255, 255, 255, 255}; |
||||
}; |
||||
|
||||
struct Star { |
||||
float x = 0.0f; |
||||
float y = 0.0f; |
||||
float speed = 0.0f; |
||||
float size = 0.0f; |
||||
float alpha = 0.0f; |
||||
}; |
||||
|
||||
class Renderer { |
||||
public: |
||||
Renderer(); |
||||
~Renderer(); |
||||
|
||||
bool init(SDL_Renderer* renderer, int targetW, int targetH); |
||||
void update(float dt); |
||||
|
||||
// Master rendering routine
|
||||
void render(const Game& game, Synth& synth); |
||||
|
||||
// Spawn effects
|
||||
void spawnLineClearParticles(const Game& game); |
||||
void spawnLandDustParticles(int gridX, int gridY, PieceType type); |
||||
|
||||
// Screenshot API
|
||||
void takeScreenshot(const std::string& filename); |
||||
|
||||
private: |
||||
SDL_Renderer* mRenderer = nullptr; |
||||
SDL_Texture* mFontTexture = nullptr; |
||||
SDL_Surface* mFontSurface = nullptr; |
||||
SDL_Surface* mBackbufferSurface = nullptr; |
||||
SDL_Surface* mScaledSurface = nullptr; |
||||
SDL_Texture* mBackbufferTexture = nullptr; |
||||
|
||||
int mTargetW = 800; |
||||
int mTargetH = 600; |
||||
float mTime = 0.0f; |
||||
float mShakeIntensity = 0.0f; |
||||
|
||||
// Drifting starfield
|
||||
std::vector<Star> mStars; |
||||
std::vector<Particle> mParticles; |
||||
|
||||
void initStars(); |
||||
void buildFontTexture(); |
||||
|
||||
// Drawing helpers
|
||||
void drawText(const std::string& text, int x, int y, int scale, SDL_Color color, bool center = false, bool glow = false); |
||||
void drawBlock(int gridX, int gridY, PieceType type, bool isGhost = false, float alphaOverride = 1.0f); |
||||
void drawGrid(const Game& game); |
||||
void drawUI(const Game& game); |
||||
void drawVisualizer(Synth& synth, int x, int y, int w, int h); |
||||
|
||||
// Helpers
|
||||
SDL_Color getPieceColor(PieceType type); |
||||
}; |
||||
@ -0,0 +1,802 @@
|
||||
#include "Synth.hpp" |
||||
#include <cmath> |
||||
#include <cstdlib> |
||||
#include <algorithm> |
||||
|
||||
#ifndef M_PI |
||||
#define M_PI 3.14159265358979323846 |
||||
#endif |
||||
|
||||
// Midi note number to frequency conversion helper
|
||||
static inline float midiToFreq(int note) { |
||||
if (note <= 0) return 0.0f; |
||||
return 440.0f * std::pow(2.0f, (note - 69.0f) / 12.0f); |
||||
} |
||||
|
||||
Synth::Synth() { |
||||
for (int i = 0; i < VIS_BUFFER_SIZE; ++i) { |
||||
mVisBuffer[i] = 0.0f; |
||||
} |
||||
initSequencer(); |
||||
} |
||||
|
||||
Synth::~Synth() { |
||||
if (mAudioDevice > 0) { |
||||
SDL_CloseAudioDevice(mAudioDevice); |
||||
} |
||||
} |
||||
|
||||
bool Synth::init() { |
||||
SDL_AudioSpec wanted, obtained; |
||||
SDL_zero(wanted); |
||||
wanted.freq = 44100; |
||||
wanted.format = AUDIO_S16SYS; // Signed 16-bit, native endian
|
||||
wanted.channels = 2; // Stereo
|
||||
#ifdef MIYOO_BUILD |
||||
wanted.samples = 1024; // Larger buffer size to satisfy Miyoo MI_AO requirements
|
||||
#else |
||||
wanted.samples = 256; // Ultra low latency
|
||||
#endif |
||||
wanted.callback = &Synth::audioCallbackWrapper; |
||||
wanted.userdata = this; |
||||
|
||||
mAudioDevice = SDL_OpenAudioDevice(nullptr, 0, &wanted, &obtained, 0); |
||||
if (mAudioDevice == 0) { |
||||
return false; |
||||
} |
||||
|
||||
// Start playing silence initially
|
||||
SDL_PauseAudioDevice(mAudioDevice, 0); |
||||
return true; |
||||
} |
||||
|
||||
void Synth::playBGM(bool play) { |
||||
std::lock_guard<std::mutex> lock(mMutex); |
||||
mBGMEnabled = play; |
||||
if (!play) { |
||||
// Stop music voices (0, 1, 2, 3)
|
||||
for (int i = 0; i < 4; ++i) { |
||||
mVoices[i].active = false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
void Synth::stopAllSFX() { |
||||
std::lock_guard<std::mutex> lock(mMutex); |
||||
for (int i = 4; i < NUM_VOICES; ++i) { |
||||
mVoices[i].active = false; |
||||
} |
||||
} |
||||
|
||||
void Synth::shufflePlaylist() { |
||||
mPlaylist[0] = 0; // Always anchor with original theme
|
||||
int variants[] = {1, 2, 3, 4, 5}; |
||||
std::random_shuffle(std::begin(variants), std::end(variants)); |
||||
mPlaylist[1] = variants[0]; |
||||
mPlaylist[2] = variants[1]; |
||||
mPlaylist[3] = variants[2]; |
||||
} |
||||
|
||||
void Synth::initSequencer() { |
||||
mSecsPerTick = 60.0f / (mBPM * 2.0f); |
||||
mTickTimer = 0.0f; |
||||
mCurrentTick = 0; |
||||
|
||||
for (int p = 0; p < NUM_PATTERNS; ++p) { |
||||
for (int i = 0; i < 64; ++i) { |
||||
mPatternLead[p][i] = 0; |
||||
mPatternBass[p][i] = 0; |
||||
} |
||||
for (int b = 0; b < 8; ++b) { |
||||
mPatternChords[p][b] = ChordType::AM; |
||||
} |
||||
} |
||||
|
||||
// PATTERN 0: Original Korobeiniki
|
||||
mPatternLead[0][0] = 76; mPatternLead[0][1] = 76; mPatternLead[0][2] = 71; mPatternLead[0][3] = 72; |
||||
mPatternLead[0][4] = 74; mPatternLead[0][5] = 74; mPatternLead[0][6] = 72; mPatternLead[0][7] = 71; |
||||
mPatternLead[0][8] = 69; mPatternLead[0][9] = 69; mPatternLead[0][10] = 69; mPatternLead[0][11] = 72; |
||||
mPatternLead[0][12] = 76; mPatternLead[0][13] = 76; mPatternLead[0][14] = 74; mPatternLead[0][15] = 72; |
||||
mPatternLead[0][16] = 71; mPatternLead[0][17] = 71; mPatternLead[0][18] = 71; mPatternLead[0][19] = 72; |
||||
mPatternLead[0][20] = 74; mPatternLead[0][21] = 74; mPatternLead[0][22] = 76; mPatternLead[0][23] = 76; |
||||
mPatternLead[0][24] = 72; mPatternLead[0][25] = 72; mPatternLead[0][26] = 69; mPatternLead[0][27] = 69; |
||||
mPatternLead[0][28] = 69; |
||||
mPatternLead[0][32] = 74; mPatternLead[0][33] = 74; mPatternLead[0][34] = 74; mPatternLead[0][35] = 77; |
||||
mPatternLead[0][36] = 81; mPatternLead[0][37] = 81; mPatternLead[0][38] = 79; mPatternLead[0][39] = 77; |
||||
mPatternLead[0][40] = 76; mPatternLead[0][41] = 76; mPatternLead[0][42] = 72; mPatternLead[0][43] = 72; |
||||
mPatternLead[0][44] = 76; mPatternLead[0][45] = 76; mPatternLead[0][46] = 74; mPatternLead[0][47] = 72; |
||||
mPatternLead[0][48] = 71; mPatternLead[0][49] = 71; mPatternLead[0][50] = 71; mPatternLead[0][51] = 72; |
||||
mPatternLead[0][52] = 74; mPatternLead[0][53] = 74; mPatternLead[0][54] = 76; mPatternLead[0][55] = 76; |
||||
mPatternLead[0][56] = 72; mPatternLead[0][57] = 72; mPatternLead[0][58] = 69; mPatternLead[0][59] = 69; |
||||
mPatternLead[0][60] = 69; |
||||
|
||||
mPatternBass[0][0] = 45; mPatternBass[0][2] = 57; mPatternBass[0][4] = 45; mPatternBass[0][6] = 57; |
||||
mPatternBass[0][8] = 45; mPatternBass[0][10] = 57; mPatternBass[0][12] = 45; mPatternBass[0][14] = 57; |
||||
mPatternBass[0][16] = 40; mPatternBass[0][18] = 52; mPatternBass[0][20] = 40; mPatternBass[0][22] = 52; |
||||
mPatternBass[0][24] = 45; mPatternBass[0][26] = 57; mPatternBass[0][28] = 45; mPatternBass[0][30] = 45; |
||||
mPatternBass[0][32] = 50; mPatternBass[0][34] = 62; mPatternBass[0][36] = 50; mPatternBass[0][38] = 62; |
||||
mPatternBass[0][40] = 45; mPatternBass[0][42] = 57; mPatternBass[0][44] = 45; mPatternBass[0][46] = 57; |
||||
mPatternBass[0][48] = 40; mPatternBass[0][50] = 52; mPatternBass[0][52] = 40; mPatternBass[0][54] = 52; |
||||
mPatternBass[0][56] = 45; mPatternBass[0][58] = 57; mPatternBass[0][60] = 45; mPatternBass[0][62] = 45; |
||||
|
||||
mPatternChords[0][0] = ChordType::AM; mPatternChords[0][1] = ChordType::AM; |
||||
mPatternChords[0][2] = ChordType::E7; mPatternChords[0][3] = ChordType::AM; |
||||
mPatternChords[0][4] = ChordType::DM; mPatternChords[0][5] = ChordType::AM; |
||||
mPatternChords[0][6] = ChordType::E7; mPatternChords[0][7] = ChordType::AM; |
||||
|
||||
// PATTERN 1: Fast octave higher walking bass
|
||||
for(int i=0; i<64; ++i) { |
||||
if(mPatternLead[0][i] > 0) mPatternLead[1][i] = mPatternLead[0][i] + 12; |
||||
} |
||||
mPatternLead[1][32] = 86; mPatternLead[1][33] = 0; mPatternLead[1][34] = 86; mPatternLead[1][35] = 89; |
||||
mPatternLead[1][36] = 93; mPatternLead[1][37] = 0; mPatternLead[1][38] = 91; mPatternLead[1][39] = 89; |
||||
mPatternLead[1][40] = 88; mPatternLead[1][41] = 0; mPatternLead[1][42] = 84; mPatternLead[1][43] = 84; |
||||
mPatternLead[1][44] = 88; mPatternLead[1][45] = 0; mPatternLead[1][46] = 86; mPatternLead[1][47] = 84; |
||||
|
||||
mPatternBass[1][0] = 45; mPatternBass[1][2] = 57; mPatternBass[1][4] = 60; mPatternBass[1][6] = 57; |
||||
mPatternBass[1][8] = 45; mPatternBass[1][10] = 57; mPatternBass[1][12] = 60; mPatternBass[1][14] = 57; |
||||
mPatternBass[1][16] = 40; mPatternBass[1][18] = 52; mPatternBass[1][20] = 56; mPatternBass[1][22] = 52; |
||||
mPatternBass[1][24] = 45; mPatternBass[1][26] = 57; mPatternBass[1][28] = 45; mPatternBass[1][30] = 45; |
||||
mPatternBass[1][32] = 50; mPatternBass[1][34] = 62; mPatternBass[1][36] = 65; mPatternBass[1][38] = 62; |
||||
mPatternBass[1][40] = 45; mPatternBass[1][42] = 57; mPatternBass[1][44] = 60; mPatternBass[1][46] = 57; |
||||
mPatternBass[1][48] = 40; mPatternBass[1][50] = 52; mPatternBass[1][52] = 56; mPatternBass[1][54] = 52; |
||||
mPatternBass[1][56] = 45; mPatternBass[1][58] = 57; mPatternBass[1][60] = 45; mPatternBass[1][62] = 45; |
||||
|
||||
for(int i=0; i<8; ++i) mPatternChords[1][i] = mPatternChords[0][i]; |
||||
|
||||
// PATTERN 2: Bridge Motif
|
||||
mPatternLead[2][0] = 81; mPatternLead[2][2] = 84; mPatternLead[2][4] = 89; mPatternLead[2][6] = 84; |
||||
mPatternBass[2][0] = 41; mPatternBass[2][2] = 53; mPatternBass[2][4] = 41; mPatternBass[2][6] = 53; |
||||
|
||||
mPatternLead[2][8] = 79; mPatternLead[2][10] = 83; mPatternLead[2][12] = 86; mPatternLead[2][14] = 83; |
||||
mPatternBass[2][8] = 43; mPatternBass[2][10] = 55; mPatternBass[2][12] = 43; mPatternBass[2][14] = 55; |
||||
|
||||
mPatternLead[2][16] = 84; mPatternLead[2][18] = 88; mPatternLead[2][20] = 91; mPatternLead[2][22] = 88; |
||||
mPatternBass[2][16] = 48; mPatternBass[2][18] = 60; mPatternBass[2][20] = 48; mPatternBass[2][22] = 60; |
||||
|
||||
mPatternLead[2][24] = 81; mPatternLead[2][26] = 84; mPatternLead[2][28] = 88; mPatternLead[2][30] = 84; |
||||
mPatternBass[2][24] = 45; mPatternBass[2][26] = 57; mPatternBass[2][28] = 45; mPatternBass[2][30] = 57; |
||||
|
||||
mPatternLead[2][32] = 89; mPatternLead[2][34] = 86; mPatternLead[2][36] = 81; mPatternLead[2][38] = 86; |
||||
mPatternBass[2][32] = 50; mPatternBass[2][34] = 62; mPatternBass[2][36] = 50; mPatternBass[2][38] = 62; |
||||
|
||||
mPatternLead[2][40] = 88; mPatternLead[2][42] = 84; mPatternLead[2][44] = 81; mPatternLead[2][46] = 84; |
||||
mPatternBass[2][40] = 45; mPatternBass[2][42] = 57; mPatternBass[2][44] = 45; mPatternBass[2][46] = 57; |
||||
|
||||
mPatternLead[2][48] = 88; mPatternLead[2][50] = 86; mPatternLead[2][52] = 83; mPatternLead[2][54] = 80; |
||||
mPatternBass[2][48] = 40; mPatternBass[2][50] = 52; mPatternBass[2][52] = 40; mPatternBass[2][54] = 52; |
||||
|
||||
mPatternLead[2][56] = 81; mPatternLead[2][57] = 81; mPatternLead[2][58] = 81; |
||||
mPatternBass[2][56] = 45; mPatternBass[2][58] = 45; mPatternBass[2][60] = 45; |
||||
|
||||
mPatternChords[2][0] = ChordType::F; mPatternChords[2][1] = ChordType::G; |
||||
mPatternChords[2][2] = ChordType::C; mPatternChords[2][3] = ChordType::AM; |
||||
mPatternChords[2][4] = ChordType::DM; mPatternChords[2][5] = ChordType::AM; |
||||
mPatternChords[2][6] = ChordType::E7; mPatternChords[2][7] = ChordType::AM; |
||||
|
||||
// PATTERN 3: Driving Resolution
|
||||
mPatternLead[3][0] = 81; mPatternLead[3][1] = 79; mPatternLead[3][2] = 77; mPatternLead[3][3] = 81; |
||||
mPatternLead[3][4] = 84; mPatternLead[3][5] = 81; mPatternLead[3][6] = 77; mPatternLead[3][7] = 81; |
||||
mPatternBass[3][0] = 41; mPatternBass[3][2] = 53; mPatternBass[3][4] = 57; mPatternBass[3][6] = 53; |
||||
|
||||
mPatternLead[3][8] = 83; mPatternLead[3][9] = 81; mPatternLead[3][10] = 79; mPatternLead[3][11] = 83; |
||||
mPatternLead[3][12] = 86; mPatternLead[3][13] = 83; mPatternLead[3][14] = 79; mPatternLead[3][15] = 83; |
||||
mPatternBass[3][8] = 43; mPatternBass[3][10] = 55; mPatternBass[3][12] = 59; mPatternBass[3][14] = 55; |
||||
|
||||
mPatternLead[3][16] = 84; mPatternLead[3][17] = 83; mPatternLead[3][18] = 84; mPatternLead[3][19] = 86; |
||||
mPatternLead[3][20] = 88; mPatternLead[3][21] = 86; mPatternLead[3][22] = 84; mPatternLead[3][23] = 88; |
||||
mPatternBass[3][16] = 48; mPatternBass[3][18] = 60; mPatternBass[3][20] = 64; mPatternBass[3][22] = 60; |
||||
|
||||
mPatternLead[3][24] = 81; mPatternLead[3][25] = 80; mPatternLead[3][26] = 81; mPatternLead[3][27] = 83; |
||||
mPatternLead[3][28] = 84; mPatternLead[3][29] = 83; mPatternLead[3][30] = 81; mPatternLead[3][31] = 84; |
||||
mPatternBass[3][24] = 45; mPatternBass[3][26] = 57; mPatternBass[3][28] = 60; mPatternBass[3][30] = 57; |
||||
|
||||
mPatternLead[3][32] = 86; mPatternLead[3][33] = 84; mPatternLead[3][34] = 86; mPatternLead[3][35] = 88; |
||||
mPatternLead[3][36] = 89; mPatternLead[3][37] = 88; mPatternLead[3][38] = 86; mPatternLead[3][39] = 89; |
||||
mPatternBass[3][32] = 50; mPatternBass[3][34] = 62; mPatternBass[3][36] = 65; mPatternBass[3][38] = 62; |
||||
|
||||
mPatternLead[3][40] = 88; mPatternLead[3][41] = 86; mPatternLead[3][42] = 84; mPatternLead[3][43] = 83; |
||||
mPatternLead[3][44] = 81; mPatternLead[3][45] = 84; mPatternLead[3][46] = 88; mPatternLead[3][47] = 84; |
||||
mPatternBass[3][40] = 45; mPatternBass[3][42] = 57; mPatternBass[3][44] = 60; mPatternBass[3][46] = 57; |
||||
|
||||
mPatternLead[3][48] = 83; mPatternLead[3][49] = 81; mPatternLead[3][50] = 80; mPatternLead[3][51] = 81; |
||||
mPatternLead[3][52] = 83; mPatternLead[3][53] = 84; mPatternLead[3][54] = 86; mPatternLead[3][55] = 83; |
||||
mPatternBass[3][48] = 40; mPatternBass[3][50] = 52; mPatternBass[3][52] = 56; mPatternBass[3][54] = 52; |
||||
|
||||
mPatternLead[3][56] = 81; mPatternLead[3][58] = 81; mPatternLead[3][60] = 69; |
||||
mPatternBass[3][56] = 45; mPatternBass[3][58] = 45; mPatternBass[3][60] = 45; |
||||
|
||||
for(int i=0; i<8; ++i) mPatternChords[3][i] = mPatternChords[2][i]; |
||||
|
||||
// PATTERN 4: Slow menacing / half-time feel of original
|
||||
for(int i=0; i<8; ++i) mPatternChords[4][i] = mPatternChords[0][i]; |
||||
mPatternLead[4][0] = 64; mPatternLead[4][4] = 59; mPatternLead[4][8] = 60; mPatternLead[4][12] = 62; |
||||
mPatternLead[4][16] = 59; mPatternLead[4][20] = 60; mPatternLead[4][24] = 57; mPatternLead[4][28] = 57; |
||||
mPatternLead[4][32] = 62; mPatternLead[4][36] = 65; mPatternLead[4][40] = 64; mPatternLead[4][44] = 60; |
||||
mPatternLead[4][48] = 59; mPatternLead[4][52] = 60; mPatternLead[4][56] = 57; mPatternLead[4][60] = 57; |
||||
|
||||
mPatternBass[4][0] = 33; mPatternBass[4][8] = 33; |
||||
mPatternBass[4][16] = 28; mPatternBass[4][24] = 33; |
||||
mPatternBass[4][32] = 38; mPatternBass[4][40] = 33; |
||||
mPatternBass[4][48] = 28; mPatternBass[4][56] = 33; |
||||
|
||||
// PATTERN 5: Frantic Arpeggiator (uses Bridge chords)
|
||||
for(int i=0; i<8; ++i) mPatternChords[5][i] = mPatternChords[2][i]; |
||||
for(int b=0; b<8; ++b) { |
||||
int root = 0; |
||||
switch(mPatternChords[5][b]) { |
||||
case ChordType::F: root = 77; break; |
||||
case ChordType::G: root = 79; break; |
||||
case ChordType::C: root = 84; break; |
||||
case ChordType::AM: root = 81; break; |
||||
case ChordType::DM: root = 86; break; |
||||
case ChordType::E7: root = 80; break; |
||||
} |
||||
for(int t=0; t<8; ++t) { |
||||
if(t%4 == 0) mPatternLead[5][b*8+t] = root; |
||||
else if(t%4 == 1) mPatternLead[5][b*8+t] = root+4;
|
||||
else if(t%4 == 2) mPatternLead[5][b*8+t] = root+7;
|
||||
else if(t%4 == 3) mPatternLead[5][b*8+t] = root+12;
|
||||
} |
||||
if(mPatternChords[5][b] == ChordType::AM || mPatternChords[5][b] == ChordType::DM) { |
||||
mPatternLead[5][b*8+1] -= 1; |
||||
mPatternLead[5][b*8+5] -= 1; |
||||
} |
||||
mPatternBass[5][b*8] = root - 36; |
||||
mPatternBass[5][b*8+2] = root - 24; |
||||
mPatternBass[5][b*8+4] = root - 36; |
||||
mPatternBass[5][b*8+6] = root - 24; |
||||
} |
||||
|
||||
shufflePlaylist(); |
||||
} |
||||
|
||||
void Synth::triggerMusicNote(int voiceIdx, uint8_t midiNote, WaveType wave, float duration, float pan, float volume) { |
||||
if (midiNote == 0) { |
||||
if (mVoices[voiceIdx].active && !mVoices[voiceIdx].releasing) { |
||||
mVoices[voiceIdx].releasing = true; |
||||
mVoices[voiceIdx].releaseAge = 0.0f; |
||||
} |
||||
return; |
||||
} |
||||
|
||||
Voice& v = mVoices[voiceIdx]; |
||||
v.active = true; |
||||
v.wave = wave; |
||||
v.frequency = midiToFreq(midiNote); |
||||
v.phase = 0.0f; |
||||
v.volume = volume; |
||||
v.pan = pan; |
||||
v.releasing = false; |
||||
v.age = 0.0f; |
||||
v.releaseAge = 0.0f; |
||||
v.sweepDuration = 0.0f; |
||||
|
||||
// Default music envelopes
|
||||
if (voiceIdx == 0) { // Lead
|
||||
v.adsr.attackTime = 0.005f; |
||||
v.adsr.decayTime = 0.03f; |
||||
v.adsr.sustainLevel = 0.6f; |
||||
v.adsr.releaseTime = duration * 0.4f; |
||||
|
||||
// Add a soft vibrato for the lead melody
|
||||
v.vibratoFreq = 6.0f; // 6 Hz LFO
|
||||
v.vibratoDepth = 0.008f; // ~0.8% frequency wobble
|
||||
} else if (voiceIdx == 1) { // Harmony
|
||||
v.adsr.attackTime = 0.01f; |
||||
v.adsr.decayTime = 0.05f; |
||||
v.adsr.sustainLevel = 0.5f; |
||||
v.adsr.releaseTime = duration * 0.3f; |
||||
v.vibratoDepth = 0.0f; |
||||
} else if (voiceIdx == 2) { // Bass
|
||||
v.adsr.attackTime = 0.005f; |
||||
v.adsr.decayTime = 0.04f; |
||||
v.adsr.sustainLevel = 0.7f; |
||||
v.adsr.releaseTime = duration * 0.2f; |
||||
v.vibratoDepth = 0.0f; |
||||
} |
||||
} |
||||
|
||||
void Synth::updateSequencer(float dt) { |
||||
if (!mBGMEnabled) return; |
||||
|
||||
mTickTimer += dt; |
||||
if (mTickTimer >= mSecsPerTick) { |
||||
mTickTimer -= mSecsPerTick; |
||||
|
||||
// Advance sequencer tick
|
||||
mCurrentTick = (mCurrentTick + 1) % 256; |
||||
if (mCurrentTick == 0) { |
||||
shufflePlaylist(); |
||||
} |
||||
|
||||
int patternIdx = mPlaylist[mCurrentTick / 64]; |
||||
int stepInPattern = mCurrentTick % 64; |
||||
|
||||
// 1. Trigger Lead Note (Voice 0)
|
||||
uint8_t leadNote = mPatternLead[patternIdx][stepInPattern]; |
||||
if (leadNote > 0) { |
||||
triggerMusicNote(0, leadNote, WaveType::SQUARE, mSecsPerTick, 0.35f, 0.12f); |
||||
} else { |
||||
if (mVoices[0].active && !mVoices[0].releasing) { |
||||
mVoices[0].releasing = true; |
||||
mVoices[0].releaseAge = 0.0f; |
||||
} |
||||
} |
||||
|
||||
// 2. Trigger Bass Note (Voice 2)
|
||||
uint8_t bassNote = mPatternBass[patternIdx][stepInPattern]; |
||||
if (bassNote > 0) { |
||||
triggerMusicNote(2, bassNote, WaveType::TRIANGLE, mSecsPerTick, 0.5f, 0.22f); |
||||
} else { |
||||
if (mVoices[2].active && !mVoices[2].releasing) { |
||||
mVoices[2].releasing = true; |
||||
mVoices[2].releaseAge = 0.0f; |
||||
} |
||||
} |
||||
|
||||
// 3. Trigger Harmony/Chord Arpeggio (Voice 1)
|
||||
int subTick = mCurrentTick % 4; // arpeggiate at 8th note speed
|
||||
uint8_t harmonyNote = 0; |
||||
|
||||
ChordType currentChord = mPatternChords[patternIdx][stepInPattern / 8]; |
||||
|
||||
if (currentChord == ChordType::F) { |
||||
uint8_t arpeggio[] = {53, 57, 60, 57}; // F3, A3, C4, A3
|
||||
harmonyNote = arpeggio[subTick]; |
||||
} else if (currentChord == ChordType::G) { |
||||
uint8_t arpeggio[] = {55, 59, 62, 59}; // G3, B3, D4, B3
|
||||
harmonyNote = arpeggio[subTick]; |
||||
} else if (currentChord == ChordType::C) { |
||||
uint8_t arpeggio[] = {48, 55, 60, 55}; // C3, G3, C4, G3
|
||||
harmonyNote = arpeggio[subTick]; |
||||
} else if (currentChord == ChordType::DM) { |
||||
uint8_t arpeggio[] = {57, 62, 65, 62}; // A3, D4, F4, D4
|
||||
harmonyNote = arpeggio[subTick]; |
||||
} else if (currentChord == ChordType::E7) { |
||||
uint8_t arpeggio[] = {56, 59, 64, 59}; // G#3, B3, E4, B3
|
||||
harmonyNote = arpeggio[subTick]; |
||||
} else { // AM
|
||||
uint8_t arpeggio[] = {57, 60, 64, 60}; // A3, C4, E4, C4
|
||||
harmonyNote = arpeggio[subTick]; |
||||
} |
||||
|
||||
triggerMusicNote(1, harmonyNote, WaveType::TRIANGLE, mSecsPerTick * 0.9f, 0.65f, 0.08f); |
||||
|
||||
// 4. Trigger Procedural Retro Drums (Voice 3) - Kick, Hat, Snare synthesis!
|
||||
int drumTick = mCurrentTick % 8; |
||||
if (drumTick == 0) { // Kick drum: fast triangle frequency sweep (A1 55Hz to A0 27Hz)
|
||||
Voice& v = mVoices[3]; |
||||
v.active = true; |
||||
v.wave = WaveType::TRIANGLE; |
||||
v.frequency = 120.0f; |
||||
v.startFreq = 120.0f; |
||||
v.targetFreq = 30.0f; |
||||
v.sweepDuration = 0.09f; |
||||
v.sweepAge = 0.0f; |
||||
v.phase = 0.0f; |
||||
v.volume = 0.45f; |
||||
v.pan = 0.5f; |
||||
v.releasing = false; |
||||
v.age = 0.0f; |
||||
v.releaseAge = 0.0f; |
||||
v.vibratoDepth = 0.0f; |
||||
v.adsr.attackTime = 0.002f; |
||||
v.adsr.decayTime = 0.08f; |
||||
v.adsr.sustainLevel = 0.1f; |
||||
v.adsr.releaseTime = 0.05f; |
||||
} else if (drumTick == 4) { // Snare drum: White noise burst + Triangle pop
|
||||
Voice& v = mVoices[3]; |
||||
v.active = true; |
||||
v.wave = WaveType::NOISE; |
||||
v.frequency = 100.0f; // dummy frequency
|
||||
v.phase = 0.0f; |
||||
v.volume = 0.26f; |
||||
v.pan = 0.5f; |
||||
v.releasing = false; |
||||
v.age = 0.0f; |
||||
v.releaseAge = 0.0f; |
||||
v.vibratoDepth = 0.0f; |
||||
v.sweepDuration = 0.0f; |
||||
v.adsr.attackTime = 0.001f; |
||||
v.adsr.decayTime = 0.12f; |
||||
v.adsr.sustainLevel = 0.05f; |
||||
v.adsr.releaseTime = 0.04f; |
||||
} else if (drumTick == 2 || drumTick == 6 || drumTick == 7) { // Hi-Hat: very fast noise pop
|
||||
Voice& v = mVoices[3]; |
||||
v.active = true; |
||||
v.wave = WaveType::NOISE; |
||||
v.frequency = 100.0f; // dummy
|
||||
v.phase = 0.0f; |
||||
v.volume = 0.09f; |
||||
v.pan = 0.55f; |
||||
v.releasing = false; |
||||
v.age = 0.0f; |
||||
v.releaseAge = 0.0f; |
||||
v.vibratoDepth = 0.0f; |
||||
v.sweepDuration = 0.0f; |
||||
v.adsr.attackTime = 0.001f; |
||||
v.adsr.decayTime = 0.025f; |
||||
v.adsr.sustainLevel = 0.0f; |
||||
v.adsr.releaseTime = 0.015f; |
||||
} |
||||
} |
||||
} |
||||
|
||||
void Synth::triggerSFX(SFXType type) { |
||||
std::lock_guard<std::mutex> lock(mMutex); |
||||
|
||||
// Choose one of the dedicated SFX voices (Voices 4, 5, 6, 7) based on sound type
|
||||
// to prevent overlap truncation.
|
||||
int voiceIdx = 4; |
||||
Voice* v = &mVoices[voiceIdx]; |
||||
|
||||
switch (type) { |
||||
case SFXType::MOVE: // Voice 4: quick, quiet triangle pluck
|
||||
v->active = true; |
||||
v->wave = WaveType::TRIANGLE; |
||||
v->frequency = 180.0f; |
||||
v->phase = 0.0f; |
||||
v->volume = 0.22f; |
||||
v->pan = 0.45f; |
||||
v->releasing = false; |
||||
v->age = 0.0f; |
||||
v->releaseAge = 0.0f; |
||||
v->sweepDuration = 0.0f; |
||||
v->vibratoDepth = 0.0f; |
||||
v->adsr.attackTime = 0.001f; |
||||
v->adsr.decayTime = 0.04f; |
||||
v->adsr.sustainLevel = 0.0f; |
||||
v->adsr.releaseTime = 0.02f; |
||||
break; |
||||
|
||||
case SFXType::ROTATE: // Voice 5: quick laser frequency sweep
|
||||
voiceIdx = 5; |
||||
v = &mVoices[voiceIdx]; |
||||
v->active = true; |
||||
v->wave = WaveType::SQUARE; |
||||
v->frequency = 330.0f; |
||||
v->startFreq = 330.0f; |
||||
v->targetFreq = 660.0f; |
||||
v->sweepDuration = 0.08f; |
||||
v->sweepAge = 0.0f; |
||||
v->phase = 0.0f; |
||||
v->volume = 0.07f; |
||||
v->pan = 0.55f; |
||||
v->releasing = false; |
||||
v->age = 0.0f; |
||||
v->releaseAge = 0.0f; |
||||
v->vibratoDepth = 0.0f; |
||||
v->adsr.attackTime = 0.002f; |
||||
v->adsr.decayTime = 0.06f; |
||||
v->adsr.sustainLevel = 0.0f; |
||||
v->adsr.releaseTime = 0.03f; |
||||
break; |
||||
|
||||
case SFXType::LAND: // Voice 4: sub-bass thud (sine decay)
|
||||
v->active = true; |
||||
v->wave = WaveType::SINE; |
||||
v->frequency = 90.0f; |
||||
v->startFreq = 90.0f; |
||||
v->targetFreq = 45.0f; |
||||
v->sweepDuration = 0.08f; |
||||
v->sweepAge = 0.0f; |
||||
v->phase = 0.0f; |
||||
v->volume = 0.35f; |
||||
v->pan = 0.5f; |
||||
v->releasing = false; |
||||
v->age = 0.0f; |
||||
v->releaseAge = 0.0f; |
||||
v->vibratoDepth = 0.0f; |
||||
v->adsr.attackTime = 0.003f; |
||||
v->adsr.decayTime = 0.10f; |
||||
v->adsr.sustainLevel = 0.0f; |
||||
v->adsr.releaseTime = 0.04f; |
||||
break; |
||||
|
||||
case SFXType::LINE_CLEAR: // Voice 6: rapid, bright ascending arpeggio (C5-E5-G5-C6)
|
||||
voiceIdx = 6; |
||||
v = &mVoices[voiceIdx]; |
||||
v->active = true; |
||||
v->wave = WaveType::SQUARE; |
||||
v->frequency = midiToFreq(72); // C5
|
||||
v->startFreq = midiToFreq(72); |
||||
v->targetFreq = midiToFreq(84); // sweeps to C6
|
||||
v->sweepDuration = 0.22f; |
||||
v->sweepAge = 0.0f; |
||||
v->phase = 0.0f; |
||||
v->volume = 0.11f; |
||||
v->pan = 0.5f; |
||||
v->releasing = false; |
||||
v->age = 0.0f; |
||||
v->releaseAge = 0.0f; |
||||
v->vibratoDepth = 0.0f; |
||||
v->adsr.attackTime = 0.005f; |
||||
v->adsr.decayTime = 0.18f; |
||||
v->adsr.sustainLevel = 0.0f; |
||||
v->adsr.releaseTime = 0.10f; |
||||
break; |
||||
|
||||
case SFXType::TETRIS_CLEAR: // Voice 6: dramatic multi-freq sweep + noise chord
|
||||
voiceIdx = 6; |
||||
v = &mVoices[voiceIdx]; |
||||
v->active = true; |
||||
v->wave = WaveType::SQUARE; |
||||
v->frequency = midiToFreq(72); // C5
|
||||
v->startFreq = midiToFreq(72); |
||||
v->targetFreq = midiToFreq(96); // sweeps high! C7
|
||||
v->sweepDuration = 0.40f; |
||||
v->sweepAge = 0.0f; |
||||
v->phase = 0.0f; |
||||
v->volume = 0.13f; |
||||
v->pan = 0.5f; |
||||
v->releasing = false; |
||||
v->age = 0.0f; |
||||
v->releaseAge = 0.0f; |
||||
v->vibratoDepth = 0.015f; // extreme vibrato!
|
||||
v->vibratoFreq = 12.0f; |
||||
v->adsr.attackTime = 0.01f; |
||||
v->adsr.decayTime = 0.35f; |
||||
v->adsr.sustainLevel = 0.0f; |
||||
v->adsr.releaseTime = 0.20f; |
||||
|
||||
// Trigger extra white noise burst on SFX voice 7 for explosive impact
|
||||
v = &mVoices[7]; |
||||
v->active = true; |
||||
v->wave = WaveType::NOISE; |
||||
v->phase = 0.0f; |
||||
v->volume = 0.22f; |
||||
v->pan = 0.5f; |
||||
v->releasing = false; |
||||
v->age = 0.0f; |
||||
v->releaseAge = 0.0f; |
||||
v->sweepDuration = 0.0f; |
||||
v->vibratoDepth = 0.0f; |
||||
v->adsr.attackTime = 0.005f; |
||||
v->adsr.decayTime = 0.30f; |
||||
v->adsr.sustainLevel = 0.0f; |
||||
v->adsr.releaseTime = 0.15f; |
||||
break; |
||||
|
||||
case SFXType::LEVEL_UP: // Voice 6: triumphant two-note chord fanfares
|
||||
voiceIdx = 6; |
||||
v = &mVoices[voiceIdx]; |
||||
v->active = true; |
||||
v->wave = WaveType::SQUARE; |
||||
v->frequency = midiToFreq(76); // E5
|
||||
v->startFreq = midiToFreq(76); |
||||
v->targetFreq = midiToFreq(88); // Sweeps to E6
|
||||
v->sweepDuration = 0.30f; |
||||
v->sweepAge = 0.0f; |
||||
v->phase = 0.0f; |
||||
v->volume = 0.11f; |
||||
v->pan = 0.40f; |
||||
v->releasing = false; |
||||
v->age = 0.0f; |
||||
v->releaseAge = 0.0f; |
||||
v->vibratoDepth = 0.0f; |
||||
v->adsr.attackTime = 0.005f; |
||||
v->adsr.decayTime = 0.20f; |
||||
v->adsr.sustainLevel = 0.0f; |
||||
v->adsr.releaseTime = 0.15f; |
||||
|
||||
// Trigger third note harmony on voice 7
|
||||
v = &mVoices[7]; |
||||
v->active = true; |
||||
v->wave = WaveType::SQUARE; |
||||
v->frequency = midiToFreq(80); // G#5
|
||||
v->startFreq = midiToFreq(80); |
||||
v->targetFreq = midiToFreq(92); // Sweeps to G#6
|
||||
v->sweepDuration = 0.30f; |
||||
v->sweepAge = 0.0f; |
||||
v->phase = 0.0f; |
||||
v->volume = 0.10f; |
||||
v->pan = 0.60f; |
||||
v->releasing = false; |
||||
v->age = 0.0f; |
||||
v->releaseAge = 0.0f; |
||||
v->vibratoDepth = 0.0f; |
||||
v->adsr.attackTime = 0.005f; |
||||
v->adsr.decayTime = 0.20f; |
||||
v->adsr.sustainLevel = 0.0f; |
||||
v->adsr.releaseTime = 0.15f; |
||||
break; |
||||
|
||||
case SFXType::GAME_OVER: // Voice 6: sad, sliding down detuned chord
|
||||
voiceIdx = 6; |
||||
v = &mVoices[voiceIdx]; |
||||
v->active = true; |
||||
v->wave = WaveType::TRIANGLE; |
||||
v->frequency = 220.0f; |
||||
v->startFreq = 220.0f; |
||||
v->targetFreq = 80.0f; // sweep down
|
||||
v->sweepDuration = 0.80f; |
||||
v->sweepAge = 0.0f; |
||||
v->phase = 0.0f; |
||||
v->volume = 0.35f; |
||||
v->pan = 0.40f; |
||||
v->releasing = false; |
||||
v->age = 0.0f; |
||||
v->releaseAge = 0.0f; |
||||
v->vibratoDepth = 0.02f; // highly detuned detuning wobble
|
||||
v->vibratoFreq = 8.0f; |
||||
v->adsr.attackTime = 0.01f; |
||||
v->adsr.decayTime = 0.60f; |
||||
v->adsr.sustainLevel = 0.2f; // Game Over chord detunes and sustains nicely!
|
||||
v->adsr.releaseTime = 0.40f; |
||||
|
||||
// Voice 7 plays detuned counter note
|
||||
v = &mVoices[7]; |
||||
v->active = true; |
||||
v->wave = WaveType::SQUARE; |
||||
v->frequency = 233.0f; // slightly detuned semitone above
|
||||
v->startFreq = 233.0f; |
||||
v->targetFreq = 83.0f; // sweep down
|
||||
v->sweepDuration = 0.80f; |
||||
v->sweepAge = 0.0f; |
||||
v->phase = 0.0f; |
||||
v->volume = 0.08f; |
||||
v->pan = 0.60f; |
||||
v->releasing = false; |
||||
v->age = 0.0f; |
||||
v->releaseAge = 0.0f; |
||||
v->vibratoDepth = 0.0f; |
||||
v->adsr.attackTime = 0.01f; |
||||
v->adsr.decayTime = 0.60f; |
||||
v->adsr.sustainLevel = 0.2f; |
||||
v->adsr.releaseTime = 0.40f; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
void Synth::updateVoice(Voice& voice, float dt) { |
||||
if (!voice.active) return; |
||||
|
||||
if (voice.releasing) { |
||||
voice.releaseAge += dt; |
||||
if (voice.releaseAge >= voice.adsr.releaseTime) { |
||||
voice.active = false; |
||||
return; |
||||
} |
||||
} else { |
||||
voice.age += dt; |
||||
if (voice.adsr.sustainLevel == 0.0f && voice.age >= voice.adsr.attackTime + voice.adsr.decayTime) { |
||||
voice.active = false; |
||||
return; |
||||
} |
||||
} |
||||
|
||||
// Apply frequency sweeps if active
|
||||
if (voice.sweepDuration > 0.0f) { |
||||
voice.sweepAge += dt; |
||||
if (voice.sweepAge >= voice.sweepDuration) { |
||||
voice.frequency = voice.targetFreq; |
||||
voice.sweepDuration = 0.0f; |
||||
} else { |
||||
float t = voice.sweepAge / voice.sweepDuration; |
||||
voice.frequency = voice.startFreq + (voice.targetFreq - voice.startFreq) * t; |
||||
} |
||||
} |
||||
} |
||||
|
||||
float Synth::generateSample(Voice& voice) { |
||||
if (!voice.active) return 0.0f; |
||||
|
||||
// Calculate ADSR envelope volume multiplier
|
||||
float envVolume = 0.0f; |
||||
if (voice.releasing) { |
||||
if (voice.releaseAge < voice.adsr.releaseTime) { |
||||
float progress = voice.releaseAge / voice.adsr.releaseTime; |
||||
envVolume = voice.adsr.sustainLevel * (1.0f - progress); |
||||
} |
||||
} else { |
||||
float age = voice.age; |
||||
if (age < voice.adsr.attackTime) { |
||||
envVolume = age / voice.adsr.attackTime; |
||||
} else if (age < voice.adsr.attackTime + voice.adsr.decayTime) { |
||||
float progress = (age - voice.adsr.attackTime) / voice.adsr.decayTime; |
||||
envVolume = 1.0f - (1.0f - voice.adsr.sustainLevel) * progress; |
||||
} else { |
||||
envVolume = voice.adsr.sustainLevel; |
||||
} |
||||
} |
||||
|
||||
if (envVolume <= 0.0f) { |
||||
return 0.0f; |
||||
} |
||||
|
||||
// Apply vibrato modulation
|
||||
float modulatedFreq = voice.frequency; |
||||
if (voice.vibratoDepth > 0.0f) { |
||||
float lfo = std::sin(voice.age * 2.0f * M_PI * voice.vibratoFreq); |
||||
modulatedFreq += lfo * voice.vibratoDepth * voice.frequency; |
||||
} |
||||
|
||||
// Phase increment
|
||||
float phaseInc = 2.0f * M_PI * modulatedFreq / 44100.0f; |
||||
voice.phase += phaseInc; |
||||
if (voice.phase >= 2.0f * M_PI) { |
||||
voice.phase -= 2.0f * M_PI; |
||||
} |
||||
|
||||
// Oscillator waveform generation
|
||||
float oscValue = 0.0f; |
||||
switch (voice.wave) { |
||||
case WaveType::SINE: |
||||
oscValue = std::sin(voice.phase); |
||||
break; |
||||
case WaveType::SQUARE: |
||||
// Custom pulse width modulation (50% standard duty cycle, slightly narrow)
|
||||
oscValue = (std::sin(voice.phase) >= 0.1f ? 1.0f : -1.0f); |
||||
break; |
||||
case WaveType::TRIANGLE: { |
||||
float normalizedPhase = voice.phase / (2.0f * M_PI); |
||||
oscValue = 4.0f * std::abs(normalizedPhase - std::floor(normalizedPhase + 0.5f)) - 1.0f; |
||||
break; |
||||
} |
||||
case WaveType::NOISE: |
||||
oscValue = (((float)std::rand() / RAND_MAX) * 2.0f - 1.0f); |
||||
break; |
||||
} |
||||
|
||||
return oscValue * voice.volume * envVolume; |
||||
} |
||||
|
||||
void Synth::audioCallback(Uint8* stream, int len) { |
||||
int frames = len / 4; // 16-bit signed stereo = 4 bytes per frame
|
||||
int16_t* out = (int16_t*)stream; |
||||
|
||||
float dt = 1.0f / 44100.0f; |
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex); |
||||
|
||||
for (int f = 0; f < frames; ++f) { |
||||
// Advance chiptune sequencer clock
|
||||
updateSequencer(dt); |
||||
|
||||
float leftMixed = 0.0f; |
||||
float rightMixed = 0.0f; |
||||
|
||||
// Render each voice and mix with panning
|
||||
for (int i = 0; i < NUM_VOICES; ++i) { |
||||
if (!mVoices[i].active) continue; |
||||
|
||||
// Render and update phase/envelope
|
||||
float voiceSample = generateSample(mVoices[i]); |
||||
updateVoice(mVoices[i], dt); |
||||
|
||||
// Stereo panning
|
||||
leftMixed += voiceSample * (1.0f - mVoices[i].pan); |
||||
rightMixed += voiceSample * mVoices[i].pan; |
||||
} |
||||
|
||||
// Clamp mixed signals to prevent audio clipping
|
||||
leftMixed = std::clamp(leftMixed, -1.0f, 1.0f); |
||||
rightMixed = std::clamp(rightMixed, -1.0f, 1.0f); |
||||
|
||||
// Convert mixed floats back to 16-bit PCM samples
|
||||
out[f * 2] = (int16_t)(leftMixed * 32767.0f); |
||||
out[f * 2 + 1] = (int16_t)(rightMixed * 32767.0f); |
||||
|
||||
// Log sample into the visualizer ring buffer
|
||||
float avgSample = (leftMixed + rightMixed) * 0.5f; |
||||
mVisBuffer[mVisWritePos] = avgSample; |
||||
mVisWritePos = (mVisWritePos + 1) % VIS_BUFFER_SIZE; |
||||
} |
||||
} |
||||
|
||||
void Synth::audioCallbackWrapper(void* userdata, Uint8* stream, int len) { |
||||
((Synth*)userdata)->audioCallback(stream, len); |
||||
} |
||||
|
||||
std::vector<float> Synth::getVisualizerBuffer() { |
||||
std::lock_guard<std::mutex> lock(mMutex); |
||||
std::vector<float> res(VIS_BUFFER_SIZE); |
||||
|
||||
// Copy the ring buffer chronologically starting from the current write position
|
||||
for (int i = 0; i < VIS_BUFFER_SIZE; ++i) { |
||||
int idx = (mVisWritePos + i) % VIS_BUFFER_SIZE; |
||||
res[i] = mVisBuffer[idx]; |
||||
} |
||||
return res; |
||||
} |
||||
@ -0,0 +1,125 @@
|
||||
#pragma once |
||||
#include <SDL2/SDL.h> |
||||
#include <mutex> |
||||
#include <vector> |
||||
|
||||
enum class WaveType { |
||||
SINE, |
||||
SQUARE, |
||||
TRIANGLE, |
||||
NOISE |
||||
}; |
||||
|
||||
enum class ChordType { |
||||
AM, |
||||
E7, |
||||
DM, |
||||
F, |
||||
G, |
||||
C |
||||
}; |
||||
|
||||
struct Envelope { |
||||
float attackTime = 0.005f; // seconds
|
||||
float decayTime = 0.05f; // seconds
|
||||
float sustainLevel = 0.7f; // 0.0 to 1.0
|
||||
float releaseTime = 0.12f; // seconds
|
||||
}; |
||||
|
||||
struct Voice { |
||||
bool active = false; |
||||
WaveType wave = WaveType::SQUARE; |
||||
float frequency = 0.0f; |
||||
float phase = 0.0f; |
||||
float volume = 0.12f; |
||||
float pan = 0.5f; // 0.0 (left) to 1.0 (right)
|
||||
|
||||
// Envelope tracking
|
||||
Envelope adsr; |
||||
float age = 0.0f; // duration active (seconds)
|
||||
float releaseAge = 0.0f; // duration in release phase (seconds)
|
||||
bool releasing = false; |
||||
|
||||
// Frequency sweeps (for sound effects)
|
||||
float startFreq = 0.0f; |
||||
float targetFreq = 0.0f; |
||||
float sweepDuration = 0.0f; |
||||
float sweepAge = 0.0f; |
||||
|
||||
// Vibrato
|
||||
float vibratoFreq = 0.0f; // LFO speed (Hz)
|
||||
float vibratoDepth = 0.0f; // pitch variation depth
|
||||
}; |
||||
|
||||
enum class SFXType { |
||||
MOVE, |
||||
ROTATE, |
||||
LAND, |
||||
LINE_CLEAR, |
||||
TETRIS_CLEAR, |
||||
LEVEL_UP, |
||||
GAME_OVER |
||||
}; |
||||
|
||||
class Synth { |
||||
public: |
||||
Synth(); |
||||
~Synth(); |
||||
|
||||
bool init(); |
||||
void playBGM(bool play); |
||||
void triggerSFX(SFXType type); |
||||
void stopAllSFX(); |
||||
|
||||
// Thread-safe copy of visualizer buffer for the GPU renderer
|
||||
std::vector<float> getVisualizerBuffer(); |
||||
|
||||
// SDL Audio Callback
|
||||
void audioCallback(Uint8* stream, int len); |
||||
|
||||
private: |
||||
static void audioCallbackWrapper(void* userdata, Uint8* stream, int len); |
||||
|
||||
SDL_AudioDeviceID mAudioDevice = 0; |
||||
bool mBGMEnabled = false; |
||||
|
||||
std::mutex mMutex; |
||||
|
||||
// 8 Dedicated voices:
|
||||
// Voice 0: BGM Lead (Square)
|
||||
// Voice 1: BGM Harmony / Echo (Pulse)
|
||||
// Voice 2: BGM Bass (Triangle)
|
||||
// Voice 3: BGM Drums / Noise (Noise/Triangle)
|
||||
// Voice 4, 5, 6, 7: Dedicated SFX voices
|
||||
static constexpr int NUM_VOICES = 8; |
||||
Voice mVoices[NUM_VOICES]; |
||||
|
||||
// Sequencer state
|
||||
int mBPM = 138; |
||||
float mSecsPerTick = 0.0f; // Seconds per eighth note tick
|
||||
float mTickTimer = 0.0f; |
||||
int mCurrentTick = 0; |
||||
|
||||
// Pattern-based Sequencer
|
||||
static constexpr int NUM_PATTERNS = 6; |
||||
uint8_t mPatternLead[NUM_PATTERNS][64]; |
||||
uint8_t mPatternBass[NUM_PATTERNS][64]; |
||||
ChordType mPatternChords[NUM_PATTERNS][8]; // 8 bars per pattern
|
||||
|
||||
// Dynamic Playlist (4 patterns per cycle)
|
||||
int mPlaylist[4]; |
||||
void shufflePlaylist(); |
||||
|
||||
// Visualizer Oscilloscope Ring Buffer
|
||||
static constexpr int VIS_BUFFER_SIZE = 512; |
||||
float mVisBuffer[VIS_BUFFER_SIZE]; |
||||
int mVisWritePos = 0; |
||||
|
||||
void initSequencer(); |
||||
void updateSequencer(float dt); |
||||
void updateVoice(Voice& voice, float dt); |
||||
float generateSample(Voice& voice); |
||||
|
||||
// Helper to start BGM notes
|
||||
void triggerMusicNote(int voiceIdx, uint8_t midiNote, WaveType wave, float duration, float pan = 0.5f, float volume = 0.1f); |
||||
}; |
||||
@ -0,0 +1,50 @@
|
||||
#!/bin/sh |
||||
# Launcher CyberMatris per Miyoo Mini Plus (OnionOS / Stock OS) |
||||
GAMEDIR=$(dirname "$0") |
||||
LOGFILE="$GAMEDIR/cybermatris.log" |
||||
exec > "$LOGFILE" 2>&1 |
||||
set -x |
||||
|
||||
echo "=== CyberMatris Launch $(date) ===" |
||||
|
||||
# Questo è fondamentale: indica a SDL2 di usare il backend video nativo Miyoo |
||||
export SDL_VIDEODRIVER=mmiyoo |
||||
unset SDL_RENDER_DRIVER |
||||
unset LD_PRELOAD |
||||
|
||||
export HOME=/mnt/SDCARD |
||||
export LD_LIBRARY_PATH="$GAMEDIR:/config/lib:/customer/lib:/mnt/SDCARD/.tmp_update/lib/parasyte:/mnt/SDCARD/usr/local/lib:/mnt/SDCARD/usr/lib/arm-linux-gnueabihf" |
||||
|
||||
# Ferma l'audioserver per liberare il device audio hardware (/dev/mi_ao) |
||||
killall -9 audioserver 2>/dev/null || true |
||||
sleep 1 |
||||
|
||||
# Rilevamento dello schermo e impostazione della risoluzione nativa |
||||
# Controlliamo la risoluzione corrente PRIMA di modificarla per evitare di forzare |
||||
# parametri fuori specifica che congelano il controller dello schermo del Miyoo Mini Plus. |
||||
if fbset | grep -q "752"; then |
||||
echo "[LAUNCHER] Rilevato schermo Miyoo Mini V4 (752x560)!" |
||||
fbset -g 752 560 752 1120 32 2>/dev/null |
||||
IS_V4=1 |
||||
export MIYOO_SCREEN_WIDTH=752 |
||||
export MIYOO_SCREEN_HEIGHT=560 |
||||
else |
||||
echo "[LAUNCHER] Schermo standard 640x480 (Miyoo Mini Plus / v1/v2/v3)" |
||||
fbset -g 640 480 640 960 32 2>/dev/null |
||||
IS_V4=0 |
||||
export MIYOO_SCREEN_WIDTH=640 |
||||
export MIYOO_SCREEN_HEIGHT=480 |
||||
fi |
||||
|
||||
cd "$GAMEDIR" |
||||
./cybermatris_miyoo |
||||
EXIT=$? |
||||
|
||||
# Ripristina sempre la modalità standard 640x480 all'uscita per evitare disallineamenti con MainUI |
||||
if [ "$IS_V4" -eq 1 ]; then |
||||
echo "[LAUNCHER] Ripristino risoluzione standard all'uscita..." |
||||
fbset -g 640 480 640 960 32 2>/dev/null |
||||
fi |
||||
|
||||
echo "=== EXIT CODE: $EXIT ===" |
||||
exit $EXIT |
||||
@ -0,0 +1,332 @@
|
||||
#include <SDL2/SDL.h> |
||||
#include <iostream> |
||||
#include <chrono> |
||||
#ifdef MIYOO_BUILD |
||||
#include <cstdlib> |
||||
#include <fcntl.h> |
||||
#include <unistd.h> |
||||
#include <sys/ioctl.h> |
||||
#include <linux/fb.h> |
||||
#endif |
||||
#include "Game.hpp" |
||||
#include "Synth.hpp" |
||||
#include "Renderer.hpp" |
||||
|
||||
int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { |
||||
// 1. Initialize SDL2 subsystems
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) { |
||||
std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl; |
||||
return -1; |
||||
} |
||||
|
||||
// 2. Create display window
|
||||
int win_w = 800; |
||||
int win_h = 600; |
||||
Uint32 win_flags = SDL_WINDOW_SHOWN; |
||||
|
||||
#ifdef MIYOO_BUILD |
||||
win_flags |= SDL_WINDOW_FULLSCREEN; |
||||
|
||||
// Default fallback
|
||||
win_w = 640; |
||||
win_h = 480; |
||||
|
||||
// 1. Try environment variables first
|
||||
const char* env_w = std::getenv("MIYOO_SCREEN_WIDTH"); |
||||
const char* env_h = std::getenv("MIYOO_SCREEN_HEIGHT"); |
||||
if (env_w && env_h) { |
||||
int parsed_w = std::atoi(env_w); |
||||
int parsed_h = std::atoi(env_h); |
||||
if (parsed_w > 0 && parsed_h > 0) { |
||||
win_w = parsed_w; |
||||
win_h = parsed_h; |
||||
std::cout << "[INFO] Detected screen resolution from environment: " << win_w << "x" << win_h << std::endl; |
||||
} |
||||
} else { |
||||
// 2. Fallback to /dev/fb0 ioctl
|
||||
int fd = open("/dev/fb0", O_RDONLY); |
||||
if (fd >= 0) { |
||||
struct fb_var_screeninfo vinfo; |
||||
if (ioctl(fd, FBIOGET_VSCREENINFO, &vinfo) >= 0) { |
||||
if (vinfo.xres > 0 && vinfo.yres > 0) { |
||||
win_w = vinfo.xres; |
||||
win_h = vinfo.yres; |
||||
std::cout << "[INFO] Detected screen resolution from /dev/fb0: " << win_w << "x" << win_h << std::endl; |
||||
} |
||||
} |
||||
close(fd); |
||||
} else { |
||||
std::cerr << "[WARNING] Failed to open /dev/fb0 for resolution query, using default 640x480" << std::endl; |
||||
} |
||||
} |
||||
#endif |
||||
|
||||
SDL_Window* window = SDL_CreateWindow( |
||||
"CyberMatris - Programmatic Retro Tetris", |
||||
SDL_WINDOWPOS_CENTERED, |
||||
SDL_WINDOWPOS_CENTERED, |
||||
win_w, win_h, |
||||
win_flags |
||||
); |
||||
if (window == nullptr) { |
||||
std::cerr << "Window could not be created! SDL_Error: " << SDL_GetError() << std::endl; |
||||
SDL_Quit(); |
||||
return -1; |
||||
} |
||||
|
||||
// 3. Create GPU Hardware Accelerated Renderer with VSync
|
||||
SDL_Renderer* renderer = SDL_CreateRenderer( |
||||
window, -1, |
||||
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC |
||||
); |
||||
#ifdef MIYOO_BUILD |
||||
if (renderer != nullptr) { |
||||
std::cerr << "[MIYOO] Hardware accelerated renderer OK" << std::endl; |
||||
SDL_RenderSetLogicalSize(renderer, 800, 600); |
||||
} |
||||
#endif |
||||
if (renderer == nullptr) { |
||||
std::cerr << "Renderer could not be created! SDL_Error: " << SDL_GetError() << std::endl; |
||||
SDL_DestroyWindow(window); |
||||
SDL_Quit(); |
||||
return -1; |
||||
} |
||||
|
||||
// 4. Initialize Procedural Audio Synthesizer
|
||||
Synth synth; |
||||
if (!synth.init()) { |
||||
std::cerr << "Warning: Could not initialize programmatic audio synthesizer! SDL_Error: " << SDL_GetError() << std::endl; |
||||
} |
||||
|
||||
// 5. Initialize Game Logic and Renderer
|
||||
Game game; |
||||
Renderer gameRenderer; |
||||
if (!gameRenderer.init(renderer, win_w, win_h)) { |
||||
std::cerr << "Failed to build procedural graphics textures!" << std::endl; |
||||
SDL_DestroyRenderer(renderer); |
||||
SDL_DestroyWindow(window); |
||||
SDL_Quit(); |
||||
return -1; |
||||
} |
||||
|
||||
bool quit = false; |
||||
SDL_Event event; |
||||
|
||||
// Track frame timing using high-precision performance counters
|
||||
uint64_t lastTime = SDL_GetPerformanceCounter(); |
||||
double freq = static_cast<double>(SDL_GetPerformanceFrequency()); |
||||
|
||||
float softDropTimer = 0.0f; |
||||
|
||||
// Flush initial queued events (e.g., from OnionOS menu launch/button presses)
|
||||
std::cout << "[INFO] Flushing initial event queue..." << std::endl; |
||||
SDL_Event trashEvent; |
||||
int flushedCount = 0; |
||||
while (SDL_PollEvent(&trashEvent)) { |
||||
flushedCount++; |
||||
} |
||||
std::cout << "[INFO] Flushed " << flushedCount << " initial events." << std::endl; |
||||
|
||||
// 6. Execution Loop
|
||||
while (!quit) { |
||||
// A. Calculate frame delta-time (dt)
|
||||
uint64_t currentTime = SDL_GetPerformanceCounter(); |
||||
float dt = static_cast<float>((currentTime - lastTime) / freq); |
||||
lastTime = currentTime; |
||||
|
||||
// Cap dt to prevent massive jumps during window drags or lags
|
||||
if (dt > 0.1f) dt = 0.1f; |
||||
|
||||
// B. Handle Inputs & Events
|
||||
while (SDL_PollEvent(&event) != 0) { |
||||
if (event.type == SDL_QUIT) { |
||||
std::cout << "[EVENT] Received SDL_QUIT signal." << std::endl; |
||||
quit = true; |
||||
} else if (event.type == SDL_KEYDOWN) { |
||||
SDL_Keycode sym = event.key.keysym.sym; |
||||
std::cout << "[EVENT] Key Down: " << sym << " (" << SDL_GetKeyName(sym) << ")" << std::endl; |
||||
|
||||
if (sym == SDLK_s) { |
||||
gameRenderer.takeScreenshot("screenshot.bmp"); |
||||
std::cout << "[INFO] Screenshot captured to screenshot.bmp" << std::endl; |
||||
} |
||||
|
||||
// START Screen Inputs
|
||||
if (game.getState() == GameState::START) { |
||||
if (sym == SDLK_RETURN || sym == SDLK_KP_ENTER || sym == SDLK_LALT) { |
||||
std::cout << "[INPUT] Start/Confirm game triggered." << std::endl; |
||||
game.setState(GameState::PLAYING); |
||||
game.reset(); |
||||
synth.playBGM(true); |
||||
} else if (sym == SDLK_ESCAPE) { |
||||
// Cooldown check (1000ms) to prevent immediate exit due to leftover events on boot
|
||||
Uint32 currentTicks = SDL_GetTicks(); |
||||
if (currentTicks > 1000) { |
||||
std::cout << "[INPUT] Quit game requested via ESCAPE/SELECT (ticks: " << currentTicks << ")" << std::endl; |
||||
quit = true; |
||||
} else { |
||||
std::cout << "[INPUT] Blocked early ESCAPE/SELECT quit request (cooldown ticks: " << currentTicks << ")" << std::endl; |
||||
} |
||||
} |
||||
} |
||||
// PLAYING State Inputs
|
||||
else if (game.getState() == GameState::PLAYING) { |
||||
switch (sym) { |
||||
case SDLK_LEFT: |
||||
game.moveLeft(); |
||||
break; |
||||
case SDLK_RIGHT: |
||||
game.moveRight(); |
||||
break; |
||||
case SDLK_UP: |
||||
case SDLK_x: |
||||
case SDLK_LALT: // Gamepad A (Rotate CW)
|
||||
game.rotate(1); // Clockwise rotation
|
||||
break; |
||||
case SDLK_z: |
||||
case SDLK_LCTRL: // Gamepad B (Rotate CCW)
|
||||
game.rotate(-1); // Counter-Clockwise rotation
|
||||
break; |
||||
case SDLK_SPACE: |
||||
game.hardDrop(); |
||||
break; |
||||
case SDLK_c: |
||||
case SDLK_LSHIFT: |
||||
game.holdPiece(); |
||||
break; |
||||
case SDLK_p: |
||||
game.setState(GameState::PAUSED); |
||||
synth.playBGM(false); |
||||
break; |
||||
case SDLK_r: |
||||
synth.stopAllSFX(); |
||||
game.reset(); |
||||
synth.playBGM(true); |
||||
break; |
||||
case SDLK_ESCAPE: |
||||
synth.stopAllSFX(); |
||||
game.setState(GameState::START); |
||||
synth.playBGM(false); |
||||
break; |
||||
default: |
||||
break; |
||||
} |
||||
} |
||||
// PAUSED State Inputs
|
||||
else if (game.getState() == GameState::PAUSED) { |
||||
if (sym == SDLK_p) { |
||||
game.setState(GameState::PLAYING); |
||||
synth.playBGM(true); |
||||
} else if (sym == SDLK_ESCAPE) { |
||||
synth.stopAllSFX(); |
||||
game.setState(GameState::START); |
||||
synth.playBGM(false); |
||||
} |
||||
} |
||||
// GAME OVER State Inputs
|
||||
else if (game.getState() == GameState::GAME_OVER) { |
||||
if (sym == SDLK_r) { |
||||
synth.stopAllSFX(); |
||||
game.setState(GameState::PLAYING); |
||||
game.reset(); |
||||
synth.playBGM(true); |
||||
} else if (sym == SDLK_ESCAPE || sym == SDLK_RETURN || sym == SDLK_KP_ENTER || sym == SDLK_LALT) { |
||||
synth.stopAllSFX(); |
||||
game.setState(GameState::START); |
||||
synth.playBGM(false); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// C. Continuous Keyboard Polling for Smooth Soft-Drop sliding
|
||||
if (game.getState() == GameState::PLAYING) { |
||||
const Uint8* keyboardState = SDL_GetKeyboardState(nullptr); |
||||
if (keyboardState[SDL_SCANCODE_DOWN]) { |
||||
softDropTimer += dt; |
||||
if (softDropTimer >= 0.04f) { // Soft drop tick speed (every 40ms)
|
||||
softDropTimer = 0.0f; |
||||
game.softDrop(); |
||||
} |
||||
} else { |
||||
softDropTimer = 0.0f; |
||||
} |
||||
} |
||||
|
||||
// D. Update Game Logic (gravity timing)
|
||||
bool lineCleared = game.update(dt); |
||||
if (lineCleared) { |
||||
// Trigger glorious pixel explosion of sparks on cleared rows
|
||||
gameRenderer.spawnLineClearParticles(game); |
||||
} |
||||
|
||||
// E. Bridge Game Events to Synth Sound Triggers and Particle Emitters
|
||||
if (game.flagMoveSFX) { |
||||
synth.triggerSFX(SFXType::MOVE); |
||||
game.flagMoveSFX = false; |
||||
} |
||||
if (game.flagRotateSFX) { |
||||
synth.triggerSFX(SFXType::ROTATE); |
||||
game.flagRotateSFX = false; |
||||
} |
||||
if (game.flagLandSFX) { |
||||
synth.triggerSFX(SFXType::LAND); |
||||
|
||||
// Spawn subtle white-gray impact smoke clouds at bottom row of contact
|
||||
auto lockedCells = game.mLastLockedCells; |
||||
if (!lockedCells.empty()) { |
||||
int lowestY = -100; |
||||
for (const auto& p : lockedCells) { |
||||
if (p.y > lowestY) lowestY = p.y; |
||||
} |
||||
for (const auto& p : lockedCells) { |
||||
if (p.y == lowestY) { |
||||
gameRenderer.spawnLandDustParticles(p.x, p.y, game.getActivePieceType()); |
||||
} |
||||
} |
||||
} |
||||
game.flagLandSFX = false; |
||||
} |
||||
if (game.flagLineClearSFX) { |
||||
synth.triggerSFX(SFXType::LINE_CLEAR); |
||||
game.flagLineClearSFX = false; |
||||
} |
||||
if (game.flagTetrisClearSFX) { |
||||
synth.triggerSFX(SFXType::TETRIS_CLEAR); |
||||
game.flagTetrisClearSFX = false; |
||||
} |
||||
if (game.flagLevelUpSFX) { |
||||
synth.triggerSFX(SFXType::LEVEL_UP); |
||||
game.flagLevelUpSFX = false; |
||||
} |
||||
if (game.flagGameOverSFX) { |
||||
synth.triggerSFX(SFXType::GAME_OVER); |
||||
synth.playBGM(false); |
||||
game.flagGameOverSFX = false; |
||||
} |
||||
|
||||
// F. Update Parallax Background and Particle physics
|
||||
gameRenderer.update(dt); |
||||
|
||||
// G. Render active frame (automatically uploads backbuffer and calls SDL_RenderPresent)
|
||||
gameRenderer.render(game, synth); |
||||
|
||||
// H. Automatic periodic screenshot disabled on Miyoo (FAT32 write-during-render issues)
|
||||
#ifndef MIYOO_BUILD |
||||
static float autoScreenshotTimer = 0.0f; |
||||
autoScreenshotTimer += dt; |
||||
if (autoScreenshotTimer >= 2.0f) { |
||||
autoScreenshotTimer = 0.0f; |
||||
gameRenderer.takeScreenshot("screenshot.bmp"); |
||||
} |
||||
#endif |
||||
} |
||||
|
||||
// 7. Cleanup and close resources
|
||||
synth.playBGM(false); |
||||
SDL_DestroyRenderer(renderer); |
||||
SDL_DestroyWindow(window); |
||||
SDL_Quit(); |
||||
|
||||
return 0; |
||||
} |
||||
|
After Width: | Height: | Size: 9.3 KiB |
Loading…
Reference in new issue