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