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
|
||||
}};
|
||||
|
||||
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 {
|
||||
// Global collector for asset loading errors shown on the loading screen
|
||||
std::vector<std::string> assetLoadErrors;
|
||||
@ -199,6 +268,10 @@ struct TetrisApp::Impl {
|
||||
int countdownGoalAsteroids = 0;
|
||||
bool countdownAdvancesChallenge = false;
|
||||
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)
|
||||
bool challengeClearFxActive = false;
|
||||
@ -465,6 +538,9 @@ int TetrisApp::Impl::init()
|
||||
ctx.challengeClearFxElapsedMs = &challengeClearFxElapsedMs;
|
||||
ctx.challengeClearFxDurationMs = &challengeClearFxDurationMs;
|
||||
ctx.challengeClearFxOrder = &challengeClearFxOrder;
|
||||
ctx.challengeStoryText = &challengeStoryText;
|
||||
ctx.challengeStoryLevel = &challengeStoryLevel;
|
||||
ctx.challengeStoryAlpha = &challengeStoryAlpha;
|
||||
ctx.playerName = &playerName;
|
||||
ctx.fullscreenFlag = &isFullscreen;
|
||||
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) {
|
||||
challengeClearFxOrder.clear();
|
||||
const auto& boardRef = game->boardRef();
|
||||
@ -926,6 +1010,36 @@ void TetrisApp::Impl::runLoop()
|
||||
if (frameMs > 100.0) frameMs = 100.0;
|
||||
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) {
|
||||
challengeClearFxElapsedMs += frameMs;
|
||||
if (challengeClearFxElapsedMs >= challengeClearFxDurationMs) {
|
||||
@ -940,6 +1054,7 @@ void TetrisApp::Impl::runLoop()
|
||||
gameplayCountdownSource = CountdownSource::ChallengeLevel;
|
||||
countdownLevel = challengeClearFxNextLevel;
|
||||
countdownGoalAsteroids = challengeClearFxNextLevel;
|
||||
captureChallengeStory(countdownLevel);
|
||||
countdownAdvancesChallenge = false; // already advanced
|
||||
gameplayCountdownActive = true;
|
||||
menuPlayCountdownArmed = false;
|
||||
@ -1343,6 +1458,12 @@ void TetrisApp::Impl::runLoop()
|
||||
: CountdownSource::MenuStart;
|
||||
countdownLevel = game ? game->challengeLevel() : 1;
|
||||
countdownGoalAsteroids = countdownLevel;
|
||||
if (gameplayCountdownSource == CountdownSource::ChallengeLevel) {
|
||||
captureChallengeStory(countdownLevel);
|
||||
} else {
|
||||
challengeStoryText.clear();
|
||||
challengeStoryLevel = 0;
|
||||
}
|
||||
countdownAdvancesChallenge = false;
|
||||
menuPlayCountdownArmed = true;
|
||||
gameplayCountdownActive = false;
|
||||
@ -1376,6 +1497,12 @@ void TetrisApp::Impl::runLoop()
|
||||
: CountdownSource::MenuStart;
|
||||
countdownLevel = game ? game->challengeLevel() : 1;
|
||||
countdownGoalAsteroids = countdownLevel;
|
||||
if (gameplayCountdownSource == CountdownSource::ChallengeLevel) {
|
||||
captureChallengeStory(countdownLevel);
|
||||
} else {
|
||||
challengeStoryText.clear();
|
||||
challengeStoryLevel = 0;
|
||||
}
|
||||
countdownAdvancesChallenge = false;
|
||||
gameplayCountdownActive = true;
|
||||
menuPlayCountdownArmed = false;
|
||||
|
||||
@ -7,6 +7,8 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
@ -610,7 +612,9 @@ void GameRenderer::renderPlayingState(
|
||||
bool challengeClearFxActive,
|
||||
const std::vector<int>* challengeClearFxOrder,
|
||||
double challengeClearFxElapsedMs,
|
||||
double challengeClearFxDurationMs
|
||||
double challengeClearFxDurationMs,
|
||||
const std::string* challengeStoryText,
|
||||
float challengeStoryAlpha
|
||||
) {
|
||||
if (!game || !pixelFont) return;
|
||||
|
||||
@ -1702,6 +1706,51 @@ void GameRenderer::renderPlayingState(
|
||||
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) {
|
||||
pixelFont->draw(renderer, logicalW - 260, 10, gravityHud, 0.9f, {200, 200, 220, 255});
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include "../../gameplay/core/Game.h"
|
||||
|
||||
// Forward declarations
|
||||
@ -36,7 +37,9 @@ public:
|
||||
bool challengeClearFxActive = false,
|
||||
const std::vector<int>* challengeClearFxOrder = nullptr,
|
||||
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)
|
||||
|
||||
@ -269,7 +269,9 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
||||
challengeClearFx,
|
||||
challengeClearOrder,
|
||||
challengeClearElapsed,
|
||||
challengeClearDuration
|
||||
challengeClearDuration,
|
||||
ctx.challengeStoryText,
|
||||
ctx.challengeStoryAlpha ? *ctx.challengeStoryAlpha : 0.0f
|
||||
);
|
||||
|
||||
// Reset to screen
|
||||
@ -363,7 +365,9 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
||||
challengeClearFx,
|
||||
challengeClearOrder,
|
||||
challengeClearElapsed,
|
||||
challengeClearDuration
|
||||
challengeClearDuration,
|
||||
ctx.challengeStoryText,
|
||||
ctx.challengeStoryAlpha ? *ctx.challengeStoryAlpha : 0.0f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,6 +73,9 @@ struct StateContext {
|
||||
double* challengeClearFxElapsedMs = nullptr;
|
||||
double* challengeClearFxDurationMs = 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
|
||||
bool* fullscreenFlag = nullptr; // Tracks current fullscreen state when available
|
||||
std::function<void(bool)> applyFullscreen; // Allows states to request fullscreen changes
|
||||
|
||||
Reference in New Issue
Block a user