Ren'Py Quest (Tasks) System recomendation

Porcus Dev

Engaged Member
Game Developer
Oct 12, 2017
2,582
4,705
Hi people,

To make it easier for the player to know what to do in my game I've been thinking about adding some quest system.

I've been running some tests with this system:


Although it seems to fulfill what I want to do and that with a lot of work I could adapt it to my game, before I wanted to know the opinion of the great experts in advanced programming that there are around here :p;)

Do you have any preferred code for these quest systems?

About the code I link to:
The first doubt I have with the code I've been testing with is that it seems you have to have defined "quests", "stages" and "goals" from the beginning, because when you modify the file that contains them, the game is not updated, until I call the code again, but this makes the states reset, and that's not good ... although surely there is a way to do it.
Besides, I don't need to separate the "tasks" into "quests", "stages" and "goals", this makes it more complicated and I simply need "tasks".

What I'd need would be two columns. In one there would be the characters and in the other there would be the tasks to be completed for the character in question. But in that second column, it would have to be possible to consult the active and finished tasks.
There can be a lot of characters and/or tasks, so it would be good if you could scroll in the boxes.
And a system, as easy as possible, to add new tasks and mark as "done" those already completed.

I know that what I ask can be complicated and that surely this can be done in several ways... I don't ask for the complete code, I just want help on what you think is the best way to do it and some examples to understand it (python is still a bit complicated for me) :cry:


Thanks in advance!!
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,979
16,236
The first doubt I have with the code I've been testing with is that it seems you have to have defined "quests", "stages" and "goals" from the beginning, because when you modify the file that contains them, the game is not updated, until I call the code again, but this makes the states reset, and that's not good ...
Even when you do something like this :
Python:
default update1 = False

label start:
    # Initial creation of "My First Quest"
    $ quests.append( Quest( "My First Quest", "blabla", "blabla", goals, stages ) )
    # Initial creation of "My Second Quest"
    $ quests.append( Quest( "My Second Quest", "blabla", "blabla", goals, stages ) )
    # You already updated the quest since the previous update.
    $ update1 = True

label after_load:
    # It's a none updated version of the quest
    if update1 is False:
        # Update quest "My First Quest"
        $ quests[0].addgoals( Goal( "new goal", "do this" ) )
        $ quests[0].addstages( Stage( "new stage", "that" ) )

        # Update quest "My Second Quest"
        $ quests[1].addgoals( Goal( "new goal", "do this" ) )
        $ quests[1].addstages( Stage( "new stage", "that" ) )
        $ update1 = True

What I'd need would be two columns. In one there would be the characters and in the other there would be the tasks to be completed for the character in question. But in that second column, it would have to be possible to consult the active and finished tasks.
Wrote in the fly :
Python:
init python:
    class Quest( renpy.python.RevertableObject ):

        def __init__( self, char ):
            self._char = char
            self._stages = []
            self._actualStage = 0

        @property
        def showStage( self ):
            if self._actualStage > len( self._stages )
                return "Error, stage not defined."
            return self._stages[self._actualStage]

        def addStage( self, task ):
            self._stages.append( task )

        def nextStage( self ):
            self._actualStage += 1

        def reset( self ):
            self._actualStage = 0

        def numberStages( self ):
            return len( self._stages )

define charA = Character( "Name of charA" )
define charB = Character( "Name of charB" )

label start:
    python:
        $ charAQuest = Quest( "charA name" )
        $ charAQuest.addStage( "what to do on first stage" )
        $ charAQuest.addStage( "what to do on second stage" )
        $ charBQuest = Quest( "charB name" )
        $ charBQuest.addStage( "what to do on first stage" )
        $ charBQuest.addStage( "what to do on second stage" )

    [start you game]

screen questLog:
    vbox:
        text "Next to do for [charA] : [charAQuest.showStage]"
        text "Next to do for [charB] : [charBQuest.showStage]"
Then for the update, you use what I said in the how-to regarding . You use the after_load label and flags to mark if an update is to do or not :
Python:
label start:
    python:
        $ charAQuest = Quest( "charA name" )
        $ charAQuest.addStage( "what to do on first stage" )
        $ charAQuest.addStage( "what to do on second stage" )
        # Added by release 0.2
        $ charAQuest.addStage( "what to do on third stage" )
        # Added by release 0.3
        $ charAQuest.addStage( "what to do on fourth stage" )
        $ charBQuest = Quest( "charB name" )
        $ charBQuest.addStage( "what to do on first stage" )
        $ charBQuest.addStage( "what to do on second stage" )
        # Added by release 0.4
        $ charBQuest.addStage( "what to do on third stage" )

    [start you game]

label after_load:
    if charAQuest.numberStages == 2:
        # Add the new stage from release 0.2
        $ charAQuest.addStage( "what to do on third stage" )
    if charAQuest.numberStages == 3:
        # Add the new stage from release 0.3
        $ charAQuest.addStage( "what to do on fourth stage" )

    if charBQuest.numberStages == 2:
        # Add the new stage from release 0.4
        $ charBQuest.addStage( "what to do on third stage" )
It's minimalist, but it should be enough for what you want to do.
 

Porcus Dev

Engaged Member
Game Developer
Oct 12, 2017
2,582
4,705
Even when you do something like this :
Python:
default update1 = False

label start:
    # Initial creation of "My First Quest"
    $ quests.append( Quest( "My First Quest", "blabla", "blabla", goals, stages ) )
    # Initial creation of "My Second Quest"
    $ quests.append( Quest( "My Second Quest", "blabla", "blabla", goals, stages ) )
    # You already updated the quest since the previous update.
    $ update1 = True

label after_load:
    # It's a none updated version of the quest
    if update1 is False:
        # Update quest "My First Quest"
        $ quests[0].addgoals( Goal( "new goal", "do this" ) )
        $ quests[0].addstages( Stage( "new stage", "that" ) )

        # Update quest "My Second Quest"
        $ quests[1].addgoals( Goal( "new goal", "do this" ) )
        $ quests[1].addstages( Stage( "new stage", "that" ) )
        $ update1 = True



Wrote in the fly :
Python:
init python:
    class Quest( renpy.python.RevertableObject ):

        def __init__( self, char ):
            self._char = char
            self._stages = []
            self._actualStage = 0

        @property
        def showStage( self ):
            if self._actualStage > len( self._stages )
                return "Error, stage not defined."
            return self._stages[self._actualStage]

        def addStage( self, task ):
            self._stages.append( task )

        def nextStage( self ):
            self._actualStage += 1

        def reset( self ):
            self._actualStage = 0

        def numberStages( self ):
            return len( self._stages )

define charA = Character( "Name of charA" )
define charB = Character( "Name of charB" )

label start:
    python:
        $ charAQuest = Quest( "charA name" )
        $ charAQuest.addStage( "what to do on first stage" )
        $ charAQuest.addStage( "what to do on second stage" )
        $ charBQuest = Quest( "charB name" )
        $ charBQuest.addStage( "what to do on first stage" )
        $ charBQuest.addStage( "what to do on second stage" )

    [start you game]

screen questLog:
    vbox:
        text "Next to do for [charA] : [charAQuest.showStage]"
        text "Next to do for [charB] : [charBQuest.showStage]"
Then for the update, you use what I said in the how-to regarding . You use the after_load label and flags to mark if an update is to do or not :
Python:
label start:
    python:
        $ charAQuest = Quest( "charA name" )
        $ charAQuest.addStage( "what to do on first stage" )
        $ charAQuest.addStage( "what to do on second stage" )
        # Added by release 0.2
        $ charAQuest.addStage( "what to do on third stage" )
        # Added by release 0.3
        $ charAQuest.addStage( "what to do on fourth stage" )
        $ charBQuest = Quest( "charB name" )
        $ charBQuest.addStage( "what to do on first stage" )
        $ charBQuest.addStage( "what to do on second stage" )
        # Added by release 0.4
        $ charBQuest.addStage( "what to do on third stage" )

    [start you game]

label after_load:
    if charAQuest.numberStages == 2:
        # Add the new stage from release 0.2
        $ charAQuest.addStage( "what to do on third stage" )
    if charAQuest.numberStages == 3:
        # Add the new stage from release 0.3
        $ charAQuest.addStage( "what to do on fourth stage" )

    if charBQuest.numberStages == 2:
        # Add the new stage from release 0.4
        $ charBQuest.addStage( "what to do on third stage" )
It's minimalist, but it should be enough for what you want to do.
Wos, this is perfect, I'm going to do tests as soon as I can and I'll tell you how it works for me.

I'm enormously grateful, you're a Crack! :love:;)
 

Porcus Dev

Engaged Member
Game Developer
Oct 12, 2017
2,582
4,705
Ok, I was able to do some tests, and after some problems because of my inexperience with python, it seems to work.

I have figured out that in order to advance to the next "Stage" you had to type the following command:
$ charAQuest.nextStage()

And it works well, the next "Stage" is shown.
But if I rewrite it, and there're no new "Stages", the error "list index out of range" appears in the line: return self._stages[self._actualStage]

It would be good if when the player finishes all the Stages, it would be empty to know that there are no more things to do.

And another doubt that I have, and perhaps with this it will become more complicated... if it can be done, good, otherwise there's no problem... would it be possible to define a number of times that the "Stages" have to be done?


Tomorrow, if I can, I'll try the first option you've written to take advantage of the code I was already testing with, although I see it more complicated than yours :p


Thank you very much! ;)
 

Porcus Dev

Engaged Member
Game Developer
Oct 12, 2017
2,582
4,705
I've been thinking that what I asked you above doesn't have too much trouble, I can leave a blank "Stage" or write something like "More in the next version..." and for repetitions I can use some variable to control it.

But from what I see the system of "Stages" is linear, you have to pass one to show the next and you can't have several enabled at once, that's right?


Thanks.
 

f95zoneuser463

Member
Game Developer
Aug 14, 2017
219
1,024
A heavily modified version of this quest-system is being used in my game "Holiday Islands". There are a bunch of bugs in the original code. Unfortunately I remember only one:
  • finding another 'item' on a collection quest will trigger an exception if the quest was completed already
I don't use the stage-system at all. Instead I only have a single dummy-stage for each quest and the actual progress is presented to the player by revealing previously hidden quests or goals. The player has all quests from the very beginning, but they are hidden.

I've tried to find a generic solution to the problem that the original code can not update quests. To the quest-class I added self._version = version and version=1 as __init__()-parameter. So each quest now has a version 1 by default.

After I make quest-changes I increase the quest-version (conveniently located nearby) and the code handles everything.

In the after_load-label questlog_update gets called. It creates a big quests[]-list and then does this, note the log.update_quest(q):
Python:
## *long quest[]-list is being created here*

## check if log already exists
updated_quests = ""
try:
    if log:
        try:
            for q in quests:
                ## update each quest
                updated_quests += log.update_quest(q)
        except Exception as exception:
            print(exception)
except NameError:
    ## create new log
    ## note the key-parameter got removed in my version
    log = Questlog(quests=quests, screen="qlog")
   
del goals
del quests
del stages

## show info screen with updated quests
if updated_quests != "":
    renpy.call_screen("s_updated_quests", updated_quests)
del updated_quests
Inside the Questlog-class the update_quest()-function updates the quest based on the quest-version.
Important:
  • keep in mind this was coded for my project, it has my tab-names hardcoded
  • to keep the progress of a partially completed quest it MUST be modified
  • manually updating the quests is probably better for small/medium size project
  • messy code!
Python:
## update_quest() takes new Quest-object as parameter
## It searches all log-quests and replaces an old quest IF the new quest has a higher version number
## -> partially completed quest will reset
## -> "Sympathy"-tab quest will not reset
## -> "Completed"-tab quest will have goals completed that already existed in the older quest version
##    That means if new goal got added it's no longer in the "Completed"-tab.
##
## returns title of updated quests or an empty string if nothing was updated
def update_quest(self, q):
    #print("qlog: q._description=" + q._description)
    t = q.tab()
    # add new tab if it not exists
    if t not in self._quests.keys():
        self._tabs.append(t)
        self._quests[t] = [ ]
        self._quests[t].append(q)
        self.assign(q.title())
        print("qlog: " + q.title() + " added to new tab "+t)
        return q.title()+"{color=#F00} | {/color}"
    # tab for quest already exists, search old quest
    oldq = self.quest(q.title())
    if oldq == None:
        print("qlog: " + q.title() + " added to tab "+t)
        self._quests[t].append(q)
        self.assign(q.title())
        return q.title()+"{color=#F00} | {/color}"
    # compare version and replace old quest if outdated
    #print("qlog: " + q.title() + " old=" + str(oldq._version) + " new=" + str(q._version))
    if q._version > oldq._version:
        found = False
        # search old quest in original tab
        for n,nquest in enumerate(self._quests[t]):
            if nquest.title() == q.title():
                found = True
                oldgoals = self._quests[t][n]._goals
                qhidden = self._quests[t][n].hidden()
                self._quests[t][n] = q # replace quest
                if t == "Sympathy":
                    self._quests[t][n]._goals = oldgoals
                self.assign(q.title())
                self._quests[t][n].hidden(qhidden)
                print("qlog: " + q.title() + " updated, new version")
                return q.title()+"{color=#F00} | {/color}"
        # search old quest in 'Completed' tab
        for n,nquest in enumerate(self._quests['Completed']):
            if nquest.title() == q.title():
                found = True
                # copy completed goals 0.10.3
                oldgoals = self._quests['Completed'][n]._goals
                #qhidden = self._quests['Completed'][n].hidden()

                del self._quests['Completed'][n]
                self._quests[t].append(q)
                self.assign(q.title())
                # compare old- and new goal-keys, complete if the same
                # due to markdone() the quest will be automatically moved to 'Completed' if all goals are completed
                newgoalkeys = self.quest(q.title())._goals.keys()
                #print("qlog: newgoals = "+str(newgoalkeys))
                for goalkey in newgoalkeys:
                    if oldgoals.has_key(goalkey):
                        self.markdone(goalkey)
                        print("qlog: auto-completed "+unicode(goalkey))
                print("qlog: " + q.title() + " updated, new version")
                return q.title()+"{color=#F00} | {/color}"
        if found == False:
            print("qlog: " + q.title() + " not found")
    #print("qlog: " + q.title() + " same version")
    return ""
 

Porcus Dev

Engaged Member
Game Developer
Oct 12, 2017
2,582
4,705
A heavily modified version of this quest-system is being used in my game "Holiday Islands". There are a bunch of bugs in the original code. Unfortunately I remember only one:
  • finding another 'item' on a collection quest will trigger an exception if the quest was completed already
I don't use the stage-system at all. Instead I only have a single dummy-stage for each quest and the actual progress is presented to the player by revealing previously hidden quests or goals. The player has all quests from the very beginning, but they are hidden.

I've tried to find a generic solution to the problem that the original code can not update quests. To the quest-class I added self._version = version and version=1 as __init__()-parameter. So each quest now has a version 1 by default.

After I make quest-changes I increase the quest-version (conveniently located nearby) and the code handles everything.

In the after_load-label questlog_update gets called. It creates a big quests[]-list and then does this, note the log.update_quest(q):
Python:
## *long quest[]-list is being created here*

## check if log already exists
updated_quests = ""
try:
    if log:
        try:
            for q in quests:
                ## update each quest
                updated_quests += log.update_quest(q)
        except Exception as exception:
            print(exception)
except NameError:
    ## create new log
    ## note the key-parameter got removed in my version
    log = Questlog(quests=quests, screen="qlog")
  
del goals
del quests
del stages

## show info screen with updated quests
if updated_quests != "":
    renpy.call_screen("s_updated_quests", updated_quests)
del updated_quests
Inside the Questlog-class the update_quest()-function updates the quest based on the quest-version.
Important:
  • keep in mind this was coded for my project, it has my tab-names hardcoded
  • to keep the progress of a partially completed quest it MUST be modified
  • manually updating the quests is probably better for small/medium size project
  • messy code!
Python:
## update_quest() takes new Quest-object as parameter
## It searches all log-quests and replaces an old quest IF the new quest has a higher version number
## -> partially completed quest will reset
## -> "Sympathy"-tab quest will not reset
## -> "Completed"-tab quest will have goals completed that already existed in the older quest version
##    That means if new goal got added it's no longer in the "Completed"-tab.
##
## returns title of updated quests or an empty string if nothing was updated
def update_quest(self, q):
    #print("qlog: q._description=" + q._description)
    t = q.tab()
    # add new tab if it not exists
    if t not in self._quests.keys():
        self._tabs.append(t)
        self._quests[t] = [ ]
        self._quests[t].append(q)
        self.assign(q.title())
        print("qlog: " + q.title() + " added to new tab "+t)
        return q.title()+"{color=#F00} | {/color}"
    # tab for quest already exists, search old quest
    oldq = self.quest(q.title())
    if oldq == None:
        print("qlog: " + q.title() + " added to tab "+t)
        self._quests[t].append(q)
        self.assign(q.title())
        return q.title()+"{color=#F00} | {/color}"
    # compare version and replace old quest if outdated
    #print("qlog: " + q.title() + " old=" + str(oldq._version) + " new=" + str(q._version))
    if q._version > oldq._version:
        found = False
        # search old quest in original tab
        for n,nquest in enumerate(self._quests[t]):
            if nquest.title() == q.title():
                found = True
                oldgoals = self._quests[t][n]._goals
                qhidden = self._quests[t][n].hidden()
                self._quests[t][n] = q # replace quest
                if t == "Sympathy":
                    self._quests[t][n]._goals = oldgoals
                self.assign(q.title())
                self._quests[t][n].hidden(qhidden)
                print("qlog: " + q.title() + " updated, new version")
                return q.title()+"{color=#F00} | {/color}"
        # search old quest in 'Completed' tab
        for n,nquest in enumerate(self._quests['Completed']):
            if nquest.title() == q.title():
                found = True
                # copy completed goals 0.10.3
                oldgoals = self._quests['Completed'][n]._goals
                #qhidden = self._quests['Completed'][n].hidden()

                del self._quests['Completed'][n]
                self._quests[t].append(q)
                self.assign(q.title())
                # compare old- and new goal-keys, complete if the same
                # due to markdone() the quest will be automatically moved to 'Completed' if all goals are completed
                newgoalkeys = self.quest(q.title())._goals.keys()
                #print("qlog: newgoals = "+str(newgoalkeys))
                for goalkey in newgoalkeys:
                    if oldgoals.has_key(goalkey):
                        self.markdone(goalkey)
                        print("qlog: auto-completed "+unicode(goalkey))
                print("qlog: " + q.title() + " updated, new version")
                return q.title()+"{color=#F00} | {/color}"
        if found == False:
            print("qlog: " + q.title() + " not found")
    #print("qlog: " + q.title() + " same version")
    return ""
Thank you very much! :D
I'll take a look at it and as soon as I can, I'll run tests.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,979
16,236
And it works well, the next "Stage" is shown.
But if I rewrite it, and there're no new "Stages", the error "list index out of range" appears in the line: return self._stages[self._actualStage]
it's the "wrote in the fly" effect, sorry. I usually handle the exception right in the code.


It would be good if when the player finishes all the Stages, it would be empty to know that there are no more things to do.
You can have a "that's all for now" last stage. With the help of a method to update a stage it works fine.


And another doubt that I have, and perhaps with this it will become more complicated... if it can be done, good, otherwise there's no problem... would it be possible to define a number of times that the "Stages" have to be done?
Yes, it's possible. It need few changes (including those implied by what is said above):
Python:
init python:

    class Quest( renpy.python.RevertableObject ):

        def __init__( self, char ):
            self._char = char
            self._stages = []
            self._iterationsLeft = []
            self._actualStage = 0

        # Return the text for the actual stage.
        @property
        def showStage( self ):
            if self._actualStage > len( self._stages ):
                return "Error, stage not defined."
            return self._stages[self._actualStage][0]

        # Return the (human understandable) number of the actual stage. 
        @property
        def actualStage( self ):
            return self._actualStage + 1

        # Return the number of iteration left for the actual stage.
        # Not include to /showStage/ to let you style this like you want.
        @property
        def iterationLeft( self ):
            if self._actualStage > len( self._stages ):
                return 0    # Expect a number.
            return self._iterationsLeft[self._actualStage]

        # Add a new stage, with an optional number of iteration for it.
        def addStage( self, task, iteration=1 ):
            self._stages.append( ( task, iteration ) )
            self._iterationsLeft.append( iteration )

        # Stage completed, go to next one.
        def nextStage( self ):
            # Do nothing if the quest is seen as finished.
            if self.isFinished is True: return
            # An interation is done for this stage.
            self._iterationsLeft[self._actualStage] -= 1
            # Go to next stage only if at least one left to do.
            if self._iterationsLeft[self._actualStage] == 0:
                self._actualStage += 1

        # Reset the whole quest.
        def reset( self ):
            self._actualStage = 0
            self._iterationsLeft = []
            for s in self._stages:
                self._iterationsLeft.append( s[1] )

        # Return the number of stages for the quest.
        def numberStages( self ):
            return len( self._stages )

        # Update a stage anywhere in the quest.
        def updateStage( self, stage, task, iteration=1 ): 
            # stage don't exist. Reminded, list index start at 0.
            if len( self._stages ) =< stage : return
            # No negative stage please.
            if stage < 0: return
            # Ensure that you don't reset the number of iterations already done.
            iterationDone = self._stages[stage][1] - self._iterationsLeft[stage]
            self._stages[stage] = ( task, iteration )
            self._iterationsLeft[stage] = iteration - iterationDone
            #  In case the update decrease the number of iteration and the 
            # player already did more than now needed. Sorry for him, he'll have
            # to do one interation anyway.
            if self._iterationsLeft[stage] < 1: self._iterationsLeft[stage] = 1

        # Update the last stage.
        def updateLastStage( self, task, iteration=1 ): 
            self.updateStage( len( self._stages - 1 ), task, iterarion )

        #  Return True if the quest is finished. Here, finished mean that the
        # player have reach the last stage actually defined. So it's more a 
        # "finish, at least for now" meaning.
        def isFinished( self ):
            return ( len( self._stages ) == self._actualStage + 1 ) and self._iterationsLeft[stage] == 0



label start:
    python:
        $ charAQuest = Quest( "charA name" )
        # A single iteration for this stage, value is implicit.
        $ charAQuest.addStage( "what to do on first stage" )
        #  This stage will have to be done twice, you need to explicitly give
        # the value.
        $ charAQuest.addStage( "what to do on second stage", 2 )
        $ charBQuest = Quest( "charB name" )
        $ charBQuest.addStage( "what to do on first stage" )
        $ charBQuest.addStage( "what to do on second stage" )

    [start you game]

label room:
    if charAQuest.actualStage == 1:
        jump questStage2

    [...]

label questStage2:
    [...]

    $ charAQuest.nextStage()
    if charAQuest.iterationLeft == 0:
        "Apparently this time she seem pleased by what I did."
    else:
        "Well, it don't seem to change anything. Perhaps should I try again later."
    [...]


screen questLog:
    vbox:
        text "Next to do for [charA] : [charAQuest.actualStage]/[charAQuest.numberStages] [charAQuest.showStage]"
        if not charAQuest.iterationLeft == 0:
            text "Still [charAQuest.iterationLeft] times."
        text "Next to do for [charB] : [charBQuest._actualStage]/[charBQuest.numberStages] [charBQuest.showStage]"
        if not charBQuest.iterationLeft == 0:
            text "Still [charBQuest.iterationLeft] times."

label after_load:
    # First ensure that all the stages are present
    if charAQuest.numberStages == 2:
        # Add the new stage from release 0.2
        $ charAQuest.addStage( "what to do on third stage" )
    if charAQuest.numberStages == 3:
        # Add the new stage from release 0.3
        # Three iterations for this one.
        $ charAQuest.addStage( "what to do on fourth stage", 3 )

    if charBQuest.numberStages == 2:
        # Add the new stage from release 0.4
        $ charBQuest.addStage( "what to do on third stage" )

    # Then only update what have changed.
    # Pass the stage from 2 iteration to 3.
    $ charAQuest.updateStage( 1, "what to do on second stage", 3 )
This time I added some comment and a example covering more things.


But from what I see the system of "Stages" is linear, you have to pass one to show the next and you can't have several enabled at once, that's right?
Right, and honestly it should always be like this. If you need to enable more that one stage at once for the same quest, then you failed in the design of the game.

Imagine that you have to climbs stairs. But at each level you need to prove that you have performed a task before behing able to go to the next stage.
You can do it all in the same quest :
  • climb to the level 1 ;
  • perform this task ;
  • climb to the level 2 ;
  • perform that task.
And effectively, if the task can be perform while climbing it can seem a good idea to do both at the same time.

But it's better to separate this in two different tasks :
[Stairway climbing]
  • climb to the level 1 ;
  • climb to the level 2.
[Stairway challenges]
  • perform this task ;
  • perform that task.

The player will still be able to do both at the same time if he want/can. But for you it will be easier to deal with this, and for the player to understand what he have to do.
Code:
label stairwayLevel1:
    if climbing.actualStage < 1:
       "You haven't reach the level yet."
   elif challenges.actualStage < 1:
       "You still have something to do before you can continue."
   else:
      "Ready to climb for the next level ?"
      "Note that this time you'll have to do that to continue further."
{/code]
 

Porcus Dev

Engaged Member
Game Developer
Oct 12, 2017
2,582
4,705
it's the "wrote in the fly" effect, sorry. I usually handle the exception right in the code.




You can have a "that's all for now" last stage. With the help of a method to update a stage it works fine.




Yes, it's possible. It need few changes (including those implied by what is said above):
Python:
init python:

    class Quest( renpy.python.RevertableObject ):

        def __init__( self, char ):
            self._char = char
            self._stages = []
            self._iterationsLeft = []
            self._actualStage = 0

        # Return the text for the actual stage.
        @property
        def showStage( self ):
            if self._actualStage > len( self._stages ):
                return "Error, stage not defined."
            return self._stages[self._actualStage][0]

        # Return the (human understandable) number of the actual stage.
        @property
        def actualStage( self ):
            return self._actualStage + 1

        # Return the number of iteration left for the actual stage.
        # Not include to /showStage/ to let you style this like you want.
        @property
        def iterationLeft( self ):
            if self._actualStage > len( self._stages ):
                return 0    # Expect a number.
            return self._iterationsLeft[self._actualStage]

        # Add a new stage, with an optional number of iteration for it.
        def addStage( self, task, iteration=1 ):
            self._stages.append( ( task, iteration ) )
            self._iterationsLeft.append( iteration )

        # Stage completed, go to next one.
        def nextStage( self ):
            # Do nothing if the quest is seen as finished.
            if self.isFinished is True: return
            # An interation is done for this stage.
            self._iterationsLeft[self._actualStage] -= 1
            # Go to next stage only if at least one left to do.
            if self._iterationsLeft[self._actualStage] == 0:
                self._actualStage += 1

        # Reset the whole quest.
        def reset( self ):
            self._actualStage = 0
            self._iterationsLeft = []
            for s in self._stages:
                self._iterationsLeft.append( s[1] )

        # Return the number of stages for the quest.
        def numberStages( self ):
            return len( self._stages )

        # Update a stage anywhere in the quest.
        def updateStage( self, stage, task, iteration=1 ):
            # stage don't exist. Reminded, list index start at 0.
            if len( self._stages ) =< stage : return
            # No negative stage please.
            if stage < 0: return
            # Ensure that you don't reset the number of iterations already done.
            iterationDone = self._stages[stage][1] - self._iterationsLeft[stage]
            self._stages[stage] = ( task, iteration )
            self._iterationsLeft[stage] = iteration - iterationDone
            #  In case the update decrease the number of iteration and the
            # player already did more than now needed. Sorry for him, he'll have
            # to do one interation anyway.
            if self._iterationsLeft[stage] < 1: self._iterationsLeft[stage] = 1

        # Update the last stage.
        def updateLastStage( self, task, iteration=1 ):
            self.updateStage( len( self._stages - 1 ), task, iterarion )

        #  Return True if the quest is finished. Here, finished mean that the
        # player have reach the last stage actually defined. So it's more a
        # "finish, at least for now" meaning.
        def isFinished( self ):
            return ( len( self._stages ) == self._actualStage + 1 ) and self._iterationsLeft[stage] == 0



label start:
    python:
        $ charAQuest = Quest( "charA name" )
        # A single iteration for this stage, value is implicit.
        $ charAQuest.addStage( "what to do on first stage" )
        #  This stage will have to be done twice, you need to explicitly give
        # the value.
        $ charAQuest.addStage( "what to do on second stage", 2 )
        $ charBQuest = Quest( "charB name" )
        $ charBQuest.addStage( "what to do on first stage" )
        $ charBQuest.addStage( "what to do on second stage" )

    [start you game]

label room:
    if charAQuest.actualStage == 1:
        jump questStage2

    [...]

label questStage2:
    [...]

    $ charAQuest.nextStage()
    if charAQuest.iterationLeft == 0:
        "Apparently this time she seem pleased by what I did."
    else:
        "Well, it don't seem to change anything. Perhaps should I try again later."
    [...]


screen questLog:
    vbox:
        text "Next to do for [charA] : [charAQuest.actualStage]/[charAQuest.numberStages] [charAQuest.showStage]"
        if not charAQuest.iterationLeft == 0:
            text "Still [charAQuest.iterationLeft] times."
        text "Next to do for [charB] : [charBQuest._actualStage]/[charBQuest.numberStages] [charBQuest.showStage]"
        if not charBQuest.iterationLeft == 0:
            text "Still [charBQuest.iterationLeft] times."

label after_load:
    # First ensure that all the stages are present
    if charAQuest.numberStages == 2:
        # Add the new stage from release 0.2
        $ charAQuest.addStage( "what to do on third stage" )
    if charAQuest.numberStages == 3:
        # Add the new stage from release 0.3
        # Three iterations for this one.
        $ charAQuest.addStage( "what to do on fourth stage", 3 )

    if charBQuest.numberStages == 2:
        # Add the new stage from release 0.4
        $ charBQuest.addStage( "what to do on third stage" )

    # Then only update what have changed.
    # Pass the stage from 2 iteration to 3.
    $ charAQuest.updateStage( 1, "what to do on second stage", 3 )
This time I added some comment and a example covering more things.




Right, and honestly it should always be like this. If you need to enable more that one stage at once for the same quest, then you failed in the design of the game.

Imagine that you have to climbs stairs. But at each level you need to prove that you have performed a task before behing able to go to the next stage.
You can do it all in the same quest :
  • climb to the level 1 ;
  • perform this task ;
  • climb to the level 2 ;
  • perform that task.
And effectively, if the task can be perform while climbing it can seem a good idea to do both at the same time.

But it's better to separate this in two different tasks :
[Stairway climbing]
  • climb to the level 1 ;
  • climb to the level 2.
[Stairway challenges]
  • perform this task ;
  • perform that task.

The player will still be able to do both at the same time if he want/can. But for you it will be easier to deal with this, and for the player to understand what he have to do.
Code:
label stairwayLevel1:
    if climbing.actualStage < 1:
       "You haven't reach the level yet."
   elif challenges.actualStage < 1:
       "You still have something to do before you can continue."
   else:
      "Ready to climb for the next level ?"
      "Note that this time you'll have to do that to continue further."
{/code]
Awesome! :eek::D
I'll try it tonight, thank you very much! :love:
 

Porcus Dev

Engaged Member
Game Developer
Oct 12, 2017
2,582
4,705
Hi anne O'nymous,

I'm sorry to bother you again :p

Could you confirm if these two lines are okay?
Lines 14 and 27
Code:
if self._actualStage > len( self._stages ):
Or would it have to be like this?:
Code:
if self._actualStage >= len( self._stages ):
Thanks in advance.


BTW, I've incorporated this into the game in the version I just published, and although I had a little trouble understanding it at first, it's working quite well, it was what I was looking for... Thank you very much! (y)
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,979
16,236
Could you confirm if these two lines are okay?
Lines 14 and 27
Code:
if self._actualStage > len( self._stages ):
Or would it have to be like this?:
Code:
if self._actualStage >= len( self._stages ):
Er... Alright, after looking at it, I messed a little. I was too focused on the fact that there can have more than one iteration of a stage, and did wrong with the end of the quest (and apparently didn't tested it). So, you're right, it should have been >=, but isFinished was also wrong.

Here's a corrected version of the code. It do exactly the same thing and works exactly in the same way. Except that this time it handle correctly the end of the quest.
Python:
    class Quest( renpy.python.RevertableObject ):

        def __init__( self, char ):
            self._char = char
            self._stages = []
            self._iterationsLeft = []
            self._actualStage = 0

        @property
        def showStage( self ):
            # Let /isFinished/ deal with this, it's better.
            if self.isFinished() is True: return "The quest is finished."
            return self._stages[self._actualStage][0]

        @property
        def actualStage( self ):
            return self._actualStage + 1

        @property
        def iterationLeft( self ):
            # Let /isFinished/ deal with this, it's better.
            if self.isFinished() is True: return 0
            return self._iterationsLeft[self._actualStage]

        def addStage( self, task, iteration=1 ):
            self._stages.append( ( task, iteration ) )
            self._iterationsLeft.append( iteration )

        def nextStage( self ):
            if self.isFinished() is True: return
            self._iterationsLeft[self._actualStage] -= 1
            if self._iterationsLeft[self._actualStage] == 0:
                self._actualStage += 1

        def reset( self ):
            self._actualStage = 0
            self._iterationsLeft = []
            for s in self._stages:
                self._iterationsLeft.append( s[1] )

        def numberStages( self ):
            return len( self._stages )

        def updateStage( self, stage, task, iteration=1 ):
            if len( self._stages ) <= stage : return
            if stage < 0: return
            iterationDone = self._stages[stage][1] - self._iterationsLeft[stage]
            self._stages[stage] = ( task, iteration )
            self._iterationsLeft[stage] = iteration - iterationDone
            if self._iterationsLeft[stage] < 1: self._iterationsLeft[stage] = 1

        def updateLastStage( self, task, iteration=1 ):
            self.updateStage( len( self._stages - 1 ), task, iterarion )

        def isFinished( self ):
            # Still at least a stage, so it can't be ended.
            if self._actualStage + 1 < len( self._stages ): return False
            # Above the last stage, so obviously finished.
            if self._actualStage + 1 > len( self._stages ): return True
            # Else, it will depend if there's still an iteration to do or not.
            return self._iterationsLeft[self._actualStage] == 0
 

Porcus Dev

Engaged Member
Game Developer
Oct 12, 2017
2,582
4,705
Er... Alright, after looking at it, I messed a little. I was too focused on the fact that there can have more than one iteration of a stage, and did wrong with the end of the quest (and apparently didn't tested it). So, you're right, it should have been >=, but isFinished was also wrong.

Here's a corrected version of the code. It do exactly the same thing and works exactly in the same way. Except that this time it handle correctly the end of the quest.
Python:
    class Quest( renpy.python.RevertableObject ):

        def __init__( self, char ):
            self._char = char
            self._stages = []
            self._iterationsLeft = []
            self._actualStage = 0

        @property
        def showStage( self ):
            # Let /isFinished/ deal with this, it's better.
            if self.isFinished() is True: return "The quest is finished."
            return self._stages[self._actualStage][0]

        @property
        def actualStage( self ):
            return self._actualStage + 1

        @property
        def iterationLeft( self ):
            # Let /isFinished/ deal with this, it's better.
            if self.isFinished() is True: return 0
            return self._iterationsLeft[self._actualStage]

        def addStage( self, task, iteration=1 ):
            self._stages.append( ( task, iteration ) )
            self._iterationsLeft.append( iteration )

        def nextStage( self ):
            if self.isFinished() is True: return
            self._iterationsLeft[self._actualStage] -= 1
            if self._iterationsLeft[self._actualStage] == 0:
                self._actualStage += 1

        def reset( self ):
            self._actualStage = 0
            self._iterationsLeft = []
            for s in self._stages:
                self._iterationsLeft.append( s[1] )

        def numberStages( self ):
            return len( self._stages )

        def updateStage( self, stage, task, iteration=1 ):
            if len( self._stages ) <= stage : return
            if stage < 0: return
            iterationDone = self._stages[stage][1] - self._iterationsLeft[stage]
            self._stages[stage] = ( task, iteration )
            self._iterationsLeft[stage] = iteration - iterationDone
            if self._iterationsLeft[stage] < 1: self._iterationsLeft[stage] = 1

        def updateLastStage( self, task, iteration=1 ):
            self.updateStage( len( self._stages - 1 ), task, iterarion )

        def isFinished( self ):
            # Still at least a stage, so it can't be ended.
            if self._actualStage + 1 < len( self._stages ): return False
            # Above the last stage, so obviously finished.
            if self._actualStage + 1 > len( self._stages ): return True
            # Else, it will depend if there's still an iteration to do or not.
            return self._iterationsLeft[self._actualStage] == 0
Hi anne O'nymous,

Sorry for not answering before, I have been very busy checking and fixing game errors o_O

But I have incorporated the changes of your new code, and WOW!, thank you very much" The "isFinished" has done miracles, hahaha
It was one of the big problems I had, if I didn't control the progress of the quests well, game crashes, great job, thank you! :love:


I wanted to ask you if you can help me with a new problem I've encountered with this system, and it's a bit strange...
I started the game from scratch and tested the evolution of the quest system every time one was added, and everything worked perfectly; but suddenly, with one of the characters I have assigned quest, it got stuck in one, and doesn't move from there, but it's strange... with the tests I've done, this is what I get:

I've defined it as: charOlivia.Quest = Quest( "Olivia" )
Stage 1 worked well
When I tried to advance from "Stage", I saw that the same text appeared on the screen, it didn't seem to advance.

This is the exact command I used for Stage 1:
$ charOliviaQuest.addStage(_( "Visita el piso de Olivia para saber más sobre sus encuentros con Scott." ))
(It worked)

These are the commands I used for Stage 2:
$ charOliviaQuest.addStage(_( "Puedes seguir visitando a Olivia para pasar buenos momentos (NOTA: Cada 3 días)." ))
$ charOliviaQuest.nextStage()

And here's the curious thing...
If through the console I see the value "charOliviaQuest.actualStage", is 2, it's correct!
But if I look at the value "charOliviaQuest.showStage" the text of Stage 1 continues to appear; although not exactly...
Appears: " u'Visita el piso de Olivia para saber m\xe1s sobre sus encuentros con Scott.'

Could it be something to do with accents? Or is it normal for renpy to store them like this? (the word "m\xe1s" would have to be "más")


THANKS IN ADVANCE!!! :giggle:
 

Porcus Dev

Engaged Member
Game Developer
Oct 12, 2017
2,582
4,705
Oops, I forgot to comment that, using a save file almost at the end of the game, and with the commands I put in "after_load", it WORKS! And the commands are the same (so it won't be a matter of accents)

And I also forgot to comment that if I continue, through console, using the command "charOliviaQuest.nextStage()" the numeric counter continues to advance without problems, the only thing that doesn't change is the stored text.

Thanks.
 

Porcus Dev

Engaged Member
Game Developer
Oct 12, 2017
2,582
4,705
And more information... even by resetting "charOliviaQuest.reset()", the same text is still stored :eek:o_O
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,979
16,236
I've defined it as: charOlivia.Quest = Quest( "Olivia" )
[...]
$ charOliviaQuest.addStage(_( "Visita el piso de Olivia para saber más sobre sus encuentros con Scott." ))
(It worked)
Er... It shouldn't works... Look at what you wrote.
You create the charOlivia.Quest, but add a stage to charOliviaQuest. In one case it's the attribute named "Quest" of the object named "charOlivia", and in the second it's the variable named "charOliviaQuest".

I tested it with a consistent naming and it works fine. So the typo error is probably in the game, and not in your message. You should have used one syntax to create the quest, and in your later test with after_load, but the other to add the stage 2 and the reset, and that what show you it as an inconstancy.


Appears: " u'Visita el piso de Olivia para saber m\xe1s sobre sus encuentros con Scott.'

Could it be something to do with accents? Or is it normal for renpy to store them like this? (the word "m\xe1s" would have to be "más")
It is and it isn't normal. Ren'py encode the strings in , and \x1e is (I assume) the " " representation of the UTF-8 character "á". Normally Ren'py should always see and show it directly as "á", but I discovered when working on my variable viewer that it's not always the case, whatever the Unicode characters.
But it doesn't affect what will be display as dialog.


And I also forgot to comment that if I continue, through console, using the command "charOliviaQuest.nextStage()" the numeric counter continues to advance without problems, the only thing that doesn't change is the stored text.
Be cautious when you use the console to test things like this. Due to the way Ren'py optimize things, the console don't always reflect the exact value of the variables. When it's just pure test, it's better to use text substitution ; things like :
Code:
    "actual [charOliviaQuest._actualStage] [charOliviaQuest.showStage]"
    $ charOliviaQuest.nextStage()
    "actual [charOliviaQuest._actualStage] [charOliviaQuest.showStage]"
If the optimization make it that the Python line is coupled with the last dialog line, the console will display the value before (so stage 1 instead of stage 2). But the dialog line will show the right value.
 
  • Like
Reactions: Porcus Dev

Porcus Dev

Engaged Member
Game Developer
Oct 12, 2017
2,582
4,705
Er... It shouldn't works... Look at what you wrote.
You create the charOlivia.Quest, but add a stage to charOliviaQuest. In one case it's the attribute named "Quest" of the object named "charOlivia", and in the second it's the variable named "charOliviaQuest".

I tested it with a consistent naming and it works fine. So the typo error is probably in the game, and not in your message. You should have used one syntax to create the quest, and in your later test with after_load, but the other to add the stage 2 and the reset, and that what show you it as an inconstancy.




It is and it isn't normal. Ren'py encode the strings in , and \x1e is (I assume) the " " representation of the UTF-8 character "á". Normally Ren'py should always see and show it directly as "á", but I discovered when working on my variable viewer that it's not always the case, whatever the Unicode characters.
But it doesn't affect what will be display as dialog.




Be cautious when you use the console to test things like this. Due to the way Ren'py optimize things, the console don't always reflect the exact value of the variables. When it's just pure test, it's better to use text substitution ; things like :
Code:
    "actual [charOliviaQuest._actualStage] [charOliviaQuest.showStage]"
    $ charOliviaQuest.nextStage()
    "actual [charOliviaQuest._actualStage] [charOliviaQuest.showStage]"
If the optimization make it that the Python line is coupled with the last dialog line, the console will display the value before (so stage 1 instead of stage 2). But the dialog line will show the right value.
Thanks as always!

But I made the writing mistake in the forum, not in the game...

In the game I have:
Code:
default charOliviaQuest = Quest( "Olivia" )
And if I test it through the console as you've recommended, I get this:
Code:
"actual [charOliviaQuest._actualStage] [charOliviaQuest.showStage]"
actual 0 Visita el piso de Olivia para saber más sobre sus encuentros con Scott.

$ charOliviaQuest.nextStage()
"actual [charOliviaQuest._actualStage] [charOliviaQuest.showStage]"
actual 1 Visita el piso de Olivia para saber más sobre sus encuentros con Scott.

......
$ charOliviaQuest.nextStage()
"actual [charOliviaQuest._actualStage] [charOliviaQuest.showStage]"
actual 4 Visita el piso de Olivia para saber más sobre sus encuentros con Scott.

The numbering changes correctly, the text doesn't... I'm going to keep doing tests...


Thanks and sorry for the inconvenience :confused:
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,979
16,236
The numbering changes correctly, the text doesn't... I'm going to keep doing tests...
Er... That's not normal (but you already know that). I'll look from my side and see if I can reproduce this behavior.
 
  • Like
Reactions: Porcus Dev

Porcus Dev

Engaged Member
Game Developer
Oct 12, 2017
2,582
4,705
Er... That's not normal (but you already know that). I'll look from my side and see if I can reproduce this behavior.
No, it's not normal... and it's working well for me with the other characters (2 before Olivia), and now I've moved on to try another one (Alexia)...

... I think your code works perfectly... Do you think it could have been a very specific random problem of the game? I started it from the beginning without doing anything strange... but as much as I review and try things, Olivia doesn't work well, but the others work like a charm, and the code used is the same... :unsure:

... I think I'm going to start again from scratch just to see if the same behavior is repeated or if this time it works well.

The only thing that worries me is... if this strange error can be repeated, and the worst thing, that I don't know how to solve it, since although I assign another "Quest" with another name, it does not pay attention to me :cry:


Thank you anne O'nymous!
 

Porcus Dev

Engaged Member
Game Developer
Oct 12, 2017
2,582
4,705
anne O'nymous,

I've started the game again... and this time, it WORKS WELL!!!

Oh, God, I don't know what happened... maybe I did something on the console that I don't remember now, I don't know, the truth is that I tried to play normally as any player would do just to see how it worked, but it's clear that something happened.

Well, on the one hand I'm glad it works... on the other hand, I'm sorry to have bothered you.


Thank you for your patience :giggle:
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,979
16,236
The only thing that worries me is... if this strange error can be repeated, and the worst thing, that I don't know how to solve it, since although I assign another "Quest" with another name, it does not pay attention to me :cry:
The fact that it don't happened when I tried with your lines make me tend for a subtle syntax error in your code. Ren'py and Python are robust, but they also are capricious when they want.


Oh, God, I don't know what happened...
If it can reassure you, it happen to me sometimes, and in fact to everyone.


Thank you for your patience :giggle:
Don't worry, it's helping you, or using my day off to paint my corridor... So I'm glad you gave me an excuse :D
 
  • Haha
Reactions: Porcus Dev