/*============================================================================*/ // // Title: ds_pof.c // Author: RH // // Description: // GUI application to host character details in the TTRPG game // 'Dark Souls: Phases of Fire' and automatically calculate the outcome of a // dice roll based on current stats and conditions. // // TODO: // - Need header file for types and defines // - menu bar should be seperate element // - window controls // - Would be nice to seperate GUI source file from actual calculations and // operations typical of a CLI c application // - CSS styling and parsing // - Look at XML UI building // - Implement Loading/Saving of character details -> csv for now, sqlite3 in // the future // - Implement full character stats, feats, class, and status conditions // - this will take some considerable effort // - Make the app look pretty - the hardest of all challenges // - Resist the urge to learn JS and build in electron /*============================================================================*/ #include #include #define NUM_CORE_STATS 6 // --------------------------------------------------------- // this struct pointer will contain pointers to various // elements all needed in attack/damage callback functions // // a struct is neccessary when passing multiple elements // in a g_signal_connect() // // it is created in main and passed into activate(), // where it is then poulated as elements are created // --------------------------------------------------------- struct atk_dmg_object { GtkEntryBuffer *dice_roll_buffer; GtkWidget *stat_modifiers[NUM_CORE_STATS]; //GtkWidget *str_modifier; GtkWidget *prof_button; GtkTextBuffer *calc_result_buffer; // dmg only: GtkWidget *cinder_wrath_check; }; // definitions for indexes of atk_dmg_object.stat_modifiers #define STR_MODIFIER 0 #define DEX_MODIFIER 1 #define CON_MODIFIER 2 #define INT_MODIFIER 3 #define WIS_MODIFIER 4 #define CHA_MODIFIER 5 static char result_buf[200] = {'\0'}; // --------------------------------------------------------- // callback function to calculate attack roll whenever the // attack button is pressed // --------------------------------------------------------- static void attack_calc(GtkWidget *widget, gpointer data) { struct atk_dmg_object *p_atk_dmg_struct = (struct atk_dmg_object *) data; int dice_roll_value = atoi(gtk_entry_buffer_get_text(p_atk_dmg_struct->dice_roll_buffer)); int str_mod_value = atoi(gtk_label_get_text((GtkLabel *) p_atk_dmg_struct->stat_modifiers[STR_MODIFIER])); int prof_value = gtk_spin_button_get_value_as_int((GtkSpinButton *) p_atk_dmg_struct->prof_button); int atk_result = dice_roll_value + prof_value + str_mod_value; sprintf(result_buf, "Attack result: %d [Dice: %d] [Proficiency: %d] [Strength: %d]", atk_result, dice_roll_value, prof_value, str_mod_value); gtk_text_buffer_set_text(p_atk_dmg_struct->calc_result_buffer, result_buf, -1); // clear the dice roll field for convenience gtk_entry_buffer_delete_text(p_atk_dmg_struct->dice_roll_buffer, 0, -1 /*delete all*/); } // --------------------------------------------------------- // callback function to calculate damage roll whenever the // damage button is pressed // --------------------------------------------------------- static void damage_calc(GtkWidget *widget, gpointer data) { struct atk_dmg_object *p_atk_dmg_struct = (struct atk_dmg_object *) data; int dice_roll_value = atoi(gtk_entry_buffer_get_text(p_atk_dmg_struct->dice_roll_buffer)); int cinder_wrath = gtk_check_button_get_active((GtkCheckButton *) p_atk_dmg_struct->cinder_wrath_check); int str_mod_value = atoi(gtk_label_get_text((GtkLabel *) p_atk_dmg_struct->stat_modifiers[STR_MODIFIER])); int prof_value = (cinder_wrath == 1) ? gtk_spin_button_get_value_as_int((GtkSpinButton *) p_atk_dmg_struct->prof_button) : 0; int additional_buff = 2; //TODO: this is for duelling, need a way to parameteris this/configure int dmg_result = dice_roll_value + prof_value + str_mod_value + additional_buff; sprintf(result_buf, "Damage result: %d [Dice: %d] [Proficiency: %d | Cinder Wrath %s] [Strength: %d] [Duelling Buff: %d]", dmg_result, dice_roll_value, prof_value, (cinder_wrath == 1) ? "Enabled" : "Disabled", str_mod_value, additional_buff); gtk_text_buffer_set_text(p_atk_dmg_struct->calc_result_buffer, result_buf, -1); // clear the dice roll field for convenience gtk_entry_buffer_delete_text(p_atk_dmg_struct->dice_roll_buffer, 0, -1 /*delete all*/); } // --------------------------------------------------------- // callback function used by all core stat spin-button // to update a stat modifier whenever the raw value in // the spin-button is changed // --------------------------------------------------------- static int stat_calc(GtkWidget *spin_button, GtkWidget *mod_label) { int raw_value = gtk_spin_button_get_value_as_int((GtkSpinButton *) spin_button); int mod_value = (int) ((raw_value - 10) / 2); char mod_buf[20] = {'\0'}; sprintf(mod_buf, "%d", mod_value); gtk_label_set_markup(GTK_LABEL(mod_label), mod_buf); } // --------------------------------------------------------- // callback function to set the character stats to those // parsed from an input file // --------------------------------------------------------- static void load_from_file_activated(GSimpleAction *action, GVariant *parameter, gpointer app) { g_print("load\n"); } // --------------------------------------------------------- // callback function to save current character stats to // a file // --------------------------------------------------------- static void save_to_file_activated(GSimpleAction *action, GVariant *parameter, gpointer app) { g_print("save\n"); } // --------------------------------------------------------- // callback function to exit the app // --------------------------------------------------------- static void quit_activated(GSimpleAction *action, GVariant *parameter, gpointer app) { g_application_quit(G_APPLICATION (app)); } // --------------------------------------------------------- // callback function to enforce numeric only entry on the // dice roll input buffer // --------------------------------------------------------- void numeric_only_text_check(GtkEditable *editable, const gchar *text, gint length, gint *position, gpointer data) { if (!isdigit(text[0])) { gtk_editable_delete_text(editable, *position-1, -1); } } // --------------------------------------------------------- // activation function - configures the application UI // --------------------------------------------------------- static void activate (GtkApplication *app, gpointer user_data) { struct atk_dmg_object *data = (struct atk_dmg_object *) user_data; GtkWidget *window; GtkWidget *grid; GtkWidget *button; GtkWidget *inscription; GtkAdjustment *adjustment; /* create a new window */ window = gtk_application_window_new(app); gtk_window_set_default_size (GTK_WINDOW (window), 600, 400); /* Here we construct the container that is going pack our core elements (buttons etc) */ grid = gtk_grid_new (); // set grid padding and row/col spacing gtk_grid_set_column_spacing((GtkGrid*) grid, 2); gtk_grid_set_row_spacing((GtkGrid*) grid, 2); gtk_widget_set_margin_start(grid, 10); gtk_widget_set_margin_end(grid, 10); // create two stacks and a switcher so we can have two tabs // one tab is for the basic attack/damage roll function using the 6 cores stats // the second tab is for full character customisation and can affect attack/damage roll options //TODO: implement the contents of second tab GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); GtkWidget *stack = gtk_stack_new (); GtkWidget *switcher = gtk_stack_switcher_new (); GtkWidget *label2 = gtk_label_new("Stack Page 2"); gtk_stack_add_titled((GtkStack *) stack, GTK_WIDGET(grid), "Calculator", "Calculator"); gtk_stack_add_titled((GtkStack *) stack, GTK_WIDGET(label2), "Character Details", "Character Details"); gtk_stack_switcher_set_stack((GtkStackSwitcher *) switcher, (GtkStack *) stack); //order matters, this way around gives tabs at top, invert for tabs at bottom gtk_box_prepend((GtkBox *) box, stack); gtk_box_prepend((GtkBox *) box, switcher); gtk_window_set_child(GTK_WINDOW(window), box); // --------------------------------------------------------- // Setup the spin buttons (increment/decrement buttons) // for the 6 core stats (STR, DEX, CON, INT, WIS, CHA) // The buttons start with a default of 10, can be manually // editied like a text box, have specified limits, and set // step sizes. // When the buttons are modified, the callback function // reads the button value and calculates the stat modifier // to then display (and use for atk/dmg calculations) // --------------------------------------------------------- // The below for-loop creates a label to display the stat type, // a spin-button to set the stat value, and a widget for displaying // the stat modifier for each of the 6 core stats GtkWidget *label = gtk_label_new (NULL); const char *stat_names[] = {"STR", "DEX", "CON", "INT", "WIS", "CHA"}; GtkWidget *str_butt, *dex_button, *con_button, *int_button, *wis_button, *cha_button; GtkWidget *stat_buttons[] = {str_butt, dex_button, con_button, int_button, wis_button, cha_button}; for(int i = 0; i < NUM_CORE_STATS; i++) { label = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (label), stat_names[i]); gtk_grid_attach (GTK_GRID (grid), label, i, 1, 1, 1); adjustment = gtk_adjustment_new (10.0, 0.0, 100.0, 1.0, 5.0, 0.0); stat_buttons[i] = gtk_spin_button_new (adjustment, 1.0, 0); gtk_grid_attach (GTK_GRID (grid), stat_buttons[i] , i, 2, 1, 1); data->stat_modifiers[i] = gtk_label_new (NULL); gtk_grid_attach(GTK_GRID (grid), data->stat_modifiers[i], i, 3, 1, 1); g_signal_connect(stat_buttons[i], "value-changed", G_CALLBACK(stat_calc), data->stat_modifiers[i]); // perform first-time stat calculation so something exists in the modifier fields stat_calc(stat_buttons[i], data->stat_modifiers[i]); } // --------------------------------------------------------- // spin button for proficiency // --------------------------------------------------------- // label and increment/decrement button for proficiency const char *proficiency = "PROF"; label = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (label), proficiency); gtk_grid_attach (GTK_GRID (grid), label, 6, 1, 1, 1); adjustment = gtk_adjustment_new (0.0, 0.0, 100.0, 1.0, 5.0, 0.0); GtkWidget *prof_button = gtk_spin_button_new (adjustment, 1.0, 0); gtk_grid_attach (GTK_GRID (grid), prof_button, 6, 2, 1, 1); //TODO: the below is not quite what I want, maybe implement a custom version using a box, entry and 2 small buttos with icons? //gtk_orientable_set_orientation((GtkOrientable*) prof_button, GTK_ORIENTATION_VERTICAL); data->prof_button = prof_button; // --------------------------------------------------------- // text entry field for the user to input their dice roll // --------------------------------------------------------- const char *dice_roll_label = "Dice Roll:"; label = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (label), dice_roll_label); gtk_grid_attach (GTK_GRID (grid), label, 0, 4, 1, 1); GtkEntryBuffer *dice_roll_buffer = gtk_entry_buffer_new(NULL, -1); GtkWidget *dice_roll_input = gtk_entry_new_with_buffer(dice_roll_buffer); gtk_grid_attach (GTK_GRID (grid), dice_roll_input, 1, 4, 1, 1); data->dice_roll_buffer = dice_roll_buffer; // assign callback function to run whenever text is entered // which will check if the entered text is a digit and remove // it if it is any other character g_signal_connect_after( gtk_editable_get_delegate(GTK_EDITABLE(dice_roll_input)), "insert-text", G_CALLBACK(numeric_only_text_check), NULL ); // --------------------------------------------------------- // action buttons and options // provide buttons to calculate attack rolls and damage // rolls based on the supplied input dice roll // also provide option to indicate if Cinder Wrath is // active or not, as this adds proficiency to the dmg value // --------------------------------------------------------- button = gtk_button_new_with_label ("Attack"); g_signal_connect(button, "clicked", G_CALLBACK (attack_calc), data); gtk_grid_attach(GTK_GRID (grid), button, 0, 5, 1, 1); button = gtk_button_new_with_label ("Damage"); g_signal_connect(button, "clicked", G_CALLBACK (damage_calc), data); gtk_grid_attach(GTK_GRID (grid), button, 1, 5, 1, 1); // checkbox to enable if raging (cinder wrath) GtkWidget *cinder_wrath_check = gtk_check_button_new_with_label("Cinder Wrath"); gtk_grid_attach (GTK_GRID (grid), cinder_wrath_check, 2, 5, 3, 1); data->cinder_wrath_check = cinder_wrath_check; // --------------------------------------------------------- // Text buffer for displaying the result of the calculation // and ensure the buffer cannot be modified by the user // --------------------------------------------------------- GtkTextBuffer *calc_result_buffer = gtk_text_buffer_new(NULL); GtkWidget *calc_result_diag = gtk_text_view_new_with_buffer(calc_result_buffer); // make it so the field cannot be edited by the user (makes the text non-input) gtk_text_view_set_editable(GTK_TEXT_VIEW(calc_result_diag), FALSE); gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(calc_result_diag), FALSE); gtk_grid_attach (GTK_GRID (grid), calc_result_diag, 0, 6, 4, 2); data->calc_result_buffer = calc_result_buffer; // --------------------------------------------------------- // Create a menu button that will allow for loading/saving // character configuration, as well as exiting the app // The menu button sits within a header bar, and uses // a Menu Model for the menu structure and contents // --------------------------------------------------------- // the menu button that will sit in the header bar GtkWidget *menubutton = gtk_menu_button_new(); gtk_menu_button_set_direction((GtkMenuButton*) menubutton, GTK_ARROW_DOWN); //DOWN //gtk_widget_set_valign (GtkWidget* widget, GtkAlign align); gtk_widget_set_halign(menubutton, GTK_ALIGN_END); // the top-level menu GMenu *settings_menu = g_menu_new(); // the different options we have in the top-level menu GMenuItem *load_file = g_menu_item_new( "Load from file", "app.load_from_file" ); g_menu_append_item(settings_menu, load_file); GMenuItem *save_file = g_menu_item_new( "Save to file", "app.save_to_file" ); g_menu_append_item(settings_menu, save_file); GMenuItem *quit_app = g_menu_item_new ( "Quit", "app.quit" ); g_menu_append_item(settings_menu, quit_app); // define short-cuts that we can associate with the menu items // + O for opening a file, + S for saving a file // + Q for exiting the application const char *load_accels[2] = { "O", NULL }; const char *save_accels[2] = { "S", NULL }; const char *quit_accels[2] = { "Q", NULL }; // set up the app actions (callbacks) that are taken when an item // in the menu is pressed static GActionEntry app_entries[] = { { "load_from_file", load_from_file_activated, NULL, NULL, NULL }, { "save_to_file", save_to_file_activated, NULL, NULL, NULL }, { "quit", quit_activated, NULL, NULL, NULL } }; g_action_map_add_action_entries(G_ACTION_MAP (app), app_entries, G_N_ELEMENTS(app_entries), app); // apply the short-cuts that are defined above gtk_application_set_accels_for_action(GTK_APPLICATION (app), "app.load_from_file", load_accels); gtk_application_set_accels_for_action(GTK_APPLICATION (app), "app.save_to_file", save_accels); gtk_application_set_accels_for_action(GTK_APPLICATION (app), "app.quit", quit_accels); gtk_menu_button_set_menu_model((GtkMenuButton*) menubutton, G_MENU_MODEL(settings_menu)); // create headerbar to contain the menu button GtkWidget *headerbar = gtk_header_bar_new(); gtk_header_bar_pack_end((GtkHeaderBar*) headerbar, menubutton); // set the title of the headerbar const char *window_title_label = "DS:PoF Attack and Damage Roll Calculator"; label = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (label), window_title_label); gtk_header_bar_set_title_widget((GtkHeaderBar*) headerbar, label); // pass the headerbar to the window gtk_window_set_titlebar(GTK_WINDOW (window), headerbar); // --------------------------------------------------------- // Open the application window // --------------------------------------------------------- gtk_window_present (GTK_WINDOW (window)); // --------------------------------------------------------- // clean up // --------------------------------------------------------- g_object_unref(load_file); g_object_unref(save_file); g_object_unref(quit_app); g_object_unref(settings_menu); } int main (int argc, char **argv) { GtkApplication *app; int status; // this struct pointer will contain pointers to various // elements all needed in attack/damage calculations // a struct is neccessary when passing multiple elements // in a g_signal_connect() struct atk_dmg_object *data = malloc(sizeof(*data)); app = gtk_application_new("org.gtk.example", 0); g_signal_connect(app, "activate", G_CALLBACK(activate), data); status = g_application_run (G_APPLICATION(app), argc, argv); g_object_unref(app); free(data); return status; }