1/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- 2 * Gnome Nibbles: Gnome Worm Game 3 * Copyright (C) 2015 Iulian-Gabriel Radu <iulian.radu67@gmail.com> 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 */ 18 19using Gtk; 20 21private enum SetupScreen 22{ 23 USUAL, 24 SPEED, 25 CONTROLS, 26 GAME 27} 28 29[GtkTemplate (ui = "/org/gnome/Nibbles/ui/nibbles.ui")] 30private class NibblesWindow : ApplicationWindow 31{ 32 /* Application and worm settings */ 33 private GLib.Settings settings; 34 private Gee.ArrayList<GLib.Settings> worm_settings; 35 36 /* state */ 37 private bool window_is_maximized; 38 private bool window_is_tiled; 39 private int window_width; 40 private int window_height; 41 42 /* Main widgets */ 43 [GtkChild] private Stack main_stack; 44 [GtkChild] private Overlay overlay; 45 46 /* HeaderBar */ 47 [GtkChild] private HeaderBar headerbar; 48 [GtkChild] private MenuButton hamburger_menu; 49 [GtkChild] private Button new_game_button; 50 [GtkChild] private Button pause_button; 51 52 /* Pre-game screen widgets */ 53 [GtkChild] private Players players; 54 [GtkChild] private Speed speed; 55 [GtkChild] private Controls controls; 56 57 /* Statusbar widgets */ 58 [GtkChild] private Stack statusbar_stack; 59 [GtkChild] private Label countdown; 60 [GtkChild] private Scoreboard scoreboard; 61 private Gdk.Pixbuf scoreboard_life; 62 63 /* Rendering of the game */ 64 private NibblesView? view; 65 66 [GtkChild] private Box game_box; 67 private Games.GridFrame frame; 68 69 /* Game being played */ 70 private NibblesGame? game = null; 71 public int cli_start_level { private get; internal construct; } 72 private int start_level { private get { return cli_start_level == 0 ? settings.get_int ("start-level") : cli_start_level; }} 73 public SetupScreen start_screen { private get; internal construct; } 74 75 /* Used for handling the game's scores */ 76 private Games.Scores.Context scores_context; 77 private Gee.LinkedList<Games.Scores.Category> scorecats; 78 79 /* HeaderBar actions */ 80 private SimpleAction new_game_action; 81 private SimpleAction pause_action; 82 private SimpleAction back_action; 83 private SimpleAction start_game_action; 84 85 private uint countdown_id = 0; 86 private const int COUNTDOWN_TIME = 3; 87 private int seconds = 0; 88 89 private const GLib.ActionEntry menu_entries[] = 90 { 91 { "hamburger", hamburger_cb }, 92 93 { "new-game", new_game_cb }, // the "New Game" button (during game), or the ctrl-N shortcut (mostly all the time) 94 { "pause", pause_cb }, 95 { "preferences", preferences_cb, "i" }, 96 { "scores", scores_cb }, 97 98 { "next-screen", next_screen_cb }, // called from first-run, players and speed 99 { "start-game", start_game }, // called from controls 100 { "back", back_cb } // called on Escape pressed; disabled only during countdown (TODO pause?) 101 }; 102 103 internal NibblesWindow (int cli_start_level, SetupScreen start_screen) 104 { 105 Object (cli_start_level: cli_start_level, start_screen: start_screen); 106 } 107 108 construct 109 { 110 add_action_entries (menu_entries, this); 111 new_game_action = (SimpleAction) lookup_action ("new-game"); 112 pause_action = (SimpleAction) lookup_action ("pause"); 113 back_action = (SimpleAction) lookup_action ("back"); 114 start_game_action = (SimpleAction) lookup_action ("start-game"); 115 116 settings = new GLib.Settings ("org.gnome.Nibbles"); 117 settings.changed.connect (settings_changed_cb); 118 add_action (settings.create_action ("sound")); 119 120 worm_settings = new Gee.ArrayList<GLib.Settings> (); 121 for (int i = 0; i < NibblesGame.MAX_WORMS; i++) 122 { 123 var name = "org.gnome.Nibbles.worm%d".printf(i); 124 worm_settings.add (new GLib.Settings (name)); 125 worm_settings[i].changed.connect (worm_settings_changed_cb); 126 } 127 128 size_allocate.connect (size_allocate_cb); 129 window_state_event.connect (window_state_event_cb); 130 set_default_size (settings.get_int ("window-width"), settings.get_int ("window-height")); 131 if (settings.get_boolean ("window-is-maximized")) 132 maximize (); 133 134 key_controller = new EventControllerKey (this); 135 key_controller.key_pressed.connect (key_press_event_cb); 136 137 /* Create game */ 138 game = new NibblesGame (start_level, 139 settings.get_int ("speed"), 140 NibblesView.GAMEDELAY, 141 settings.get_boolean ("fakes"), 142 NibblesView.WIDTH, 143 NibblesView.HEIGHT); 144 game.log_score.connect (log_score_cb); 145 game.level_completed.connect (level_completed_cb); 146 game.notify["is-paused"].connect (() => { 147 if (game.is_paused) 148 statusbar_stack.set_visible_child_name ("paused"); 149 else 150 statusbar_stack.set_visible_child_name ("scoreboard"); 151 }); 152 153 /* Create view */ 154 view = new NibblesView (game, 155 settings.get_int ("tile-size"), 156 !settings.get_boolean ("sound")); 157 view.show (); 158 159 frame = new Games.GridFrame (NibblesView.WIDTH, NibblesView.HEIGHT); 160 game_box.pack_start (frame); 161 162 /* Create scoreboard */ 163 scoreboard_life = NibblesView.load_pixmap_file ("scoreboard-life.svg", 2 * view.tile_size, 2 * view.tile_size); 164 165 frame.add (view); 166 frame.show (); 167 168 /* Number of worms */ 169 game.numhumans = settings.get_int ("players"); 170 int numai = settings.get_int ("ai"); 171 if (numai + game.numhumans > NibblesGame.MAX_WORMS 172 || numai + game.numhumans < 4) 173 { 174 numai = 4 - game.numhumans; 175 settings.set_int ("ai", numai); 176 } 177 game.numai = numai; 178 players.set_values (game.numhumans, numai); 179 180 /* Speed screen */ 181 speed.set_values (settings.get_int ("speed"), 182 settings.get_boolean ("fakes")); 183 184 /* Controls screen */ 185 controls.load_pixmaps (view.tile_size); 186 187 /* Check whether to display the first run screen */ 188 if (start_screen == SetupScreen.GAME) 189 { 190 game.numhumans = settings.get_int ("players"); 191 game.numai = settings.get_int ("ai"); 192 game.speed = settings.get_int ("speed"); 193 game.fakes = settings.get_boolean ("fakes"); 194 game.create_worms (); 195 game.load_worm_properties (worm_settings); 196 197 start_game (); 198 } 199 else if (start_screen == SetupScreen.CONTROLS) 200 { 201 game.numhumans = settings.get_int ("players"); 202 game.numai = settings.get_int ("ai"); 203 game.speed = settings.get_int ("speed"); 204 game.fakes = settings.get_boolean ("fakes"); 205 206 show_controls_screen (); 207 } 208 else if (start_screen == SetupScreen.SPEED) 209 { 210 game.numhumans = settings.get_int ("players"); 211 game.numai = settings.get_int ("ai"); 212 213 main_stack.set_visible_child_name ("speed"); 214 } 215 else if (settings.get_boolean ("first-run")) 216 { 217 FirstRun first_run_panel = new FirstRun (); 218 first_run_panel.show (); 219 main_stack.add_named (first_run_panel, "first-run"); 220 221 // new_game_action.set_enabled (true); 222 pause_action.set_enabled (false); 223 back_action.set_enabled (false); 224 225 main_stack.set_visible_child (first_run_panel); 226 } 227 else 228 show_new_game_screen (); 229 230 /* Create scores */ 231 create_scores (); 232 } 233 234 internal void on_shutdown () 235 { 236 settings.delay (); 237 // window state 238 settings.set_int ("window-width", window_width); 239 settings.set_int ("window-height", window_height); 240 settings.set_boolean ("window-is-maximized", window_is_maximized); 241 242 // game properties 243 settings.set_int ("tile-size", view.tile_size); // TODO why?! 244 settings.set_int ("speed", game.speed); 245 settings.set_boolean ("fakes", game.fakes); 246 settings.apply (); 247 } 248 249 private bool countdown_cb () 250 { 251 seconds--; 252 253 if (seconds == 0) 254 { 255 statusbar_stack.set_visible_child_name ("scoreboard"); 256 view.name_labels.hide (); 257 258 game.start (/* add initial bonus */ true); 259 260 pause_action.set_enabled (true); 261 262 countdown_id = 0; 263 return Source.REMOVE; 264 } 265 266 countdown.set_label (seconds.to_string ()); 267 return Source.CONTINUE; 268 } 269 270 /*\ 271 * * Window events 272 \*/ 273 274 /* The reason this event handler is found here (and not in nibbles-view.vala 275 * which would be a more suitable place) is to avoid a weird behavior of having 276 * your first key press ignored everytime by the start of a new level, thus 277 * making your worm unresponsive to your command. 278 */ 279 private EventControllerKey key_controller; // for keeping in memory 280 private bool key_press_event_cb (EventControllerKey _key_controller, uint keyval, uint keycode, Gdk.ModifierType state) 281 { 282 if (hamburger_menu.active) 283 return false; 284 else if ((!) (Gdk.keyval_name (keyval) ?? "") == "F1") 285 return ((Nibbles) application).on_f1_pressed (state); // TODO fix dance done with the F1 & <Control>F1 shortcuts that show help overlay 286 else 287 return game.handle_keypress (keyval); 288 } 289 290 private void size_allocate_cb (Allocation allocation) 291 { 292 if (window_is_maximized || window_is_tiled) 293 return; 294 get_size (out window_width, out window_height); 295 } 296 297 private bool window_state_event_cb (Gdk.EventWindowState event) 298 { 299 if ((event.changed_mask & Gdk.WindowState.MAXIMIZED) != 0) 300 window_is_maximized = (event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0; 301 /* We don’t save this state, but track it for saving size allocation */ 302 if ((event.changed_mask & Gdk.WindowState.TILED) != 0) 303 window_is_tiled = (event.new_window_state & Gdk.WindowState.TILED) != 0; 304 return false; 305 } 306 307 private void start_game () 308 { 309 settings.set_boolean ("first-run", false); 310 311 if (game.is_paused) 312 set_pause_button_label (/* paused */ false); 313 game.reset (start_level); 314 315 view.new_level (game.current_level); 316 view.connect_worm_signals (); 317 318 scoreboard.clear (); 319 foreach (var worm in game.worms) 320 { 321 var color = game.worm_props.@get (worm).color; 322 scoreboard.register (worm, NibblesView.colorval_name_untranslated (color), scoreboard_life); 323 worm.notify["lives"].connect (scoreboard.update); 324 worm.notify["score"].connect (scoreboard.update); 325 } 326 game.add_worms (); 327 328 view.create_name_labels (); 329 330 show_game_view (); 331 332 start_game_with_countdown (); 333 } 334 335 private void start_game_with_countdown () 336 { 337 statusbar_stack.set_visible_child_name ("countdown"); 338 339 new_game_action.set_enabled (true); 340 back_action.set_enabled (true); 341 342 seconds = COUNTDOWN_TIME; 343 view.name_labels.show (); 344 345 countdown.set_label (COUNTDOWN_TIME.to_string ()); 346 countdown_id = Timeout.add_seconds (1, countdown_cb); 347 } 348 349 private void restart_game () 350 { 351 view.new_level (game.current_level); 352 353 game.add_worms (); 354 start_game_with_countdown (); 355 } 356 357 private void new_game_cb () 358 { 359 var child_name = main_stack.get_visible_child_name (); 360 switch (child_name) 361 { 362 case "first-run": 363 case "number_of_players": 364 case "speed": 365 next_screen_cb (); 366 break; 367 case "controls": 368 start_game (); 369 break; 370 case "game_box": 371 if (end_of_game) // TODO better 372 { 373 game_over_label.destroy (); 374 score_label.destroy (); 375 points_left_label.destroy (); 376 play_again_button.destroy (); 377 msg_label.destroy (); 378 379 view.show (); 380 end_of_game = false; 381 382 show_new_game_screen (); 383 } 384 else 385 show_new_game_dialog (); 386 break; 387 } 388 } 389 private void show_new_game_dialog () 390 { 391 if (countdown_id != 0) 392 { 393 Source.remove (countdown_id); 394 countdown_id = 0; 395 } 396 397 if (game.is_running) 398 game.stop (); 399 400 var dialog = new MessageDialog (this, 401 DialogFlags.MODAL, 402 MessageType.WARNING, 403 ButtonsType.OK_CANCEL, 404 /* Translators: message displayed in a MessageDialog, when the player tries to start a game while one is running */ 405 _("Are you sure you want to start a new game?")); 406 407 408 /* Translators: message displayed in a MessageDialog, when the player tries to start a game while one is running */ 409 dialog.secondary_text = _("If you start a new game, the current one will be lost."); 410 411 var button = (Button) dialog.get_widget_for_response (ResponseType.OK); 412 /* Translators: label of a button displayed in a MessageDialog, when the player tries to start a game while one is running */ 413 button.set_label (_("_New Game")); 414 dialog.response.connect ((response_id) => { 415 if (response_id == ResponseType.OK) 416 show_new_game_screen (); 417 if ((response_id == ResponseType.CANCEL || response_id == ResponseType.DELETE_EVENT) 418 && !game.is_paused) 419 { 420 if (seconds == 0) 421 game.start (/* add initial bonus */ false); 422 else 423 countdown_id = Timeout.add_seconds (1, countdown_cb); 424 425 view.grab_focus (); 426 } 427 428 dialog.destroy (); 429 }); 430 431 dialog.show (); 432 } 433 434 private void pause_cb () 435 { 436 if (game != null) 437 { 438 if (game.is_running) 439 { 440 game.pause (); 441 set_pause_button_label (/* paused */ true); 442 } 443 else 444 { 445 game.unpause (); 446 set_pause_button_label (/* paused */ false); 447 view.grab_focus (); 448 } 449 } 450 } 451 452 private void set_pause_button_label (bool paused) 453 { 454 if (paused) 455 { 456 /* Translators: label of the Pause button, when the game is paused */ 457 pause_button.set_label (_("_Resume")); 458 } 459 else 460 { 461 /* Translators: label of the Pause button, when the game is running */ 462 pause_button.set_label (_("_Pause")); // duplicated in nibbles.ui 463 } 464 } 465 466 private void hamburger_cb () 467 { 468 hamburger_menu.active = !hamburger_menu.active; 469 } 470 471 /*\ 472 * * Settings changed events 473 \*/ 474 475 private void settings_changed_cb (string key) 476 { 477 switch (key) 478 { 479 case "speed": 480 game.speed = settings.get_int (key); 481 break; 482 case "sound": 483 view.is_muted = !settings.get_boolean (key); 484 break; 485 case "fakes": 486 game.fakes = settings.get_boolean (key); 487 break; 488 } 489 } 490 491 private void worm_settings_changed_cb (GLib.Settings changed_worm_settings, string key) 492 { 493 /* Empty worm properties means game has not started yet */ 494 if (game.worm_props.size == 0) 495 return; 496 497 var id = worm_settings.index_of (changed_worm_settings); 498 499 if (id >= game.numworms) 500 return; 501 502 var worm = game.worms[id]; 503 var properties = game.worm_props.@get (worm); 504 505 switch (key) 506 { 507 case "color": 508 properties.color = changed_worm_settings.get_enum ("color"); 509 break; 510 case "key-up": 511 properties.up = changed_worm_settings.get_int ("key-up"); 512 break; 513 case "key-down": 514 properties.down = changed_worm_settings.get_int ("key-down"); 515 break; 516 case "key-left": 517 properties.left = changed_worm_settings.get_int ("key-left"); 518 break; 519 case "key-right": 520 properties.right = changed_worm_settings.get_int ("key-right"); 521 break; 522 } 523 524 game.worm_props.@set (worm, properties); 525 526 if (id < game.numhumans) 527 update_start_game_action (); 528 } 529 530 /*\ 531 * * Switching the stack 532 \*/ 533 534 private inline void next_screen_cb () 535 { 536 var child_name = main_stack.get_visible_child_name (); 537 switch (child_name) 538 { 539 case "first-run": 540 show_new_game_screen (/* after first run */ true); 541 break; 542 case "number_of_players": 543 show_speed_screen (); 544 break; 545 case "speed": 546 leave_speed_screen (); 547 show_controls_screen (); 548 break; 549 case "controls": 550 assert_not_reached (); 551 default: 552 return; 553 } 554 } 555 556 private void show_new_game_screen (bool after_first_run = false) 557 { 558 if (countdown_id != 0) 559 { 560 Source.remove (countdown_id); 561 countdown_id = 0; 562 } 563 564 if (game.is_running) 565 game.stop (); 566 567 headerbar.set_title (Nibbles.PROGRAM_NAME); 568 569 new_game_action.set_enabled (true); 570 pause_action.set_enabled (false); 571 back_action.set_enabled (true); 572 573 new_game_button.hide (); 574 pause_button.hide (); 575 576 if (after_first_run) 577 main_stack.set_transition_type (StackTransitionType.SLIDE_UP); 578 else 579 main_stack.set_transition_type (StackTransitionType.NONE); 580 main_stack.set_visible_child_name ("number_of_players"); 581 main_stack.set_transition_type (StackTransitionType.SLIDE_UP); 582 } 583 584 private void show_speed_screen () 585 { 586 int numhumans, numai; 587 players.get_values (out numhumans, out numai); 588 game.numhumans = numhumans; 589 game.numai = numai; 590 settings.set_int ("players", numhumans); 591 settings.set_int ("ai", numai); 592 593 main_stack.set_visible_child_name ("speed"); 594 } 595 596 private void leave_speed_screen () 597 { 598 int game_speed; 599 bool fakes; 600 speed.get_values (out game_speed, out fakes); 601 game.speed = game_speed; 602 game.fakes = fakes; 603 settings.set_int ("speed", game_speed); 604 settings.set_boolean ("fakes", fakes); 605 } 606 607 private void show_controls_screen () 608 { 609 controls.clean (); 610 game.create_worms (); 611 game.load_worm_properties (worm_settings); 612 update_start_game_action (); 613 614 controls.prepare (game.worms, game.worm_props); 615 616 main_stack.set_visible_child_name ("controls"); 617 } 618 619 private void update_start_game_action () 620 { 621 GenericSet<uint> keys = new GenericSet<uint> (direct_hash, direct_equal); 622 for (int i = 0; i < game.numhumans; i++) 623 { 624 WormProperties worm_prop = game.worm_props.@get (game.worms.@get (i)); 625 if (worm_prop.up == 0 626 || worm_prop.down == 0 627 || worm_prop.left == 0 628 || worm_prop.right == 0 629 // other keys of the same worm 630 || worm_prop.up == worm_prop.down 631 || worm_prop.up == worm_prop.left 632 || worm_prop.up == worm_prop.right 633 || worm_prop.down == worm_prop.left 634 || worm_prop.down == worm_prop.right 635 || worm_prop.right == worm_prop.left 636 // keys of already checked worms 637 || keys.contains (worm_prop.up) 638 || keys.contains (worm_prop.down) 639 || keys.contains (worm_prop.left) 640 || keys.contains (worm_prop.right)) 641 { 642 start_game_action.set_enabled (false); 643 return; 644 } 645 keys.add (worm_prop.up); 646 keys.add (worm_prop.down); 647 keys.add (worm_prop.left); 648 keys.add (worm_prop.right); 649 } 650 start_game_action.set_enabled (true); 651 } 652 653 private void show_game_view () 654 { 655 /* FIXME: If there's a transition set, on Wayland, the ClutterEmbed 656 * will show outside the game's window. Don't change the transition 657 * type when that's no longer a problem. 658 */ 659 main_stack.set_transition_type (StackTransitionType.NONE); 660 new_game_button.show (); 661 pause_button.show (); 662 663 /* Translators: title of the headerbar, while a game is running; the %d is replaced by the level number */ 664 headerbar.set_title (_("Level %d").printf (game.current_level)); // TODO unduplicate, 1/2 665 main_stack.set_visible_child_name ("game_box"); 666 667 main_stack.set_transition_type (StackTransitionType.SLIDE_UP); 668 } 669 670 private void back_cb () 671 { 672 main_stack.set_transition_type (StackTransitionType.SLIDE_DOWN); 673 674 var child_name = main_stack.get_visible_child_name (); 675 switch (child_name) 676 { 677 case "first-run": 678 assert_not_reached (); 679 case "number_of_players": 680 break; 681 case "speed": 682 main_stack.set_visible_child_name ("number_of_players"); 683 break; 684 case "controls": 685 main_stack.set_visible_child_name ("speed"); 686 break; 687 case "game_box": 688 new_game_cb (); 689 break; 690 } 691 692 main_stack.set_transition_type (StackTransitionType.SLIDE_UP); 693 } 694 695 /*\ 696 * * Scoring 697 \*/ 698 699 private Games.Scores.Category? category_request (string key) 700 { 701 foreach (var cat in scorecats) 702 { 703 if (key == cat.key) 704 return cat; 705 } 706 return null; 707 } 708 709 private string? get_new_scores_key (string old_key) 710 { 711 switch (old_key) 712 { 713 case "1.0": 714 return "fast"; 715 case "2.0": 716 return "medium"; 717 case "3.0": 718 return "slow"; 719 case "4.0": 720 return "beginner"; 721 case "1.1": 722 return "fast-fakes"; 723 case "2.1": 724 return "medium-fakes"; 725 case "3.1": 726 return "slow-fakes"; 727 case "4.1": 728 return "beginner-fakes"; 729 } 730 return null; 731 } 732 733 private void create_scores () 734 { 735 scorecats = new Gee.LinkedList<Games.Scores.Category> (); 736 /* Translators: Difficulty level displayed on the scores dialog */ 737 scorecats.add (new Games.Scores.Category ("beginner", _("Beginner"))); 738 /* Translators: Difficulty level displayed on the scores dialog */ 739 scorecats.add (new Games.Scores.Category ("slow", _("Slow"))); 740 /* Translators: Difficulty level displayed on the scores dialog */ 741 scorecats.add (new Games.Scores.Category ("medium", _("Medium"))); 742 /* Translators: Difficulty level displayed on the scores dialog */ 743 scorecats.add (new Games.Scores.Category ("fast", _("Fast"))); 744 /* Translators: Difficulty level with fake bonuses, displayed on the scores dialog */ 745 scorecats.add (new Games.Scores.Category ("beginner-fakes", _("Beginner with Fakes"))); 746 /* Translators: Difficulty level with fake bonuses, displayed on the scores dialog */ 747 scorecats.add (new Games.Scores.Category ("slow-fakes", _("Slow with Fakes"))); 748 /* Translators: Difficulty level with fake bonuses, displayed on the scores dialog */ 749 scorecats.add (new Games.Scores.Category ("medium-fakes", _("Medium with Fakes"))); 750 /* Translators: Difficulty level with fake bonuses, displayed on the scores dialog */ 751 scorecats.add (new Games.Scores.Category ("fast-fakes", _("Fast with Fakes"))); 752 753 scores_context = new Games.Scores.Context.with_importer_and_icon_name ( 754 "gnome-nibbles", 755 /* Translators: label displayed on the scores dialog, preceding a difficulty. */ 756 _("Difficulty Level:"), 757 this, 758 category_request, 759 Games.Scores.Style.POINTS_GREATER_IS_BETTER, 760 new Games.Scores.DirectoryImporter.with_convert_func (get_new_scores_key), 761 "org.gnome.Nibbles"); 762 } 763 764 private Games.Scores.Category get_scores_category (int speed, bool fakes) 765 { 766 string key = null; 767 switch (speed) 768 { 769 case 1: 770 key = "fast"; 771 break; 772 case 2: 773 key = "medium"; 774 break; 775 case 3: 776 key = "slow"; 777 break; 778 case 4: 779 key = "beginner"; 780 break; 781 } 782 783 if (fakes) 784 key = key + "-fakes"; 785 786 foreach (var cat in scorecats) 787 { 788 if (key == cat.key) 789 return cat; 790 } 791 792 return scorecats.first (); 793 } 794 795 private void log_score_cb (int score, int level_reached) 796 { 797 /* Disable these here to prevent the user clicking the buttons before the score is saved */ 798 new_game_action.set_enabled (false); 799 pause_action.set_enabled (false); 800 back_action.set_enabled (false); 801 802 var scores = scores_context.get_high_scores (get_scores_category (game.speed, game.fakes)); 803 var lowest_high_score = (scores.size == 10 ? scores.last ().score : -1); 804 805 if (game.numhumans != 1) 806 { 807 game_over (score, lowest_high_score, level_reached); 808 return; 809 } 810 811 if (game.skip_score) 812 { 813 game_over (score, lowest_high_score, level_reached); 814 return; 815 } 816 817 scores_context.add_score.begin (score, 818 get_scores_category (game.speed, game.fakes), 819 null, 820 (object, result) => { 821 try 822 { 823 scores_context.add_score.end (result); 824 } 825 catch (GLib.Error e) 826 { 827 warning ("Failed to add score: %s", e.message); 828 } 829 830 game_over (score, lowest_high_score, level_reached); 831 }); 832 } 833 834 private void scores_cb () 835 { 836 var should_unpause = false; 837 if (game.is_running) 838 { 839 pause_action.activate (null); 840 should_unpause = true; 841 } 842 843 scores_context.run_dialog (); 844 845 // Be quite careful about whether to unpause. Don't unpause if the game has not started. 846 if (should_unpause) 847 pause_action.activate (null); 848 } 849 850 private void level_completed_cb () 851 { 852 if (game.current_level == NibblesGame.MAX_LEVEL) 853 return; 854 855 view.hide (); 856 857 new_game_action.set_enabled (false); 858 pause_action.set_enabled (false); 859 back_action.set_enabled (false); 860 861 /* Translators: label that appears at the end of a level; the %d is the number of the level that was completed */ 862 var label = new Label (_("Level %d Completed!").printf (game.current_level)); 863 label.halign = Align.CENTER; 864 label.valign = Align.START; 865 label.set_margin_top (150); 866 label.get_style_context ().add_class ("menu-title"); 867 label.show (); 868 869 /* Translators: label of a button that appears at the end of a level; starts next level */ 870 var button = new Button.with_label (_("_Next Level")); 871 button.set_use_underline (true); 872 button.halign = Align.CENTER; 873 button.valign = Align.END; 874 button.set_margin_bottom (100); 875 button.get_style_context ().add_class ("suggested-action"); 876 button.set_can_default (true); 877 button.clicked.connect (() => { 878 label.destroy (); 879 button.destroy (); 880 881 /* Translators: title of the headerbar, while a game is running; the %d is replaced by the level number */ 882 headerbar.set_title (_("Level %d").printf (game.current_level)); // TODO unduplicate, 2/2 883 884 view.show (); 885 886 restart_game (); 887 }); 888 889 overlay.add_overlay (label); 890 overlay.add_overlay (button); 891 892 overlay.grab_default (); 893 894 Timeout.add (500, () => { 895 button.show (); 896 button.grab_default (); 897 898 return Source.REMOVE; 899 }); 900 } 901 902 private void preferences_cb (SimpleAction action, Variant? variant) 903 requires (variant != null) 904 { 905 var should_unpause = false; 906 if (game.is_running) 907 { 908 pause_action.activate (null); 909 should_unpause = true; 910 } 911 912 PreferencesDialog preferences_dialog = new PreferencesDialog (this, settings, worm_settings, ((!) variant).get_int32 (), game.numhumans); 913 914 preferences_dialog.destroy.connect (() => { 915 if (should_unpause) 916 pause_action.activate (null); 917 }); 918 919 preferences_dialog.present (); 920 } 921 922 private bool end_of_game = false; 923 private Label game_over_label; 924 private Label msg_label; 925 private Label score_label; 926 private Label points_left_label; 927 private Button play_again_button; 928 private void game_over (int score, long lowest_high_score, int level_reached) 929 { 930 var is_high_score = (score > lowest_high_score); 931 var is_game_won = (level_reached == NibblesGame.MAX_LEVEL + 1); 932 933 /* Translators: label displayed at the end of a level, if the player finished all the levels */ 934 game_over_label = new Label (is_game_won ? _("Congratulations!") 935 936 937 /* Translators: label displayed at the end of a level, if the player did not finished all the levels */ 938 : _("Game Over!")); 939 game_over_label.halign = Align.CENTER; 940 game_over_label.valign = Align.START; 941 game_over_label.set_margin_top (150); 942 game_over_label.get_style_context ().add_class ("menu-title"); 943 game_over_label.show (); 944 945 /* Translators: label displayed at the end of a level, if the player finished all the levels */ 946 msg_label = new Label (_("You have completed the game.")); 947 msg_label.halign = Align.CENTER; 948 msg_label.valign = Align.START; 949 msg_label.set_margin_top (window_height / 3); 950 msg_label.get_style_context ().add_class ("menu-title"); 951 msg_label.show (); 952 953 var score_string = ngettext ("%d Point", "%d Points", score); 954 score_string = score_string.printf (score); 955 score_label = new Label (@"<b>$(score_string)</b>"); 956 score_label.set_use_markup (true); 957 score_label.halign = Align.CENTER; 958 score_label.valign = Align.START; 959 score_label.set_margin_top (window_height / 3 + 80); 960 score_label.show (); 961 962 var points_left = lowest_high_score - score; 963 /* Translators: label displayed at the end of a level, if the player did not score enough to have its score saved */ 964 points_left_label = new Label (_("(%ld more points to reach the leaderboard)").printf (points_left)); 965 points_left_label.halign = Align.CENTER; 966 points_left_label.valign = Align.START; 967 points_left_label.set_margin_top (window_height / 3 + 100); 968 points_left_label.show (); 969 970 /* Translators: label of a button displayed at the end of a level; restarts the game */ 971 play_again_button = new Button.with_label (_("_Play Again")); 972 play_again_button.set_use_underline (true); 973 play_again_button.halign = Align.CENTER; 974 play_again_button.valign = Align.END; 975 play_again_button.set_margin_bottom (100); 976 play_again_button.get_style_context ().add_class ("suggested-action"); 977 play_again_button.set_action_name ("win.new-game"); 978 play_again_button.show (); 979 980 overlay.add_overlay (game_over_label); 981 if (is_game_won) 982 overlay.add_overlay (msg_label); 983 if (game.numhumans == 1) 984 overlay.add_overlay (score_label); 985 if (game.numhumans == 1 && !is_high_score) 986 overlay.add_overlay (points_left_label); 987 overlay.add_overlay (play_again_button); 988 989 play_again_button.grab_focus (); 990 991 view.hide (); 992 end_of_game = true; 993 new_game_action.set_enabled (true); 994 pause_action.set_enabled (false); 995 back_action.set_enabled (false); 996 } 997} 998 999[GtkTemplate (ui = "/org/gnome/Nibbles/ui/first-run.ui")] 1000private class FirstRun : Box 1001{ 1002} 1003