Author Topic: Building a Quest UI [SOLVED!]  (Read 6083 times)

deadsuperhero

  • There's no sense crying over spilled tears.
    • I can help with backgrounds
    • I can help with characters
    • I can help with making music
    • I can help with scripting
Building a Quest UI [SOLVED!]
« on: 15 Jul 2020, 23:01 »
Edit: This has been solved. Check out the solution!

I'm in the process of building out an adventure RPG in Adventure Game Studio, and over the past few months I've been making some pretty good progress.
Recently, though, I've wanted to implement a quest system, as a sort of visual way to assign things to the player and give them the opportunity to track things they have left to do.

In the process of building this, I've come across a few constaints, and haven't totally worked out the bugs yet. I'd like to touch upon what I've gotten working so far, along with a few ideas I've had.

Creating a Quest
Currently, Quest creation is little more than adding a new String to a ListBox. It looks like this:

Code: Adventure Game Studio
  1. function createQuest(const string questTitle,  const string questDesc) {
  2.   questList.AddItem(questTitle);
  3.   qDesc.Text = questDesc;
  4. }
  5.  

What we end up with looks like this:

https://i.imgur.com/YwdvbGu.png

The Description Problem
Anyone with experience in AGS will immediately realize there's a bit of a problem with doing things this way: the description is not actually being stored! The UI label just gets updated every single time a new quest gets assigned.

I've looked through the forums a couple of times to determine how exactly I'm supposed to update the label upon selecting a particular ListBox element. Thankfully, SelectedIndex is a thing, so we can at the very least update the label based on what the description is, like so...

Code: Adventure Game Studio
  1. function questList_OnSelectionChanged(GUIControl *control)
  2. {
  3.   if (questList.Items[questList.SelectedIndex] == "Sample Quest") {
  4.     qDesc.Text = "The interface should update when selecting this";
  5.   }
  6.  
  7.    if (questList.Items[questList.SelectedIndex] == "Get Ready for Work") {
  8.      qDesc.Text = "You need to get ready to go to the office. Don't be late!";
  9.   }
  10.  
  11.   else {
  12.     qDesc.Text = "No description. Please select a quest.";
  13.   }
  14. }
  15.  

Unfortunately, this is kind of unwieldy, and goes against the principle of DRY (Don't Repeat Yourself) in programming. It might not be a problem if I only have a dozen or so quests, but it doesn't exactly scale up if I want a game with 60+ quests or something. It also kind of goes against the initial function pattern I was hoping to follow.

Another headache that I've noticed is that SelectedIndex appears to only work on matching results against an exact title. One thing I'd love to figure out is whether or not it's possible to use some other kind of value, like an ID or something. My thought is that, if I could somehow store the description somewhere, and associate that with the title, I might be able to update certain elements through that association.

A Possible Solution - Structs!
So, I've been banging my head against the wall, trying to figure out how to store Quests as a type of Script Object that can be referenced in the UI. I came across the AGS Documentation on Structs, and it almost seems perfect...

So, in the global script header, I defined my Quest struct like so:

Code: Adventure Game Studio
  1. struct Quest {
  2.   String title;
  3.   String desc;
  4.   int ID;
  5.   bool complete;
  6.   import function Create(int questID, const string questTitle, const string questDesc);
  7. };
  8.  

I also have this function in the Global Script:

Code: Adventure Game Studio
  1. function Quest::Create(int questID, const string questTitle, const string questDesc) {
  2.   this.title = questTitle;
  3.   this.desc = questDesc;
  4.   this.ID = questID;
  5.   this.complete = false;
  6.   questList.AddItem(this.title);
  7. }
  8.  

The editor compiles with no problems, and this seems to be a semantically better way to generate quests. Now, whenever I try to create a new quest, I define an ID, the title, and the description, and it should get added to the Quest UI.

Let's try this with a Sample Quest...

Code: Adventure Game Studio
  1.   // Generate fake quest
  2.  Quest.Create(1,  "Sample Quest", "The interface should update when selecting this");
  3.  

This is unfortunately where I hit my first serious snag with Structs. Upon attempting to compile, the editor gives me an error:

Code: [Select]
GlobalScript.asc(198): Error (line 198): must have an instance of the struct to access a non-static member

At this point, I'm not really sure about what I need to do to move forward. This is the first time I've tried to work with Structs, and it's not 100% clear how I'm supposed to make them accessible to global or local scripts. Unfortunately, many of the examples I've seen of Structs being used are fairly abstract, or fit use-cases that are put together quite differently.

Goals and Questions
Ideally, what I'd like to do is successfully generate a Quest struct, and update the Quest UI so that the ListBox gets populated and the Label gets changed in a more efficient way.
Here are my questions on "How do I...?"

  • What am I doing wrong in creating the struct from the function?
  • Can SelectedIndex use something other than the Quest Title for setting the label? Such as an ID?
  • If I have to use the Quest Title for SelectedIndex, how might I be able to parse out other values of an existing Quest struct and update the UI accordingly?
  • Given that ListBox has a relatively limited API, is there a way to include sprites inside of the ListBox to represent completion state?
« Last Edit: 11 Jan 2022, 09:50 by deadsuperhero »
Check out my game: Life is a Party!

The fediverse needs great indie game developers! Find me there!

deadsuperhero

  • There's no sense crying over spilled tears.
    • I can help with backgrounds
    • I can help with characters
    • I can help with making music
    • I can help with scripting
Re: Building a Quest UI
« Reply #1 on: 15 Jul 2020, 23:33 »
Update: I managed to wrap my head around the first problem with Structs. It turns out that I needed to define an instance for my quest creation function to work.

So now, the Header looks like this:

Code: Adventure Game Studio
  1. struct Quest {
  2.   String title;
  3.   String desc;
  4.   int ID;
  5.   bool complete;
  6.   import function Create(int questID, const string questTitle, const string questDesc);
  7. };
  8.  
  9. import Quest playerQuests;
  10.  

And the Global Script has this at the top:

Code: Adventure Game Studio
  1. Quest playerQuests;
  2. export playerQuests;
  3.  

Meaning that I can now call the creation function like so:

Code: Adventure Game Studio
  1. playerQuests.Create(2,  "Get Ready for Work", "You need to get ready to go to the office. Don't be late!");
  2.  

While the naming convention could probably be cleaned up a bit  :tongue:, I'm relatively happy to see that the result matches exactly what I was doing before, but now I can create the struct and theoretically access it.

The new question now is, how do I reference values stored in the struct to update my GUI label?
Check out my game: Life is a Party!

The fediverse needs great indie game developers! Find me there!

Re: Building a Quest UI
« Reply #2 on: 15 Jul 2020, 23:35 »
You need to create an array of struct instances:

Code: Adventure Game Studio
  1. // header
  2. import Quest quest[60];
  3.  
  4. // main script
  5. Quest quest[60]; // quest[0], quest[1], ..., quest[59]
  6. export quest;
(import and export lines are only required if you want to access your quest array from other scripts, like room scripts)

Now you can call  quest[0].Create(...);


On to SelectedIndex: the quest array stores quest data, now you need an active_quest array:
(this array allows a) an arbitrary order of active quests b) doesn't have holes, if quests 1 and 3 are active but 2 isn't)
Code: Adventure Game Studio
  1. int active_quest[60]; // create 60 integer slots
  2. int active_quests = 0;  // stores amount of active quests and also first free slot
  3.  
  4. // adding quest #3 as first active quest:
  5. active_quest[active_quests] = 3;
  6. active_quests++;
  7.  
  8. // OnSelectionChanged
  9.   qDesc.Text = quest[active_quest[questList.SelectedIndex]].desc;
The way this works:  questList.SelectedIndex  is 0, so  active_quest[questList.SelectedIndex]  is 3
and  quest[active_quest[questList.SelectedIndex]] is therefore  quest[3]


Listbox doesn't support sprites; you need to either draw the GUI yourself or use buttons to display images

deadsuperhero

  • There's no sense crying over spilled tears.
    • I can help with backgrounds
    • I can help with characters
    • I can help with making music
    • I can help with scripting
Re: Building a Quest UI
« Reply #3 on: 15 Jul 2020, 23:42 »
Woah, nice! I'll give that a stab and report back!  :cheesy:
Check out my game: Life is a Party!

The fediverse needs great indie game developers! Find me there!

deadsuperhero

  • There's no sense crying over spilled tears.
    • I can help with backgrounds
    • I can help with characters
    • I can help with making music
    • I can help with scripting
Re: Building a Quest UI
« Reply #4 on: 15 Jul 2020, 23:59 »
Okay, I've implemented everything, unfortunately the game now crashes when clicking on Quests in the UI.

Code: [Select]
Error: Null string supplied to CheckForTranslations
I'm maybe getting the impression that perhaps the active_quest variable isn't actually touching the struct itself, which is what I need to access.
Maybe it's not a bad idea to set a quest as active upon creation?
Check out my game: Life is a Party!

The fediverse needs great indie game developers! Find me there!

deadsuperhero

  • There's no sense crying over spilled tears.
    • I can help with backgrounds
    • I can help with characters
    • I can help with making music
    • I can help with scripting
Re: Building a Quest UI
« Reply #5 on: 16 Jul 2020, 00:18 »
Okay, I got it! It's a bit hacky for now, but the creation function looks like this:

Code: Adventure Game Studio
  1. function Epic::Create(int questID,  const string questTitle, const string questDesc) {
  2.   this.title = questTitle;
  3.   this.desc = questDesc;
  4.   this.complete = false;
  5.   this.ID = questID;
  6.   questList.AddItem(this.title);
  7.   active_quest[active_quests] = questID;
  8.   active_quests++;
  9. }
  10.  

It's a little hacky, because unfortunately quest creation now looks like this:

Code: [Select]
Quest[2].Create(2,  "Get Ready for Work", "You need to get ready to go to the office. Don't be late!");
But it's something I can live with for now.  :wink:
Check out my game: Life is a Party!

The fediverse needs great indie game developers! Find me there!

deadsuperhero

  • There's no sense crying over spilled tears.
    • I can help with backgrounds
    • I can help with characters
    • I can help with making music
    • I can help with scripting
Re: Building a Quest UI
« Reply #6 on: 16 Jul 2020, 01:28 »
Okay, this has been really fruitful! I guess the last thing at this point: I have a secondary listbox that contains Tasks. A Task Struct looks like this:

Code: Adventure Game Studio
  1. struct Task {
  2.   String desc;
  3.   int ID;
  4.   bool complete;
  5.   import function Create(int questID,  const string taskDesc);
  6. };
  7.  

The associated Task creation function looks like this:

Code: Adventure Game Studio
  1. function Task::Create(int questID,  const string taskDesc) {
  2.   this.desc = taskDesc;
  3.   this.ID = questID;
  4.   this.complete = false;
  5.   subTasks.AddItem(this.desc);
  6. }
  7.  

Tasks get added to a secondary ListBox in the UI called subTasks. How can I change the content of this ListBox when selecting its corresponding Quest?
The Task Struct does accept a questID parameter just like Quests do... I was hoping perhaps there was a way to associate one struct to the other this way, but it's not completely clear how the subTasks ListBox might get updated accordingly.
Check out my game: Life is a Party!

The fediverse needs great indie game developers! Find me there!

deadsuperhero

  • There's no sense crying over spilled tears.
    • I can help with backgrounds
    • I can help with characters
    • I can help with making music
    • I can help with scripting
Re: Building a Quest UI
« Reply #7 on: 16 Jul 2020, 06:08 »
I apologize if my multiple posts on this thread have been disruptive; this is largely serving as an internal monologue as I stumble through ideas of how to do this.

For updating the list of subTasks, the best I can figure is that I'll need to:

1. derive the quest ID from the currently selected quest.
2. Look up which Task structs have a matching quest ID in them
3. Clear the list so that there's nothing on it.
4. Repopulate the List with relevant Task items

I may just bang my head against the wall some more tonight and give up, but I'll probably have something put together tomorrow.

Deriving the ID, Spitting Out Values

So, for the first experiment, I tried to pluck out the ID by running a check: if the existing selection's title matches with an existing Quest struct, spit out relevant details about what's stored in that particular quest struct.

Code: Adventure Game Studio
  1.  
  2. function questList_OnSelectionChanged(GUIControl *control)
  3. {
  4.   // Dummy function for deriving values from selection
  5.   String current_selection = quest[active_quest[questList.SelectedIndex]].title;
  6.   int list_id = quest[active_quest[questList.SelectedIndex]].ID;
  7.   int struct_id = quest[list_id].ID;
  8.  
  9.   // If the current selection matches a Struct that has this title, tell us some stuff.
  10.   if (current_selection == quest[struct_id].title) {
  11.     Display("This matches an existing Quest struct!");
  12.     String name = String.Format("%s",  quest[struct_id].title);
  13.     String splain = String.Format("%s", quest[struct_id].desc);
  14.     bool status = quest[struct_id].complete;
  15.     String fin = String.Format("%d", status);
  16.     Display("%s is the quest", name);
  17.     Display("%d is the ID", struct_id);
  18.     Display("Description: %s", splain);
  19.     Display("Completion status: %s", fin);
  20.   }
  21.  
  22.   // Existing function to adjust the label
  23.   qDesc.Text = quest[active_quest[questList.SelectedIndex]].desc;
  24. }}
  25.  
  26.  

So, this part of the debugging script mostly works...if I click on a Sample Quest with a questID of 1, the script will spit out the Title of Sample Quest, an associated description, an ID of 1, and a Completion Status of 0.

Here's the part where everything goes to hell: Let's say that I complete a quest, removing it from the list. Somehow, suddenly, the items that came after the Sample Quest now suffer from an off-by-one error, meaning that the second entry returns the output of what should've come before it.

It seems that somehow, the struct_id value is still somehow dependent on the list's position integer. I was hoping that putting together struct_id would be a way to work around this, but here we are...

I might have to try constructing this another way...
« Last Edit: 16 Jul 2020, 07:29 by DeadSuperHero »
Check out my game: Life is a Party!

The fediverse needs great indie game developers! Find me there!

Re: Building a Quest UI
« Reply #8 on: 16 Jul 2020, 07:47 »
This:
Code: [Select]
  int struct_id = quest[list_id].ID;seems unnecessary, list_id should already be the struct_id you're looking for.

Your points 1-4 look exactly right, that's how I'd do it, here's the basic idea:
Code: Adventure Game Studio
  1.   int quest_id = quest[active_quest[questList.SelectedIndex]].ID;
  2.  
  3.   // TODO: clear subTasks
  4.  
  5.   for (int i = 0; i < tasks_count; i++) {
  6.     if (task[i].questID == quest_id) subTasks.AddItem(task[i].desc);
  7.   }
« Last Edit: 16 Jul 2020, 07:52 by Khris »

deadsuperhero

  • There's no sense crying over spilled tears.
    • I can help with backgrounds
    • I can help with characters
    • I can help with making music
    • I can help with scripting
Re: Building a Quest UI
« Reply #9 on: 16 Jul 2020, 08:11 »
Yeah, for some reason I was worried that the list_id was actually just reflecting the integer of the list position, and that I needed to further derive the actual ID stored inside of the struct for that. Removing a quest from the list causes subsequent quests coming after it to spit everything out for a prior entry.

I'll putz around in the morning and see what comes of this.  :wink:
Check out my game: Life is a Party!

The fediverse needs great indie game developers! Find me there!

deadsuperhero

  • There's no sense crying over spilled tears.
    • I can help with backgrounds
    • I can help with characters
    • I can help with making music
    • I can help with scripting
Re: Building a Quest UI
« Reply #10 on: 17 Jul 2020, 05:44 »
So, I've almost got it working now, but with one caveat: the first quest can add and remove quests without any problem, but any subsequent quest never gets the task added, despite the system confirming it exists in the relevant struct.

Global Creation Function:
Code: Adventure Game Studio
  1. function Quest::Create(int questID,  const string questTitle, const string questDesc) {
  2.   this.title = questTitle;
  3.   this.desc = questDesc;
  4.   this.complete = false;
  5.   this.ID = questID;
  6.   questList.AddItem(this.title);
  7.   active_quest[active_quests] = this.ID;
  8.   active_quests++;
  9. }
  10.  
  11. function Task::Create(int questID,  const string taskDesc) {
  12.   this.desc = taskDesc;
  13.   this.parent = questID;
  14.   this.complete = false;
  15.   if (this.parent == quest[this.parent].ID && this.complete == false) {
  16.       subTasks.AddItem(this.desc);
  17.     }
  18.   quest[this.parent].children ++; // Tell the parent quest how many tasks it has
  19. }
  20.  

Global Header Declarations:
Code: Adventure Game Studio
  1. struct Quest {
  2.   String title;
  3.   String desc;
  4.   int ID;
  5.   int children;
  6.   bool complete;
  7.   bool active;
  8.   import function Create(int questID, const string questTitle, const string questDesc);
  9.   import function Complete();
  10. };
  11.  
  12. struct Task {
  13.   String desc;
  14.   int parent;
  15.   bool complete;
  16.   import function Create(int questID,  const string taskDesc);
  17.   import function Complete();
  18.   int count;
  19. };
  20.  
  21. import Quest quest[60];
  22. import Task task[100];
  23.  

GUI Function:

Code: Adventure Game Studio
  1. unction questList_OnSelectionChanged(GUIControl *control)
  2. {
  3.   // Dummy function for deriving values from selection
  4.  int quest_id = quest[active_quest[questList.SelectedIndex]].ID;
  5.  int task_count = quest[quest_id].children;
  6.   }
  7.  
  8.   //Adjust Label
  9.   qDesc.Text = quest[active_quest[questList.SelectedIndex]].desc;
  10.  
  11.  
  12.   // Clear the List
  13.  subTasks.Clear();
  14.  
  15.  
  16.   // Populate the List with just the relevant Tasks.
  17.   for (int i = 0; i < task_count; i++) {
  18.     if (task[i].parent == quest[quest_id].ID && task[i].complete == false) {
  19.       subTasks.AddItem(task[i].desc);
  20.     }
  21.   }
  22.  
  23. }
  24.  

The problem is this: if I create a second quest and a task for that second quest, it briefly shows up appended to the first quest's tasks in the UI:

Creating tasks for first quest:
Code: Adventure Game Studio
  1.  quest[0].Create(0,  "Get Ready for Work", "You need to get ready to go to the office. Don't be late!");
  2.  task[0].Create(0, "Get Dressed");
  3.  task[1].Create(0, "Eat Breakfast");
  4.  task[2].Create(0, "Drink Coffee");
  5.  

Creating tasks for second quest:
Code: Adventure Game Studio
  1.    quest[1].Create(1,  "Second Quest", "It's time to start the second adventure!");
  2.    task[4].Create(1,  "Do the first action of the second quest.");
  3.  

Additionally, "Completing" the first quest causes an off-by-one error with the second quest, where the values being returned are the details of the first quest.
I think now that the problem might have something to do with how the activequest is being declared: currently, it's declared in the create function for every single quest.
Check out my game: Life is a Party!

The fediverse needs great indie game developers! Find me there!

Re: Building a Quest UI
« Reply #11 on: 17 Jul 2020, 08:02 »
One thing that might cause issues:
ints that aren't initialized to any value start out being 0, and your code skips creating  task[3]  which means  task[3].parent  is 0, i.e. the ID of the first quest.

deadsuperhero

  • There's no sense crying over spilled tears.
    • I can help with backgrounds
    • I can help with characters
    • I can help with making music
    • I can help with scripting
Re: Building a Quest UI
« Reply #12 on: 17 Jul 2020, 09:38 »
It's a good thought, but unfortunately no dice.

One thought is that maybe new tasks are somehow getting pinned to whatever quest is highlighted at the top of the ListBox. Weird thought, but maybe the creation call for tasks doesn't need to actually add the item to the ListBox at all?

My gut says that maybe this is what is happening: the task gets added to the highlighted active quest. Then, my validation check to see if the IDs match removes it from the listBox, because of course it doesn't match (my latest version of the population code has a second if statement using a NOT operator). Only, because I've done this, there's now no task to actually append to the correct quest.

One fun part of programming is realizing how many potential edge cases a seemingly simple idea misses.  :wink:
Check out my game: Life is a Party!

The fediverse needs great indie game developers! Find me there!

deadsuperhero

  • There's no sense crying over spilled tears.
    • I can help with backgrounds
    • I can help with characters
    • I can help with making music
    • I can help with scripting
Re: Building a Quest UI
« Reply #13 on: 08 Nov 2020, 17:08 »
I'm back from the abyss! I was away at Air Force Basic Training for the last few months, but I have some time now to pick this up again.

I'm still having some challenges with this code. I've managed to clean up a lot of things, but there are two outstanding problems:

1. The "phantom task" issue still happens. No matter what I do, tasks for the secondary quest never show up, despite my counter indicating that
2. If I mark a Quest as "Completed", any Tasks that aren't complete end up getting appended to whatever the next active Quest is.

The biggest wall I seem to be running into is that of associating objects in one array to objects in another array. With regards to the second problem, it would seem straightforward that I could somehow set a boolean state on child objects associated with a parent. However, the process of trying to write some kind of query that can properly crawl from parent to child feels pretty much impossible.

I thought maybe one solution would be to nest arrays inside of a struct in some fashion. It'd be gross, but if I could get an array containing a list of task IDs...it wouldn't be impossible to parse out, because some kind of hierarchy exists to map tasks to a quest. Unfortunately, I can't figure out for the life of me how that might work.
Check out my game: Life is a Party!

The fediverse needs great indie game developers! Find me there!

deadsuperhero

  • There's no sense crying over spilled tears.
    • I can help with backgrounds
    • I can help with characters
    • I can help with making music
    • I can help with scripting
Re: Building a Quest UI
« Reply #14 on: 08 Nov 2020, 22:51 »
Okay, I think I've gotten to the root of the phantom task issue now!  :cheesy: (It's not solved yet, but after extensive testing and hacking around, I think I understand the problem).

First off, here's the code of what I have so far.

Code: Adventure Game Studio
  1. // Initialize some variables
  2. Quest quest[60];
  3. export quest;
  4. Task task[100];
  5. export task;
  6.  
  7. int task_counter = -1;
  8. int quest_counter = -1;
  9. int active_quest[60];
  10. int active_quests = 0;
  11.  
  12. // Creation Functions
  13.  
  14. function Quest::Create(const string questTitle, const string questDesc) {
  15.   this.title = questTitle;
  16.   this.desc = questDesc;
  17.   this.complete = false;
  18.   questList.AddItem(this.title);
  19.   // Generate an ID using a counter function. Basically, we guess what the array should be based on what the counter says.
  20.   // We start from an index of -1, because the actual index starts from 0.
  21.   quest_counter +=1;
  22.   this.ID = quest_counter;
  23.   // Set the quest as Active
  24.   active_quest[active_quests] = this.ID;
  25.   active_quests++;
  26. }
  27.  
  28. function Task::Create(int questID,  const string taskDesc) {
  29.   this.desc = taskDesc;
  30.   this.parent = questID;
  31.   this.complete = false;
  32.   quest[this.parent].children ++;
  33.   task_counter +=1;
  34.   this.ID = task_counter;
  35. }
  36.  
  37. // Completion Functions
  38.  
  39.  function Task::Complete() {
  40.   this.complete = true;
  41. }
  42.  
  43. function Quest::Complete() {
  44.   this.complete = true;
  45.   int q = questList.ItemCount;
  46.   while (q > 0) {
  47.     q--;
  48.     if (questList.Items[q] == this.title) {
  49.       questList.RemoveItem(q);
  50.     }
  51.   }
  52. }
  53.  
  54. // Update Tasks in the GUI (called later on)
  55.  
  56. function updateTasks(int quest_id, int task_count) {
  57. // First, clear out the task list completely
  58.  if (subTasks.ItemCount > 0) {
  59.       subTasks.Clear(); // for now, we'll just destroy the task upon completion until we have a better way to update ListBox
  60.     }
  61.  
  62. // Populate the List with just the relevant Tasks.
  63.   for (int i = 0; i < task_count; i++) {
  64.     if ((task[i].parent == quest_id) && (task[i].complete == false)) {
  65.       subTasks.AddItem(task[i].desc);
  66.     }
  67. }
  68.  
  69. // Quest GUI Functions
  70. function questList_OnSelectionChanged(GUIControl *control)
  71. {
  72.  int quest_id = quest[active_quest[questList.SelectedIndex]].ID;
  73.  int task_count = quest[quest_id].children;
  74.  String splain = String.Format("%s", quest[quest_id].desc);
  75.  
  76.   //Adjust Label
  77.   qDesc.Text = splain;
  78.  
  79.  
  80.   // Update Listed Tasks
  81.   updateTasks(quest_id, task_count);
  82.  
  83. }
  84.  
  85.  

So, this almost, ALMOST works! The problem I'm up against now: it seems that the quest_id in the GUI never increments above 0 when it comes to rendering the subTasks list. In other words, Tasks only show up for the first quest, but nothing after that. I have no idea why this is, or how I might go about fixing it. I'm almost convinced it has something to do with this block:

Code: Adventure Game Studio
  1. // Populate the List with just the relevant Tasks.
  2.   for (int i = 0; i < task_count; i++) {
  3.     if ((task[i].parent == quest_id) && (task[i].complete == false)) {
  4.       subTasks.AddItem(task[i].desc);
  5.     }
  6. }
  7.  

It seems like maybe the "i" variable is getting parsed as zero when
Code: Adventure Game Studio
  1. task[i].parent
is called. I'm still noodling on how to work around this...
« Last Edit: 08 Nov 2020, 22:52 by DeadSuperHero »
Check out my game: Life is a Party!

The fediverse needs great indie game developers! Find me there!

deadsuperhero

  • There's no sense crying over spilled tears.
    • I can help with backgrounds
    • I can help with characters
    • I can help with making music
    • I can help with scripting
Re: Building a Quest UI
« Reply #15 on: 11 Jan 2022, 09:49 »
Hey, so just wanted to bring this to a close - I ended up solving this today! I've been messing with Structs a lot for a different game, and the solution of associating things together ended up being a lot simpler than I was initially giving credit.

TL;DR - The Quest system I was trying to build failed because it was trying to use some kind of ID as a reference point to look up related tasks, but everything was convoluted and built on some very silly assumptions of how best to generate Struct objects and parse through them.

Here's a breakdown of what my system actually does now:

First - I found it much easier to create Struct objects as a separate function not related at all to the Struct object itself. Instead, just get a count of all Quests and Tasks in the game, and increment that number to determine the ID of the next one.

Code: Adventure Game Studio
  1. function createQuest(String questTitle,  String questDesc) {
  2.     // Currently refactoring, might not work
  3.   int i = questCount;
  4.   quest[i].title = questTitle;
  5.   quest[i].desc = questDesc;
  6.   quest[i].complete = false;
  7.   questList.AddItem(quest[i].title);
  8.   questCount++;
  9. }
  10.  
  11. function createTask(String pq, String taskDesc) {
  12.   int i = taskCount;
  13.   task[i].desc = taskDesc;
  14.   task[i].parentQuest = pq;
  15.   task[i].complete = false;
  16.   taskCount++;
  17. }
  18.  

What I was trying to do before for Struct object generation was super unwieldy. Here's a before and after comparison:

Before:
Code: Adventure Game Studio
  1. // Create a Quest
  2. quest[1].Create("Get Ready for Work", "You need to get ready to go to the office. Don't be late!");
  3.  
  4. // Assign three Tasks to Quest 1
  5. task[0].Create(1`, "Get Dressed");
  6. task[1].Create(1, "Eat Breakfast");
  7. task[2].Create(1, "Drink Coffee");
  8.  

After:
Code: Adventure Game Studio
  1. // Create a Quest
  2. createQuest("Get Ready for Work", "You need to get ready to go to the office. Don't be late!");
  3.  
  4. // Assign three Tasks to Quest 1
  5. createTask("Get Ready for Work", "Get Dressed");
  6. createTask("Get Ready for Work", "Eat Breakfast");
  7. createTask("Get Ready for Work", "Drink Coffee");
  8.  

The great thing now is that, the new way of doing things doesn't care about IDs - they're unimportant. Quest names in this system are unique enough, and are what I'm using to do all the lookup logic! AGS literally doesn't care.


Code: Adventure Game Studio
  1. // Refreshing the Quest List, which is called every time the Quest UI is invoked.
  2. function refreshQuests() {
  3.   // Refresh the List of Quests
  4.   if (questList.ItemCount > 0) {
  5.     questList.Clear();
  6.   }
  7.  
  8.   // Add Completed Quests
  9.     for (int i = 0; i < questCount; i++) {
  10.       if (quest[i].complete == false) {
  11.          questList.AddItem(quest[i].title);
  12.       }
  13.   }
  14.  
  15.   // Set the Selected Index
  16.   int comboIndex = questList.SelectedIndex + completedQuests;
  17.  
  18.   // Refresh the tasks for good measure
  19.   lookupTasks(quest[comboIndex].title);
  20.  
  21.  
  22. // Refreshing the Task List, technically this shows up higher in the script so that the prior function can call it.
  23. function lookupTasks(String questTitle) {
  24.  
  25.     // Refresh the Task List
  26.     if (subTasks.ItemCount > 0) {
  27.       subTasks.Clear();
  28.     }
  29.    
  30.     // List all of the Tasks the game knows about
  31.     for (int i = 0; i < taskCount; i++) {
  32.       if ((task[i].parentQuest == questTitle) && (task[i].complete == false)) {
  33.       subTasks.AddItem(task[i].desc);
  34.     }
  35.   }
  36. }
  37.  
  38.  

A few weird things I had to do to cover my butt:

Whenever the Quest UI is invoked, it needs to be told to select the topmost item on the Quest list, in order to pull in the Task items associated with a quest.

Code: Adventure Game Studio
  1. function btnQuests_OnClick(GUIControl *control, MouseButton button)
  2. {
  3.   int comboIndex = questList.SelectedIndex + completedQuests;
  4.   if (questList.ItemCount > 0) { // Game crashes if it tries to do all this stuff with no quests in the system, so this loop is an extra bit of protection
  5.     refreshQuests();
  6.     qDesc.Text = quest[comboIndex].desc; // Sets the description of the Current Text
  7.     gQuests.Visible = true;
  8.   }
  9.   else {
  10.     Display("You have no quests.");
  11.   }
  12. }
  13.  

Whenever a selection on the Quest list gets updated, I have to dynamically change everything:

Code: Adventure Game Studio
  1. function questList_OnSelectionChanged(GUIControl *control)
  2. {
  3.  int comboIndex = questList.SelectedIndex + completedQuests;
  4.  String selectedQuest = quest[comboIndex].title;
  5.  String selectedDesc = quest[comboIndex].desc;
  6.  
  7.   // Check the quest details
  8.   lookupQuest(selectedQuest);
  9.  
  10.   //Adjust Label
  11.   qDesc.Text = selectedDesc;
  12.  
  13.  
  14.   // Update Listed Tasks
  15.   lookupTasks(selectedQuest);
  16.  
  17. }
  18.  

Finally, I had to do one really weird thing to account for Completed quests that no longer show up in the UI:

Code: Adventure Game Studio
  1. function completeQuest(String questTitle) {
  2.  
  3.  
  4.   // Mark all matching tasks as Complete
  5.   for (int i = 0; i < questCount; i++) {
  6.     if ((quest[i].title == questTitle) && (quest[i].complete == false)) {
  7.        quest[i].complete = true;
  8.        completedQuests++; // Increment this so that other quests still properly align with the index
  9.     }
  10.   }
  11.  
  12. // Mark all matching tasks as Complete
  13.   for (int i = 0; i < taskCount; i++) {
  14.     if ((task[i].parentQuest == questTitle) && (task[i].complete == false)) {
  15.       task[i].complete = true;
  16.     }
  17.   }
  18.  
  19. }
  20.  

The last part is really important, and addresses the Phantom Task issue from before: when you remove an item from a ListBox, the index for each item in that ListBox changes. If you have a Quest called "Go to Work" with an ID of 0, and a Quest called "Go Home" with an ID of 1, and then mark "Go to Work" as Complete...the selected Index value of "Go Home" is 0 on the ListBox, which will confuse all of the fancy lookup scripts that we've put together. This is gross, and sucks, but the solution is basically to adjust the index value to compensate for the amount of quests that are now marked as Completed.

So, basically, that's it! I apologize for the eyebleed here. In the future, I'd like to offer some kind of template that makes this all super easy, so that other AGS devs can have fun building their own quests with it.
Check out my game: Life is a Party!

The fediverse needs great indie game developers! Find me there!