Replace loss dialog with KABOOM! overlay; bevel the New Game button
- Losing no longer pops a dialog: the board draws a tilted KABOOM! banner (theme-independent, auto-scaling, dark translucent panel) over the revealed mines; restart via the New Game button or R/Ctrl+N - Win dialog unchanged (time + new-record note, delayed for the confetti) - New Game button gets an explicit theme-aware bevel (gradient, border, hover/active) so it reads as a button even with themes that flatten widgets
This commit is contained in:
@@ -355,6 +355,58 @@ void BoardWidget::on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, in
|
|||||||
if (!particles_.empty()) {
|
if (!particles_.empty()) {
|
||||||
draw_particles(cr);
|
draw_particles(cr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field_ && field_->state() == GameState::Lost) {
|
||||||
|
draw_game_over_overlay(cr, width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::draw_game_over_overlay(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
|
||||||
|
const char* msg = "KABOOM!";
|
||||||
|
|
||||||
|
cr->save();
|
||||||
|
|
||||||
|
// Scale the font to fit both dimensions
|
||||||
|
double font_size = std::clamp(std::min(width, height) * 0.3, 24.0, 120.0);
|
||||||
|
cr->select_font_face("Sans", Cairo::ToyFontFace::Slant::NORMAL, Cairo::ToyFontFace::Weight::BOLD);
|
||||||
|
cr->set_font_size(font_size);
|
||||||
|
Cairo::TextExtents ext;
|
||||||
|
cr->get_text_extents(msg, ext);
|
||||||
|
if (ext.width > width * 0.8) {
|
||||||
|
font_size *= (width * 0.8) / ext.width;
|
||||||
|
cr->set_font_size(font_size);
|
||||||
|
cr->get_text_extents(msg, ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
double cx = (width - ext.width) / 2.0 - ext.x_bearing;
|
||||||
|
double cy = (height - ext.height) / 2.0 - ext.y_bearing;
|
||||||
|
|
||||||
|
// Slight tilt for drama
|
||||||
|
cr->translate(width / 2.0, height / 2.0);
|
||||||
|
cr->rotate(-0.06);
|
||||||
|
cr->translate(-width / 2.0, -height / 2.0);
|
||||||
|
|
||||||
|
// Translucent panel for readability over the revealed board
|
||||||
|
double pad = font_size * 0.35;
|
||||||
|
double px = cx - pad;
|
||||||
|
double py = cy - pad;
|
||||||
|
double pw = ext.width + 2 * pad;
|
||||||
|
double ph = ext.height + 2 * pad;
|
||||||
|
double radius = font_size * 0.12;
|
||||||
|
cr->set_source_rgba(0.0, 0.0, 0.0, 0.5);
|
||||||
|
cr->move_to(px + radius, py);
|
||||||
|
cr->arc(px + pw - radius, py + radius, radius, -M_PI / 2, 0);
|
||||||
|
cr->arc(px + pw - radius, py + ph - radius, radius, 0, M_PI / 2);
|
||||||
|
cr->arc(px + radius, py + ph - radius, radius, M_PI / 2, M_PI);
|
||||||
|
cr->arc(px + radius, py + radius, radius, M_PI, 3 * M_PI / 2);
|
||||||
|
cr->close_path();
|
||||||
|
cr->fill();
|
||||||
|
|
||||||
|
cr->set_source_rgb(1.0, 0.42, 0.08); // Explosive orange
|
||||||
|
cr->move_to(cx, cy);
|
||||||
|
cr->show_text(msg);
|
||||||
|
|
||||||
|
cr->restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BoardWidget::draw_cell(const Cairo::RefPtr<Cairo::Context>& cr, int x, int y, double size, const Cell& cell, const Palette& pal) {
|
void BoardWidget::draw_cell(const Cairo::RefPtr<Cairo::Context>& cr, int x, int y, double size, const Cell& cell, const Palette& pal) {
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ private:
|
|||||||
void draw_flag(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size, const Palette& pal);
|
void draw_flag(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size, const Palette& pal);
|
||||||
void draw_bomb(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size, bool exploded, const Palette& pal);
|
void draw_bomb(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size, bool exploded, const Palette& pal);
|
||||||
void draw_particles(const Cairo::RefPtr<Cairo::Context>& cr);
|
void draw_particles(const Cairo::RefPtr<Cairo::Context>& cr);
|
||||||
|
void draw_game_over_overlay(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height);
|
||||||
|
|
||||||
std::shared_ptr<Minefield> field_;
|
std::shared_ptr<Minefield> field_;
|
||||||
|
|
||||||
|
|||||||
+24
-10
@@ -39,6 +39,20 @@ popover.background > contents { background-color: @theme_bg_color; }
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
.status-bar button {
|
||||||
|
background-image: linear-gradient(to top, alpha(@theme_fg_color, 0.16), alpha(@theme_fg_color, 0.05));
|
||||||
|
border: 1px solid alpha(@theme_fg_color, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 18px;
|
||||||
|
color: @theme_fg_color;
|
||||||
|
}
|
||||||
|
.status-bar button:hover {
|
||||||
|
background-image: linear-gradient(to top, alpha(@theme_fg_color, 0.24), alpha(@theme_fg_color, 0.12));
|
||||||
|
}
|
||||||
|
.status-bar button:active {
|
||||||
|
background-image: none;
|
||||||
|
background-color: alpha(@theme_fg_color, 0.2);
|
||||||
|
}
|
||||||
)CSS");
|
)CSS");
|
||||||
Gtk::StyleContext::add_provider_for_display(
|
Gtk::StyleContext::add_provider_for_display(
|
||||||
get_display(), app_css, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
get_display(), app_css, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||||
@@ -239,8 +253,10 @@ void MainWindow::on_game_state_changed() {
|
|||||||
if (state == GameState::Won) {
|
if (state == GameState::Won) {
|
||||||
secs = std::chrono::duration_cast<std::chrono::seconds>(minefield_->get_elapsed_time()).count();
|
secs = std::chrono::duration_cast<std::chrono::seconds>(minefield_->get_elapsed_time()).count();
|
||||||
new_best = leaderboard_.record(current_difficulty_.name, secs);
|
new_best = leaderboard_.record(current_difficulty_.name, secs);
|
||||||
|
show_game_over_dialog(new_best, secs);
|
||||||
}
|
}
|
||||||
show_game_over_dialog(state == GameState::Won, new_best, secs);
|
// Lost: no dialog — the board draws a KABOOM! overlay and the
|
||||||
|
// player restarts via the New Game button.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update timer immediately to reflect end time if stopped
|
// Update timer immediately to reflect end time if stopped
|
||||||
@@ -258,15 +274,15 @@ bool MainWindow::on_timer_tick() {
|
|||||||
return true; // Keep calling
|
return true; // Keep calling
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::show_game_over_dialog(bool won, bool new_best, int seconds) {
|
void MainWindow::show_game_over_dialog(bool new_best, int seconds) {
|
||||||
// Delay so the win animation/board state is visible before the modal
|
// Delay so the win animation/board state is visible before the modal
|
||||||
// dialog covers it.
|
// dialog covers it.
|
||||||
if (dialog_timeout_conn_.connected()) return;
|
if (dialog_timeout_conn_.connected()) return;
|
||||||
dialog_timeout_conn_ = Glib::signal_timeout().connect([this, won, new_best, seconds]() {
|
dialog_timeout_conn_ = Glib::signal_timeout().connect([this, new_best, seconds]() {
|
||||||
dialog_timeout_conn_.disconnect();
|
dialog_timeout_conn_.disconnect();
|
||||||
|
|
||||||
// A new game may have started while the dialog was pending
|
// A new game may have started while the dialog was pending
|
||||||
if (!minefield_ || minefield_->state() == GameState::Ready) return false;
|
if (!minefield_ || minefield_->state() != GameState::Won) return false;
|
||||||
|
|
||||||
// Reuse a single dialog instance: creating a new make_managed dialog
|
// Reuse a single dialog instance: creating a new make_managed dialog
|
||||||
// per game over leaks (it's never added to a container, so it's never
|
// per game over leaks (it's never added to a container, so it's never
|
||||||
@@ -286,12 +302,10 @@ void MainWindow::show_game_over_dialog(bool won, bool new_best, int seconds) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
game_over_dialog_->set_title(won ? "Congratulations!" : "Game Over");
|
game_over_dialog_->set_title("Congratulations!");
|
||||||
game_over_dialog_->set_message(won ? "Congratulations!" : "Game Over");
|
game_over_dialog_->set_message("Congratulations!");
|
||||||
std::string secondary = won
|
std::string secondary = "You cleared the minefield in " + format_time(seconds) +
|
||||||
? "You cleared the minefield in " + format_time(seconds) +
|
(new_best ? " — new best time!" : ".");
|
||||||
(new_best ? " — new best time!" : ".")
|
|
||||||
: "Better luck next time.";
|
|
||||||
game_over_dialog_->set_secondary_text(secondary);
|
game_over_dialog_->set_secondary_text(secondary);
|
||||||
game_over_dialog_->show();
|
game_over_dialog_->show();
|
||||||
return false; // One-shot
|
return false; // One-shot
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ private:
|
|||||||
void start_new_game(const GameDifficulty& difficulty);
|
void start_new_game(const GameDifficulty& difficulty);
|
||||||
void on_game_state_changed();
|
void on_game_state_changed();
|
||||||
bool on_timer_tick();
|
bool on_timer_tick();
|
||||||
void show_game_over_dialog(bool won, bool new_best, int seconds);
|
void show_game_over_dialog(bool new_best, int seconds);
|
||||||
|
|
||||||
// Widgets
|
// Widgets
|
||||||
Gtk::HeaderBar header_bar_;
|
Gtk::HeaderBar header_bar_;
|
||||||
|
|||||||
Reference in New Issue
Block a user