added challenge level text
This commit is contained in:
@ -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;
|
||||||
|
|||||||
@ -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});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user