Initial release: SDL Windows Tetris
This commit is contained in:
252
src/Audio.cpp
Normal file
252
src/Audio.cpp
Normal file
@ -0,0 +1,252 @@
|
||||
// Audio.cpp - Windows Media Foundation MP3 decoding
|
||||
#include "Audio.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <mfapi.h>
|
||||
#include <mfidl.h>
|
||||
#include <mfreadwrite.h>
|
||||
#include <objbase.h>
|
||||
#include <wrl/client.h>
|
||||
#pragma comment(lib, "mfplat.lib")
|
||||
#pragma comment(lib, "mfreadwrite.lib")
|
||||
#pragma comment(lib, "mfuuid.lib")
|
||||
#pragma comment(lib, "ole32.lib")
|
||||
using Microsoft::WRL::ComPtr;
|
||||
#endif
|
||||
|
||||
Audio& Audio::instance(){ static Audio inst; return inst; }
|
||||
|
||||
bool Audio::init(){ if(outSpec.freq!=0) return true; outSpec.format=SDL_AUDIO_S16; outSpec.channels=outChannels; outSpec.freq=outRate;
|
||||
#ifdef _WIN32
|
||||
if(!mfStarted){ if(FAILED(MFStartup(MF_VERSION))) { std::fprintf(stderr,"[Audio] MFStartup failed\n"); } else mfStarted=true; }
|
||||
#endif
|
||||
return true; }
|
||||
|
||||
#ifdef _WIN32
|
||||
static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int& outRate, int& outCh){
|
||||
outPCM.clear(); outRate=44100; outCh=2;
|
||||
ComPtr<IMFSourceReader> reader;
|
||||
wchar_t wpath[MAX_PATH]; int wlen = MultiByteToWideChar(CP_UTF8,0,path.c_str(),-1,wpath,MAX_PATH); if(!wlen) return false;
|
||||
if(FAILED(MFCreateSourceReaderFromURL(wpath,nullptr,&reader))) return false;
|
||||
// Request PCM output
|
||||
ComPtr<IMFMediaType> outType; MFCreateMediaType(&outType); outType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); outType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM); outType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, 2); outType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, 44100); outType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 4); outType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 44100*4); outType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 16); outType->SetUINT32(MF_MT_AUDIO_CHANNEL_MASK, 3); reader->SetCurrentMediaType((DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, NULL, outType.Get());
|
||||
reader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE);
|
||||
while(true){ DWORD flags=0; ComPtr<IMFSample> sample; if(FAILED(reader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM,0,nullptr,&flags,nullptr,&sample))) break; if(flags & MF_SOURCE_READERF_ENDOFSTREAM) break; if(!sample) continue; ComPtr<IMFMediaBuffer> buffer; if(FAILED(sample->ConvertToContiguousBuffer(&buffer))) continue; BYTE* data=nullptr; DWORD maxLen=0, curLen=0; if(SUCCEEDED(buffer->Lock(&data,&maxLen,&curLen)) && curLen){ size_t samples = curLen/2; size_t oldSz = outPCM.size(); outPCM.resize(oldSz + samples); std::memcpy(outPCM.data()+oldSz, data, curLen); } if(data) buffer->Unlock(); }
|
||||
outRate=44100; outCh=2; return !outPCM.empty(); }
|
||||
#endif
|
||||
|
||||
void Audio::addTrack(const std::string& path){ AudioTrack t; t.path=path;
|
||||
#ifdef _WIN32
|
||||
if(decodeMP3(path, t.pcm, t.rate, t.channels)) t.ok=true; else std::fprintf(stderr,"[Audio] Failed to decode %s\n", path.c_str());
|
||||
#else
|
||||
std::fprintf(stderr,"[Audio] MP3 unsupported on this platform (stub): %s\n", path.c_str());
|
||||
#endif
|
||||
tracks.push_back(std::move(t)); }
|
||||
|
||||
void Audio::shuffle(){
|
||||
std::lock_guard<std::mutex> lock(tracksMutex);
|
||||
std::shuffle(tracks.begin(), tracks.end(), rng);
|
||||
}
|
||||
|
||||
bool Audio::ensureStream(){
|
||||
if(audioStream) return true;
|
||||
audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &outSpec, &Audio::streamCallback, this);
|
||||
if(!audioStream){
|
||||
std::fprintf(stderr,"[Audio] SDL_OpenAudioDeviceStream failed: %s\n", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Audio::start(){ if(!ensureStream()) return; if(!playing){ current=-1; nextTrack(); SDL_ResumeAudioStreamDevice(audioStream); playing=true; } }
|
||||
|
||||
void Audio::toggleMute(){ muted=!muted; }
|
||||
|
||||
void Audio::nextTrack(){ if(tracks.empty()) return; for(size_t i=0;i<tracks.size(); ++i){ current = (current + 1) % (int)tracks.size(); if(tracks[current].ok){ tracks[current].cursor=0; return; } } current=-1; }
|
||||
|
||||
void Audio::feed(Uint32 bytesWanted, SDL_AudioStream* stream){
|
||||
if(bytesWanted==0) return;
|
||||
// Prepare a buffer of int16 samples for the output device
|
||||
const size_t outSamples = bytesWanted / sizeof(int16_t);
|
||||
std::vector<int16_t> mix(outSamples, 0);
|
||||
|
||||
// 1) Mix music into buffer (if not muted)
|
||||
if(!muted && current >= 0){
|
||||
size_t cursorBytes = 0;
|
||||
while(cursorBytes < bytesWanted){
|
||||
if(current < 0) break;
|
||||
auto &trk = tracks[current];
|
||||
size_t samplesAvail = trk.pcm.size() - trk.cursor; // samples (int16)
|
||||
if(samplesAvail == 0){ nextTrack(); if(current < 0) break; continue; }
|
||||
size_t samplesNeeded = (bytesWanted - cursorBytes) / sizeof(int16_t);
|
||||
size_t toCopy = (samplesAvail < samplesNeeded) ? samplesAvail : samplesNeeded;
|
||||
if(toCopy == 0) break;
|
||||
// Mix add with clamp
|
||||
size_t startSample = cursorBytes / sizeof(int16_t);
|
||||
for(size_t i=0;i<toCopy;++i){
|
||||
int v = (int)mix[startSample+i] + (int)trk.pcm[trk.cursor+i];
|
||||
if(v>32767) v=32767; if(v<-32768) v=-32768; mix[startSample+i] = (int16_t)v;
|
||||
}
|
||||
trk.cursor += toCopy;
|
||||
cursorBytes += (Uint32)(toCopy * sizeof(int16_t));
|
||||
if(trk.cursor >= trk.pcm.size()) nextTrack();
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Mix active SFX
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(sfxMutex);
|
||||
for(size_t si=0; si<activeSfx.size(); ){
|
||||
auto &s = activeSfx[si];
|
||||
size_t samplesAvail = s.pcm.size() - s.cursor;
|
||||
if(samplesAvail == 0){ activeSfx.erase(activeSfx.begin()+si); continue; }
|
||||
size_t toCopy = (samplesAvail < outSamples) ? samplesAvail : outSamples;
|
||||
for(size_t i=0;i<toCopy;++i){
|
||||
int v = (int)mix[i] + (int)s.pcm[s.cursor+i];
|
||||
if(v>32767) v=32767; if(v<-32768) v=-32768; mix[i] = (int16_t)v;
|
||||
}
|
||||
s.cursor += toCopy;
|
||||
++si;
|
||||
}
|
||||
}
|
||||
|
||||
// Submit mixed audio
|
||||
if(!mix.empty()) SDL_PutAudioStreamData(stream, mix.data(), (int)bytesWanted);
|
||||
}
|
||||
|
||||
void Audio::playSfx(const std::vector<int16_t>& pcm, int channels, int rate, float volume){
|
||||
if(pcm.empty()) return;
|
||||
if(!ensureStream()) return;
|
||||
// Convert input to device format (S16, stereo, 44100)
|
||||
SDL_AudioSpec src{}; src.format=SDL_AUDIO_S16; src.channels=(Uint8)channels; src.freq=rate;
|
||||
SDL_AudioSpec dst{}; dst.format=SDL_AUDIO_S16; dst.channels=(Uint8)outChannels; dst.freq=outRate;
|
||||
SDL_AudioStream* cvt = SDL_CreateAudioStream(&src, &dst);
|
||||
if(!cvt) return;
|
||||
// Apply volume while copying into a temp buffer
|
||||
std::vector<int16_t> volBuf(pcm.size());
|
||||
for(size_t i=0;i<pcm.size();++i){
|
||||
int v = (int)(pcm[i] * volume);
|
||||
if(v>32767) v=32767; if(v<-32768) v=-32768; volBuf[i]=(int16_t)v;
|
||||
}
|
||||
SDL_PutAudioStreamData(cvt, volBuf.data(), (int)(volBuf.size()*sizeof(int16_t)));
|
||||
SDL_FlushAudioStream(cvt);
|
||||
int bytes = SDL_GetAudioStreamAvailable(cvt);
|
||||
if(bytes>0){
|
||||
std::vector<int16_t> out(bytes/2);
|
||||
SDL_GetAudioStreamData(cvt, out.data(), bytes);
|
||||
std::lock_guard<std::mutex> lock(sfxMutex);
|
||||
activeSfx.push_back(SfxPlay{ std::move(out), 0 });
|
||||
}
|
||||
SDL_DestroyAudioStream(cvt);
|
||||
}
|
||||
|
||||
void SDLCALL Audio::streamCallback(void* userdata, SDL_AudioStream* stream, int additional, int total){ Uint32 want = additional>0 ? (Uint32)additional : (Uint32)total; if(!want) want=4096; reinterpret_cast<Audio*>(userdata)->feed(want, stream); }
|
||||
|
||||
void Audio::addTrackAsync(const std::string& path) {
|
||||
std::lock_guard<std::mutex> lock(pendingTracksMutex);
|
||||
pendingTracks.push_back(path);
|
||||
}
|
||||
|
||||
void Audio::startBackgroundLoading() {
|
||||
if (loadingThread.joinable()) return; // Already running
|
||||
loadingComplete = false;
|
||||
loadedCount = 0;
|
||||
loadingThread = std::thread(&Audio::backgroundLoadingThread, this);
|
||||
}
|
||||
|
||||
void Audio::backgroundLoadingThread() {
|
||||
#ifdef _WIN32
|
||||
// Initialize COM and MF for this thread
|
||||
HRESULT hrCom = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
HRESULT hrMF = MFStartup(MF_VERSION);
|
||||
bool mfInitialized = SUCCEEDED(hrMF);
|
||||
|
||||
if (!mfInitialized) {
|
||||
std::fprintf(stderr, "[Audio] Failed to initialize MF on background thread\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Copy pending tracks to avoid holding the mutex during processing
|
||||
std::vector<std::string> tracksToProcess;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pendingTracksMutex);
|
||||
tracksToProcess = pendingTracks;
|
||||
}
|
||||
|
||||
for (const std::string& path : tracksToProcess) {
|
||||
AudioTrack t;
|
||||
t.path = path;
|
||||
#ifdef _WIN32
|
||||
if (mfInitialized && decodeMP3(path, t.pcm, t.rate, t.channels)) {
|
||||
t.ok = true;
|
||||
} else {
|
||||
std::fprintf(stderr, "[Audio] Failed to decode %s\n", path.c_str());
|
||||
}
|
||||
#else
|
||||
std::fprintf(stderr, "[Audio] MP3 unsupported on this platform (stub): %s\n", path.c_str());
|
||||
#endif
|
||||
|
||||
// Thread-safe addition to tracks
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(tracksMutex);
|
||||
tracks.push_back(std::move(t));
|
||||
}
|
||||
|
||||
loadedCount++;
|
||||
|
||||
// Small delay to prevent overwhelming the system
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Cleanup MF and COM for this thread
|
||||
if (mfInitialized) {
|
||||
MFShutdown();
|
||||
}
|
||||
if (SUCCEEDED(hrCom)) {
|
||||
CoUninitialize();
|
||||
}
|
||||
#endif
|
||||
|
||||
loadingComplete = true;
|
||||
}
|
||||
|
||||
void Audio::waitForLoadingComplete() {
|
||||
if (loadingThread.joinable()) {
|
||||
loadingThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
bool Audio::isLoadingComplete() const {
|
||||
return loadingComplete;
|
||||
}
|
||||
|
||||
int Audio::getLoadedTrackCount() const {
|
||||
return loadedCount;
|
||||
}
|
||||
|
||||
void Audio::shutdown(){
|
||||
// Stop background loading thread first
|
||||
if (loadingThread.joinable()) {
|
||||
loadingThread.join();
|
||||
}
|
||||
|
||||
if(audioStream){ SDL_DestroyAudioStream(audioStream); audioStream=nullptr; }
|
||||
tracks.clear();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pendingTracksMutex);
|
||||
pendingTracks.clear();
|
||||
}
|
||||
playing=false;
|
||||
#ifdef _WIN32
|
||||
if(mfStarted){ MFShutdown(); mfStarted=false; }
|
||||
#endif
|
||||
}
|
||||
53
src/Audio.h
Normal file
53
src/Audio.h
Normal file
@ -0,0 +1,53 @@
|
||||
// Audio.h - MP3 playlist playback (Windows Media Foundation backend) + SDL3 stream
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <random>
|
||||
#include <cstdint>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
|
||||
struct AudioTrack { std::string path; std::vector<int16_t> pcm; int channels=2; int rate=44100; size_t cursor=0; bool ok=false; };
|
||||
|
||||
class Audio {
|
||||
public:
|
||||
static Audio& instance();
|
||||
bool init(); // initialize backend (MF on Windows)
|
||||
void addTrack(const std::string& path); // decode MP3 -> PCM16 stereo 44100
|
||||
void addTrackAsync(const std::string& path); // add track for background loading
|
||||
void startBackgroundLoading(); // start background thread for loading
|
||||
void waitForLoadingComplete(); // wait for all tracks to finish loading
|
||||
bool isLoadingComplete() const; // check if background loading is done
|
||||
int getLoadedTrackCount() const; // get number of tracks loaded so far
|
||||
void shuffle(); // randomize order
|
||||
void start(); // begin playback
|
||||
void toggleMute();
|
||||
// Queue a sound effect to mix over the music (pcm can be mono/stereo, any rate; will be converted)
|
||||
void playSfx(const std::vector<int16_t>& pcm, int channels, int rate, float volume);
|
||||
void shutdown();
|
||||
private:
|
||||
Audio()=default; ~Audio()=default; Audio(const Audio&)=delete; Audio& operator=(const Audio&)=delete;
|
||||
static void SDLCALL streamCallback(void* userdata, SDL_AudioStream* stream, int additional, int total);
|
||||
void feed(Uint32 bytesWanted, SDL_AudioStream* stream);
|
||||
void nextTrack();
|
||||
bool ensureStream();
|
||||
void backgroundLoadingThread(); // background thread function
|
||||
|
||||
std::vector<AudioTrack> tracks; int current=-1; bool playing=false; bool muted=false; std::mt19937 rng{std::random_device{}()};
|
||||
SDL_AudioStream* audioStream=nullptr; SDL_AudioSpec outSpec{}; int outChannels=2; int outRate=44100; bool mfStarted=false;
|
||||
|
||||
// Threading support
|
||||
std::vector<std::string> pendingTracks;
|
||||
std::thread loadingThread;
|
||||
std::mutex tracksMutex;
|
||||
std::mutex pendingTracksMutex;
|
||||
std::atomic<bool> loadingComplete{false};
|
||||
std::atomic<int> loadedCount{0};
|
||||
|
||||
// SFX mixing support
|
||||
struct SfxPlay { std::vector<int16_t> pcm; size_t cursor=0; };
|
||||
std::vector<SfxPlay> activeSfx;
|
||||
std::mutex sfxMutex;
|
||||
};
|
||||
21
src/Font.cpp
Normal file
21
src/Font.cpp
Normal file
@ -0,0 +1,21 @@
|
||||
// Font.cpp - implementation of FontAtlas
|
||||
#include "Font.h"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
bool FontAtlas::init(const std::string& path, int basePt) { fontPath = path; baseSize = basePt; return true; }
|
||||
|
||||
void FontAtlas::shutdown() { for (auto &kv : cache) if (kv.second) TTF_CloseFont(kv.second); cache.clear(); }
|
||||
|
||||
TTF_Font* FontAtlas::getSized(int ptSize) {
|
||||
auto it = cache.find(ptSize); if (it!=cache.end()) return it->second;
|
||||
TTF_Font* f = TTF_OpenFont(fontPath.c_str(), ptSize);
|
||||
if (!f) return nullptr; cache[ptSize] = f; return f;
|
||||
}
|
||||
|
||||
void FontAtlas::draw(SDL_Renderer* r, float x, float y, const std::string& text, float scale, SDL_Color color) {
|
||||
if (scale <= 0) return; int pt = int(baseSize * scale); if (pt < 8) pt = 8; TTF_Font* f = getSized(pt); if (!f) return;
|
||||
SDL_Surface* surf = TTF_RenderText_Blended(f, text.c_str(), text.length(), color); if (!surf) return;
|
||||
SDL_Texture* tex = SDL_CreateTextureFromSurface(r, surf);
|
||||
if (tex) { SDL_FRect dst{ x, y, (float)surf->w, (float)surf->h }; SDL_RenderTexture(r, tex, nullptr, &dst); SDL_DestroyTexture(tex); }
|
||||
SDL_DestroySurface(surf);
|
||||
}
|
||||
18
src/Font.h
Normal file
18
src/Font.h
Normal file
@ -0,0 +1,18 @@
|
||||
// Font.h - Font rendering abstraction with simple size cache
|
||||
#pragma once
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
struct SDL_Renderer;
|
||||
|
||||
class FontAtlas {
|
||||
public:
|
||||
bool init(const std::string& path, int basePt);
|
||||
void shutdown();
|
||||
void draw(SDL_Renderer* r, float x, float y, const std::string& text, float scale, SDL_Color color);
|
||||
private:
|
||||
std::string fontPath;
|
||||
int baseSize{24};
|
||||
std::unordered_map<int, TTF_Font*> cache; // point size -> font*
|
||||
TTF_Font* getSized(int ptSize);
|
||||
};
|
||||
262
src/Game.cpp
Normal file
262
src/Game.cpp
Normal file
@ -0,0 +1,262 @@
|
||||
// Game.cpp - Implementation of core Tetris game logic
|
||||
#include "Game.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
// Piece rotation bitmasks (row-major 4x4). Bit 0 = (0,0).
|
||||
static const std::array<Shape, PIECE_COUNT> SHAPES = {{
|
||||
Shape{ 0x0F00, 0x2222, 0x00F0, 0x4444 }, // I
|
||||
Shape{ 0x0660, 0x0660, 0x0660, 0x0660 }, // O
|
||||
Shape{ 0x0E40, 0x4C40, 0x4E00, 0x4640 }, // T
|
||||
Shape{ 0x06C0, 0x4620, 0x06C0, 0x4620 }, // S
|
||||
Shape{ 0x0C60, 0x2640, 0x0C60, 0x2640 }, // Z
|
||||
Shape{ 0x08E0, 0x6440, 0x0E20, 0x44C0 }, // J
|
||||
Shape{ 0x02E0, 0x4460, 0x0E80, 0xC440 }, // L
|
||||
}};
|
||||
|
||||
void Game::reset(int startLevel_) {
|
||||
std::fill(board.begin(), board.end(), 0);
|
||||
std::fill(blockCounts.begin(), blockCounts.end(), 0);
|
||||
bag.clear();
|
||||
_score = 0; _lines = 0; _level = startLevel_ + 1; startLevel = startLevel_;
|
||||
gravityMs = 800.0 * std::pow(0.85, startLevel_); // speed-up for higher starts
|
||||
fallAcc = 0; _elapsedSec = 0; gameOver=false; paused=false;
|
||||
hold = Piece{}; hold.type = PIECE_COUNT; canHold=true;
|
||||
refillBag(); spawn();
|
||||
}
|
||||
|
||||
void Game::refillBag() {
|
||||
bag.clear();
|
||||
for (int i=0;i<PIECE_COUNT;++i) bag.push_back(static_cast<PieceType>(i));
|
||||
std::shuffle(bag.begin(), bag.end(), rng);
|
||||
}
|
||||
|
||||
void Game::spawn() {
|
||||
if (bag.empty()) refillBag();
|
||||
PieceType pieceType = bag.back();
|
||||
|
||||
// I-piece needs to start one row higher due to its height when vertical
|
||||
int spawnY = (pieceType == I) ? -2 : -1;
|
||||
|
||||
cur = Piece{ pieceType, 0, 3, spawnY };
|
||||
|
||||
// Check if the newly spawned piece collides with existing blocks
|
||||
if (collides(cur)) {
|
||||
gameOver = true;
|
||||
return; // Don't proceed with spawning if it causes a collision
|
||||
}
|
||||
|
||||
bag.pop_back();
|
||||
blockCounts[cur.type]++; // Increment count for this piece type
|
||||
canHold = true;
|
||||
|
||||
// Prepare next piece
|
||||
if (bag.empty()) refillBag();
|
||||
PieceType nextType = bag.back();
|
||||
int nextSpawnY = (nextType == I) ? -2 : -1;
|
||||
nextPiece = Piece{ nextType, 0, 3, nextSpawnY };
|
||||
}
|
||||
|
||||
bool Game::cellFilled(const Piece& p, int cx, int cy) {
|
||||
if (p.type == PIECE_COUNT) return false;
|
||||
const uint16_t mask = SHAPES[p.type][p.rot];
|
||||
const int bit = cy*4 + cx;
|
||||
return (mask >> bit) & 1;
|
||||
}
|
||||
|
||||
bool Game::collides(const Piece& p) const {
|
||||
for (int cy=0; cy<4; ++cy) {
|
||||
for (int cx=0; cx<4; ++cx) if (cellFilled(p,cx,cy)) {
|
||||
int gx = p.x + cx; int gy = p.y + cy;
|
||||
if (gx < 0 || gx >= COLS || gy >= ROWS) return true;
|
||||
if (gy >= 0 && board[gy*COLS + gx] != 0) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Game::lockPiece() {
|
||||
for (int cy=0; cy<4; ++cy) {
|
||||
for (int cx=0; cx<4; ++cx) if (cellFilled(cur,cx,cy)) {
|
||||
int gx = cur.x + cx; int gy = cur.y + cy;
|
||||
if (gy >= 0 && gy < ROWS) board[gy*COLS + gx] = static_cast<int>(cur.type)+1;
|
||||
if (gy < 0) gameOver = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for completed lines but don't clear them yet - let the effect system handle it
|
||||
int cleared = checkLines();
|
||||
if (cleared > 0) {
|
||||
// JS scoring system: base points per clear, multiplied by (level+1) in JS.
|
||||
// Our _level is 1-based (JS level + 1), so multiplier == _level.
|
||||
int base = 0;
|
||||
switch (cleared) {
|
||||
case 1: base = 40; break; // SINGLE
|
||||
case 2: base = 100; break; // DOUBLE
|
||||
case 3: base = 300; break; // TRIPLE
|
||||
case 4: base = 1200; break; // TETRIS
|
||||
default: base = 0; break;
|
||||
}
|
||||
_score += base * std::max(1, _level);
|
||||
|
||||
// Update total lines
|
||||
_lines += cleared;
|
||||
|
||||
// JS level progression (NES-like) using starting level rules
|
||||
// startLevel is 0-based in JS; our _level is JS level + 1
|
||||
const int threshold = (startLevel + 1) * 10;
|
||||
int oldLevel = _level;
|
||||
// First level up happens when total lines equal threshold
|
||||
// After that, every 10 lines (when (lines - threshold) % 10 == 0)
|
||||
if (_lines == threshold) {
|
||||
_level += 1;
|
||||
} else if (_lines > threshold && ((_lines - threshold) % 10 == 0)) {
|
||||
_level += 1;
|
||||
}
|
||||
|
||||
if (_level > oldLevel) {
|
||||
gravityMs = std::max(60.0, gravityMs * 0.85);
|
||||
if (levelUpCallback) {
|
||||
levelUpCallback(_level);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger sound effect callback for line clears
|
||||
if (soundCallback) {
|
||||
soundCallback(cleared);
|
||||
}
|
||||
}
|
||||
|
||||
if (!gameOver) spawn();
|
||||
}
|
||||
|
||||
int Game::checkLines() {
|
||||
completedLines.clear();
|
||||
|
||||
// Check each row from bottom to top
|
||||
for (int y = ROWS - 1; y >= 0; --y) {
|
||||
bool full = true;
|
||||
for (int x = 0; x < COLS; ++x) {
|
||||
if (board[y*COLS + x] == 0) {
|
||||
full = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (full) {
|
||||
completedLines.push_back(y);
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<int>(completedLines.size());
|
||||
}
|
||||
|
||||
void Game::clearCompletedLines() {
|
||||
if (completedLines.empty()) return;
|
||||
|
||||
actualClearLines();
|
||||
completedLines.clear();
|
||||
}
|
||||
|
||||
void Game::actualClearLines() {
|
||||
if (completedLines.empty()) return;
|
||||
|
||||
int write = ROWS - 1;
|
||||
for (int y = ROWS - 1; y >= 0; --y) {
|
||||
// Check if this row should be cleared
|
||||
bool shouldClear = std::find(completedLines.begin(), completedLines.end(), y) != completedLines.end();
|
||||
|
||||
if (!shouldClear) {
|
||||
// Keep this row, move it down if necessary
|
||||
if (write != y) {
|
||||
for (int x = 0; x < COLS; ++x) {
|
||||
board[write*COLS + x] = board[y*COLS + x];
|
||||
}
|
||||
}
|
||||
--write;
|
||||
}
|
||||
// If shouldClear is true, we skip this row (effectively removing it)
|
||||
}
|
||||
|
||||
// Clear the top rows that are now empty
|
||||
for (int y = write; y >= 0; --y) {
|
||||
for (int x = 0; x < COLS; ++x) {
|
||||
board[y*COLS + x] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Game::tryMoveDown() {
|
||||
Piece p = cur; p.y += 1; if (!collides(p)) { cur = p; return true; } return false;
|
||||
}
|
||||
|
||||
void Game::tickGravity(double frameMs) {
|
||||
if (paused) return; // Don't tick gravity when paused
|
||||
fallAcc += frameMs;
|
||||
while (fallAcc >= gravityMs) {
|
||||
// Attempt to move down by one row
|
||||
if (tryMoveDown()) {
|
||||
// Award soft drop points only if player is actively holding Down
|
||||
// JS: POINTS.SOFT_DROP = 1 per cell for soft drop
|
||||
if (softDropping) {
|
||||
_score += 1;
|
||||
}
|
||||
} else {
|
||||
// Can't move down further, lock piece
|
||||
lockPiece();
|
||||
if (gameOver) break;
|
||||
}
|
||||
fallAcc -= gravityMs;
|
||||
}
|
||||
}
|
||||
|
||||
void Game::softDropBoost(double frameMs) {
|
||||
if (!paused) fallAcc += frameMs * 10.0;
|
||||
}
|
||||
|
||||
void Game::hardDrop() {
|
||||
if (paused) return;
|
||||
// Count how many rows we drop for scoring parity with JS
|
||||
int rows = 0;
|
||||
while (tryMoveDown()) { rows++; }
|
||||
// JS: POINTS.HARD_DROP = 1 per cell
|
||||
if (rows > 0) {
|
||||
_score += rows * 1;
|
||||
}
|
||||
lockPiece();
|
||||
}
|
||||
|
||||
void Game::rotate(int dir) {
|
||||
if (paused) return;
|
||||
Piece p = cur; p.rot = (p.rot + dir + 4) % 4; const int kicks[5]={0,-1,1,-2,2};
|
||||
for (int dx : kicks) { p.x = cur.x + dx; if (!collides(p)) { cur = p; return; } }
|
||||
}
|
||||
|
||||
void Game::move(int dx) {
|
||||
if (paused) return;
|
||||
Piece p = cur; p.x += dx; if (!collides(p)) cur = p;
|
||||
}
|
||||
|
||||
void Game::holdCurrent() {
|
||||
if (paused || !canHold) return;
|
||||
|
||||
if (hold.type == PIECE_COUNT) {
|
||||
// First hold - just store current piece and spawn new one
|
||||
hold = cur;
|
||||
// I-piece needs to start one row higher due to its height when vertical
|
||||
int holdSpawnY = (hold.type == I) ? -2 : -1;
|
||||
hold.x = 3; hold.y = holdSpawnY; hold.rot = 0;
|
||||
spawn();
|
||||
} else {
|
||||
// Swap current with held piece
|
||||
Piece temp = hold;
|
||||
hold = cur;
|
||||
// I-piece needs to start one row higher due to its height when vertical
|
||||
int holdSpawnY = (hold.type == I) ? -2 : -1;
|
||||
int currentSpawnY = (temp.type == I) ? -2 : -1;
|
||||
hold.x = 3; hold.y = holdSpawnY; hold.rot = 0;
|
||||
cur = temp;
|
||||
cur.x = 3; cur.y = currentSpawnY; cur.rot = 0;
|
||||
}
|
||||
|
||||
canHold = false; // Can only hold once per piece spawn
|
||||
}
|
||||
99
src/Game.h
Normal file
99
src/Game.h
Normal file
@ -0,0 +1,99 @@
|
||||
// Game.h - Core Tetris game logic (board, piece mechanics, scoring events only)
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <random>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
enum PieceType { I, O, T, S, Z, J, L, PIECE_COUNT };
|
||||
using Shape = std::array<uint16_t, 4>; // four rotation bitmasks
|
||||
|
||||
class Game {
|
||||
public:
|
||||
static constexpr int COLS = 10;
|
||||
static constexpr int ROWS = 20;
|
||||
static constexpr int TILE = 28; // logical cell size in pixels (render layer decides use)
|
||||
|
||||
struct Piece { PieceType type{PIECE_COUNT}; int rot{0}; int x{3}; int y{-2}; };
|
||||
|
||||
explicit Game(int startLevel = 0) { reset(startLevel); }
|
||||
void reset(int startLevel = 0);
|
||||
|
||||
// Simulation -----------------------------------------------------------
|
||||
void tickGravity(double frameMs); // advance gravity accumulator & drop
|
||||
void softDropBoost(double frameMs); // accelerate fall while held
|
||||
void hardDrop(); // instant drop & lock
|
||||
void setSoftDropping(bool on) { softDropping = on; } // mark if player holds Down
|
||||
void move(int dx); // horizontal move
|
||||
void rotate(int dir); // +1 cw, -1 ccw (simple wall-kick)
|
||||
void holdCurrent(); // swap with hold (once per spawn)
|
||||
|
||||
// Accessors -----------------------------------------------------------
|
||||
const std::array<int, COLS*ROWS>& boardRef() const { return board; }
|
||||
const Piece& current() const { return cur; }
|
||||
const Piece& next() const { return nextPiece; }
|
||||
const Piece& held() const { return hold; }
|
||||
bool canHoldPiece() const { return canHold; }
|
||||
bool isGameOver() const { return gameOver; }
|
||||
bool isPaused() const { return paused; }
|
||||
void setPaused(bool p) { paused = p; }
|
||||
int score() const { return _score; }
|
||||
int lines() const { return _lines; }
|
||||
int level() const { return _level; }
|
||||
int startLevelBase() const { return startLevel; }
|
||||
double elapsed() const { return _elapsedSec; }
|
||||
void addElapsed(double frameMs) { if (!paused) _elapsedSec += frameMs/1000.0; }
|
||||
|
||||
// Block statistics
|
||||
const std::array<int, PIECE_COUNT>& getBlockCounts() const { return blockCounts; }
|
||||
|
||||
// Line clearing effects support
|
||||
bool hasCompletedLines() const { return !completedLines.empty(); }
|
||||
const std::vector<int>& getCompletedLines() const { return completedLines; }
|
||||
void clearCompletedLines(); // Actually remove the lines from the board
|
||||
|
||||
// Sound effect callbacks
|
||||
using SoundCallback = std::function<void(int)>; // Callback for line clear sounds (number of lines)
|
||||
using LevelUpCallback = std::function<void(int)>; // Callback for level up sounds
|
||||
void setSoundCallback(SoundCallback callback) { soundCallback = callback; }
|
||||
void setLevelUpCallback(LevelUpCallback callback) { levelUpCallback = callback; }
|
||||
|
||||
// Shape helper --------------------------------------------------------
|
||||
static bool cellFilled(const Piece& p, int cx, int cy);
|
||||
|
||||
private:
|
||||
std::array<int, COLS*ROWS> board{}; // 0 empty else color index
|
||||
Piece cur{}, hold{}, nextPiece{}; // current, held & next piece
|
||||
bool canHold{true};
|
||||
bool paused{false};
|
||||
std::vector<PieceType> bag; // 7-bag randomizer
|
||||
std::mt19937 rng{ std::random_device{}() };
|
||||
std::array<int, PIECE_COUNT> blockCounts{}; // Count of each piece type used
|
||||
|
||||
int _score{0};
|
||||
int _lines{0};
|
||||
int _level{1};
|
||||
double gravityMs{800.0};
|
||||
double fallAcc{0.0};
|
||||
double _elapsedSec{0.0};
|
||||
bool gameOver{false};
|
||||
int startLevel{0};
|
||||
bool softDropping{false}; // true while player holds Down key
|
||||
|
||||
// Line clearing support
|
||||
std::vector<int> completedLines; // Rows that are complete and ready for effects
|
||||
|
||||
// Sound effect callbacks
|
||||
SoundCallback soundCallback;
|
||||
LevelUpCallback levelUpCallback;
|
||||
|
||||
// Internal helpers ----------------------------------------------------
|
||||
void refillBag();
|
||||
void spawn();
|
||||
bool collides(const Piece& p) const;
|
||||
void lockPiece();
|
||||
int checkLines(); // Find completed lines and store them
|
||||
void actualClearLines(); // Actually remove lines from board
|
||||
bool tryMoveDown(); // one-row fall; returns true if moved
|
||||
};
|
||||
297
src/LineEffect.cpp
Normal file
297
src/LineEffect.cpp
Normal file
@ -0,0 +1,297 @@
|
||||
// LineEffect.cpp - Implementation of line clearing visual and audio effects
|
||||
#include "LineEffect.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
LineEffect::Particle::Particle(float px, float py)
|
||||
: x(px), y(py), size(1.5f + static_cast<float>(rand()) / RAND_MAX * 4.0f), alpha(1.0f) {
|
||||
|
||||
// Random velocity for explosive effect
|
||||
float angle = static_cast<float>(rand()) / RAND_MAX * 2.0f * M_PI;
|
||||
float speed = 80.0f + static_cast<float>(rand()) / RAND_MAX * 150.0f; // More explosive speed
|
||||
vx = std::cos(angle) * speed;
|
||||
vy = std::sin(angle) * speed - 30.0f; // Less upward bias for more spread
|
||||
|
||||
// Bright explosive colors (oranges, yellows, whites, reds)
|
||||
int colorType = rand() % 4;
|
||||
switch (colorType) {
|
||||
case 0: // Orange/Fire
|
||||
color = {255, static_cast<Uint8>(140 + rand() % 100), static_cast<Uint8>(30 + rand() % 50), 255};
|
||||
break;
|
||||
case 1: // Yellow/Gold
|
||||
color = {255, 255, static_cast<Uint8>(100 + rand() % 155), 255};
|
||||
break;
|
||||
case 2: // White
|
||||
color = {255, 255, 255, 255};
|
||||
break;
|
||||
case 3: // Red/Pink
|
||||
color = {255, static_cast<Uint8>(100 + rand() % 100), static_cast<Uint8>(100 + rand() % 100), 255};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void LineEffect::Particle::update() {
|
||||
x += vx * 0.016f; // Assume ~60 FPS
|
||||
y += vy * 0.016f;
|
||||
vy += 250.0f * 0.016f; // Stronger gravity for explosive effect
|
||||
vx *= 0.98f; // Air resistance
|
||||
alpha -= 0.12f; // Fast fade for explosive burst
|
||||
if (alpha < 0.0f) alpha = 0.0f;
|
||||
|
||||
// Shrink particles as they fade
|
||||
if (size > 0.5f) size -= 0.05f;
|
||||
}
|
||||
|
||||
void LineEffect::Particle::render(SDL_Renderer* renderer) {
|
||||
if (alpha <= 0.0f) return;
|
||||
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
Uint8 adjustedAlpha = static_cast<Uint8>(alpha * 255.0f);
|
||||
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, adjustedAlpha);
|
||||
|
||||
// Draw particle as a small circle
|
||||
for (int i = 0; i < static_cast<int>(size); ++i) {
|
||||
for (int j = 0; j < static_cast<int>(size); ++j) {
|
||||
float dx = i - size/2.0f;
|
||||
float dy = j - size/2.0f;
|
||||
if (dx*dx + dy*dy <= (size/2.0f)*(size/2.0f)) {
|
||||
SDL_RenderPoint(renderer, x + dx, y + dy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LineEffect::LineEffect() : renderer(nullptr), state(AnimationState::IDLE), timer(0.0f),
|
||||
rng(std::random_device{}()), audioStream(nullptr) {
|
||||
}
|
||||
|
||||
LineEffect::~LineEffect() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool LineEffect::init(SDL_Renderer* r) {
|
||||
renderer = r;
|
||||
initAudio();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LineEffect::shutdown() {
|
||||
if (audioStream) {
|
||||
SDL_DestroyAudioStream(audioStream);
|
||||
audioStream = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void LineEffect::initAudio() {
|
||||
// For now, we'll generate simple beep sounds procedurally
|
||||
// In a full implementation, you'd load WAV files
|
||||
|
||||
// Generate a simple line clear beep (440Hz for 0.2 seconds)
|
||||
int sampleRate = 44100;
|
||||
int duration = static_cast<int>(0.2f * sampleRate);
|
||||
lineClearSample.resize(duration * 2); // Stereo
|
||||
|
||||
for (int i = 0; i < duration; ++i) {
|
||||
float t = static_cast<float>(i) / sampleRate;
|
||||
float wave = std::sin(2.0f * M_PI * 440.0f * t) * 0.3f; // 440Hz sine wave
|
||||
int16_t sample = static_cast<int16_t>(wave * 32767.0f);
|
||||
lineClearSample[i * 2] = sample; // Left channel
|
||||
lineClearSample[i * 2 + 1] = sample; // Right channel
|
||||
}
|
||||
|
||||
// Generate a higher pitched tetris sound (880Hz for 0.4 seconds)
|
||||
duration = static_cast<int>(0.4f * sampleRate);
|
||||
tetrisSample.resize(duration * 2);
|
||||
|
||||
for (int i = 0; i < duration; ++i) {
|
||||
float t = static_cast<float>(i) / sampleRate;
|
||||
float wave = std::sin(2.0f * M_PI * 880.0f * t) * 0.4f; // 880Hz sine wave
|
||||
int16_t sample = static_cast<int16_t>(wave * 32767.0f);
|
||||
tetrisSample[i * 2] = sample; // Left channel
|
||||
tetrisSample[i * 2 + 1] = sample; // Right channel
|
||||
}
|
||||
}
|
||||
|
||||
void LineEffect::startLineClear(const std::vector<int>& rows, int gridX, int gridY, int blockSize) {
|
||||
if (rows.empty()) return;
|
||||
|
||||
clearingRows = rows;
|
||||
state = AnimationState::FLASH_WHITE;
|
||||
timer = 0.0f;
|
||||
particles.clear();
|
||||
|
||||
// Create particles for each clearing row
|
||||
for (int row : rows) {
|
||||
createParticles(row, gridX, gridY, blockSize);
|
||||
}
|
||||
|
||||
// Play appropriate sound
|
||||
playLineClearSound(static_cast<int>(rows.size()));
|
||||
}
|
||||
|
||||
void LineEffect::createParticles(int row, int gridX, int gridY, int blockSize) {
|
||||
// Create particles spread across the row with explosive pattern
|
||||
int particlesPerRow = 35; // More particles for dramatic explosion effect
|
||||
|
||||
for (int i = 0; i < particlesPerRow; ++i) {
|
||||
// Create particles along the entire row width
|
||||
float x = gridX + (static_cast<float>(i) / (particlesPerRow - 1)) * (10 * blockSize);
|
||||
float y = gridY + row * blockSize + blockSize / 2.0f;
|
||||
|
||||
// Add some randomness to position
|
||||
x += (static_cast<float>(rand()) / RAND_MAX - 0.5f) * blockSize * 0.8f;
|
||||
y += (static_cast<float>(rand()) / RAND_MAX - 0.5f) * blockSize * 0.6f;
|
||||
|
||||
particles.emplace_back(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
bool LineEffect::update(float deltaTime) {
|
||||
if (state == AnimationState::IDLE) return true;
|
||||
|
||||
timer += deltaTime;
|
||||
|
||||
switch (state) {
|
||||
case AnimationState::FLASH_WHITE:
|
||||
if (timer >= FLASH_DURATION) {
|
||||
state = AnimationState::EXPLODE_BLOCKS;
|
||||
timer = 0.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case AnimationState::EXPLODE_BLOCKS:
|
||||
updateParticles();
|
||||
if (timer >= EXPLODE_DURATION) {
|
||||
state = AnimationState::BLOCKS_DROP;
|
||||
timer = 0.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case AnimationState::BLOCKS_DROP:
|
||||
updateParticles();
|
||||
if (timer >= DROP_DURATION) {
|
||||
state = AnimationState::IDLE;
|
||||
clearingRows.clear();
|
||||
particles.clear();
|
||||
return true; // Effect complete
|
||||
}
|
||||
break;
|
||||
|
||||
case AnimationState::IDLE:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // Effect still running
|
||||
}
|
||||
|
||||
void LineEffect::updateParticles() {
|
||||
// Update all particles
|
||||
for (auto& particle : particles) {
|
||||
particle.update();
|
||||
}
|
||||
|
||||
// Remove dead particles
|
||||
particles.erase(
|
||||
std::remove_if(particles.begin(), particles.end(),
|
||||
[](const Particle& p) { return !p.isAlive(); }),
|
||||
particles.end()
|
||||
);
|
||||
}
|
||||
|
||||
void LineEffect::render(SDL_Renderer* renderer, int gridX, int gridY, int blockSize) {
|
||||
if (state == AnimationState::IDLE) return;
|
||||
|
||||
switch (state) {
|
||||
case AnimationState::FLASH_WHITE:
|
||||
renderFlash(gridX, gridY, blockSize);
|
||||
break;
|
||||
|
||||
case AnimationState::EXPLODE_BLOCKS:
|
||||
renderExplosion();
|
||||
break;
|
||||
|
||||
case AnimationState::BLOCKS_DROP:
|
||||
renderExplosion();
|
||||
break;
|
||||
|
||||
case AnimationState::IDLE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void LineEffect::renderFlash(int gridX, int gridY, int blockSize) {
|
||||
// Create a flashing white effect with varying opacity
|
||||
float progress = timer / FLASH_DURATION;
|
||||
float flashIntensity = std::sin(progress * M_PI * 6.0f) * 0.5f + 0.5f; // Fewer flashes for quicker effect
|
||||
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
Uint8 alpha = static_cast<Uint8>(flashIntensity * 180.0f); // Slightly less intense
|
||||
|
||||
for (int row : clearingRows) {
|
||||
// Draw white rectangle covering the entire row with blue glow effect
|
||||
SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha);
|
||||
|
||||
SDL_FRect flashRect = {
|
||||
static_cast<float>(gridX - 4),
|
||||
static_cast<float>(gridY + row * blockSize - 4),
|
||||
static_cast<float>(10 * blockSize + 8),
|
||||
static_cast<float>(blockSize + 8)
|
||||
};
|
||||
|
||||
SDL_RenderFillRect(renderer, &flashRect);
|
||||
|
||||
// Add blue glow border
|
||||
SDL_SetRenderDrawColor(renderer, 100, 150, 255, alpha / 2);
|
||||
for (int i = 1; i <= 3; ++i) {
|
||||
SDL_FRect glowRect = {
|
||||
flashRect.x - i,
|
||||
flashRect.y - i,
|
||||
flashRect.w + 2*i,
|
||||
flashRect.h + 2*i
|
||||
};
|
||||
SDL_RenderRect(renderer, &glowRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LineEffect::renderExplosion() {
|
||||
for (auto& particle : particles) {
|
||||
particle.render(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
void LineEffect::playLineClearSound(int lineCount) {
|
||||
if (!audioStream) {
|
||||
// Create audio stream for sound effects
|
||||
SDL_AudioSpec spec = {};
|
||||
spec.format = SDL_AUDIO_S16;
|
||||
spec.channels = 2;
|
||||
spec.freq = 44100;
|
||||
|
||||
audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
|
||||
if (!audioStream) {
|
||||
printf("Warning: Could not create audio stream for line clear effects\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Choose appropriate sound based on line count
|
||||
const std::vector<int16_t>* sample = nullptr;
|
||||
|
||||
if (lineCount == 4) {
|
||||
sample = &tetrisSample; // Special sound for Tetris
|
||||
printf("TETRIS! 4 lines cleared!\n");
|
||||
} else {
|
||||
sample = &lineClearSample; // Regular line clear sound
|
||||
printf("Line clear: %d lines\n", lineCount);
|
||||
}
|
||||
|
||||
if (sample && !sample->empty()) {
|
||||
SDL_PutAudioStreamData(audioStream, sample->data(),
|
||||
static_cast<int>(sample->size() * sizeof(int16_t)));
|
||||
}
|
||||
}
|
||||
71
src/LineEffect.h
Normal file
71
src/LineEffect.h
Normal file
@ -0,0 +1,71 @@
|
||||
// LineEffect.h - Line clearing visual and audio effects
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
#include <vector>
|
||||
#include <random>
|
||||
|
||||
class LineEffect {
|
||||
public:
|
||||
struct Particle {
|
||||
float x, y;
|
||||
float vx, vy;
|
||||
float size;
|
||||
float alpha;
|
||||
SDL_Color color;
|
||||
|
||||
Particle(float px, float py);
|
||||
void update();
|
||||
void render(SDL_Renderer* renderer);
|
||||
bool isAlive() const { return alpha > 0.0f; }
|
||||
};
|
||||
|
||||
enum class AnimationState {
|
||||
IDLE,
|
||||
FLASH_WHITE,
|
||||
EXPLODE_BLOCKS,
|
||||
BLOCKS_DROP
|
||||
};
|
||||
|
||||
LineEffect();
|
||||
~LineEffect();
|
||||
|
||||
bool init(SDL_Renderer* renderer);
|
||||
void shutdown();
|
||||
|
||||
// Start line clear effect for the specified rows
|
||||
void startLineClear(const std::vector<int>& rows, int gridX, int gridY, int blockSize);
|
||||
|
||||
// Update and render the effect
|
||||
bool update(float deltaTime); // Returns true if effect is complete
|
||||
void render(SDL_Renderer* renderer, int gridX, int gridY, int blockSize);
|
||||
|
||||
// Audio
|
||||
void playLineClearSound(int lineCount);
|
||||
|
||||
bool isActive() const { return state != AnimationState::IDLE; }
|
||||
|
||||
private:
|
||||
SDL_Renderer* renderer;
|
||||
AnimationState state;
|
||||
float timer;
|
||||
std::vector<int> clearingRows;
|
||||
std::vector<Particle> particles;
|
||||
std::mt19937 rng;
|
||||
|
||||
// Audio resources
|
||||
SDL_AudioStream* audioStream;
|
||||
std::vector<int16_t> lineClearSample;
|
||||
std::vector<int16_t> tetrisSample;
|
||||
|
||||
// Animation timing - Flash then immediate explosion effect
|
||||
static constexpr float FLASH_DURATION = 0.12f; // Very brief white flash
|
||||
static constexpr float EXPLODE_DURATION = 0.15f; // Quick explosive effect
|
||||
static constexpr float DROP_DURATION = 0.05f; // Almost instant block drop
|
||||
|
||||
void createParticles(int row, int gridX, int gridY, int blockSize);
|
||||
void updateParticles();
|
||||
void renderFlash(int gridX, int gridY, int blockSize);
|
||||
void renderExplosion();
|
||||
bool loadAudioSample(const std::string& path, std::vector<int16_t>& sample);
|
||||
void initAudio();
|
||||
};
|
||||
81
src/Scores.cpp
Normal file
81
src/Scores.cpp
Normal file
@ -0,0 +1,81 @@
|
||||
// Scores.cpp - Implementation of ScoreManager
|
||||
#include "Scores.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
|
||||
ScoreManager::ScoreManager(size_t maxScores) : maxEntries(maxScores) {}
|
||||
|
||||
std::string ScoreManager::filePath() const {
|
||||
static std::string path; if (!path.empty()) return path;
|
||||
char* base = SDL_GetPrefPath("example","tetris_sdl3");
|
||||
if (base) { path = std::string(base)+"highscores.txt"; SDL_free(base);} else path="highscores.txt";
|
||||
return path;
|
||||
}
|
||||
|
||||
void ScoreManager::load() {
|
||||
scores.clear();
|
||||
std::ifstream f(filePath());
|
||||
if (!f) {
|
||||
// Create sample high scores if file doesn't exist
|
||||
createSampleScores();
|
||||
save();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
std::istringstream iss(line);
|
||||
ScoreEntry e;
|
||||
iss >> e.score >> e.lines >> e.level >> e.timeSec;
|
||||
if (iss) {
|
||||
// Try to read name (rest of line after timeSec)
|
||||
std::string remaining;
|
||||
std::getline(iss, remaining);
|
||||
if (!remaining.empty() && remaining[0] == ' ') {
|
||||
e.name = remaining.substr(1); // Remove leading space
|
||||
}
|
||||
scores.push_back(e);
|
||||
}
|
||||
if (scores.size() >= maxEntries) break;
|
||||
}
|
||||
|
||||
if (scores.empty()) {
|
||||
createSampleScores();
|
||||
save();
|
||||
}
|
||||
|
||||
std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;});
|
||||
}
|
||||
|
||||
void ScoreManager::save() const {
|
||||
std::ofstream f(filePath(), std::ios::trunc);
|
||||
for (auto &e : scores) {
|
||||
f << e.score << ' ' << e.lines << ' ' << e.level << ' ' << e.timeSec << ' ' << e.name << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void ScoreManager::submit(int score, int lines, int level, double timeSec) {
|
||||
scores.push_back(ScoreEntry{score,lines,level,timeSec});
|
||||
std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;});
|
||||
if (scores.size()>maxEntries) scores.resize(maxEntries);
|
||||
save();
|
||||
}
|
||||
|
||||
void ScoreManager::createSampleScores() {
|
||||
scores = {
|
||||
{159840, 189, 14, 972, "GREGOR"},
|
||||
{156340, 132, 12, 714, "GREGOR"},
|
||||
{155219, 125, 12, 696, "GREGOR"},
|
||||
{141823, 123, 10, 710, "GREGOR"},
|
||||
{140079, 71, 11, 410, "GREGOR"},
|
||||
{116012, 121, 10, 619, "GREGOR"},
|
||||
{112643, 137, 13, 689, "GREGOR"},
|
||||
{99190, 61, 10, 378, "GREGOR"},
|
||||
{93648, 107, 10, 629, "GREGOR"},
|
||||
{89041, 115, 10, 618, "GREGOR"},
|
||||
{88600, 55, 9, 354, "GREGOR"},
|
||||
{86346, 141, 13, 723, "GREGOR"}
|
||||
};
|
||||
}
|
||||
20
src/Scores.h
Normal file
20
src/Scores.h
Normal file
@ -0,0 +1,20 @@
|
||||
// Scores.h - High score persistence manager
|
||||
#pragma once
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
struct ScoreEntry { int score{}; int lines{}; int level{}; double timeSec{}; std::string name{"PLAYER"}; };
|
||||
|
||||
class ScoreManager {
|
||||
public:
|
||||
explicit ScoreManager(size_t maxScores = 12);
|
||||
void load();
|
||||
void save() const;
|
||||
void submit(int score, int lines, int level, double timeSec);
|
||||
const std::vector<ScoreEntry>& all() const { return scores; }
|
||||
private:
|
||||
std::vector<ScoreEntry> scores;
|
||||
size_t maxEntries;
|
||||
std::string filePath() const; // resolve path (SDL pref path or local)
|
||||
void createSampleScores(); // create sample high scores
|
||||
};
|
||||
319
src/SoundEffect.cpp
Normal file
319
src/SoundEffect.cpp
Normal file
@ -0,0 +1,319 @@
|
||||
// SoundEffect.cpp - Implementation of sound effects system
|
||||
#include "SoundEffect.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include "Audio.h"
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <mfapi.h>
|
||||
#include <mfidl.h>
|
||||
#include <mfreadwrite.h>
|
||||
#include <objbase.h>
|
||||
#include <wrl/client.h>
|
||||
#pragma comment(lib, "mfplat.lib")
|
||||
#pragma comment(lib, "mfreadwrite.lib")
|
||||
#pragma comment(lib, "mfuuid.lib")
|
||||
#pragma comment(lib, "ole32.lib")
|
||||
using Microsoft::WRL::ComPtr;
|
||||
#endif
|
||||
|
||||
// SoundEffect implementation
|
||||
bool SoundEffect::load(const std::string& filePath) {
|
||||
// Determine file type by extension
|
||||
std::string extension = filePath.substr(filePath.find_last_of('.') + 1);
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
|
||||
|
||||
bool success = false;
|
||||
if (extension == "wav") {
|
||||
success = loadWAV(filePath);
|
||||
} else if (extension == "mp3") {
|
||||
success = loadMP3(filePath);
|
||||
} else {
|
||||
std::fprintf(stderr, "[SoundEffect] Unsupported file format: %s\n", extension.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
std::printf("[SoundEffect] Loaded: %s (%d channels, %d Hz, %zu samples)\n",
|
||||
filePath.c_str(), channels, sampleRate, pcmData.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundEffect::play(float volume) {
|
||||
if (!loaded || pcmData.empty()) {
|
||||
std::printf("[SoundEffect] Cannot play - loaded=%d, pcmData.size()=%zu\n", loaded, pcmData.size());
|
||||
return;
|
||||
}
|
||||
|
||||
std::printf("[SoundEffect] Playing sound with %zu samples at volume %.2f\n", pcmData.size(), volume);
|
||||
|
||||
// Calculate final volume
|
||||
float finalVolume = defaultVolume * volume;
|
||||
finalVolume = (std::max)(0.0f, (std::min)(1.0f, finalVolume));
|
||||
|
||||
// Use the simple audio player to play this sound
|
||||
SimpleAudioPlayer::instance().playSound(pcmData, channels, sampleRate, finalVolume);
|
||||
}
|
||||
|
||||
void SoundEffect::setVolume(float volume) {
|
||||
defaultVolume = (std::max)(0.0f, (std::min)(1.0f, volume));
|
||||
}
|
||||
|
||||
// SimpleAudioPlayer implementation
|
||||
SimpleAudioPlayer& SimpleAudioPlayer::instance() {
|
||||
static SimpleAudioPlayer inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
bool SimpleAudioPlayer::init() {
|
||||
if (initialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
std::printf("[SimpleAudioPlayer] Initialized\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
void SimpleAudioPlayer::shutdown() {
|
||||
initialized = false;
|
||||
std::printf("[SimpleAudioPlayer] Shut down\n");
|
||||
}
|
||||
|
||||
void SimpleAudioPlayer::playSound(const std::vector<int16_t>& pcmData, int channels, int sampleRate, float volume) {
|
||||
if (!initialized || pcmData.empty()) {
|
||||
return;
|
||||
}
|
||||
// Route through shared Audio mixer so SFX always play over music
|
||||
Audio::instance().playSfx(pcmData, channels, sampleRate, volume);
|
||||
}
|
||||
|
||||
bool SoundEffect::loadWAV(const std::string& filePath) {
|
||||
SDL_AudioSpec wavSpec;
|
||||
Uint8* wavBuffer;
|
||||
Uint32 wavLength;
|
||||
|
||||
if (!SDL_LoadWAV(filePath.c_str(), &wavSpec, &wavBuffer, &wavLength)) {
|
||||
std::fprintf(stderr, "[SoundEffect] Failed to load WAV file %s: %s\n",
|
||||
filePath.c_str(), SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store audio format info
|
||||
channels = wavSpec.channels;
|
||||
sampleRate = wavSpec.freq;
|
||||
|
||||
// Convert to 16-bit signed if needed
|
||||
if (wavSpec.format == SDL_AUDIO_S16) {
|
||||
// Already in the right format
|
||||
size_t samples = wavLength / sizeof(int16_t);
|
||||
pcmData.resize(samples);
|
||||
std::memcpy(pcmData.data(), wavBuffer, wavLength);
|
||||
} else {
|
||||
// Need to convert format
|
||||
SDL_AudioSpec srcSpec = wavSpec;
|
||||
SDL_AudioSpec dstSpec = wavSpec;
|
||||
dstSpec.format = SDL_AUDIO_S16;
|
||||
|
||||
SDL_AudioStream* converter = SDL_CreateAudioStream(&srcSpec, &dstSpec);
|
||||
if (converter) {
|
||||
SDL_PutAudioStreamData(converter, wavBuffer, wavLength);
|
||||
SDL_FlushAudioStream(converter);
|
||||
|
||||
int convertedLength = SDL_GetAudioStreamAvailable(converter);
|
||||
if (convertedLength > 0) {
|
||||
pcmData.resize(convertedLength / sizeof(int16_t));
|
||||
SDL_GetAudioStreamData(converter, pcmData.data(), convertedLength);
|
||||
}
|
||||
|
||||
SDL_DestroyAudioStream(converter);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_free(wavBuffer);
|
||||
return !pcmData.empty();
|
||||
}
|
||||
|
||||
bool SoundEffect::loadMP3(const std::string& filePath) {
|
||||
#ifdef _WIN32
|
||||
static bool mfInitialized = false;
|
||||
if (!mfInitialized) {
|
||||
if (FAILED(MFStartup(MF_VERSION))) {
|
||||
std::fprintf(stderr, "[SoundEffect] MFStartup failed\n");
|
||||
return false;
|
||||
}
|
||||
mfInitialized = true;
|
||||
}
|
||||
|
||||
ComPtr<IMFSourceReader> reader;
|
||||
wchar_t wpath[MAX_PATH];
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, filePath.c_str(), -1, wpath, MAX_PATH);
|
||||
if (!wlen) {
|
||||
std::fprintf(stderr, "[SoundEffect] Failed to convert path to wide char\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FAILED(MFCreateSourceReaderFromURL(wpath, nullptr, &reader))) {
|
||||
std::fprintf(stderr, "[SoundEffect] Failed to create source reader for %s\n", filePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Request PCM output
|
||||
ComPtr<IMFMediaType> outType;
|
||||
MFCreateMediaType(&outType);
|
||||
outType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
|
||||
outType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM);
|
||||
outType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, 2);
|
||||
outType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, 44100);
|
||||
outType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 4);
|
||||
outType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 44100 * 4);
|
||||
outType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 16);
|
||||
outType->SetUINT32(MF_MT_AUDIO_CHANNEL_MASK, 3);
|
||||
reader->SetCurrentMediaType((DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, NULL, outType.Get());
|
||||
reader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE);
|
||||
|
||||
// Read all samples
|
||||
std::vector<int16_t> tempData;
|
||||
while (true) {
|
||||
DWORD flags = 0;
|
||||
ComPtr<IMFSample> sample;
|
||||
if (FAILED(reader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, nullptr, &flags, nullptr, &sample))) {
|
||||
break;
|
||||
}
|
||||
if (flags & MF_SOURCE_READERF_ENDOFSTREAM) {
|
||||
break;
|
||||
}
|
||||
if (!sample) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ComPtr<IMFMediaBuffer> buffer;
|
||||
if (FAILED(sample->ConvertToContiguousBuffer(&buffer))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BYTE* data = nullptr;
|
||||
DWORD maxLen = 0, curLen = 0;
|
||||
if (SUCCEEDED(buffer->Lock(&data, &maxLen, &curLen)) && curLen) {
|
||||
size_t samples = curLen / 2;
|
||||
size_t oldSize = tempData.size();
|
||||
tempData.resize(oldSize + samples);
|
||||
std::memcpy(tempData.data() + oldSize, data, curLen);
|
||||
}
|
||||
if (data) {
|
||||
buffer->Unlock();
|
||||
}
|
||||
}
|
||||
|
||||
if (tempData.empty()) {
|
||||
std::fprintf(stderr, "[SoundEffect] No audio data decoded from %s\n", filePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
pcmData = std::move(tempData);
|
||||
channels = 2;
|
||||
sampleRate = 44100;
|
||||
return true;
|
||||
#else
|
||||
std::fprintf(stderr, "[SoundEffect] MP3 support not available on this platform\n");
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// SoundEffectManager implementation
|
||||
SoundEffectManager& SoundEffectManager::instance() {
|
||||
static SoundEffectManager inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
bool SoundEffectManager::init() {
|
||||
if (initialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Initialize the simple audio player
|
||||
SimpleAudioPlayer::instance().init();
|
||||
|
||||
initialized = true;
|
||||
std::printf("[SoundEffectManager] Initialized\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundEffectManager::shutdown() {
|
||||
soundEffects.clear();
|
||||
SimpleAudioPlayer::instance().shutdown();
|
||||
initialized = false;
|
||||
std::printf("[SoundEffectManager] Shut down\n");
|
||||
}
|
||||
|
||||
bool SoundEffectManager::loadSound(const std::string& id, const std::string& filePath) {
|
||||
if (!initialized) {
|
||||
std::fprintf(stderr, "[SoundEffectManager] Not initialized\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto soundEffect = std::make_unique<SoundEffect>();
|
||||
if (!soundEffect->load(filePath)) {
|
||||
std::fprintf(stderr, "[SoundEffectManager] Failed to load sound: %s\n", filePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove existing sound with the same ID
|
||||
soundEffects.erase(
|
||||
std::remove_if(soundEffects.begin(), soundEffects.end(),
|
||||
[&id](const auto& pair) { return pair.first == id; }),
|
||||
soundEffects.end());
|
||||
|
||||
soundEffects.emplace_back(id, std::move(soundEffect));
|
||||
std::printf("[SoundEffectManager] Loaded sound '%s' from %s\n", id.c_str(), filePath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundEffectManager::playSound(const std::string& id, float volume) {
|
||||
if (!enabled || !initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = std::find_if(soundEffects.begin(), soundEffects.end(),
|
||||
[&id](const auto& pair) { return pair.first == id; });
|
||||
|
||||
if (it != soundEffects.end()) {
|
||||
it->second->play(volume * masterVolume);
|
||||
} else {
|
||||
std::fprintf(stderr, "[SoundEffectManager] Sound not found: %s\n", id.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void SoundEffectManager::playRandomSound(const std::vector<std::string>& soundIds, float volume) {
|
||||
if (!enabled || !initialized || soundIds.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> dis(0, soundIds.size() - 1);
|
||||
|
||||
const std::string& selectedId = soundIds[dis(gen)];
|
||||
playSound(selectedId, volume);
|
||||
}
|
||||
|
||||
void SoundEffectManager::setMasterVolume(float volume) {
|
||||
masterVolume = (std::max)(0.0f, (std::min)(1.0f, volume));
|
||||
}
|
||||
|
||||
void SoundEffectManager::setEnabled(bool enabled_) {
|
||||
enabled = enabled_;
|
||||
}
|
||||
|
||||
bool SoundEffectManager::isEnabled() const {
|
||||
return enabled;
|
||||
}
|
||||
94
src/SoundEffect.h
Normal file
94
src/SoundEffect.h
Normal file
@ -0,0 +1,94 @@
|
||||
// SoundEffect.h - Single sound effect player using SDL3 audio
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
class SoundEffect {
|
||||
public:
|
||||
SoundEffect() = default;
|
||||
~SoundEffect() = default;
|
||||
|
||||
// Load a sound effect from file (WAV or MP3)
|
||||
bool load(const std::string& filePath);
|
||||
|
||||
// Play the sound effect
|
||||
void play(float volume = 1.0f);
|
||||
|
||||
// Set default volume for this sound effect
|
||||
void setVolume(float volume);
|
||||
|
||||
// Get PCM data for mixing
|
||||
const std::vector<int16_t>& getPCMData() const { return pcmData; }
|
||||
int getChannels() const { return channels; }
|
||||
int getSampleRate() const { return sampleRate; }
|
||||
bool isLoaded() const { return loaded; }
|
||||
|
||||
private:
|
||||
std::vector<int16_t> pcmData;
|
||||
int channels = 2;
|
||||
int sampleRate = 44100;
|
||||
bool loaded = false;
|
||||
float defaultVolume = 1.0f;
|
||||
|
||||
bool loadWAV(const std::string& filePath);
|
||||
bool loadMP3(const std::string& filePath);
|
||||
};
|
||||
|
||||
// Simple audio player for immediate playback
|
||||
class SimpleAudioPlayer {
|
||||
public:
|
||||
static SimpleAudioPlayer& instance();
|
||||
|
||||
bool init();
|
||||
void shutdown();
|
||||
|
||||
// Play a sound effect immediately
|
||||
void playSound(const std::vector<int16_t>& pcmData, int channels, int sampleRate, float volume = 1.0f);
|
||||
|
||||
private:
|
||||
SimpleAudioPlayer() = default;
|
||||
~SimpleAudioPlayer() = default;
|
||||
SimpleAudioPlayer(const SimpleAudioPlayer&) = delete;
|
||||
SimpleAudioPlayer& operator=(const SimpleAudioPlayer&) = delete;
|
||||
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
// Helper class to manage multiple sound effects
|
||||
class SoundEffectManager {
|
||||
public:
|
||||
static SoundEffectManager& instance();
|
||||
|
||||
bool init();
|
||||
void shutdown();
|
||||
|
||||
// Load a sound effect and assign it an ID
|
||||
bool loadSound(const std::string& id, const std::string& filePath);
|
||||
|
||||
// Play a sound effect by ID
|
||||
void playSound(const std::string& id, float volume = 1.0f);
|
||||
|
||||
// Play a random sound from a group
|
||||
void playRandomSound(const std::vector<std::string>& soundIds, float volume = 1.0f);
|
||||
|
||||
// Set master volume for all sound effects
|
||||
void setMasterVolume(float volume);
|
||||
|
||||
// Enable/disable sound effects
|
||||
void setEnabled(bool enabled);
|
||||
bool isEnabled() const;
|
||||
|
||||
private:
|
||||
SoundEffectManager() = default;
|
||||
~SoundEffectManager() = default;
|
||||
SoundEffectManager(const SoundEffectManager&) = delete;
|
||||
SoundEffectManager& operator=(const SoundEffectManager&) = delete;
|
||||
|
||||
std::vector<std::pair<std::string, std::unique_ptr<SoundEffect>>> soundEffects;
|
||||
float masterVolume = 1.0f;
|
||||
bool enabled = true;
|
||||
bool initialized = false;
|
||||
};
|
||||
41
src/Starfield.cpp
Normal file
41
src/Starfield.cpp
Normal file
@ -0,0 +1,41 @@
|
||||
// Starfield.cpp - implementation
|
||||
#include "Starfield.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <random>
|
||||
|
||||
void Starfield::init(int count, int w, int h)
|
||||
{
|
||||
stars.clear();
|
||||
stars.reserve(count);
|
||||
std::mt19937 rng{std::random_device{}()};
|
||||
std::uniform_real_distribution<float> dx(0.f, (float)w), dy(0.f, (float)h), dz(0.2f, 1.f);
|
||||
for (int i = 0; i < count; ++i)
|
||||
stars.push_back({dx(rng), dy(rng), dz(rng), 15.f + 35.f * dz(rng)});
|
||||
lastW = w;
|
||||
lastH = h;
|
||||
}
|
||||
|
||||
void Starfield::update(float dt, int w, int h)
|
||||
{
|
||||
if (w != lastW || h != lastH || stars.empty())
|
||||
init((w * h) / 8000 + 120, w, h);
|
||||
for (auto &s : stars)
|
||||
{
|
||||
s.y += s.speed * dt;
|
||||
if (s.y > h)
|
||||
{
|
||||
s.y -= h;
|
||||
s.x = (float)(rand() % w);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Starfield::draw(SDL_Renderer *r) const
|
||||
{
|
||||
SDL_SetRenderDrawColor(r, 255, 255, 255, 255);
|
||||
for (auto &s : stars)
|
||||
{
|
||||
SDL_FRect fr{s.x, s.y, 1.f * s.z, 1.f * s.z};
|
||||
SDL_RenderFillRect(r, &fr);
|
||||
}
|
||||
}
|
||||
15
src/Starfield.h
Normal file
15
src/Starfield.h
Normal file
@ -0,0 +1,15 @@
|
||||
// Starfield.h - Procedural starfield background effect
|
||||
#pragma once
|
||||
#include <vector>
|
||||
struct SDL_Renderer; // fwd
|
||||
|
||||
class Starfield {
|
||||
public:
|
||||
void init(int count, int w, int h);
|
||||
void update(float dt, int w, int h);
|
||||
void draw(SDL_Renderer* r) const;
|
||||
private:
|
||||
struct Star { float x,y,z,speed; };
|
||||
std::vector<Star> stars;
|
||||
int lastW{0}, lastH{0};
|
||||
};
|
||||
164
src/Starfield3D.cpp
Normal file
164
src/Starfield3D.cpp
Normal file
@ -0,0 +1,164 @@
|
||||
// Starfield3D.cpp - 3D Parallax Starfield Implementation
|
||||
#include "Starfield3D.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
Starfield3D::Starfield3D() : rng(std::random_device{}()), width(800), height(600), centerX(400), centerY(300) {
|
||||
}
|
||||
|
||||
void Starfield3D::init(int w, int h, int starCount) {
|
||||
width = w;
|
||||
height = h;
|
||||
centerX = width * 0.5f;
|
||||
centerY = height * 0.5f;
|
||||
|
||||
stars.resize(starCount);
|
||||
createStarfield();
|
||||
}
|
||||
|
||||
void Starfield3D::resize(int w, int h) {
|
||||
width = w;
|
||||
height = h;
|
||||
centerX = width * 0.5f;
|
||||
centerY = height * 0.5f;
|
||||
}
|
||||
|
||||
float Starfield3D::randomFloat(float min, float max) {
|
||||
std::uniform_real_distribution<float> dist(min, max);
|
||||
return dist(rng);
|
||||
}
|
||||
|
||||
int Starfield3D::randomRange(int min, int max) {
|
||||
std::uniform_int_distribution<int> dist(min, max - 1);
|
||||
return dist(rng);
|
||||
}
|
||||
|
||||
void Starfield3D::setRandomDirection(Star3D& star) {
|
||||
star.targetVx = randomFloat(-MAX_VELOCITY, MAX_VELOCITY);
|
||||
star.targetVy = randomFloat(-MAX_VELOCITY, MAX_VELOCITY);
|
||||
|
||||
// Allow stars to move both toward and away from viewer
|
||||
if (randomFloat(0.0f, 1.0f) < REVERSE_PROBABILITY) {
|
||||
// Move away from viewer (positive Z)
|
||||
star.targetVz = STAR_SPEED * randomFloat(0.5f, 1.0f);
|
||||
} else {
|
||||
// Move toward viewer (negative Z)
|
||||
star.targetVz = -STAR_SPEED * randomFloat(0.7f, 1.3f);
|
||||
}
|
||||
|
||||
star.changing = true;
|
||||
star.changeTimer = randomFloat(30.0f, 120.0f); // Direction change lasts 30-120 frames
|
||||
}
|
||||
|
||||
void Starfield3D::updateStar(int index) {
|
||||
Star3D& star = stars[index];
|
||||
|
||||
star.x = randomFloat(-25.0f, 25.0f);
|
||||
star.y = randomFloat(-25.0f, 25.0f);
|
||||
star.z = randomFloat(1.0f, MAX_DEPTH);
|
||||
|
||||
// Give stars initial velocities in all possible directions
|
||||
if (randomFloat(0.0f, 1.0f) < 0.5f) {
|
||||
// Half stars start moving toward viewer
|
||||
star.vx = randomFloat(-0.1f, 0.1f);
|
||||
star.vy = randomFloat(-0.1f, 0.1f);
|
||||
star.vz = -STAR_SPEED * randomFloat(0.8f, 1.2f);
|
||||
} else {
|
||||
// Half stars start moving in random directions
|
||||
star.vx = randomFloat(-0.2f, 0.2f);
|
||||
star.vy = randomFloat(-0.2f, 0.2f);
|
||||
|
||||
// 30% chance to start moving away
|
||||
if (randomFloat(0.0f, 1.0f) < 0.3f) {
|
||||
star.vz = STAR_SPEED * randomFloat(0.5f, 0.8f);
|
||||
} else {
|
||||
star.vz = -STAR_SPEED * randomFloat(0.8f, 1.2f);
|
||||
}
|
||||
}
|
||||
|
||||
star.targetVx = star.vx;
|
||||
star.targetVy = star.vy;
|
||||
star.targetVz = star.vz;
|
||||
star.changing = false;
|
||||
star.changeTimer = 0.0f;
|
||||
star.type = randomRange(0, COLOR_COUNT);
|
||||
|
||||
// Give some stars initial direction variations
|
||||
if (randomFloat(0.0f, 1.0f) < 0.4f) {
|
||||
setRandomDirection(star);
|
||||
}
|
||||
}
|
||||
|
||||
void Starfield3D::createStarfield() {
|
||||
for (size_t i = 0; i < stars.size(); ++i) {
|
||||
updateStar(static_cast<int>(i));
|
||||
}
|
||||
}
|
||||
|
||||
void Starfield3D::update(float deltaTime) {
|
||||
const float frameRate = 60.0f; // Target 60 FPS for consistency
|
||||
const float frameMultiplier = deltaTime * frameRate;
|
||||
|
||||
for (size_t i = 0; i < stars.size(); ++i) {
|
||||
Star3D& star = stars[i];
|
||||
|
||||
// Randomly change direction occasionally
|
||||
if (!star.changing && randomFloat(0.0f, 1.0f) < DIRECTION_CHANGE_PROBABILITY * frameMultiplier) {
|
||||
setRandomDirection(star);
|
||||
}
|
||||
|
||||
// Update velocities to approach target values
|
||||
if (star.changing) {
|
||||
// Smoothly transition to target velocities
|
||||
const float change = VELOCITY_CHANGE * frameMultiplier;
|
||||
star.vx += (star.targetVx - star.vx) * change;
|
||||
star.vy += (star.targetVy - star.vy) * change;
|
||||
star.vz += (star.targetVz - star.vz) * change;
|
||||
|
||||
// Decrement change timer
|
||||
star.changeTimer -= frameMultiplier;
|
||||
if (star.changeTimer <= 0.0f) {
|
||||
star.changing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update position using current velocity
|
||||
star.x += star.vx * frameMultiplier;
|
||||
star.y += star.vy * frameMultiplier;
|
||||
star.z += star.vz * frameMultiplier;
|
||||
|
||||
// Handle boundaries - reset star if it moves out of bounds, too close, or too far
|
||||
if (star.z <= MIN_Z ||
|
||||
star.z >= MAX_Z ||
|
||||
std::abs(star.x) > 50.0f ||
|
||||
std::abs(star.y) > 50.0f) {
|
||||
updateStar(static_cast<int>(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Starfield3D::drawStar(SDL_Renderer* renderer, float x, float y, int type) {
|
||||
const SDL_Color& color = STAR_COLORS[type % COLOR_COUNT];
|
||||
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
|
||||
|
||||
// Draw star as a small rectangle (1x1 pixel)
|
||||
SDL_FRect rect{x, y, 1.0f, 1.0f};
|
||||
SDL_RenderFillRect(renderer, &rect);
|
||||
}
|
||||
|
||||
void Starfield3D::draw(SDL_Renderer* renderer) {
|
||||
for (const Star3D& star : stars) {
|
||||
// Calculate perspective projection factor
|
||||
const float k = DEPTH_FACTOR / star.z;
|
||||
|
||||
// Calculate screen position with perspective
|
||||
const float px = star.x * k + centerX;
|
||||
const float py = star.y * k + centerY;
|
||||
|
||||
// Only draw stars that are within the viewport
|
||||
if (px >= 0.0f && px <= static_cast<float>(width) &&
|
||||
py >= 0.0f && py <= static_cast<float>(height)) {
|
||||
drawStar(renderer, px, py, star.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/Starfield3D.h
Normal file
77
src/Starfield3D.h
Normal file
@ -0,0 +1,77 @@
|
||||
// Starfield3D.h - 3D Parallax Starfield Effect
|
||||
// Creates a parallax starfield effect that simulates 3D space movement
|
||||
// By projecting 2D coordinates into 3D space with perspective
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <vector>
|
||||
#include <random>
|
||||
|
||||
class Starfield3D {
|
||||
public:
|
||||
Starfield3D();
|
||||
~Starfield3D() = default;
|
||||
|
||||
// Initialize the starfield with dimensions
|
||||
void init(int width, int height, int starCount = 160);
|
||||
|
||||
// Update starfield animation (call every frame)
|
||||
void update(float deltaTime);
|
||||
|
||||
// Draw the starfield
|
||||
void draw(SDL_Renderer* renderer);
|
||||
|
||||
// Update dimensions when window resizes
|
||||
void resize(int width, int height);
|
||||
|
||||
private:
|
||||
// Star representation in 3D space
|
||||
struct Star3D {
|
||||
float x, y, z; // 3D position
|
||||
float vx, vy, vz; // Current velocities
|
||||
float targetVx, targetVy, targetVz; // Target velocities for smooth transitions
|
||||
bool changing; // Whether star is currently changing direction
|
||||
float changeTimer; // Timer for direction change duration
|
||||
int type; // Star type (determines color/brightness)
|
||||
|
||||
Star3D() : x(0), y(0), z(0), vx(0), vy(0), vz(0),
|
||||
targetVx(0), targetVy(0), targetVz(0),
|
||||
changing(false), changeTimer(0), type(0) {}
|
||||
};
|
||||
|
||||
// Configuration constants
|
||||
static constexpr float MAX_DEPTH = 32.0f;
|
||||
static constexpr float STAR_SPEED = 0.4f;
|
||||
static constexpr float DEPTH_FACTOR = 256.0f;
|
||||
static constexpr float MIN_Z = 0.1f;
|
||||
static constexpr float MAX_Z = 50.0f;
|
||||
static constexpr float DIRECTION_CHANGE_PROBABILITY = 0.008f;
|
||||
static constexpr float MAX_VELOCITY = 0.3f;
|
||||
static constexpr float VELOCITY_CHANGE = 0.03f;
|
||||
static constexpr float REVERSE_PROBABILITY = 0.4f;
|
||||
|
||||
// Private methods
|
||||
void createStarfield();
|
||||
void updateStar(int index);
|
||||
void setRandomDirection(Star3D& star);
|
||||
float randomFloat(float min, float max);
|
||||
int randomRange(int min, int max);
|
||||
void drawStar(SDL_Renderer* renderer, float x, float y, int type);
|
||||
|
||||
// Member variables
|
||||
std::vector<Star3D> stars;
|
||||
std::mt19937 rng;
|
||||
|
||||
int width, height;
|
||||
float centerX, centerY;
|
||||
|
||||
// Star colors (RGB values)
|
||||
static constexpr SDL_Color STAR_COLORS[] = {
|
||||
{255, 255, 255, 255}, // White
|
||||
{170, 170, 170, 255}, // Light gray
|
||||
{153, 153, 153, 255}, // Medium gray
|
||||
{119, 119, 119, 255}, // Dark gray
|
||||
{85, 85, 85, 255} // Very dark gray
|
||||
};
|
||||
static constexpr int COLOR_COUNT = 5;
|
||||
};
|
||||
57
src/StateManager.h
Normal file
57
src/StateManager.h
Normal file
@ -0,0 +1,57 @@
|
||||
// StateManager.h - typed app state router with lifecycle hooks
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
enum class AppState {
|
||||
Loading = 0,
|
||||
Menu = 1,
|
||||
LevelSelect = 2,
|
||||
Playing = 3,
|
||||
GameOver = 4,
|
||||
Settings = 5
|
||||
};
|
||||
|
||||
class StateManager {
|
||||
public:
|
||||
using EventHandler = std::function<void(const SDL_Event&)>;
|
||||
using LifecycleHook = std::function<void()>;
|
||||
|
||||
explicit StateManager(AppState initial = AppState::Loading) : current(initial) {}
|
||||
|
||||
void registerHandler(AppState state, EventHandler handler) {
|
||||
handlers[static_cast<int>(state)] = std::move(handler);
|
||||
}
|
||||
|
||||
void registerOnEnter(AppState state, LifecycleHook hook) {
|
||||
onEnterHooks[static_cast<int>(state)] = std::move(hook);
|
||||
}
|
||||
|
||||
void registerOnExit(AppState state, LifecycleHook hook) {
|
||||
onExitHooks[static_cast<int>(state)] = std::move(hook);
|
||||
}
|
||||
|
||||
void setState(AppState state) {
|
||||
// Call exit hook for current state
|
||||
auto it = onExitHooks.find(static_cast<int>(current));
|
||||
if (it != onExitHooks.end() && it->second) it->second();
|
||||
current = state;
|
||||
// Call enter hook for new state
|
||||
auto it2 = onEnterHooks.find(static_cast<int>(current));
|
||||
if (it2 != onEnterHooks.end() && it2->second) it2->second();
|
||||
}
|
||||
|
||||
AppState getState() const { return current; }
|
||||
|
||||
void handleEvent(const SDL_Event& e) const {
|
||||
auto it = handlers.find(static_cast<int>(current));
|
||||
if (it != handlers.end() && it->second) it->second(e);
|
||||
}
|
||||
|
||||
private:
|
||||
AppState current;
|
||||
std::unordered_map<int, EventHandler> handlers;
|
||||
std::unordered_map<int, LifecycleHook> onEnterHooks;
|
||||
std::unordered_map<int, LifecycleHook> onExitHooks;
|
||||
};
|
||||
1633
src/main.cpp
Normal file
1633
src/main.cpp
Normal file
File diff suppressed because it is too large
Load Diff
37
src/states/LoadingState.cpp
Normal file
37
src/states/LoadingState.cpp
Normal file
@ -0,0 +1,37 @@
|
||||
// LoadingState.cpp
|
||||
#include "LoadingState.h"
|
||||
#include "../Game.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <cstdio>
|
||||
|
||||
LoadingState::LoadingState(StateContext& ctx) : State(ctx) {}
|
||||
|
||||
void LoadingState::onEnter() {
|
||||
loadStart = SDL_GetTicks();
|
||||
loadingProgress = 0.0;
|
||||
musicLoaded = false;
|
||||
currentTrackLoading = 0;
|
||||
totalTracks = 0;
|
||||
// Kick off audio loading if available
|
||||
if (ctx.game) {
|
||||
// audio initialization handled elsewhere (main still creates Audio)
|
||||
}
|
||||
}
|
||||
|
||||
void LoadingState::onExit() {
|
||||
}
|
||||
|
||||
void LoadingState::handleEvent(const SDL_Event& e) {
|
||||
(void)e; // no direct event handling in loading screen
|
||||
}
|
||||
|
||||
void LoadingState::update(double frameMs) {
|
||||
// Progress calculation is done in main; keep this simple
|
||||
// This stub allows later migration of Audio::background loading
|
||||
loadingProgress = std::min(1.0, loadingProgress + frameMs / 1000.0);
|
||||
}
|
||||
|
||||
void LoadingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
|
||||
(void)renderer; (void)logicalScale; (void)logicalVP;
|
||||
// Rendering is still performed in main for now; this placeholder keeps the API.
|
||||
}
|
||||
20
src/states/LoadingState.h
Normal file
20
src/states/LoadingState.h
Normal file
@ -0,0 +1,20 @@
|
||||
// LoadingState.h
|
||||
#pragma once
|
||||
#include "State.h"
|
||||
|
||||
class LoadingState : public State {
|
||||
public:
|
||||
LoadingState(StateContext& ctx);
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void handleEvent(const SDL_Event& e) override;
|
||||
void update(double frameMs) override;
|
||||
void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override;
|
||||
|
||||
private:
|
||||
double loadingProgress = 0.0;
|
||||
Uint64 loadStart = 0;
|
||||
bool musicLoaded = false;
|
||||
int currentTrackLoading = 0;
|
||||
int totalTracks = 0;
|
||||
};
|
||||
33
src/states/MenuState.cpp
Normal file
33
src/states/MenuState.cpp
Normal file
@ -0,0 +1,33 @@
|
||||
// MenuState.cpp
|
||||
#include "MenuState.h"
|
||||
#include "../Scores.h"
|
||||
#include "../Font.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <cstdio>
|
||||
|
||||
MenuState::MenuState(StateContext& ctx) : State(ctx) {}
|
||||
|
||||
void MenuState::onEnter() {
|
||||
// nothing for now
|
||||
}
|
||||
|
||||
void MenuState::onExit() {
|
||||
}
|
||||
|
||||
void MenuState::handleEvent(const SDL_Event& e) {
|
||||
// Menu-specific key handling moved from main; main still handles mouse for now
|
||||
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
||||
if (ctx.startLevelSelection && *ctx.startLevelSelection >= 0) {
|
||||
// keep simple: allow L/S toggles handled globally in main for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MenuState::update(double frameMs) {
|
||||
(void)frameMs;
|
||||
}
|
||||
|
||||
void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
|
||||
(void)renderer; (void)logicalScale; (void)logicalVP;
|
||||
// Main still performs actual rendering for now; this placeholder keeps the API.
|
||||
}
|
||||
13
src/states/MenuState.h
Normal file
13
src/states/MenuState.h
Normal file
@ -0,0 +1,13 @@
|
||||
// MenuState.h
|
||||
#pragma once
|
||||
#include "State.h"
|
||||
|
||||
class MenuState : public State {
|
||||
public:
|
||||
MenuState(StateContext& ctx);
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void handleEvent(const SDL_Event& e) override;
|
||||
void update(double frameMs) override;
|
||||
void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override;
|
||||
};
|
||||
51
src/states/State.h
Normal file
51
src/states/State.h
Normal file
@ -0,0 +1,51 @@
|
||||
// State.h - base class and shared context for app states
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
// Forward declarations for frequently used types
|
||||
class Game;
|
||||
class ScoreManager;
|
||||
class Starfield;
|
||||
class Starfield3D;
|
||||
class FontAtlas;
|
||||
class LineEffect;
|
||||
|
||||
// Shared context passed to states so they can access common resources
|
||||
struct StateContext {
|
||||
// Core subsystems (may be null if not available)
|
||||
Game* game = nullptr;
|
||||
ScoreManager* scores = nullptr;
|
||||
Starfield* starfield = nullptr;
|
||||
Starfield3D* starfield3D = nullptr;
|
||||
FontAtlas* font = nullptr;
|
||||
FontAtlas* pixelFont = nullptr;
|
||||
LineEffect* lineEffect = nullptr;
|
||||
|
||||
// Textures
|
||||
SDL_Texture* logoTex = nullptr;
|
||||
SDL_Texture* backgroundTex = nullptr;
|
||||
SDL_Texture* blocksTex = nullptr;
|
||||
|
||||
// Audio / SFX - forward declared types in main
|
||||
// Pointers to booleans/flags used by multiple states
|
||||
bool* musicEnabled = nullptr;
|
||||
int* startLevelSelection = nullptr;
|
||||
int* hoveredButton = nullptr;
|
||||
};
|
||||
|
||||
class State {
|
||||
public:
|
||||
explicit State(StateContext& ctx) : ctx(ctx) {}
|
||||
virtual ~State() = default;
|
||||
|
||||
virtual void onEnter() {}
|
||||
virtual void onExit() {}
|
||||
virtual void handleEvent(const SDL_Event& e) {}
|
||||
virtual void update(double frameMs) {}
|
||||
virtual void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {}
|
||||
|
||||
protected:
|
||||
StateContext& ctx;
|
||||
};
|
||||
17
src/tetris.code-workspace
Normal file
17
src/tetris.code-workspace
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"path": "../../../games/Tetris"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.background": "#59140D",
|
||||
"titleBar.activeBackground": "#7D1D12",
|
||||
"titleBar.activeForeground": "#FFFCFC"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user