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