Display inventory during dialogs

Started by nanokankk, Fri 31/03/2023 09:12:53

Previous topic - Next topic

nanokankk

Hi everyone,

I'm building a game with a custom inventory (each inventory item is a button) and a custom dialog GUIs (similar to Broken Sword). Custom inventory GUI is displayed when the mouse cursor is above a certain Y coordinate.

During a dialog, I would like the player to be able to choose inventory items as dialog options. At the moment, I'm struggling with making the inventory GUI to respond to mouse events during a dialog thread. If I manually display the inventory GUI during the dialog thread, the GUI is shown but all its GUIControl Buttons do not respond to mouse over, mouse click, etc...

Do you have any suggestion on how to make other GUIs functional during an AGS dialog thread? I suppose one alternative could be to implement a fully custom dialog system, but I'd like to avoid that as much as possible. On another note, has anyone built a game on which inventory items were also possible dialog options?

Thank you!

Khris

Afaik, the most important thing you have to do is enable the "run game loops..." option in General Settings -> Dialogs.
Otherwise, many Global script functions won't run during a dialog, since the game is effectively paused.

Snarky

Quote from: nanokankk on Fri 31/03/2023 09:12:53I suppose one alternative could be to implement a fully custom dialog system, but I'd like to avoid that as much as possible.

I might be wrong, but I believe this is your only option, because that is what you are in fact trying to do: a dialog system where you pick inventory items as responses is pretty much the archetypal example of custom dialog options.

Quote from: nanokankk on Fri 31/03/2023 09:12:53On another note, has anyone built a game on which inventory items were also possible dialog options?

Yes, certainly people have. You may want to check out the Custom Dialog GUI Module.

Crimson Wizard

Quote from: Snarky on Fri 31/03/2023 09:48:11I might be wrong, but I believe this is your only option, because that is what you are in fact trying to do: a dialog system where you pick inventory items as responses is pretty much the archetypal example of custom dialog options.

Theoretically, one could use regular dialogs, with an option to "select item" which starts a custom GUI, and then restarts a previous dialog.

nanokankk

Quote from: Khris on Fri 31/03/2023 09:17:56Afaik, the most important thing you have to do is enable the "run game loops..." option in General Settings -> Dialogs.
Otherwise, many Global script functions won't run during a dialog, since the game is effectively paused.

Thanks for replying so quickly! I do have that enabled already, but no effect  ???

Khris

In that case you need to show your code I guess. What have you tried to far?

nanokankk

Quote from: Khris on Fri 31/03/2023 12:18:47In that case you need to show your code I guess. What have you tried to far?

Code is WIP, so it's a bit messy at the moment.

DialogBox
Code: ags
#define DIALOG_BOX_BG_START_SPRITE 204
#define DIALOG_BOX_BG_START_SPRITE_WIDTH 10
#define DIALOG_BOX_BG_MIDDLE_SPRITE 203
#define DIALOG_BOX_BG_MIDDLE_SPRITE_WIDTH 52
#define DIALOG_BOX_BG_END_SPRITE 205
#define DIALOG_BOX_BG_END_SPRITE_WIDTH 3
#define OPTION_BUTTON_RIGHT_GAP 13
#define OPTION_BUTTON_TOP_GAP 10
#define OPTION_BUTTON_WIDTH 41
#define OPTION_BUTTON_HEIGHT 41
#define OPTION_BUTTON_NORMAL_SPRITE 97
#define OPTION_BUTTON_MOUSE_OVER_SPRITE 100
#define OPTION_BUTTON_PUSHED_SPRITE 98

int dialog_box_width;
int dialog_box_height;

int option_button_sprites[];
int enabled_options_indexes[];
int enabled_options_count;

Label * dialog_label;
Fader dialog_label_fader;

bool mouse_was_down;
bool mouse_was_clicked;
int hovered_button_index = -1;
int hovered_option_index = -1;

void InitDialogBox(int width, int height, GUI * dialog_label_gui)
{
  dialog_box_width = width;
  dialog_box_height = height;
  
  dialog_label_gui.Centre();
  dialog_label_gui.Y = Screen.Height - dialog_label_gui.Height - dialog_box_height;
  dialog_label_fader.SetGUI(dialog_label_gui);

  for (int i = 0; i < dialog_label_gui.ControlCount; i++)
  {
    Label * maybe_dialog_label = dialog_label_gui.Controls[i].AsLabel;
    if (maybe_dialog_label != null)
    {
      dialog_label = maybe_dialog_label;
    }
  }

  if (dialog_label == null)
  {
    AbortGame("Dialog Label GUI has no label");
  }
}

/**********************************************
 * Private
 **********************************************/

void LogDebug(String message)
{
  #ifdef DIALOG_BOX_LOG_ENABLED
  logger.LogDebug(String.Format("\[DialogBox] %s", message));
  #endif
}

void ValidateOptionIndex(DialogOptionsRenderingInfo * info, int option_index)
{
  if (option_index < 0 || option_index >= info.DialogToRender.OptionCount)
  {
    AbortGame("Option index out of bounds: %d", option_index);
  }
}

bool IsOptionEnabled(DialogOptionsRenderingInfo * info, int option_index)
{
  ValidateOptionIndex(info, option_index);

  DialogOptionState option_state = info.DialogToRender.GetOptionState(option_index+1);
  return option_state == eOptionOn;
}

void UpdateEnabledOptionsIndexes(DialogOptionsRenderingInfo * info)
{
  enabled_options_count = 0;

  int j = 0;
  for (int i = 0; i < info.DialogToRender.OptionCount; i++)
  {
    if (IsOptionEnabled(info, i))
    {
      enabled_options_count++;
      enabled_options_indexes[j] = i;
      j++;
    }
  }
  
  LogDebug(String.Format("%d options enabled", enabled_options_count));
}

int GetOptionSprite(DialogOptionsRenderingInfo * info, int option_index)
{
  ValidateOptionIndex(info, option_index);

  String option_text = info.DialogToRender.GetOptionText(option_index+1);
  int split_index = option_text.IndexOf(" ");
  String option_sprite_sub = option_text.Substring(0, split_index + 1);
  return option_sprite_sub.AsInt;
}

String GetOptionLabel(DialogOptionsRenderingInfo * info, int option_index)
{
  ValidateOptionIndex(info, option_index);

  String option_text = info.DialogToRender.GetOptionText(option_index+1);
  int split_index = option_text.IndexOf(" ");
  return option_text.Substring(split_index + 1, option_text.Length);
}

void ShowHoveredOptionLabel(DialogOptionsRenderingInfo * info)
{
  if (hovered_option_index != -1)
  {
    dialog_label.Text = GetOptionLabel(info, hovered_option_index);
    dialog_label_fader.FadeIn(DIALOG_BOX_LABEL_SHOW_TWEEN_SECONDS);
  }
}

void HideOptionLabel()
{
  dialog_label_fader.FadeOut(DIALOG_BOX_LABEL_HIDE_TWEEN_SECONDS);
}

void UpdateHoveredOptionIndex(DialogOptionsRenderingInfo * info, int mouse_x, int mouse_y)
{
  int button_x = info.X + DIALOG_BOX_BG_START_SPRITE_WIDTH;
  int button_y = info.Y + OPTION_BUTTON_TOP_GAP;
    
  for (int i = 0; i < enabled_options_count; i++)
  {
    if (
      mouse_x >= button_x && mouse_x <= button_x + OPTION_BUTTON_WIDTH &&
      mouse_y >= button_y && mouse_y <= button_y + OPTION_BUTTON_HEIGHT
    )
    {
      hovered_button_index = i;
      hovered_option_index = enabled_options_indexes[i];
      return;
    }
    
    button_x = button_x + OPTION_BUTTON_WIDTH + OPTION_BUTTON_RIGHT_GAP;
  }
  
  hovered_button_index = -1;
  hovered_option_index = -1;
}

bool UpdateOptionButtonSprites(DialogOptionsRenderingInfo * info)
{
  bool changed = false;

  for (int i = 0; i < enabled_options_count; i++)
  {
    int option_index = enabled_options_indexes[i];

    if (i == hovered_button_index)
    {
      if (mouse_was_down)
      {
        if (option_button_sprites[option_index] != OPTION_BUTTON_PUSHED_SPRITE)
        {
          option_button_sprites[option_index] = OPTION_BUTTON_PUSHED_SPRITE;
          changed = true;
        }
      }
      else if (option_button_sprites[option_index] != OPTION_BUTTON_MOUSE_OVER_SPRITE)
      {
        option_button_sprites[option_index] = OPTION_BUTTON_MOUSE_OVER_SPRITE;
        changed = true;
      }
    }
    else if (option_button_sprites[option_index] != OPTION_BUTTON_NORMAL_SPRITE)
    {
      option_button_sprites[option_index] = OPTION_BUTTON_NORMAL_SPRITE;
      changed = true;
    }
  }
  
  return changed;
}

void RunActiveOption(DialogOptionsRenderingInfo * info)
{
  if (hovered_option_index != -1)
  {
    LogDebug(String.Format("Option %d selected: %s", hovered_option_index, info.DialogToRender.GetOptionText(hovered_option_index + 1)));

    info.ActiveOptionID = hovered_option_index + 1;
    info.RunActiveOption();
  }
}

void UpdateMouseButtonState()
{
  if (mouse.IsButtonDown(eMouseLeft))
  {
    if (mouse_was_down)
    {
      // Mouse is still down
    }
    else
    {
      // Mouse is down
      mouse_was_down = true;
    }
  }
  else
  {
    // Mouse is clicked
    if (mouse_was_down)
    {
      mouse_was_down = false;
      mouse_was_clicked = true;
    }
    else
    {
      // Mouse is idle
      mouse_was_clicked = false;
    }
  }
}

void SetDialogOptions(DialogOptionsRenderingInfo * info)
{
  mouse_was_down = false;
  mouse_was_clicked = false;
  hovered_button_index = -1;
  hovered_option_index = -1;
  enabled_options_count = -1;
  
  int option_count = info.DialogToRender.OptionCount;  
  option_button_sprites = new int[option_count];
  enabled_options_indexes = new int[option_count];

  info.X = (Screen.Width - dialog_box_width) / 2;
  info.Y = Screen.Height - dialog_box_height - info.X;
  info.Width = dialog_box_width;
  info.Height = dialog_box_height;

  UpdateEnabledOptionsIndexes(info);
  UpdateOptionButtonSprites(info);
}

void Render(DialogOptionsRenderingInfo * info)
{
  DrawingSurface * surface = info.Surface;
  surface.Clear();
  
  int option_count = info.DialogToRender.OptionCount;
  
  // Draw left background section
  if (option_count > 0)
  {
    surface.DrawImage(0, 0, DIALOG_BOX_BG_START_SPRITE);
  }

  int bg_x = DIALOG_BOX_BG_START_SPRITE_WIDTH;
  int button_x = DIALOG_BOX_BG_START_SPRITE_WIDTH;
  for (int i = 0; i < option_count; i++)
  {
    if (!IsOptionEnabled(info, i))
    {
      continue;
    }
    
    // Draw middle background section
    surface.DrawImage(bg_x, 0, DIALOG_BOX_BG_MIDDLE_SPRITE);
    
    // Draw option button
    surface.DrawImage(button_x, OPTION_BUTTON_TOP_GAP, option_button_sprites[i]);
    
    // Draw option image
    surface.DrawImage(button_x, OPTION_BUTTON_TOP_GAP, GetOptionSprite(info, i));
    
    bg_x = bg_x + DIALOG_BOX_BG_MIDDLE_SPRITE_WIDTH;
    button_x = button_x + OPTION_BUTTON_WIDTH + OPTION_BUTTON_RIGHT_GAP;
  }

  // Draw right background section
  if (option_count > 0)
  {
    surface.DrawImage(bg_x, 0, DIALOG_BOX_BG_END_SPRITE);
  }

  surface.Release();
}

void RepeatedlyExecute(DialogOptionsRenderingInfo * info)
{
  UpdateMouseButtonState();
  UpdateHoveredOptionIndex(info, mouse.x, mouse.y);
  bool should_render = UpdateOptionButtonSprites(info);

  if (should_render)
  {
    info.Update();
  }
  
  if (hovered_option_index != -1)
  {
    if (mouse_was_clicked)
    {
      HideOptionLabel();
      Inventory.Hide();
      RunActiveOption(info);
    }
    else
    {
      ShowHoveredOptionLabel(info);
    }
  }
  else
  {
    HideOptionLabel();
  }
}

void OnKeyPress(DialogOptionsRenderingInfo * info, eKeyCode keycode)
{
  if (keycode == eKeyEscape)
  {
    // FIXME: this assumes last option stops the dialog
    info.ActiveOptionID = info.DialogToRender.OptionCount;
    info.RunActiveOption();
  }
}

/**********************************************
* AGS Dialog Hooks
**********************************************/

void dialog_options_get_dimensions(DialogOptionsRenderingInfo * info)
{
  LogDebug(String.Format("Initializing dialog %d with %d options", info.DialogToRender.ID, info.DialogToRender.OptionCount));

  SetDialogOptions(info);
}

void dialog_options_render(DialogOptionsRenderingInfo * info)
{
  LogDebug("Rendering");

  Inventory.Show();
  Render(info);
}

void dialog_options_repexec(DialogOptionsRenderingInfo * info)
{
  RepeatedlyExecute(info);
}

void dialog_options_key_press(DialogOptionsRenderingInfo * info, eKeyCode keycode)
{
  LogDebug(String.Format("Key %d pressed", keycode));

  OnKeyPress(info, keycode);
}

Inventory
Code: ags
GUI * inventory_gui;
InvWindow * inv_window;
Fader fader;

bool slot_states[];
ContainerButton slot_buttons[50];
InventoryItem * slot_items[];
int slot_count;

int dragged_item_index;

void LogDebug(String message)
{
  #ifdef INVENTORY_LOG_ENABLED
  logger.LogDebug(String.Format("\[Inventory] %s", message));
  #endif
}

/**********************************************
 * Getters/Setters
 **********************************************/

GUI * get_InventoryGUI(static Inventory)
{
  return inventory_gui;
}

GUI * set_InventoryGUI(static Inventory, GUI * the_gui)
{
  inventory_gui = the_gui;
  fader.SetGUI(inventory_gui);

  slot_count = 0;

  for (int i = 0; i < inventory_gui.ControlCount; i++)
  {
    Button * maybe_slot_button = inventory_gui.Controls[i].AsButton;
    if (maybe_slot_button != null && maybe_slot_button != MenuButton)
    {
      slot_count++;
    }
    
    InvWindow * maybe_inv_window = inventory_gui.Controls[i].AsInvWindow;
    if (maybe_inv_window != null)
    {
      inv_window = maybe_inv_window;
    }
  }
  if (inv_window == null)
  {
    AbortGame("Inventory GUI has no inventory window");
  }
  if (slot_count == 0)
  {
    AbortGame("Inventory GUI has no slot buttons");
  }
  
  slot_states = new bool[slot_count];
  slot_items = new InventoryItem[slot_count];
  dragged_item_index = -1;

  for (int i = 0, j = 0; i < inventory_gui.ControlCount; i++)
  {
    Button * maybe_slot_button = inventory_gui.Controls[i].AsButton;
    if (maybe_slot_button != null && maybe_slot_button != MenuButton)
    {
      slot_states[j] = false;
      slot_buttons[j].Button = maybe_slot_button;
      slot_items[j] = null;
      j++;
    }
  }
}

int get_SlotCount(static Inventory)
{
  return slot_count;
}

int get_ItemCount(static Inventory)
{
  return inv_window.ItemCount;
}

/**********************************************
 * Private
 **********************************************/

void ValidateSlotIndex(int slot_index)
{
  if (slot_index < 0 || slot_index >= slot_count)
  {
    AbortGame("Provided slot index out of bounds: %d", slot_index);
  }
}

void Show()
{
  int cycles = fader.FadeIn(INVENTORY_SHOW_TWEEN_SECONDS);
  if (cycles > 0)
  {
    LogDebug("GUI ON");
  }
}

void Hide()
{
  int cycles = fader.FadeOut(INVENTORY_HIDE_TWEEN_SECONDS);
  if (cycles > 0)
  {
    LogDebug("GUI OFF");
  }
}

void HandleVisible()
{
  if (mouse.y < INVENTORY_POPUP_Y_POS)
  {
    Show();
  }
  else if (mouse.y > gInventory.Height)
  {
    Hide();
  }
}

void ClearSlot(int slot_index)
{
  ValidateSlotIndex(slot_index);
  
  slot_states[slot_index] = false;
  slot_items[slot_index] = null;
  slot_buttons[slot_index].ClearItem();
}

void SetSlotItem(int slot_index, InventoryItem * item)
{
  ValidateSlotIndex(slot_index);

  if (slot_items[slot_index] == item)
  {
    return;
  }

  if (slot_states[slot_index])
  {
    ClearSlot(slot_index);
  }
  
  slot_states[slot_index] = true;
  slot_items[slot_index] = item;
  // Expects dragged sprite number to be base sprite number + 2
  slot_buttons[slot_index].SetItem(item.Graphic, item.Graphic + 2);
}

void SetSlotButtonEnabled(int slot_index, bool enabled)
{
  ValidateSlotIndex(slot_index);

  slot_buttons[slot_index].Enabled = enabled;
  slot_buttons[slot_index].Clickable = enabled;

  if (enabled)
  {
    slot_buttons[slot_index].SetDraggingMode(false);
    LogDebug(String.Format("Slot %d enabled", slot_index));
  }
  else
  {
    slot_buttons[slot_index].SetDraggingMode(true);
    LogDebug(String.Format("Slot %d disabled", slot_index));
  }
}

void RenderSlots()
{
  int i;
  for (i = 0; i < inv_window.ItemCount; i++)
  {
    SetSlotItem(i, inv_window.ItemAtIndex[i]);
    LogDebug(String.Format("Slot %d: '%s'", i+1, slot_items[i].Name));
  }
  for (; i < slot_count; i++)
  {
    ClearSlot(i);
  }
}

int GetItemIndexAtScreenXY(int mouse_x, int mouse_y)
{
  int index = -1;

  GUIControl * the_control = GUIControl.GetAtScreenXY(mouse_x, mouse_y);
  if (the_control == null)
  {
    return index;
  }

  for (int i = 0; i < inv_window.ItemCount; i++)
  {
    if (the_control == slot_buttons[i].Button)
    {
      InventoryItem * item = inv_window.ItemAtIndex[i];
      if (item != null)
      {
        index = i;
        break;
      }
    }
  }

  return index;
}

void HandleMouseDragStart(int mouse_x, int mouse_y)
{
  int index = GetItemIndexAtScreenXY(mouse_x, mouse_y);
  if (index != -1)
  {
    LogDebug(String.Format("DragStart '%s' (slot %d)", slot_items[index].Name, index));

    // make dragged item slot not clickable
    dragged_item_index = index;
    SetSlotButtonEnabled(index, false);
  }
}

void HandleMouseDragEnd(int mouse_x, int mouse_y)
{
  if (dragged_item_index != -1)
  {
    LogDebug(String.Format("DragEnd '%s' (slot %d)", slot_items[dragged_item_index].Name, dragged_item_index));

    SetSlotButtonEnabled(dragged_item_index, true);
    dragged_item_index = -1;
  }
}

/**********************************************
 * Public
 **********************************************/

static void Inventory::Show()
{
  Show();
}

static void Inventory::Hide()
{
  Hide();
}

static void Inventory::HandleMouseEvent(MouseEvent event, int x, int y)
{
  switch (event)
  {
    case eMouseDragStart:
      HandleMouseDragStart(x, y);
      break;
    case eMouseDragEnd:
      HandleMouseDragEnd(x, y);
      break;
  }
}

static void Inventory::RepeatedlyExecute()
{
  HandleVisible();
}

static void Inventory::OnEvent(EventType event, int data)
{
  switch (event)
  {
    case eEventAddInventory:
    case eEventLoseInventory:
      LogDebug("Inventory changed");
      RenderSlots();
      break;
  }
}

static Button * Inventory::SlotButtonAtIndex(int slot_index)
{
  ValidateSlotIndex(slot_index);
  
  return slot_buttons[slot_index].Button;
}

void SafeAddInventory(this Character *, InventoryItem * the_item)
{ 
  if (inv_window.ItemCount < slot_count)
  {
    this.AddInventory(the_item);
  }
}

int GetIndex(this InventoryItem *)
{
  int index = -1;

  for (int i = 0; i < inv_window.ItemCount; i++)
  {
    if (inv_window.ItemAtIndex[i] == this)
    {
      index = i;
      break;
    }
  }

  return index;
}

Button * GetSlotButton(this InventoryItem *)
{
  int index = this.GetIndex();

  if (index == -1)
  {
    return null;
  }

  return slot_buttons[index].Button;
}

InventoryItem * ActuallyGetAtScreenXY(static InventoryItem, int mouse_x, int mouse_y)
{
  int index = GetItemIndexAtScreenXY(mouse_x, mouse_y);

  if (index == -1)
  {
    return null;
  }

  return inv_window.ItemAtIndex[index];
}

InventoryItem * GetAtSlotIndex(static InventoryItem, int slot_index)
{
  ValidateSlotIndex(slot_index);

  return slot_items[slot_index];
}

nanokankk

Quote from: Khris on Fri 31/03/2023 12:18:47What have you tried to far?

I've verified that the repeatedly_execute_always function is being called while the DialogBox is displayed. However, I'm not using that function to render anything on the screen at the moment.

nanokankk

#8
I had "When player interface is disabled, GUIs should" set to "Display normally" as I didnt' want to grey out any controls. I changed it to "Grey out all their controls" and I can see that, during a Dialog, all Inventory buttons are greyed out which tells me the player interface is disabled. I had a suspition that this was the reason buttons weren't unresponsive, but now I can confirm it.

I've tried overriding the Enabled property of the inventory buttons during the dialog repexec loop, but that has no effect. Any way to enable GUI controls during dialogs?

deadsuperhero

#9
Hey, just in case you were curious about ways to implement this thing, I'm building an inventory-based dialogue system for one of my games. My project is fully open source; you're welcome to take a gander and see how stuff works:

Kind of in a transitional phase right now from an older system, so it currently only works on one character (blonde guy at a party) and most of it needs to be restructured, but it might give you some ideas!

https://codeberg.org/UnicornSausage/deep-cuts/issues/5

The basic concept I have is that certain items are assigned to an off-screen character whose only purpose is to store them.

A speech GUI draws some buttons and counts how many items that character has, then does some magic by filling single-item inventory windows above each button. Those windows ignore clicks, the button underneath checks what item is supposed to be in that slot, then it runs a dialog action on characters when clicked.
The fediverse needs great indie game developers! Find me there!

SMF spam blocked by CleanTalk