used tileset sprite sheet for asteroids

This commit is contained in:
2025-12-20 13:50:56 +01:00
parent 34447f0245
commit 970259e3d6
10 changed files with 178 additions and 31 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

View File

@ -137,6 +137,7 @@ struct TetrisApp::Impl {
int mainScreenH = 0; int mainScreenH = 0;
SDL_Texture* blocksTex = nullptr; SDL_Texture* blocksTex = nullptr;
SDL_Texture* asteroidsTex = nullptr;
SDL_Texture* scorePanelTex = nullptr; SDL_Texture* scorePanelTex = nullptr;
SDL_Texture* statisticsPanelTex = nullptr; SDL_Texture* statisticsPanelTex = nullptr;
SDL_Texture* nextPanelTex = nullptr; SDL_Texture* nextPanelTex = nullptr;
@ -163,6 +164,7 @@ struct TetrisApp::Impl {
std::vector<std::string> tripleSounds; std::vector<std::string> tripleSounds;
std::vector<std::string> tetrisSounds; std::vector<std::string> tetrisSounds;
bool suppressLineVoiceForLevelUp = false; bool suppressLineVoiceForLevelUp = false;
bool skipNextLevelUpJingle = false;
AppState state = AppState::Loading; AppState state = AppState::Loading;
double loadingProgress = 0.0; double loadingProgress = 0.0;
@ -184,12 +186,18 @@ struct TetrisApp::Impl {
float menuFadeAlpha = 0.0f; float menuFadeAlpha = 0.0f;
double MENU_PLAY_FADE_DURATION_MS = 450.0; double MENU_PLAY_FADE_DURATION_MS = 450.0;
AppState menuFadeTarget = AppState::Menu; AppState menuFadeTarget = AppState::Menu;
enum class CountdownSource { MenuStart, ChallengeLevel };
bool menuPlayCountdownArmed = false; bool menuPlayCountdownArmed = false;
bool gameplayCountdownActive = false; bool gameplayCountdownActive = false;
double gameplayCountdownElapsed = 0.0; double gameplayCountdownElapsed = 0.0;
int gameplayCountdownIndex = 0; int gameplayCountdownIndex = 0;
double GAMEPLAY_COUNTDOWN_STEP_MS = 400.0; double GAMEPLAY_COUNTDOWN_STEP_MS = 400.0;
std::array<const char*, 4> GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" }; std::array<const char*, 4> GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" };
CountdownSource gameplayCountdownSource = CountdownSource::MenuStart;
int countdownLevel = 0;
int countdownGoalAsteroids = 0;
bool countdownAdvancesChallenge = false;
double gameplayBackgroundClockMs = 0.0; double gameplayBackgroundClockMs = 0.0;
std::unique_ptr<StateManager> stateMgr; std::unique_ptr<StateManager> stateMgr;
@ -369,8 +377,12 @@ int TetrisApp::Impl::init()
}); });
game->setLevelUpCallback([this](int /*newLevel*/) { game->setLevelUpCallback([this](int /*newLevel*/) {
SoundEffectManager::instance().playSound("new_level", 1.0f); if (skipNextLevelUpJingle) {
SoundEffectManager::instance().playSound("lets_go", 1.0f); skipNextLevelUpJingle = false;
} else {
SoundEffectManager::instance().playSound("new_level", 1.0f);
SoundEffectManager::instance().playSound("lets_go", 1.0f);
}
suppressLineVoiceForLevelUp = true; suppressLineVoiceForLevelUp = true;
}); });
@ -419,6 +431,7 @@ int TetrisApp::Impl::init()
ctx.logoSmallW = logoSmallW; ctx.logoSmallW = logoSmallW;
ctx.logoSmallH = logoSmallH; ctx.logoSmallH = logoSmallH;
ctx.backgroundTex = nullptr; ctx.backgroundTex = nullptr;
ctx.asteroidsTex = asteroidsTex;
ctx.blocksTex = blocksTex; ctx.blocksTex = blocksTex;
ctx.scorePanelTex = scorePanelTex; ctx.scorePanelTex = scorePanelTex;
ctx.statisticsPanelTex = statisticsPanelTex; ctx.statisticsPanelTex = statisticsPanelTex;
@ -989,7 +1002,8 @@ void TetrisApp::Impl::runLoop()
Assets::PANEL_SCORE, Assets::PANEL_SCORE,
Assets::PANEL_STATS, Assets::PANEL_STATS,
Assets::NEXT_PANEL, Assets::NEXT_PANEL,
Assets::HOLD_PANEL Assets::HOLD_PANEL,
Assets::ASTEROID_SPRITE
}; };
for (auto &p : queuedPaths) { for (auto &p : queuedPaths) {
loadingManager->queueTexture(p); loadingManager->queueTexture(p);
@ -1024,6 +1038,7 @@ void TetrisApp::Impl::runLoop()
logoSmallTex = assetLoader.getTexture(Assets::LOGO); logoSmallTex = assetLoader.getTexture(Assets::LOGO);
mainScreenTex = assetLoader.getTexture(Assets::MAIN_SCREEN); mainScreenTex = assetLoader.getTexture(Assets::MAIN_SCREEN);
blocksTex = assetLoader.getTexture(Assets::BLOCKS_SPRITE); blocksTex = assetLoader.getTexture(Assets::BLOCKS_SPRITE);
asteroidsTex = assetLoader.getTexture(Assets::ASTEROID_SPRITE);
scorePanelTex = assetLoader.getTexture(Assets::PANEL_SCORE); scorePanelTex = assetLoader.getTexture(Assets::PANEL_SCORE);
statisticsPanelTex = assetLoader.getTexture(Assets::PANEL_STATS); statisticsPanelTex = assetLoader.getTexture(Assets::PANEL_STATS);
nextPanelTex = assetLoader.getTexture(Assets::NEXT_PANEL); nextPanelTex = assetLoader.getTexture(Assets::NEXT_PANEL);
@ -1056,6 +1071,7 @@ void TetrisApp::Impl::runLoop()
legacyLoad(Assets::LOGO, logoSmallTex, &logoSmallW, &logoSmallH); legacyLoad(Assets::LOGO, logoSmallTex, &logoSmallW, &logoSmallH);
legacyLoad(Assets::MAIN_SCREEN, mainScreenTex, &mainScreenW, &mainScreenH); legacyLoad(Assets::MAIN_SCREEN, mainScreenTex, &mainScreenW, &mainScreenH);
legacyLoad(Assets::BLOCKS_SPRITE, blocksTex); legacyLoad(Assets::BLOCKS_SPRITE, blocksTex);
legacyLoad(Assets::ASTEROID_SPRITE, asteroidsTex);
legacyLoad(Assets::PANEL_SCORE, scorePanelTex); legacyLoad(Assets::PANEL_SCORE, scorePanelTex);
legacyLoad(Assets::PANEL_STATS, statisticsPanelTex); legacyLoad(Assets::PANEL_STATS, statisticsPanelTex);
legacyLoad(Assets::NEXT_PANEL, nextPanelTex); legacyLoad(Assets::NEXT_PANEL, nextPanelTex);
@ -1208,12 +1224,30 @@ void TetrisApp::Impl::runLoop()
break; break;
} }
if (state == AppState::Playing && game && game->getMode() == GameMode::Challenge && !gameplayCountdownActive) {
int queuedLevel = game->consumeQueuedChallengeLevel();
if (queuedLevel > 0) {
gameplayCountdownSource = CountdownSource::ChallengeLevel;
countdownLevel = queuedLevel;
countdownGoalAsteroids = queuedLevel;
countdownAdvancesChallenge = true;
gameplayCountdownActive = true;
menuPlayCountdownArmed = false;
gameplayCountdownElapsed = 0.0;
gameplayCountdownIndex = 0;
game->setPaused(true);
SoundEffectManager::instance().playSound("new_level", 1.0f);
skipNextLevelUpJingle = true;
}
}
ctx.logoTex = logoTex; ctx.logoTex = logoTex;
ctx.logoSmallTex = logoSmallTex; ctx.logoSmallTex = logoSmallTex;
ctx.logoSmallW = logoSmallW; ctx.logoSmallW = logoSmallW;
ctx.logoSmallH = logoSmallH; ctx.logoSmallH = logoSmallH;
ctx.backgroundTex = backgroundTex; ctx.backgroundTex = backgroundTex;
ctx.blocksTex = blocksTex; ctx.blocksTex = blocksTex;
ctx.asteroidsTex = asteroidsTex;
ctx.scorePanelTex = scorePanelTex; ctx.scorePanelTex = scorePanelTex;
ctx.statisticsPanelTex = statisticsPanelTex; ctx.statisticsPanelTex = statisticsPanelTex;
ctx.nextPanelTex = nextPanelTex; ctx.nextPanelTex = nextPanelTex;
@ -1232,6 +1266,12 @@ void TetrisApp::Impl::runLoop()
} }
if (menuFadeTarget == AppState::Playing) { if (menuFadeTarget == AppState::Playing) {
gameplayCountdownSource = (game && game->getMode() == GameMode::Challenge)
? CountdownSource::ChallengeLevel
: CountdownSource::MenuStart;
countdownLevel = game ? game->challengeLevel() : 1;
countdownGoalAsteroids = countdownLevel;
countdownAdvancesChallenge = false;
menuPlayCountdownArmed = true; menuPlayCountdownArmed = true;
gameplayCountdownActive = false; gameplayCountdownActive = false;
gameplayCountdownIndex = 0; gameplayCountdownIndex = 0;
@ -1259,6 +1299,12 @@ void TetrisApp::Impl::runLoop()
} }
if (menuFadePhase == MenuFadePhase::None && menuPlayCountdownArmed && !gameplayCountdownActive && state == AppState::Playing) { if (menuFadePhase == MenuFadePhase::None && menuPlayCountdownArmed && !gameplayCountdownActive && state == AppState::Playing) {
gameplayCountdownSource = (game && game->getMode() == GameMode::Challenge)
? CountdownSource::ChallengeLevel
: CountdownSource::MenuStart;
countdownLevel = game ? game->challengeLevel() : 1;
countdownGoalAsteroids = countdownLevel;
countdownAdvancesChallenge = false;
gameplayCountdownActive = true; gameplayCountdownActive = true;
menuPlayCountdownArmed = false; menuPlayCountdownArmed = false;
gameplayCountdownElapsed = 0.0; gameplayCountdownElapsed = 0.0;
@ -1275,6 +1321,10 @@ void TetrisApp::Impl::runLoop()
gameplayCountdownActive = false; gameplayCountdownActive = false;
gameplayCountdownElapsed = 0.0; gameplayCountdownElapsed = 0.0;
gameplayCountdownIndex = 0; gameplayCountdownIndex = 0;
if (gameplayCountdownSource == CountdownSource::ChallengeLevel && countdownAdvancesChallenge && game) {
game->beginNextChallengeLevel();
}
countdownAdvancesChallenge = false;
game->setPaused(false); game->setPaused(false);
} }
} }
@ -1285,6 +1335,7 @@ void TetrisApp::Impl::runLoop()
menuPlayCountdownArmed = false; menuPlayCountdownArmed = false;
gameplayCountdownElapsed = 0.0; gameplayCountdownElapsed = 0.0;
gameplayCountdownIndex = 0; gameplayCountdownIndex = 0;
countdownAdvancesChallenge = false;
game->setPaused(false); game->setPaused(false);
} }
@ -1520,6 +1571,7 @@ void TetrisApp::Impl::runLoop()
&pixelFont, &pixelFont,
&lineEffect, &lineEffect,
blocksTex, blocksTex,
asteroidsTex,
ctx.statisticsPanelTex, ctx.statisticsPanelTex,
scorePanelTex, scorePanelTex,
nextPanelTex, nextPanelTex,
@ -1674,6 +1726,27 @@ void TetrisApp::Impl::runLoop()
float textX = (winW - static_cast<float>(textW)) * 0.5f; float textX = (winW - static_cast<float>(textW)) * 0.5f;
float textY = (winH - static_cast<float>(textH)) * 0.5f; float textY = (winH - static_cast<float>(textH)) * 0.5f;
if (gameplayCountdownSource == CountdownSource::ChallengeLevel) {
char levelBuf[32];
std::snprintf(levelBuf, sizeof(levelBuf), "LEVEL %d", countdownLevel);
int lvlW = 0, lvlH = 0;
float lvlScale = 2.5f;
pixelFont.measure(levelBuf, lvlScale, lvlW, lvlH);
float levelX = (winW - static_cast<float>(lvlW)) * 0.5f;
float levelY = winH * 0.32f;
pixelFont.draw(renderer, levelX, levelY, levelBuf, lvlScale, SDL_Color{140, 210, 255, 255});
char goalBuf[64];
std::snprintf(goalBuf, sizeof(goalBuf), "ASTEROIDS: %d", countdownGoalAsteroids);
int goalW = 0, goalH = 0;
float goalScale = 1.7f;
pixelFont.measure(goalBuf, goalScale, goalW, goalH);
float goalX = (winW - static_cast<float>(goalW)) * 0.5f;
float goalY = levelY + static_cast<float>(lvlH) + 14.0f;
pixelFont.draw(renderer, goalX, goalY, goalBuf, goalScale, SDL_Color{220, 245, 255, 255});
textY = goalY + static_cast<float>(goalH) + 38.0f;
}
SDL_Color textColor = isFinalCue ? SDL_Color{255, 230, 90, 255} : SDL_Color{255, 255, 255, 255}; SDL_Color textColor = isFinalCue ? SDL_Color{255, 230, 90, 255} : SDL_Color{255, 255, 255, 255};
pixelFont.draw(renderer, textX, textY, label, textScale, textColor); pixelFont.draw(renderer, textX, textY, label, textScale, textColor);

View File

@ -643,6 +643,7 @@ bool ApplicationManager::initializeGame() {
} else { m_stateContext.logoSmallW = 0; m_stateContext.logoSmallH = 0; } } else { m_stateContext.logoSmallW = 0; m_stateContext.logoSmallH = 0; }
m_stateContext.backgroundTex = m_assetManager->getTexture("background"); m_stateContext.backgroundTex = m_assetManager->getTexture("background");
m_stateContext.blocksTex = m_assetManager->getTexture("blocks"); m_stateContext.blocksTex = m_assetManager->getTexture("blocks");
m_stateContext.asteroidsTex = m_assetManager->getTexture("asteroids");
m_stateContext.musicEnabled = &m_musicEnabled; m_stateContext.musicEnabled = &m_musicEnabled;
m_stateContext.musicStarted = &m_musicStarted; m_stateContext.musicStarted = &m_musicStarted;
m_stateContext.musicLoaded = &m_musicLoaded; m_stateContext.musicLoaded = &m_musicLoaded;
@ -1162,6 +1163,7 @@ void ApplicationManager::setupStateHandlers() {
m_stateContext.pixelFont, m_stateContext.pixelFont,
m_stateContext.lineEffect, m_stateContext.lineEffect,
m_stateContext.blocksTex, m_stateContext.blocksTex,
m_stateContext.asteroidsTex,
m_stateContext.statisticsPanelTex, m_stateContext.statisticsPanelTex,
m_stateContext.scorePanelTex, m_stateContext.scorePanelTex,
m_stateContext.nextPanelTex, m_stateContext.nextPanelTex,

View File

@ -64,6 +64,8 @@ void Game::reset(int startLevel_) {
_comboCount = 0; _comboCount = 0;
challengeComplete = false; challengeComplete = false;
challengeLevelActive = false; challengeLevelActive = false;
challengeAdvanceQueued = false;
challengeQueuedLevel = 0;
// Initialize gravity using NES timing table (ms per cell by level) // Initialize gravity using NES timing table (ms per cell by level)
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier); gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
fallAcc = 0; gameOver=false; paused=false; fallAcc = 0; gameOver=false; paused=false;
@ -109,6 +111,8 @@ void Game::startChallengeRun(int startingLevel) {
challengeSeedBase = static_cast<uint32_t>(SDL_GetTicks()); challengeSeedBase = static_cast<uint32_t>(SDL_GetTicks());
} }
challengeRng.seed(challengeSeedBase + static_cast<uint32_t>(lvl)); challengeRng.seed(challengeSeedBase + static_cast<uint32_t>(lvl));
challengeAdvanceQueued = false;
challengeQueuedLevel = 0;
setupChallengeLevel(lvl, false); setupChallengeLevel(lvl, false);
} }
@ -116,6 +120,8 @@ void Game::beginNextChallengeLevel() {
if (mode != GameMode::Challenge || challengeComplete) { if (mode != GameMode::Challenge || challengeComplete) {
return; return;
} }
challengeAdvanceQueued = false;
challengeQueuedLevel = 0;
int next = challengeLevelIndex + 1; int next = challengeLevelIndex + 1;
if (next > ASTEROID_MAX_LEVEL) { if (next > ASTEROID_MAX_LEVEL) {
challengeComplete = true; challengeComplete = true;
@ -131,6 +137,8 @@ void Game::setupChallengeLevel(int level, bool preserveStats) {
startLevel = challengeLevelIndex; startLevel = challengeLevelIndex;
challengeComplete = false; challengeComplete = false;
challengeLevelActive = true; challengeLevelActive = true;
challengeAdvanceQueued = false;
challengeQueuedLevel = 0;
// Refresh deterministic RNG for this level // Refresh deterministic RNG for this level
challengeRng.seed(challengeSeedBase + static_cast<uint32_t>(challengeLevelIndex)); challengeRng.seed(challengeSeedBase + static_cast<uint32_t>(challengeLevelIndex));
@ -318,6 +326,16 @@ void Game::setPaused(bool p) {
paused = p; paused = p;
} }
int Game::consumeQueuedChallengeLevel() {
if (!challengeAdvanceQueued) {
return 0;
}
int next = challengeQueuedLevel;
challengeAdvanceQueued = false;
challengeQueuedLevel = 0;
return next;
}
void Game::setSoftDropping(bool on) { void Game::setSoftDropping(bool on) {
if (softDropping == on) { if (softDropping == on) {
return; return;
@ -521,7 +539,18 @@ void Game::actualClearLines() {
if (mode == GameMode::Challenge) { if (mode == GameMode::Challenge) {
if (asteroidsRemainingCount <= 0) { if (asteroidsRemainingCount <= 0) {
beginNextChallengeLevel(); int nextLevel = challengeLevelIndex + 1;
if (nextLevel > ASTEROID_MAX_LEVEL) {
challengeComplete = true;
challengeLevelActive = false;
challengeAdvanceQueued = false;
challengeQueuedLevel = 0;
} else {
challengeAdvanceQueued = true;
challengeQueuedLevel = nextLevel;
challengeLevelActive = false;
setPaused(true);
}
} }
} }
} }

View File

@ -67,6 +67,10 @@ public:
int asteroidsRemaining() const { return asteroidsRemainingCount; } int asteroidsRemaining() const { return asteroidsRemainingCount; }
int asteroidsTotal() const { return asteroidsTotalThisLevel; } int asteroidsTotal() const { return asteroidsTotalThisLevel; }
bool isChallengeComplete() const { return challengeComplete; } bool isChallengeComplete() const { return challengeComplete; }
bool isChallengeLevelActive() const { return challengeLevelActive; }
bool isChallengeAdvanceQueued() const { return challengeAdvanceQueued; }
int queuedChallengeLevel() const { return challengeQueuedLevel; }
int consumeQueuedChallengeLevel(); // returns next level if queued, else 0
int startLevelBase() const { return startLevel; } int startLevelBase() const { return startLevel; }
double elapsed() const; // Now calculated from start time double elapsed() const; // Now calculated from start time
void updateElapsedTime(); // Update elapsed time from system clock void updateElapsedTime(); // Update elapsed time from system clock
@ -168,6 +172,8 @@ private:
uint32_t challengeSeedBase{0}; uint32_t challengeSeedBase{0};
std::mt19937 challengeRng{ std::random_device{}() }; std::mt19937 challengeRng{ std::random_device{}() };
bool challengeLevelActive{false}; bool challengeLevelActive{false};
bool challengeAdvanceQueued{false};
int challengeQueuedLevel{0};
// Internal helpers ---------------------------------------------------- // Internal helpers ----------------------------------------------------
void refillBag(); void refillBag();

View File

@ -282,6 +282,62 @@ void GameRenderer::drawRect(SDL_Renderer* renderer, float x, float y, float w, f
SDL_RenderFillRect(renderer, &fr); SDL_RenderFillRect(renderer, &fr);
} }
static void drawAsteroid(SDL_Renderer* renderer, SDL_Texture* asteroidTex, float x, float y, float size, const AsteroidCell& cell) {
auto outlineGravity = [&](float inset, SDL_Color color) {
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
SDL_FRect glow{ x + inset, y + inset, size - inset * 2.0f, size - inset * 2.0f };
SDL_RenderRect(renderer, &glow);
};
if (asteroidTex) {
const float SPRITE_SIZE = 90.0f;
int col = 0;
switch (cell.type) {
case AsteroidType::Normal: col = 0; break;
case AsteroidType::Armored: col = 1; break;
case AsteroidType::Falling: col = 2; break;
case AsteroidType::Core: col = 3; break;
}
int row = std::clamp<int>(cell.visualState, 0, 2);
SDL_FRect src{ col * SPRITE_SIZE, row * SPRITE_SIZE, SPRITE_SIZE, SPRITE_SIZE };
SDL_FRect dst{ x, y, size, size };
SDL_RenderTexture(renderer, asteroidTex, &src, &dst);
if (cell.gravityEnabled) {
outlineGravity(2.0f, SDL_Color{255, 230, 120, 180});
}
return;
}
// Fallback: draw a colored quad (previous implementation)
SDL_Color base{};
switch (cell.type) {
case AsteroidType::Normal: base = SDL_Color{172, 138, 104, 255}; break;
case AsteroidType::Armored: base = SDL_Color{130, 150, 176, 255}; break;
case AsteroidType::Falling: base = SDL_Color{210, 120, 82, 255}; break;
case AsteroidType::Core: base = SDL_Color{198, 78, 200, 255}; break;
}
float hpScale = std::clamp(static_cast<float>(cell.hitsRemaining) / 3.0f, 0.25f, 1.0f);
SDL_Color fill{
static_cast<Uint8>(base.r * hpScale + 40 * (1.0f - hpScale)),
static_cast<Uint8>(base.g * hpScale + 40 * (1.0f - hpScale)),
static_cast<Uint8>(base.b * hpScale + 40 * (1.0f - hpScale)),
255
};
SDL_SetRenderDrawColor(renderer, fill.r, fill.g, fill.b, fill.a);
SDL_FRect body{x, y, size - 1.0f, size - 1.0f};
SDL_RenderFillRect(renderer, &body);
SDL_Color outline = base;
outline.a = 220;
SDL_FRect border{x + 1.0f, y + 1.0f, size - 2.0f, size - 2.0f};
SDL_SetRenderDrawColor(renderer, outline.r, outline.g, outline.b, outline.a);
SDL_RenderRect(renderer, &border);
if (cell.gravityEnabled) {
outlineGravity(2.0f, SDL_Color{255, 230, 120, 180});
}
}
void GameRenderer::drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType) { void GameRenderer::drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType) {
if (!blocksTex || blockType < 0 || blockType >= PIECE_COUNT) { if (!blocksTex || blockType < 0 || blockType >= PIECE_COUNT) {
// Fallback to colored rectangle if texture isn't available // Fallback to colored rectangle if texture isn't available
@ -515,6 +571,7 @@ void GameRenderer::renderPlayingState(
FontAtlas* pixelFont, FontAtlas* pixelFont,
LineEffect* lineEffect, LineEffect* lineEffect,
SDL_Texture* blocksTex, SDL_Texture* blocksTex,
SDL_Texture* asteroidsTex,
SDL_Texture* statisticsPanelTex, SDL_Texture* statisticsPanelTex,
SDL_Texture* scorePanelTex, SDL_Texture* scorePanelTex,
SDL_Texture* nextPanelTex, SDL_Texture* nextPanelTex,
@ -960,32 +1017,7 @@ void GameRenderer::renderPlayingState(
bool isAsteroid = challengeMode && asteroidCells[cellIdx].has_value(); bool isAsteroid = challengeMode && asteroidCells[cellIdx].has_value();
if (isAsteroid) { if (isAsteroid) {
const AsteroidCell& cell = *asteroidCells[cellIdx]; const AsteroidCell& cell = *asteroidCells[cellIdx];
SDL_Color base{}; drawAsteroid(renderer, asteroidsTex, bx, by, finalBlockSize, cell);
switch (cell.type) {
case AsteroidType::Normal: base = SDL_Color{172, 138, 104, 255}; break;
case AsteroidType::Armored: base = SDL_Color{130, 150, 176, 255}; break;
case AsteroidType::Falling: base = SDL_Color{210, 120, 82, 255}; break;
case AsteroidType::Core: base = SDL_Color{198, 78, 200, 255}; break;
}
float hpScale = std::clamp(static_cast<float>(cell.hitsRemaining) / 3.0f, 0.25f, 1.0f);
SDL_Color fill{
static_cast<Uint8>(base.r * hpScale + 40 * (1.0f - hpScale)),
static_cast<Uint8>(base.g * hpScale + 40 * (1.0f - hpScale)),
static_cast<Uint8>(base.b * hpScale + 40 * (1.0f - hpScale)),
255
};
drawRect(renderer, bx, by, finalBlockSize - 1, finalBlockSize - 1, fill);
// Subtle outline to differentiate types
SDL_Color outline = base;
outline.a = 220;
SDL_FRect border{bx + 1.0f, by + 1.0f, finalBlockSize - 2.0f, finalBlockSize - 2.0f};
SDL_SetRenderDrawColor(renderer, outline.r, outline.g, outline.b, outline.a);
SDL_RenderRect(renderer, &border);
if (cell.gravityEnabled) {
SDL_SetRenderDrawColor(renderer, 255, 230, 120, 180);
SDL_FRect glow{bx + 2.0f, by + 2.0f, finalBlockSize - 4.0f, finalBlockSize - 4.0f};
SDL_RenderRect(renderer, &glow);
}
} else { } else {
drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1); drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1);
} }

View File

@ -21,6 +21,7 @@ public:
FontAtlas* pixelFont, FontAtlas* pixelFont,
LineEffect* lineEffect, LineEffect* lineEffect,
SDL_Texture* blocksTex, SDL_Texture* blocksTex,
SDL_Texture* asteroidsTex,
SDL_Texture* statisticsPanelTex, SDL_Texture* statisticsPanelTex,
SDL_Texture* scorePanelTex, SDL_Texture* scorePanelTex,
SDL_Texture* nextPanelTex, SDL_Texture* nextPanelTex,

View File

@ -7,7 +7,8 @@ namespace Assets {
inline constexpr const char* LOGO = "assets/images/spacetris.png"; inline constexpr const char* LOGO = "assets/images/spacetris.png";
inline constexpr const char* MAIN_SCREEN = "assets/images/main_screen.png"; inline constexpr const char* MAIN_SCREEN = "assets/images/main_screen.png";
inline constexpr const char* BLOCKS_SPRITE = "assets/images/blocks90px_003.png"; inline constexpr const char* BLOCKS_SPRITE = "assets/images/blocks90px_003.png";
inline constexpr const char* ASTEROID_SPRITE = "assets/images/asteroids_001.png";
inline constexpr const char* PANEL_SCORE = "assets/images/panel_score.png"; inline constexpr const char* PANEL_SCORE = "assets/images/panel_score.png";
inline constexpr const char* PANEL_STATS = "assets/images/statistics_panel.png"; inline constexpr const char* PANEL_STATS = "assets/images/statistics_panel.png";
inline constexpr const char* NEXT_PANEL = "assets/images/next_panel.png"; inline constexpr const char* NEXT_PANEL = "assets/images/next_panel.png";

View File

@ -241,6 +241,7 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
ctx.pixelFont, ctx.pixelFont,
ctx.lineEffect, ctx.lineEffect,
ctx.blocksTex, ctx.blocksTex,
ctx.asteroidsTex,
ctx.statisticsPanelTex, ctx.statisticsPanelTex,
ctx.scorePanelTex, ctx.scorePanelTex,
ctx.nextPanelTex, ctx.nextPanelTex,
@ -329,6 +330,7 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
ctx.pixelFont, ctx.pixelFont,
ctx.lineEffect, ctx.lineEffect,
ctx.blocksTex, ctx.blocksTex,
ctx.asteroidsTex,
ctx.statisticsPanelTex, ctx.statisticsPanelTex,
ctx.scorePanelTex, ctx.scorePanelTex,
ctx.nextPanelTex, ctx.nextPanelTex,

View File

@ -40,6 +40,7 @@ struct StateContext {
// backgroundTex is set once in `main.cpp` and passed to states via this context. // backgroundTex is set once in `main.cpp` and passed to states via this context.
// Prefer reading this field instead of relying on any `extern SDL_Texture*` globals. // Prefer reading this field instead of relying on any `extern SDL_Texture*` globals.
SDL_Texture* blocksTex = nullptr; SDL_Texture* blocksTex = nullptr;
SDL_Texture* asteroidsTex = nullptr;
SDL_Texture* scorePanelTex = nullptr; SDL_Texture* scorePanelTex = nullptr;
SDL_Texture* statisticsPanelTex = nullptr; SDL_Texture* statisticsPanelTex = nullptr;
SDL_Texture* nextPanelTex = nullptr; SDL_Texture* nextPanelTex = nullptr;