Spherical forums

Sphere Development => Sphere Support => Script Support => Topic started by: Jacob on July 04, 2013, 12:42:48 am

Title: Easier quest creation?
Post by: Jacob on July 04, 2013, 12:42:48 am
Hey everyone, as I've been working on creating quests and miniquests in my game, I've come across what may be an issue, especially on the really large, and complicated quests I have in mind to create.

Currently, my quest creation system is basically the same as what was on the old Sphere tutorial for 'Creating RPG Events', or however it was titled. It uses a case system to progress through different quests. In example, this is just a snippet from the small side quest (an investigation quest) that I'm testing in the game:

Code: (javascript) [Select]

function bradleyTalk()
{
  switch (monkeyingAround)
  {
    case 0:
      Text("Chef: \n\n\tWelcome to Bradley's Bakery! I'm Bradley. Can I help you? \n\nYou: \n\n\tMmm, what are you cooking? It smells excellent. \n\nBradley: \n\n\tI'm baking a special recipe of mine - ring cake. See, it's basically just dough and honey, and I make it in a circular shape, like a ring! They are excellent with milk. \n\nYou: \n\n\tSounds great, I'll have to remember that! Good-bye. \n\nBradley: \n\n\tCome again soon!");
    break;

    case 1:
      Text("Bradley: \n\n\tHello, sir! How may I help you today? \n\nYou: \n\n\tHello, I'm investigating a series of thefts from the marketplace. Is there any information you can provide to me about the issue? \n\nBradley: \n\n\tHmm, come to think of it, I HAVE been missing several sacks of flour recently. Not sure what thief would take something as cheap as flour. \n\nYou: \n\n\tThanks, I'll work from that.");
    ++monkeyingAround;
    break;

... and so forth


So basically, I was wondering if there is a method of creating quests with Sphere that would be less jumbled, less confusing, less time-consuming, and less easy to mess up. Would it be possible to create a basic quest creation tool for Sphere, perhaps for future updates, that uses a case-based system?
Title: Re: Easier quest creation?
Post by: DaVince on July 06, 2013, 07:50:59 am
That would be nice. In script, something like this could do the trick:

Code: (javascript) [Select]
var monkeyingAround = new Quest("Monkeying around");

monkeyingAround.add(1, "Bradley", function()
{
  Text("Bradley: \n\n\tHello, sir! How may I help you today? \n\nYou: \n\n\tHello, I'm investigating a series of thefts from the marketplace. Is there any information you can provide to me about the issue? \n\nBradley: \n\n\tHmm, come to think of it, I HAVE been missing several sacks of flour recently. Not sure what thief would take something as cheap as flour. \n\nYou: \n\n\tThanks, I'll work from that.");
  this.update();  //Update quest progress
});

monkeyingAround.add(1, "Steve", function()
{
  Text("Steve: \n\n\tHuh? I know nothing...");
});

And then bind these conversation bits to characters on map entry, or something. Well, it's just a quickly thought up idea.
Title: Re: Easier quest creation?
Post by: alpha123 on July 06, 2013, 11:22:01 am
Basically you're doing some sort of ad-hoc state machine (https://en.wikipedia.org/wiki/Finite-state_machine). I suggest switching to a real FSM (such as this (https://github.com/jhund/js-finite-state-machine), although you might have to modify that to get it to work in Sphere). Writing your own wouldn't be too hard either. It could be very simple, such as DaVince's example above (which is effectively an FSM in disguise) or more complicated to handle interleaved quests and stuff.
The switch way gets complicated very fast (I've worked on a project that uses a 200+ line switch statement as a sort of ad-hoc FSM...), not to mention is harder to read. A real state machine will be both more flexible and easier to manage.
Title: Re: Easier quest creation?
Post by: DaVince on July 06, 2013, 11:43:18 am
alpha123, that Github project you linked to seems pretty useful. I'm new to state machines, so I'm wondering if I'm understanding it properly. Is this is how a snippet of the example works?

Code: (javascript) [Select]

fsm.addTransition(
  "Press Power Button", //Name of this transition
  "Power Off", //Condition: if it's in the "Power Off" state
  function() { this.sendEvent("Press Reset Button"); },  //Transition to "Reset" state
  "Power On"  //Set state to "Power On"
);


Now I'm trying to imagine how that would work with quests and in-game events... This seems pretty powerful, in any case.
Title: Re: Easier quest creation?
Post by: alpha123 on July 06, 2013, 01:19:45 pm
That's correct DaVince (although I don't know this particular library very well, the source is clear enough on this point). Here's how I think it would look for quests:

Code: (javascript) [Select]
var quest = new FSM("start");
quest.addTransition("1", "start", function () { Text("stuff...."); }, "2");
quest.addTransition("2", "1", function () { Text("more stuff..."); }, "3");
// and so on
// Then in someone's talk script
if (quest.currentState == "start") // if we haven't started this quest yet
    quest.sendEvent("1");
else
    quest.sendEvent(quest.currentState); // I'm not 100% sure this will do what we want, but I think so
Title: Re: Easier quest creation?
Post by: DaVince on July 06, 2013, 01:37:05 pm
Code: (javascript) [Select]
else
    quest.sendEvent(quest.currentState); // I'm not 100% sure this will do what we want, but I think so

It seems to me like it would then run the state "start", which isn't defined to have behaviour. Think the else would even be needed if the quest wasn't in the right state to begin with? Or are you thinking along the lines of defining that to invoke some default non-quest character line?
Title: Re: Easier quest creation?
Post by: alpha123 on July 06, 2013, 01:52:06 pm

It seems to me like it would then run the state "start", which isn't defined to have behaviour. Think the else would even be needed if the quest wasn't in the right state to begin with? Or are you thinking along the lines of defining that to invoke some default non-quest character line?

I don't think so, since we transition into "1" if we're at "start". But maybe you're right; the library turned out to be more complicated than it first looked. :(
Title: Re: Easier quest creation?
Post by: DaVince on July 06, 2013, 01:57:24 pm
Oops, no, you're right. I was thinking in the perspective of another game character who might not say anything about the quest when it hasn't started, but contributes to it when it's in a specific state. But your code was intended for the quest giver, I suppose.
Title: Re: Easier quest creation?
Post by: Radnen on July 06, 2013, 03:32:29 pm
In blockman I've used an approach similar to persist/analogue:

Here's the data file,
Code: (javascript) [Select]

// Jomba Seeds quest-line

({
count: 0,
amount: 6,
name: "Jomba Seeds",

/* start: function() {}, */

desc: function() {
switch (this.state) {
                        // initial state:
case 0: return "My friend needs me for something.";

                        // progress states:
case 1: return "He wants me to collect jomba seeds, but I am still undecided.";
case 2: return "I have collected " + this.count + " seeds.";
case 3: return "I have collected enough seeds for my friend.";

                        // termination states:
case 4: return "I have finished this task.";
case 5: return "I turned down my friends offer.";
}
},

finish: function() {
Game.player.addExp(75);
}
})


And here's an entity interacting with said datafile:
Code: (javascript) [Select]

switch (questsys.quest("jomba").state) {
case 1:
Textbox.showText("Granfurt", "Great! What are friends for?");
Textbox.showText("Granfurt", "So, this morning I was cleaning the basement for a party I'll be having and...");
Textbox.showText("Granfurt", "I realized I ran out of *Jomba Seeds* for the feast...");
Textbox.showText("Granfurt", "I need you to find some for me.");
Textbox.showText("player", "I'm busy, why don't you do it yourself?");
Textbox.showText("Granfurt", "But you have clearance to leave, right?");
Textbox.showText("player", "Yes I do, though there's a reason as to why this place is walled in!");
Textbox.showText("Granfurt", "That was only because of the Mutagen Wars. I'm sure you'll be fine.");
switch (Textbox.showChoices("Fine. I'll do it.", "Go do it yourself.")) {
case 0:
Textbox.showText("Granfurt", "Thanks!");
questsys.quest("jomba").state = 2;
break;
case 1:
Textbox.showText("Granfurt", "Arg! I guess I'll have to ask someone else.");
questsys.quest("jomba").state = 5;
questsys.finish("jomba");
break;
}
break;
case 2: Textbox.showText("Granfurt", "Hmm, I need more seeds than that."); break;
case 3:
Textbox.showText("Granfurt", "It looks like you have all the seeds!");
Textbox.showText("player", "Yeah well it was a pain. Here you go.");
Textbox.showText("Granfurt", "Thanks for doing that for me!");
questsys.quest("jomba").state = 4;
questsys.finish("jomba");
break;
case 4: Textbox.showText("Granfurt", "Thanks for doing that for me!"); break;
case 5: Textbox.showText("Granfurt", "Well, I guess I don't get my seeds then."); break;
}


I wanted it to be lightweight, and have it so that all entities who interact with the data file basically check the state of the quest and react to that state. I wish there wasn't something so cryptic: what does state 3 mean? You can read the description you wrote for each state in the 'desc' callback, but they must all have a definition there. Anyways, Skyrim does something very similar to this in the background and some of their bugs do show it.

The only last solution is to put the entity dialogue right inside of the quest itself, but that just moves the code from one place to another.

My goal with this was to also create a framework that was subsequently easy to save and load, took less typing to update and receive quests with a lightweight api, and made sure to do things like stumbling upon a quest you hadn't started and keep those variables around until you do find the quest giver (or you put it into a state that just says, for example: "Somebody in this town lost their doll").

As with all state machines you must be thorough for each entity that interacts with the quest : How does it start? End? What's it like in progress? What if they stumble upon it without starting it? Can they do that?
Title: Re: Easier quest creation?
Post by: Jacob on July 07, 2013, 08:20:18 pm
Sorry it took me so long to respond, I've been busy in my life outside of my game development, I nearly forgot about this topic. :P

Anyways, this quest idea is an experiment in a more complex case-based system. A simpler, straight-forward (mini)quest in my game is like this:

Code: (javascript) [Select]
 var fireLily = 0;

function priestFL()
{
   switch (fireLily)
   {
      case 0:
         Text("Priest: \n\n\tOh, Ilxder! I was looking for you earlier. You see, my first anniversary with my wife is tomorrow, and I'd like to get her something nice. Do you have some time to spare? \n\nYou: \n\n\tSure, what do you need? \n\nPriest: \n\n\tCould you get a Fire Lily for me? They're a rare flower that only grows on this island. You can find some by walking east along the North Shore. \n\nYou: \n\n\tAlright.");
      ++fireLily;
      break;

      case 1:
         Text("Priest: \n\n\tHave you acquired a fire lily yet? \n\nYou: \n\n\tNo, not yet. \n\nPriest: \n\n\tPlease, do hurry! I wish to have that flower for tomorrow.");
      break;
 
      case 2:
         Text("Priest: \n\n\tHave you acquired a fire lily yet? \n\nYou: \n\n\tYes, I did. Here it is! \n\n*You hand the flower to the priest* \n\nPriest: \n\n\tThank you so much, Ilxder! Now I can give my wife something really nice - the priestly salary isn't very large, as you can well imagine. \n\n*You have completed the quest: 'Fire Lillies'*");
      ++fireLily;
      break;

      case 3:
         Text("Priest: \n\n\tThank you for obtaining that flower for me! Can I do anything else for you? \n\nYou: \n\n\tNo, thank you. \n\nPriest: \n\n\tAlright. Let me know if you need anything more. \n\nYou: \n\n\tI will. Good day! \n\nPriest: \n\n\tTylbur's blessings. \n\n\t* You have completed the Fire Lily side quest *");
      break;
   }
}

function fireLilyGather()
{
   switch (fireLily)
   {
      case 0:
         Text("~ These flowers are beautiful, but I can't think of anything I might need them for ~");
      break;
 
      case 1:
         Text("*You pick the flower*");
      ++fireLily;
      break;

      case 2:
         Text("~ I already picked the flower for the priest. I have no other need for them. ~");
      break;

      case 3:
         Text("~ I already picked the flower for the priest. I have no other need for them. ~");
      break;
   }
}


Whereas, the full quest script for this 'Monkeying Around' is a more complex system, where I have to lay out the plan and which character has new information and during which specific case they HAVE the new info. This is easy to lay out in my head, but it can be easily messed up (and has been) while I've been designing this little quest that appears during the tutorial. Here's the full script that I have so far:

Code: (javascript) [Select]
var monkeyingAround = 0;


function captainTalk()
{
   switch (monkeyingAround)
   {
      case 0:
         Text("Captain:\n\n\tPlease address all of your concerns to the office of the.. oh, Ilxder sir! I've been getting too many complaints from the merchants in the market about some sort of thief taking all of their products. So what can I do for you, sir? \n\nYou: \n\n\tI could look into that issue for you. \n\nCaptain: \n\n\tWould you? I'd definitely be grateful. \n\nYou: \n\n\tSure! \n\n* This is an investigation-style quest. You will encounter many of these quests throughout your time on Steorth. *");
      ++monkeyingAround;
      break;
     
      case 1:
         Text("Captain:\n\n\tDid you look into the thefts from the marketplace yet? \n\nYou: \n\n\tNo, not yet. \n\nCaptain: \n\n\tPlease try to look into the issue as soon as possible.");
      break;
     
      case 2:
         Text("Captain:\n\n\tDid you look into the thefts yet? \n\nYou: \n\n\tI'm currently gathering info from the merchants on the thefts. \n\nCaptain: \n\n\tVery good. Please report back to me if you find anything out.");
      break;
     
      case 3:
         Text("Captain:\n\n\tAnything new? \n\nYou: \n\n\tYes. I spoke to some of the merchants, and from what I gather, it's not a human thief, but an animal. \n\nCaptain: \n\n\tAn animal, you say? Hmm, strange. Anything else? \n\nYou: \n\n\tI'm afraid not, yet.");
     
      case 4:  
         Text("Captain: \n\n\tFind anything more about the thefts yet? \n\nYou: \n\n\tYes, I have. It turns out that a little monkey, presumably from the eastern end of the Island, has been sneaking into the buildings around the square and apparently stealing the merchandise. \n\nCaptain: \n\n\tA monkey? That's odd, they usually keep to themselves. Well done, thanks for the help. We'll keep an eye out for the little critters in the future. \n\n* You've completed 'Monkeying Around'! *");
      ++monkeyingAround;
      break;
     
      case 5:
         Text("Captain: \n\n\tThank you for investigating that issue for me. \n\nYou: \n\n\tNo trouble at all, Captain. I always like to see things going well on Halmuna. \n\nCaptain: \n\n\tIndeed. I'll be sure to call for you if I need help in the future!");
   }
}
     
function bradleyTalk()
{
   switch (monkeyingAround)
   {
      case 0:
         Text("Chef: \n\n\tWelcome to Bradley's Bakery! I'm Bradley. Can I help you? \n\nYou: \n\n\tMmm, what are you cooking? It smells excellent. \n\nBradley: \n\n\tI'm baking a special recipe of mine - ring cake. See, it's basically just dough and honey, and I make it in a circular shape, like a ring! They are excellent with milk. \n\nYou: \n\n\tSounds great, I'll have to remember that! Good-bye. \n\nBradley: \n\n\tCome again soon!");
      break;
        
      case 1:
         Text("Bradley: \n\n\tHello, sir! How may I help you today? \n\nYou: \n\n\tHello, I'm investigating a series of thefts from the marketplace. Is there any information you can provide to me about the issue? \n\nBradley: \n\n\tHmm, come to think of it, I HAVE been missing several sacks of flour recently. Not sure what thief would take something as cheap as flour. \n\nYou: \n\n\tThanks, I'll work from that.");
      ++monkeyingAround;
      break;
        
      case 2:
         Text("Bradley: \n\n\tDid you find any more evidence concerning the thefts yet? \n\nYou: \n\n\tNo, not yet. If you find any new evidence, please let either myself or the Captain of the Guard know. \n\nBradley: \n\n\tCan-do, sir!");
      break;
     
      case 3:
         Text("Bradley: \n\n\tDid you find out any more about the thefts? \n\nYou: \n\n\tYes, but not much. It's not a human that's stealing the merchandise, but a creature, reportedly with a small tail. \n\nBradley: \n\n\tThat is really strange. I'll be sure to keep an eye out for anything suspicious! \n\nYou: \n\n\tThank you, Bradley.");
        
      case 4:  
         Text("Bradley: \n\n\tDid you find any more evidence concerning the thefts yet? \n\nYou: \n\n\tYes, actually, I did. It turns out a monkey has been stealing things from the merchants in the marketplace. \n\nBradley: \n\n\tA monkey? I thought they didn't come this far west. \n\nYou: \n\n\tI thought the same, but apparently that's not the case. Perhaps one of the local residents owns one as a pet.");
      break;
        
      case 5:
         Text("Bradley: \n\n\tWelcome back, sir! \n\nYou: \n\n\tHello, Bradley. Had any more run-ins with monkeys recently? \n\nBradley: \n\n\tNo, everything has been really quiet recently. Thanks for asking though!");
      break;
   }
}

function richardTalk()
{
   switch (monkeyingAround)
   {
      case 0:
         Text("Richard: \n\n\tHello, good sir, and welcome to Richard's Research Lab! Is there anything I can help you with? \n\nYou: \n\n\tNot at the minute, but thank you very much, Richard! \n\nRichard: \n\n\tAlright, feel free to speak to me if you change your mind.");
      break;
     
      case 1:
         Text("You: \n\n\tRichard, have you seen any suspicious activity recently? I'm investigating a theft issue that has been plaguing the marketplace. \n\nRichard: \n\n\tNo, I can't say I have, sir. \n\nYou: \n\n\tVery well. If you see anything suspicious, please report it to either myself, or the Captain of the Guard.");
      break;
     
      case 2:
         Text("You: \n\n\tHave you seen anything suspicious recently, Richard? \n\nRichard: \n\n\tYes, sir, I caught only a glimpse of what appeared to be a small tail on a creature as it left my store. \n\nYou: \n\n\tA tail... hmm, I'll speak to the other people around here and see if they have any possible ideas of what this could be about.");
      ++monkeyingAround;
      break;
     
      case 3:
         Text("Richard: \n\n\tDid you happen to find anything yet? \n\nYou: \n\n\tNo, not yet. I'm still asking the others around here. \n\nRichard: \n\n\tPlease hurry and solve this problem. I don't think I can afford my medicine being stolen constantly like this!");
      break;
     
      case 4:
         Text("Richard: \n\n\tDid you happen to find anything yet? \n\nYou: \n\n\tYes, it turns out this creature is just a monkey! \n\nRichard: \n\n\tA monkey?? Here? Why? \n\nYou: \n\n\tI know about as much as you do, on that aspect. I'm going to report it to the captain.");
      break;
     
      case 5:
         Text("You: \n\n\tHave you had any more issues with theft recently? \n\nRichard: \n\n\tNo, sir! Thank you for sorting this all out!");
      break;
   }
}


Of course, this is still a work-in-progress.

The thing I often have a problem with, in implementing others' scripts and libraries in my game, is that certain functions and variables may not be properly defined, or it may be just a different coding style, so I'd have a harder time implementing it myself. I suppose, since I'm not that great at programming, that my coding style is probably not very good, but I'd say it's probably too late in the game's progress to redo it all. Perhaps for my next Sphere project, things will be programmed better.






@DaVince - Your example makes sense to me, but I think I'd essentially be doing the same thing as before, in just a different style - like, I'd still be writing 'case 0', 'case 1', 'case 2' and so forth, right? I'm not sure, as I said, I still have a hard time programming with things, even after experimenting with it and reading books for nearly the past four years. :-\


EDIT: Honestly, I can work out most of the logic of programming, particularly in JS, but it's the actual coding process that I have problems with. Just an FYI.
Title: Re: Easier quest creation?
Post by: DaVince on July 08, 2013, 06:43:05 am
You could make it more readable for yourself by identifying 0, 1, etc. as "quest start", "picked flower", whatever. That could expose it to possible issues of making typos, but it would be much clearer what a certain quest point is about.
Title: Re: Easier quest creation?
Post by: Jacob on July 08, 2013, 03:55:00 pm
Oh, so you mean put just comment lines by each one, or is there an actual code definition I could use?
Title: Re: Easier quest creation?
Post by: Fat Cerberus on July 08, 2013, 04:11:15 pm
Code: (javascript) [Select]
if (monkeyingAround == "pig fed") {
  // dialogue
  monkeyingAround = "done";
} else if (monkeyingAround == "done") {
  // thanks for feeding my pig!
}


That kind of thing.  You can do the same with switch as well.  You just compare against a string instead of a number.
Title: Re: Easier quest creation?
Post by: Jacob on July 08, 2013, 05:27:41 pm
Yeah, but wouldn't that end up being a bit more complicated in the end? I mean, it's more or less the same thing I'm running right now, except your example involves if statements, and mine just runs on case numbers. It would probably be more time-consuming to do if statements, possibly more confusing, since I'd be writing all of these different lines, when I could essentially do the same thing with the current system. Plus, I'd have to define what terms like 'pig fed' and 'done' mean. Wouldn't that take more time to do in the long run?
Title: Re: Easier quest creation?
Post by: Fat Cerberus on July 08, 2013, 05:44:41 pm
You can use switch/case with strings as well as numbers, you don't have to use the if/elseIf if you don't want to.  But not sure what you mean on defining what the terms mean: the point is that you use a string that describes the current state of the the quest, rather than using numbers that you'll have to document somewhere else what they mean.
Title: Re: Easier quest creation?
Post by: DaVince on July 08, 2013, 06:13:01 pm
^ What Lord English said. You can basically take your own code and replace 0, 1, 2 with some descriptive strings, if that makes things more convenient for you.
Title: Re: Easier quest creation?
Post by: Jacob on July 08, 2013, 11:05:29 pm
Like I said, I'm quite amateur when it comes to programming, so I get confused easily with these things. :P

Okay, so basically I understand what both of you are talking about; implementing it into my game may be quite the task though, as I'd have to remember to change my coding style for when I create quests. The second part is time consumption. I really don't want to have to spend all of my time writing code for quests when I still have to write gameplay mechanics, do world maps, create unique dialogue (I don't want two NPCs to have the same dialogue), and so forth, but if that's the only way to do it, I suppose I'll have to go with it.
Title: Re: Easier quest creation?
Post by: Radnen on July 09, 2013, 12:17:39 am
My idea may seem complex but it's easy to write and use. I made my quest system - no matter how complex it looks to save time in writing future quests.
Title: Re: Easier quest creation?
Post by: Jacob on July 09, 2013, 11:39:43 am
I'll take a look at your work and consider my options and get back to you all if I end up doing anything that I need help with. Thanks for the patience with my ignorance haha
Title: Re: Easier quest creation?
Post by: Radnen on July 09, 2013, 02:09:49 pm
Well, what I posted is not the full code. There's still the underlying questsys library. Once I release it I can make a tutorial.
Title: Re: Easier quest creation?
Post by: Jacob on July 09, 2013, 10:46:37 pm
Okay, that sounds good. :)