added challenge level text

This commit is contained in:
2025-12-20 20:47:04 +01:00
parent 6c48af0bec
commit eb9822dac7
5 changed files with 190 additions and 4 deletions

View File

@ -82,6 +82,75 @@ static const std::array<SDL_Color, PIECE_COUNT + 1> COLORS = {{
SDL_Color{255, 160, 0, 255}, // L SDL_Color{255, 160, 0, 255}, // L
}}; }};
static std::string GetLevelStoryText(int level) {
int lvl = std::clamp(level, 1, 100);
// Milestones
switch (lvl) {
case 1: return "Launch log: training run, light debris ahead.";
case 25: return "Checkpoint: dense field reported, shields ready.";
case 50: return "Midway brief: hull stress rising, stay sharp.";
case 75: return "Emergency corridor: comms unstable, proceed blind.";
case 100: return "Final anomaly: unknown mass ahead, hold course.";
default: break;
}
struct Pool { int minL, maxL; std::vector<std::string> lines; };
static const std::vector<Pool> pools = {
{1, 10, {
"Departure logged: light debris, stay on vector.",
"Training sector: minimal drift, keep sensors warm.",
"Calm approach: verify thrusters and nav locks.",
"Outer ring dust: watch for slow movers.",
"Clear lanes ahead: focus on smooth rotations."
}},
{11, 25, {
"Asteroid belt thickening; micro-impacts likely.",
"Density rising: plot short burns only.",
"Field report: medium fragments, unpredictable spin.",
"Warning: overlapping paths, reduce horizontal drift.",
"Rock chorus ahead; keep payload stable."
}},
{26, 40, {
"Unstable sector: abandoned relays drifting erratic.",
"Salvage echoes detected; debris wakes may tug.",
"Hull groans recorded; inert structures nearby.",
"Navigation buoys dark; trust instruments only.",
"Magnetic static rising; expect odd rotations."
}},
{41, 60, {
"Core corridor: heavy asteroids, minimal clearance.",
"Impact risk high: armor checks recommended.",
"Dense stone flow; time burns carefully.",
"Grav eddies noted; blocks may drift late.",
"Core shards are brittle; expect sudden splits."
}},
{61, 80, {
"Critical zone: alarms pinned, route unstable.",
"Emergency pattern: glide, then cut thrust.",
"Sensors flare; debris ionized, visibility low.",
"Thermals spiking; keep pieces tight and fast.",
"Silent channel; assume worst-case collision."
}},
{81, 100, {
"Unknown space: signals warp, gravity unreliable.",
"Anomaly bloom ahead; shapes flicker unpredictably.",
"Final drift: void sings through hull plates.",
"Black sector: map useless, fly by instinct.",
"Edge of chart: nothing responds, just move."
}}
};
for (const auto& pool : pools) {
if (lvl >= pool.minL && lvl <= pool.maxL && !pool.lines.empty()) {
size_t idx = static_cast<size_t>((lvl - pool.minL) % pool.lines.size());
return pool.lines[idx];
}
}
return "Mission log update unavailable.";
}
struct TetrisApp::Impl { struct TetrisApp::Impl {
// Global collector for asset loading errors shown on the loading screen // Global collector for asset loading errors shown on the loading screen
std::vector<std::string> assetLoadErrors; std::vector<std::string> assetLoadErrors;
@ -199,6 +268,10 @@ struct TetrisApp::Impl {
int countdownGoalAsteroids = 0; int countdownGoalAsteroids = 0;
bool countdownAdvancesChallenge = false; bool countdownAdvancesChallenge = false;
double gameplayBackgroundClockMs = 0.0; double gameplayBackgroundClockMs = 0.0;
std::string challengeStoryText;
int challengeStoryLevel = 0;
float challengeStoryAlpha = 0.0f;
double challengeStoryClockMs = 0.0;
// Challenge clear FX (celebratory board explosion before countdown) // Challenge clear FX (celebratory board explosion before countdown)
bool challengeClearFxActive = false; bool challengeClearFxActive = false;
@ -465,6 +538,9 @@ int TetrisApp::Impl::init()
ctx.challengeClearFxElapsedMs = &challengeClearFxElapsedMs; ctx.challengeClearFxElapsedMs = &challengeClearFxElapsedMs;
ctx.challengeClearFxDurationMs = &challengeClearFxDurationMs; ctx.challengeClearFxDurationMs = &challengeClearFxDurationMs;
ctx.challengeClearFxOrder = &challengeClearFxOrder; ctx.challengeClearFxOrder = &challengeClearFxOrder;
ctx.challengeStoryText = &challengeStoryText;
ctx.challengeStoryLevel = &challengeStoryLevel;
ctx.challengeStoryAlpha = &challengeStoryAlpha;
ctx.playerName = &playerName; ctx.playerName = &playerName;
ctx.fullscreenFlag = &isFullscreen; ctx.fullscreenFlag = &isFullscreen;
ctx.applyFullscreen = [this](bool enable) { ctx.applyFullscreen = [this](bool enable) {
@ -585,6 +661,14 @@ void TetrisApp::Impl::runLoop()
} }
}; };
auto captureChallengeStory = [this](int level) {
int lvl = std::clamp(level, 1, 100);
challengeStoryLevel = lvl;
challengeStoryText = GetLevelStoryText(lvl);
challengeStoryClockMs = 0.0;
challengeStoryAlpha = 0.0f;
};
auto startChallengeClearFx = [this](int nextLevel) { auto startChallengeClearFx = [this](int nextLevel) {
challengeClearFxOrder.clear(); challengeClearFxOrder.clear();
const auto& boardRef = game->boardRef(); const auto& boardRef = game->boardRef();
@ -926,6 +1010,36 @@ void TetrisApp::Impl::runLoop()
if (frameMs > 100.0) frameMs = 100.0; if (frameMs > 100.0) frameMs = 100.0;
gameplayBackgroundClockMs += frameMs; gameplayBackgroundClockMs += frameMs;
auto clearChallengeStory = [this]() {
challengeStoryText.clear();
challengeStoryLevel = 0;
challengeStoryAlpha = 0.0f;
challengeStoryClockMs = 0.0;
};
// Update challenge story fade/timeout
if (state == AppState::Playing && game && game->getMode() == GameMode::Challenge && !challengeStoryText.empty()) {
const double fadeInMs = 320.0;
const double holdMs = 3200.0;
const double fadeOutMs = 900.0;
const double totalMs = fadeInMs + holdMs + fadeOutMs;
challengeStoryClockMs += frameMs;
if (challengeStoryClockMs >= totalMs) {
clearChallengeStory();
} else {
double a = 1.0;
if (challengeStoryClockMs < fadeInMs) {
a = challengeStoryClockMs / fadeInMs;
} else if (challengeStoryClockMs > fadeInMs + holdMs) {
double t = challengeStoryClockMs - (fadeInMs + holdMs);
a = std::max(0.0, 1.0 - t / fadeOutMs);
}
challengeStoryAlpha = static_cast<float>(std::clamp(a, 0.0, 1.0));
}
} else {
clearChallengeStory();
}
if (challengeClearFxActive) { if (challengeClearFxActive) {
challengeClearFxElapsedMs += frameMs; challengeClearFxElapsedMs += frameMs;
if (challengeClearFxElapsedMs >= challengeClearFxDurationMs) { if (challengeClearFxElapsedMs >= challengeClearFxDurationMs) {
@ -940,6 +1054,7 @@ void TetrisApp::Impl::runLoop()
gameplayCountdownSource = CountdownSource::ChallengeLevel; gameplayCountdownSource = CountdownSource::ChallengeLevel;
countdownLevel = challengeClearFxNextLevel; countdownLevel = challengeClearFxNextLevel;
countdownGoalAsteroids = challengeClearFxNextLevel; countdownGoalAsteroids = challengeClearFxNextLevel;
captureChallengeStory(countdownLevel);
countdownAdvancesChallenge = false; // already advanced countdownAdvancesChallenge = false; // already advanced
gameplayCountdownActive = true; gameplayCountdownActive = true;
menuPlayCountdownArmed = false; menuPlayCountdownArmed = false;
@ -1343,6 +1458,12 @@ void TetrisApp::Impl::runLoop()
: CountdownSource::MenuStart; : CountdownSource::MenuStart;
countdownLevel = game ? game->challengeLevel() : 1; countdownLevel = game ? game->challengeLevel() : 1;
countdownGoalAsteroids = countdownLevel; countdownGoalAsteroids = countdownLevel;
if (gameplayCountdownSource == CountdownSource::ChallengeLevel) {
captureChallengeStory(countdownLevel);
} else {
challengeStoryText.clear();
challengeStoryLevel = 0;
}
countdownAdvancesChallenge = false; countdownAdvancesChallenge = false;
menuPlayCountdownArmed = true; menuPlayCountdownArmed = true;
gameplayCountdownActive = false; gameplayCountdownActive = false;
@ -1376,6 +1497,12 @@ void TetrisApp::Impl::runLoop()
: CountdownSource::MenuStart; : CountdownSource::MenuStart;
countdownLevel = game ? game->challengeLevel() : 1; countdownLevel = game ? game->challengeLevel() : 1;
countdownGoalAsteroids = countdownLevel; countdownGoalAsteroids = countdownLevel;
if (gameplayCountdownSource == CountdownSource::ChallengeLevel) {
captureChallengeStory(countdownLevel);
} else {
challengeStoryText.clear();
challengeStoryLevel = 0;
}
countdownAdvancesChallenge = false; countdownAdvancesChallenge = false;
gameplayCountdownActive = true; gameplayCountdownActive = true;
menuPlayCountdownArmed = false; menuPlayCountdownArmed = false;

View File

@ -7,6 +7,8 @@
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <sstream>
#include <string>
#include <limits> #include <limits>
#include <random> #include <random>
#include <vector> #include <vector>
@ -610,7 +612,9 @@ void GameRenderer::renderPlayingState(
bool challengeClearFxActive, bool challengeClearFxActive,
const std::vector<int>* challengeClearFxOrder, const std::vector<int>* challengeClearFxOrder,
double challengeClearFxElapsedMs, double challengeClearFxElapsedMs,
double challengeClearFxDurationMs double challengeClearFxDurationMs,
const std::string* challengeStoryText,
float challengeStoryAlpha
) { ) {
if (!game || !pixelFont) return; if (!game || !pixelFont) return;
@ -1702,6 +1706,51 @@ void GameRenderer::renderPlayingState(
pixelFont->draw(renderer, statsTextX, baseY + line.offsetY, line.text, line.scale, line.color); pixelFont->draw(renderer, statsTextX, baseY + line.offsetY, line.text, line.scale, line.color);
} }
// Challenge story / briefing line near level indicator
if (challengeStoryText && !challengeStoryText->empty() && challengeStoryAlpha > 0.0f && game->getMode() == GameMode::Challenge) {
float alpha = std::clamp(challengeStoryAlpha, 0.0f, 1.0f);
SDL_Color storyColor{160, 220, 255, static_cast<Uint8>(std::lround(210.0f * alpha))};
SDL_Color shadowColor{0, 0, 0, static_cast<Uint8>(std::lround(120.0f * alpha))};
auto drawWrapped = [&](const std::string& text, float x, float y, float maxW, float scale, SDL_Color color) {
std::istringstream iss(text);
std::string word;
std::string line;
float cursorY = y;
int lastH = 0;
while (iss >> word) {
std::string candidate = line.empty() ? word : (line + " " + word);
int w = 0, h = 0;
pixelFont->measure(candidate, scale, w, h);
if (w > maxW && !line.empty()) {
pixelFont->draw(renderer, x + 1.0f, cursorY + 1.0f, line, scale, shadowColor);
pixelFont->draw(renderer, x, cursorY, line, scale, color);
cursorY += h + 4.0f;
line = word;
lastH = h;
} else {
line = candidate;
lastH = h;
}
}
if (!line.empty()) {
pixelFont->draw(renderer, x + 1.0f, cursorY + 1.0f, line, scale, shadowColor);
pixelFont->draw(renderer, x, cursorY, line, scale, color);
cursorY += lastH + 4.0f;
}
};
float storyX = statsTextX;
float storyY = baseY + 112.0f;
float maxW = 230.0f;
if (scorePanelMetricsValid && scorePanelWidth > 40.0f) {
storyX = scorePanelLeftX + 14.0f;
maxW = std::max(160.0f, scorePanelWidth - 28.0f);
}
drawWrapped(*challengeStoryText, storyX, storyY, maxW, 0.7f, storyColor);
}
if (debugEnabled) { if (debugEnabled) {
pixelFont->draw(renderer, logicalW - 260, 10, gravityHud, 0.9f, {200, 200, 220, 255}); pixelFont->draw(renderer, logicalW - 260, 10, gravityHud, 0.9f, {200, 200, 220, 255});
} }

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <vector> #include <vector>
#include <string>
#include "../../gameplay/core/Game.h" #include "../../gameplay/core/Game.h"
// Forward declarations // Forward declarations
@ -36,7 +37,9 @@ public:
bool challengeClearFxActive = false, bool challengeClearFxActive = false,
const std::vector<int>* challengeClearFxOrder = nullptr, const std::vector<int>* challengeClearFxOrder = nullptr,
double challengeClearFxElapsedMs = 0.0, double challengeClearFxElapsedMs = 0.0,
double challengeClearFxDurationMs = 0.0 double challengeClearFxDurationMs = 0.0,
const std::string* challengeStoryText = nullptr,
float challengeStoryAlpha = 0.0f
); );
// Render the pause overlay (full screen) // Render the pause overlay (full screen)

View File

@ -269,7 +269,9 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
challengeClearFx, challengeClearFx,
challengeClearOrder, challengeClearOrder,
challengeClearElapsed, challengeClearElapsed,
challengeClearDuration challengeClearDuration,
ctx.challengeStoryText,
ctx.challengeStoryAlpha ? *ctx.challengeStoryAlpha : 0.0f
); );
// Reset to screen // Reset to screen
@ -363,7 +365,9 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
challengeClearFx, challengeClearFx,
challengeClearOrder, challengeClearOrder,
challengeClearElapsed, challengeClearElapsed,
challengeClearDuration challengeClearDuration,
ctx.challengeStoryText,
ctx.challengeStoryAlpha ? *ctx.challengeStoryAlpha : 0.0f
); );
} }
} }

View File

@ -73,6 +73,9 @@ struct StateContext {
double* challengeClearFxElapsedMs = nullptr; double* challengeClearFxElapsedMs = nullptr;
double* challengeClearFxDurationMs = nullptr; double* challengeClearFxDurationMs = nullptr;
std::vector<int>* challengeClearFxOrder = nullptr; std::vector<int>* challengeClearFxOrder = nullptr;
std::string* challengeStoryText = nullptr; // Per-level briefing string for Challenge mode
int* challengeStoryLevel = nullptr; // Cached level for the current story line
float* challengeStoryAlpha = nullptr; // Current render alpha for story text fade
std::string* playerName = nullptr; // Shared player name buffer for highscores/options std::string* playerName = nullptr; // Shared player name buffer for highscores/options
bool* fullscreenFlag = nullptr; // Tracks current fullscreen state when available bool* fullscreenFlag = nullptr; // Tracks current fullscreen state when available
std::function<void(bool)> applyFullscreen; // Allows states to request fullscreen changes std::function<void(bool)> applyFullscreen; // Allows states to request fullscreen changes