1 /* Atomix -- a little puzzle game about atoms and molecules.
2 * Copyright (C) 1999-2001 Jens Finke
3 * Copyright (C) 2005 Guilherme de S. Pastore
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 2 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, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 */
19
20 #include "config.h"
21
22 #include "board-gtk.h"
23 #include "playfield.h"
24 #include "main.h"
25 #include "goal.h"
26 #include "level.h"
27 #include "goal-view.h"
28 #include "clock.h"
29 #include "undo.h"
30 #include <libgnome-games-support.h>
31
32 AtomixApp *app;
33
34 typedef enum
35 {
36 GAME_ACTION_NEW,
37 GAME_ACTION_END,
38 GAME_ACTION_PAUSE,
39 GAME_ACTION_CONTINUE,
40 GAME_ACTION_SKIP,
41 GAME_ACTION_UNDO,
42 GAME_ACTION_FINISHED,
43 GAME_ACTION_RESTART,
44 } GameAction;
45
46 static void controller_handle_action (GameAction action);
47 static void set_game_not_running_state (void);
48 static gboolean set_next_level (void);
49 static void setup_level (void);
50 static void level_cleanup_view (void);
51 static void atomix_exit (void);
52 static void game_init (void);
53 static void update_statistics (void);
54 static void view_congratulations (void);
55 static void calculate_score (void);
56
57 /* ===============================================================
58
59 Menu callback functions
60
61 -------------------------------------------------------------- */
verb_GameNew_cb(GSimpleAction * action,GVariant * variant,gpointer data)62 static void verb_GameNew_cb (GSimpleAction *action, GVariant *variant, gpointer data)
63 {
64 controller_handle_action (GAME_ACTION_NEW);
65 }
66
verb_GameEnd_cb(GSimpleAction * action,GVariant * variant,gpointer data)67 static void verb_GameEnd_cb (GSimpleAction *action, GVariant *variant, gpointer data)
68 {
69 controller_handle_action (GAME_ACTION_END);
70 }
71
verb_GameSkip_cb(GSimpleAction * action,GVariant * variant,gpointer data)72 static void verb_GameSkip_cb (GSimpleAction *action, GVariant *variant, gpointer data)
73 {
74 controller_handle_action (GAME_ACTION_SKIP);
75 }
76
verb_GameReset_cb(GSimpleAction * action,GVariant * variant,gpointer data)77 static void verb_GameReset_cb (GSimpleAction *action, GVariant *variant, gpointer data)
78 {
79 controller_handle_action (GAME_ACTION_RESTART);
80 }
81
verb_GamePause_cb(GSimpleAction * action,GVariant * variant,gpointer data)82 static void verb_GamePause_cb (GSimpleAction *action, GVariant *variant, gpointer data)
83 {
84 if (app->state != GAME_STATE_PAUSED) {
85 controller_handle_action (GAME_ACTION_PAUSE);
86 } else {
87 controller_handle_action (GAME_ACTION_CONTINUE);
88 }
89 }
90
verb_GameUndo_cb(GSimpleAction * action,GVariant * variant,gpointer data)91 static void verb_GameUndo_cb (GSimpleAction *action, GVariant *variant, gpointer data)
92 {
93 controller_handle_action (GAME_ACTION_UNDO);
94 }
95
verb_GameExit_cb(GSimpleAction * action,GVariant * variant,gpointer data)96 static void verb_GameExit_cb (GSimpleAction *action, GVariant *variant, gpointer data)
97 {
98 atomix_exit ();
99 }
100
verb_HelpAbout_cb(GSimpleAction * action,GVariant * variant,gpointer data)101 static void verb_HelpAbout_cb (GSimpleAction *action, GVariant *variant, gpointer data)
102 {
103 const char *authors[] =
104 {
105 "Guilherme de S. Pastore <gpastore@gnome.org>",
106 "Jens Finke <jens@triq.net>",
107 "Robert Roth <robert.roth.off@gmail.com>",
108 NULL
109 };
110
111 const char *artists[] =
112 {
113 "Jakub Steiner <jimmac@ximian.com>",
114 NULL
115 };
116
117
118 gtk_show_about_dialog(GTK_WINDOW(app->mainwin),
119 "program-name", _("Atomix"),
120 "logo-icon-name", "atomix",
121 "version", VERSION,
122 "comments", _("A puzzle game about atoms and molecules"),
123 "website", "https://wiki.gnome.org/Apps/Atomix",
124 "authors", authors,
125 "artists", artists,
126 "translator_credits", _("translator-credits"),
127 NULL);
128 }
129
on_app_destroy_event(GtkWidget * widget,GdkEvent * event,gpointer user_data)130 static gboolean on_app_destroy_event (GtkWidget *widget, GdkEvent *event,
131 gpointer user_data)
132 {
133 atomix_exit ();
134
135 return TRUE;
136 }
137
138 /* ===============================================================
139
140 Game steering functions
141
142 -------------------------------------------------------------- */
143
controller_handle_action(GameAction action)144 static void controller_handle_action (GameAction action)
145 {
146 switch (app->state)
147 {
148 case GAME_STATE_NOT_RUNNING:
149 if (action == GAME_ACTION_NEW && set_next_level ())
150 {
151 app->level_no = 1;
152 app->score = 0;
153 setup_level ();
154 app->state = GAME_STATE_RUNNING_UNMOVED;
155 }
156 break;
157
158 case GAME_STATE_RUNNING_UNMOVED:
159 case GAME_STATE_RUNNING:
160 switch (action)
161 {
162 case GAME_ACTION_END:
163 level_cleanup_view ();
164 set_game_not_running_state ();
165 break;
166
167 case GAME_ACTION_PAUSE:
168 clock_stop (CLOCK (app->clock));
169 board_gtk_hide ();
170 app->state = GAME_STATE_PAUSED;
171 break;
172
173 case GAME_ACTION_SKIP:
174 level_cleanup_view ();
175
176 if (set_next_level ())
177 setup_level ();
178 else
179 set_game_not_running_state ();
180
181 break;
182
183 case GAME_ACTION_FINISHED:
184 calculate_score ();
185 if (level_manager_is_last_level (app->lm, app->level))
186 {
187 view_congratulations ();
188 level_cleanup_view ();
189 set_game_not_running_state ();
190 }
191 else
192 {
193 level_cleanup_view ();
194 set_next_level ();
195 setup_level ();
196 }
197 break;
198
199 case GAME_ACTION_RESTART:
200 g_assert (app->state != GAME_STATE_RUNNING_UNMOVED);
201
202 level_cleanup_view ();
203 setup_level ();
204 break;
205
206 case GAME_ACTION_UNDO:
207 g_assert (app->state != GAME_STATE_RUNNING_UNMOVED);
208
209 board_gtk_undo_move ();
210 break;
211
212 case GAME_ACTION_NEW:
213 case GAME_ACTION_CONTINUE:
214 default:
215 break;
216 }
217 break;
218
219 case GAME_STATE_PAUSED:
220 if (action == GAME_ACTION_CONTINUE)
221 {
222 clock_resume (CLOCK(app->clock));
223 board_gtk_show ();
224 app->state = (undo_exists())?GAME_STATE_RUNNING:GAME_STATE_RUNNING_UNMOVED;
225 }
226 break;
227 default:
228 g_assert_not_reached ();
229 }
230
231 update_menu_item_state ();
232 update_statistics ();
233 gtk_widget_grab_focus (GTK_WIDGET (app->fi_matrix));
234 }
235
236 static void
add_score_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)237 add_score_cb (GObject *source_object, GAsyncResult *res, gpointer user_data)
238 {
239 GamesScoresContext *context = GAMES_SCORES_CONTEXT (source_object);
240 GError *error = NULL;
241
242 games_scores_context_add_score_finish (context, res, &error);
243 if (error != NULL) {
244 g_warning ("Failed to add score: %s", error->message);
245 g_error_free (error);
246 }
247 }
248
249
set_game_not_running_state(void)250 static void set_game_not_running_state (void)
251 {
252 board_gtk_show_logo (TRUE);
253
254 if (app->level)
255 g_object_unref (app->level);
256
257 if (app->goal)
258 g_object_unref (app->goal);
259
260 if (app->score > 0) {
261 games_scores_context_add_score (app->high_scores,
262 app->score,
263 app->current_category,
264 NULL,
265 add_score_cb,
266 NULL);
267 }
268
269 app->level = NULL;
270 app->goal = NULL;
271 app->score = 0;
272 app->state = GAME_STATE_NOT_RUNNING;
273 }
274
set_next_level(void)275 static gboolean set_next_level (void)
276 {
277 Level *next_level;
278 PlayField *goal_pf;
279
280 next_level = level_manager_get_next_level (app->lm, app->level);
281
282 if (app->level)
283 {
284 g_object_unref (app->level);
285 }
286 app->level = NULL;
287
288 if (app->goal)
289 {
290 g_object_unref (app->goal);
291 }
292 app->goal = NULL;
293
294 if (next_level == NULL)
295 {
296 return FALSE;
297 }
298
299 app->level = next_level;
300 app->level_no++;
301 goal_pf = level_get_goal (app->level);
302 app->goal = goal_new (goal_pf);
303
304 g_object_unref (goal_pf);
305
306 return TRUE;
307 }
308
setup_level(void)309 static void setup_level (void)
310 {
311 PlayField *env_pf;
312 PlayField *sce_pf;
313
314 g_return_if_fail (app != NULL);
315
316 if (app->level == NULL)
317 return;
318
319 g_return_if_fail (app->goal != NULL);
320
321 /* init board */
322 env_pf = level_get_environment (app->level);
323 sce_pf = level_get_scenario (app->level);
324 board_gtk_show_logo (FALSE);
325 board_gtk_init_level (env_pf, sce_pf, app->goal);
326
327 /* init goal */
328 goal_view_render (app->goal);
329
330 /* init clock */
331 clock_reset (CLOCK(app->clock));
332 clock_start (CLOCK(app->clock));
333
334 g_object_unref (env_pf);
335 g_object_unref (sce_pf);
336 }
337
level_cleanup_view(void)338 static void level_cleanup_view (void)
339 {
340 board_gtk_clear ();
341 goal_view_clear ();
342
343 clock_stop (CLOCK(app->clock));
344 }
345
atomix_exit(void)346 static void atomix_exit (void)
347 {
348 #ifdef DEBUG
349 g_message ("Destroy application");
350 #endif
351
352 g_return_if_fail (app != NULL);
353
354 if (app->state != GAME_STATE_NOT_RUNNING)
355 set_game_not_running_state ();
356
357 board_gtk_destroy ();
358
359 if (app->level)
360 g_object_unref (app->level);
361
362 if (app->lm)
363 g_object_unref (app->lm);
364
365 if (app->theme)
366 g_object_unref (app->theme);
367
368 if (app->tm)
369 g_object_unref (app->tm);
370
371 /* quit application */
372 gtk_widget_destroy (app->mainwin);
373
374 }
375
game_init(void)376 static void game_init (void)
377 {
378 g_return_if_fail (app != NULL);
379
380 /* init theme manager */
381 app->tm = theme_manager_new ();
382 theme_manager_init_themes (app->tm);
383 app->theme = theme_manager_get_theme (app->tm, "default");
384 g_assert (app->theme != NULL);
385
386 /* init level manager */
387 app->lm = level_manager_new ();
388 level_manager_init_levels (app->lm);
389
390 /* init level statistics */
391 app->level = NULL;
392 app->level_no = 0;
393 app->score = 0;
394 clock_set_format (CLOCK(app->clock), "%M:%S");
395 clock_reset (CLOCK(app->clock));
396
397 /* init the board */
398 board_gtk_init (app->theme, GTK_FIXED (app->fi_matrix));
399
400 /* init goal */
401 goal_view_init (app->theme, GTK_FIXED (app->fi_goal));
402
403 /* update user visible information */
404 app->state = GAME_STATE_NOT_RUNNING;
405 update_menu_item_state ();
406 update_statistics ();
407
408 gtk_widget_grab_focus (GTK_WIDGET (app->fi_matrix));
409 }
410
game_level_finished(void)411 void game_level_finished (void)
412 {
413 controller_handle_action (GAME_ACTION_FINISHED);
414 }
415
calculate_score(void)416 static void calculate_score (void)
417 {
418 gint seconds = clock_get_elapsed (CLOCK (app->clock));
419
420 if (seconds > 300)
421 return;
422
423 if (app->score == 0)
424 app->score = 300 - seconds;
425 else
426 app->score = app->score * (2 - (seconds / 300));
427 }
428
view_congratulations(void)429 static void view_congratulations (void)
430 {
431 GtkWidget *dlg;
432 dlg = gtk_message_dialog_new (GTK_WINDOW (app->mainwin),
433 GTK_DIALOG_MODAL,
434 GTK_MESSAGE_INFO,
435 GTK_BUTTONS_CLOSE,
436 "%s",
437 _("Congratulations! You have finished all Atomix levels."));
438 gtk_dialog_run (GTK_DIALOG (dlg));
439 gtk_widget_destroy (GTK_WIDGET (dlg));
440 }
441
442 /* ===============================================================
443
444 UI update functions
445
446 -------------------------------------------------------------- */
update_statistics(void)447 static void update_statistics (void)
448 {
449 gchar *str_buffer;
450
451 g_return_if_fail (app != NULL);
452
453 if (app->state == GAME_STATE_NOT_RUNNING)
454 {
455 /* don't show anything */
456 gtk_label_set_text (GTK_LABEL (app->lb_level), "");
457 gtk_label_set_text (GTK_LABEL (app->lb_name), "");
458 gtk_label_set_text (GTK_LABEL (app->lb_formula), "");
459 gtk_label_set_text (GTK_LABEL (app->lb_score), "");
460 gtk_widget_hide (GTK_WIDGET (app->clock));
461 }
462 else
463 {
464 /* set level number */
465 str_buffer = g_new0 (gchar, 10);
466 g_snprintf (str_buffer, 10, "%i", app->level_no);
467 gtk_label_set_text (GTK_LABEL (app->lb_level), str_buffer);
468
469 /* set levelname */
470 gtk_label_set_text (GTK_LABEL (app->lb_name),
471 _(level_get_name (app->level)));
472
473 /* set the formula of the compound */
474 gtk_label_set_markup (GTK_LABEL (app->lb_formula),
475 level_get_formula (app->level));
476
477 /* set score */
478 g_snprintf (str_buffer, 10, "%i", app->score);
479 gtk_label_set_text (GTK_LABEL (app->lb_score), str_buffer);
480
481 /* show clock */
482 gtk_widget_show (GTK_WIDGET (app->clock));
483
484 g_free (str_buffer);
485 }
486 }
487
488 typedef struct
489 {
490 const gchar *cmd;
491 gboolean enabled;
492 } CmdEnable;
493
494
495 static const GActionEntry app_entries[] =
496 {
497 { "GameAbout", verb_HelpAbout_cb, NULL, NULL, NULL},
498 { "GameQuit", verb_GameExit_cb, NULL, NULL, NULL}
499 };
500
501 static const GActionEntry win_entries[] =
502 {
503 { "GameNew", verb_GameNew_cb, NULL, NULL, NULL},
504 { "GameEnd", verb_GameEnd_cb, NULL, NULL, NULL},
505 { "LevelSkip", verb_GameSkip_cb, NULL, NULL, NULL},
506 { "LevelReset", verb_GameReset_cb, NULL, NULL, NULL},
507 { "GameUndo", verb_GameUndo_cb, NULL, NULL, NULL},
508 { "GamePause", verb_GamePause_cb, NULL, NULL, NULL},
509 };
510
511 static const CmdEnable not_running[] =
512 {
513 { "GameNew", TRUE },
514 { "GameEnd", FALSE },
515 { "LevelSkip", FALSE },
516 { "LevelReset", FALSE },
517 { "GameUndo", FALSE },
518 { "GamePause", FALSE },
519 { NULL, FALSE }
520 };
521
522 static const CmdEnable running_unmoved[] =
523 {
524 { "GameNew", FALSE },
525 { "GameEnd", TRUE },
526 { "LevelSkip", TRUE },
527 { "LevelReset", FALSE },
528 { "GameUndo", FALSE },
529 { "GamePause", TRUE },
530 { NULL, FALSE }
531 };
532
533 static const CmdEnable running[] =
534 {
535 { "GameNew", FALSE },
536 { "GameEnd", TRUE },
537 { "LevelSkip", TRUE },
538 { "LevelReset", TRUE },
539 { "GameUndo", TRUE },
540 { "GamePause", TRUE },
541 { NULL, FALSE }
542 };
543
544 static const CmdEnable paused[] =
545 {
546 { "GameNew", FALSE },
547 { "GameEnd", FALSE },
548 { "LevelSkip", FALSE },
549 { "LevelReset", FALSE },
550 { "GameUndo", FALSE },
551 { "GamePause", TRUE },
552 { NULL, FALSE }
553 };
554
555 static const CmdEnable *state_sensitivity[] =
556 {
557 not_running, running_unmoved, running, paused
558 };
559
update_menu_item_state(void)560 void update_menu_item_state (void)
561 {
562 gint i;
563 const CmdEnable *cmd_list = state_sensitivity[app->state];
564 GAction *action;
565
566 for (i = 0; cmd_list[i].cmd != NULL; i++)
567 {
568 action = g_action_map_lookup_action (G_ACTION_MAP (app->mainwin), cmd_list[i].cmd);
569 g_simple_action_set_enabled (G_SIMPLE_ACTION (action), cmd_list[i].enabled);
570 }
571 }
572
get_scores_category(const gchar * category_key,void * user_data)573 static GamesScoresCategory* get_scores_category(const gchar* category_key, void* user_data)
574 {
575 return app->current_category;
576 }
577 /* ===============================================================
578
579 GUI creation functions
580
581 -------------------------------------------------------------- */
create_gui(GApplication * app_instance)582 static AtomixApp *create_gui (GApplication *app_instance)
583 {
584 gchar *ui_path;
585 GtkBuilder *builder;
586 GtkWidget *stats_grid;
587 GtkWidget *time_label;
588 GtkWidget *headerbar;
589 GtkWidget *primary_menu_button;
590
591 app = g_new0 (AtomixApp, 1);
592 app->level = NULL;
593 app->app_instance = app_instance;
594
595 builder = gtk_builder_new ();
596
597 ui_path = g_build_filename (PKGDATADIR, "ui", "interface.ui", NULL);
598 gtk_builder_add_from_file (builder, ui_path, NULL);
599 g_free (ui_path);
600
601 ui_path = g_build_filename (PKGDATADIR, "ui", "menu.ui", NULL);
602 gtk_builder_add_from_file (builder, ui_path, NULL);
603 g_free (ui_path);
604
605 app->mainwin = GTK_WIDGET (gtk_builder_get_object (builder, "mainwin"));
606
607 app->current_category = games_scores_category_new ("atomix", "Best scores");
608 app->high_scores = games_scores_context_new("atomix", "Best scores",
609 GTK_WINDOW (app->mainwin),
610 get_scores_category,
611 NULL, GAMES_SCORES_STYLE_POINTS_GREATER_IS_BETTER);
612
613 g_signal_connect (G_OBJECT (app->mainwin), "delete_event",
614 (GCallback) on_app_destroy_event, app);
615
616 /* create canvas widgets */
617 app->fi_matrix = GTK_WIDGET (gtk_builder_get_object (builder, "game_fixed"));
618 app->fi_goal = GTK_WIDGET (gtk_builder_get_object (builder, "preview_fixed"));
619
620 stats_grid = GTK_WIDGET (gtk_builder_get_object (builder, "stats_grid"));
621 time_label = GTK_WIDGET (gtk_builder_get_object (builder, "time_label"));
622 app->clock = clock_new ();
623 gtk_grid_attach_next_to (GTK_GRID (stats_grid), app->clock,
624 time_label, GTK_POS_RIGHT, 1, 1);
625
626 app->lb_level = GTK_WIDGET (gtk_builder_get_object (builder, "level_value"));
627 app->lb_name = GTK_WIDGET (gtk_builder_get_object (builder, "molecule_value"));
628 app->lb_formula = GTK_WIDGET (gtk_builder_get_object (builder, "formula_value"));
629 app->lb_score = GTK_WIDGET (gtk_builder_get_object (builder, "score_value"));
630
631 gtk_widget_show_all (GTK_WIDGET (app->mainwin));
632
633 headerbar = GTK_WIDGET (gtk_builder_get_object(builder, "headerbar"));
634 gtk_application_add_window (GTK_APPLICATION (app->app_instance), GTK_WINDOW (app->mainwin));
635
636 const char *new_game_accels[] = {"<Primary>N", NULL};
637 gtk_application_set_accels_for_action (GTK_APPLICATION (app->app_instance), "win.GameNew", new_game_accels);
638 const char *pause_game_accels[] = {"<Primary>P", NULL};
639 gtk_application_set_accels_for_action (GTK_APPLICATION (app->app_instance), "win.GamePause", pause_game_accels);
640 const char *skip_level_accels[] = {"<Primary>S", NULL};
641 gtk_application_set_accels_for_action (GTK_APPLICATION (app->app_instance), "win.LevelSkip", skip_level_accels);
642 const char *undo_move_accels[] = {"<Primary>Z", NULL};
643 gtk_application_set_accels_for_action (GTK_APPLICATION (app->app_instance), "win.GameUndo", undo_move_accels);
644
645 gtk_window_set_titlebar (GTK_WINDOW (app->mainwin), headerbar);
646 GMenu * menu = G_MENU (gtk_builder_get_object (builder, "primary-menu"));
647
648 primary_menu_button = GTK_WIDGET (gtk_builder_get_object (builder, "primary-menu-button"));
649 gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (primary_menu_button), G_MENU_MODEL (menu));
650
651 g_object_unref (builder);
652
653 return app;
654 }
655
656 static void
app_activate(GApplication * app_instance,gpointer user_data)657 app_activate (GApplication *app_instance, gpointer user_data)
658 {
659
660 if (app == NULL) {
661 /* make a few initalisations here */
662 g_action_map_add_action_entries (G_ACTION_MAP (app_instance), app_entries, G_N_ELEMENTS (app_entries), app_instance);
663
664 app = create_gui (app_instance);
665
666 g_action_map_add_action_entries (G_ACTION_MAP (app->mainwin), win_entries, G_N_ELEMENTS (win_entries), app_instance);
667
668 game_init ();
669 gtk_widget_set_size_request (GTK_WIDGET (app->mainwin), 678, 520);
670
671 //gtk_window_set_resizable (GTK_WINDOW (app->mainwin), FALSE);
672 gtk_widget_show (app->mainwin);
673 } else {
674 gtk_window_present (GTK_WINDOW (app->mainwin));
675 }
676 }
677
main(int argc,char * argv[])678 int main (int argc, char *argv[])
679 {
680 GtkApplication *gtk_app;
681
682 int status;
683
684 g_set_application_name (_("Atomix"));
685
686 bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
687 bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
688 textdomain (GETTEXT_PACKAGE);
689
690 gtk_app = gtk_application_new ("org.gnome.atomix", G_APPLICATION_FLAGS_NONE);
691 g_signal_connect (gtk_app, "activate", G_CALLBACK (app_activate), NULL);
692
693 status = g_application_run (G_APPLICATION (gtk_app), argc, argv);
694 g_object_unref (gtk_app);
695
696 return status;
697 }
698