Files
OneINT_DS_PoF/ds_pof.c
Rob d1704d51ed Update ds_pof.c
Substantial code clean up.
Enforce digit input on dice roll entry
Added some spacing between main grid rows and columns
2025-07-07 08:44:39 +01:00

420 lines
18 KiB
C

/*============================================================================*/
//
// 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 <gtk/gtk.h>
#include <ctype.h>
#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
// <ctrl> + O for opening a file, <ctrl> + S for saving a file
// <ctrl> + Q for exiting the application
const char *load_accels[2] = { "<Ctrl>O", NULL };
const char *save_accels[2] = { "<Ctrl>S", NULL };
const char *quit_accels[2] = { "<Ctrl>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;
}