Ren'Py Combat system

Lilou_5

New Member
May 4, 2020
10
2
I found this code but it has some errors.

You don't have permission to view the spoiler content. Log in or register now.

When I press it to start, the screen gets stuck.

I move parts of the code to a new file and then it lets you play, but when I start to play again it only lets me play the end of the battle.

and also the "return" always ends the game.

Is there any way to fix this code or do I have to look for another one? And where do you recommend?
 
Apr 24, 2020
192
257
I couldn't really try the code quickly without the formatting.

As for why the game always ends, you need to use call instead of jump.

call adds the label to the stack, whereas jump resets it (or however you explain it). But don't just replace all jump with call since there certainly is a limit to the stack size.

In short, your game ends because there is nothing to return to.
 
Last edited:
  • Like
Reactions: Lilou_5

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,957
16,191
Alright, point one, please when you include code in your message, use the [code] and [/code] tags to format it. It will take in count the indentation and put the code inside a scrolling window to limit the size of the message. When it's Ren'py or Python code, this is mandatory, because a change in the indentation of the code imply a change in the code itself.

Secondly, hm, what do you mean by "the screen gets stuck". I tried it at home and I failed to make it fail ; or if you prefer it worked perfectly whatever I did, and when it didn't it was for the reason I through and not something that I would describe as a screen getting stuck.

Thirdly, it's natural that the end the game. You can see this statement has having two behavior.
The first, and most used, one is to permit to return to the calling point. A label is ed, which create a derivation of the original game flow. Then when you want to return to the said original game flow, you use the return statement.
Ren'py keep track of the calling point (and so where it need to return) by using a stack, and you can know the size of this stack with the function.
But there's an implicit second behavior. Part of the core of Ren'py... is wrote by using the Ren'py script language. The game don't magically reach the start label, but in fact call it. Therefore, when the size of the stack is null, a return statement end the game ; it make it return inside the part you where before hitting the "start" button.

Python:
label start:
    # Get the size/depth of the calling stack
    $ stack = renpy.call_stack_depth()
    # "[variable]" make Ren'py display the content of this variable. It's
    # a text interpolation.
    # "[[" is used to escape the "[" to prevent the behavior explained
    # above. And finally, the behavior of "]" depend if an interpolation
    # have been started (in which case it end it) or not (in which case
    # the character will be displayed).
    # Therefore, "[[[stack]]" mean: display the value of the /stack/
    # variable, between brackets.
    "[[[stack]] Hey, how yo..."
    call calledLabel
    $ stack = renpy.call_stack_depth()
    "[[[stack]] As I was saying, how you doing ?"
    "END"
    # The size of the stack is 0, so the game end.
    return

label calledLabel:
    $ stack = renpy.call_stack_depth()
    "[[[stack]] What ? Don't go away like this !"
    call anotherLabel
    $ stack = renpy.call_stack_depth()
    "[[[stack]] Don't be afraid, come closer."
    # The size of the stack is NOT 0, so you return to the
    # calling point.
    return

label anotherLabel:
    $ stack = renpy.call_stack_depth()
    "[[[stack]] Come back please !"
    return


Now, as for the questions that don't regard the problem you encountered, what to say ?

The code is really minimalist. It give a global idea about how you can do it, but nothing more.
The principle is simple :
  1. Init the values ;
  2. Display the combat user interface ;
  3. Ask the player for his action ;
  4. Compute the result of this action ;
  5. Update the values depending of this result ;
  6. Decide the action of the opponent ;
  7. Compute the result of this action :
  8. Update the values depending of this result ;
  9. Loop to 3 if both parties are still alive.
But as I said, in this case it's a minimalist version of this principle. The player is given only two choices, they are displayed by using a menu, and the opponent have only one possible action.

Something more evolved can be :
Note: WARNING, code wrote on the fly.
It should works, but I don't have the possibility to test it right now, so there's perhaps some typo or small errors.
Python:
label start:
    "Get ready..."
    "Fight !"
    call combatInit
    "The winner is [_return]"
    "END"
    return

label combatInit:
    call MCInitCombat
    # This let you have different opponents. Just call the
    # right label depending of which one is in this combat.
    call BadGuyInitCombat
    # You can combinate /call/ and /jump/ without problem.
    # A /call/ is ended only when a /return/ statement is
    # proceeded, not necessarily at the end of a label.
    jump combatMainLoop

label MCInitCombat:
    $ MC_HP = 30
    $ MC_armor = 1
    $ MC_weapon = "sword"
    $ MC_STRONG = 2
    # [Here, init whatever other variables you have]

    # The initialization is finished for the MC, return
    # to /combatInit/ to now initialize the opponent.
    return

label BadGuyInitCombat:
    # Always use the save variables for the opponent,
    # just change the values depend of which one it is.
    $ OP_HP = 25
    $ OP_armor = 0
    $ OP_weapon = "dagger"
    $ OP_STRONG = 0
    # [Here, init whatever other variables you have]

    # The initialization is finished for the opponent, return
    # to /combatInit/ to now start the combat.
    return

# Just as example of what I said above regarding the
# need to keep the same variables.
label BossInitCombat:
    # It's a boss, he have more HP,
    $ OP_HP = 45
    # a better armor,
    $ OP_armor = 3
    # and a better weapon.
    $ OP_weapon = "2 hands sword"
    $ OP_STRONG = 1
    # [Here, init whatever other variables you have]
    return

label combatMainLoop:
    # Display the combat interface, but do it by calling
    # the screen. It mean that Ren'py will stop the processing
    # of the game, until the player it a button on the screen.
    call screen combatUI
    # /_return/ is a special variable that store the value
    # returned by the label or, in this case, the screen.
    # Storing it in another variable is always a good idea,
    # because the value of /_return/ is reset each time
    # you return from something.
    # Therefore, it let you call label or screen as part of
    # the process.
    $ result = _return
    if result == "heal":
        # The player choose to heal himself.
        $ MC_HP += 10
    elif result == "hit":
        # The player choose a regular hit.
        # Call the computing label, saying, through the
        # parameter, that it's a regular hit, gave by the MC
        # to the Opponent.
        call isHit( False, "MC", "OP" )
    elif result == "strong":
        # The player choose a strong hit.
        # Call the computing label, saying, through the
        # parameter, that it's a strong hit, gave by the MC
        # to the Opponent.
        call isHit( True, "MC", "OP" )
        $ MC_STRONG -= 1

    # Decide what action will be done by the opponent
    $ rdm = renpy.random.randint(0, 20 )
    # 5 chance to have a strong hit. If there's a strong
    # hit possible. Else it's a regular one.
    if rdm > 15 and OP_STRONG > 0:
        call isHit( True, "OP", "MC" )
        $ OP_STRONG -= 1
    else:
        call isHit( False, "OP", "MC" )

    # If both still have HP, next loop.
    if MC_HP > 0 and OP_HP > 0:
        jump combatMainLoop
    # Else if the MC is still alive, he won.
    elif MC_HP > 0:
        return "MC"
    # Else it's the opponent who won.
    else:
        return "Opponent"


# A called label can have parameters, which permit to
# have a single label that can handle more than a single
# case. The values will be stored in the order they are
# passed.
# Here, first parameter in /strong/, second in /hitBy/,
# third in /target/.
call isHIT( strong, hitBy, target )
    # Get the weapon that hit.
    $ weapon = getattr( store, hitBy + "_weapon" )
    # Compute its damages
    if weapon == "dagger":
        $ damage = 2
    elif weapon == "sword":
        $ damage = 5
    elif weapon == "2 hands sword":
        $ damage = 8
    else:
        # It's a bug, do no damages.
        $ damage = 0

   # It's a strong attack, it double the damages
   if strong is True:
       $ damage *= 2

   # Compute the new HP
   $ HP = getattr( store, target + "_HP" )
   if HP < 0:
       $ HP = 0
   # And assign them
   $ setattr( store, target + "_HP", HP )

   # Finished, return to the combat loop.
   return


screen combatUI()

    frame:
        # MC HP in the top left corner.
        text "MC: [MC_HP]":
            xalign 0.0 yalign 0.0
        # Opponent HP in the top right corner.
        text "Opponent: [OP_HP]":
            xalign 1.0 yalign 0.0

        # MC possible actions in the middle
        vbox:
            xalign 0.5 yalign 0.5
            textbutton "Take a potion":
                # The action is to return from the screen,
                # while returning the value "heal".
                action Return( "heal" )
            textbutton "Regular hit":
                action Return( "hit" )
            textbutton "Strong hit":
                action Return( "strong" )
                # The button will be sensitive (and so clickable)
                # Only if the MC still have the possibility to do
                # a strong hit.
                sensitive MC_STRONG > 0
Featured in this code : screen action, and also setattr and getattr Python instruction, for which you'll find explanations here, with a complement two messages after it.
 
Last edited:

Lilou_5

New Member
May 4, 2020
10
2
Thank you very much.
I'll start practicing.
Note: WARNING, code wrote on the fly.
It should works, but I don't have the possibility to test it right now, so there's perhaps some typo or small errors.
This error comes out.

Code:
# It's a strong attack, it double the damages
   if strong is True:
       $ damage *= 2
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,957
16,191
This error comes out.

Code:
# It's a strong attack, it double the damages
   if strong is True:
       $ damage *= 2
Oops, it's a problem of indentation, you've to add 1 space before the "if". There's also a problem with this label, it should start by :
Code:
label isHIT( strong, hitBy, target ):
I forgot the ":" in the code and used "call" instead of "label" :(


I'll try to find time to test this tomorrow and post a correct version if there's more errors.
 
  • Like
Reactions: Lilou_5

Lilou_5

New Member
May 4, 2020
10
2
Code:
label start:
    "Get ready..."
    "Fight !"
    call combatInit
    "The winner is [_return]"
    "END"
    return

label combatInit:
    call MCInitCombat
    # This let you have different opponents. Just call the
    # right label depending of which one is in this combat.
    call BadGuyInitCombat
    # You can combinate /call/ and /jump/ without problem.
    # A /call/ is ended only when a /return/ statement is
    # proceeded, not necessarily at the end of a label.
    jump combatMainLoop

label MCInitCombat:
    $ MC_HP = 30
    $ MC_armor = 1
    $ MC_weapon = "sword"
    $ MC_STRONG = 2
    # [Here, init whatever other variables you have]

    # The initialization is finished for the MC, return
    # to /combatInit/ to now initialize the opponent.
    return

label BadGuyInitCombat:
    # Always use the save variables for the opponent,
    # just change the values depend of which one it is.
    $ OP_HP = 25
    $ OP_armor = 0
    $ OP_weapon = "dagger"
    $ OP_STRONG = 0
    # [Here, init whatever other variables you have]

    # The initialization is finished for the opponent, return
    # to /combatInit/ to now start the combat.
    return

# Just as example of what I said above regarding the
# need to keep the same variables.
label BossInitCombat:
    # It's a boss, he have more HP,
    $ OP_HP = 45
    # a better armor,
    $ OP_armor = 3
    # and a better weapon.
    $ OP_weapon = "2 hands sword"
    $ OP_STRONG = 1
    # [Here, init whatever other variables you have]
    return

label combatMainLoop:
    # Display the combat interface, but do it by calling
    # the screen. It mean that Ren'py will stop the processing
    # of the game, until the player it a button on the screen.
    call screen combatUI
    # /_return/ is a special variable that store the value
    # returned by the label or, in this case, the screen.
    # Storing it in another variable is always a good idea,
    # because the value of /_return/ is reset each time
    # you return from something.
    # Therefore, it let you call label or screen as part of
    # the process.
    $ result = _return
    if result == "heal":
        # The player choose to heal himself.
        $ MC_HP += 10
    elif result == "hit":
        # The player choose a regular hit.
        # Call the computing label, saying, through the
        # parameter, that it's a regular hit, gave by the MC
        # to the Opponent.
        call isHit( False, "MC", "OP" )
    elif result == "strong":
        # The player choose a strong hit.
        # Call the computing label, saying, through the
        # parameter, that it's a strong hit, gave by the MC
        # to the Opponent.
        call isHit( True, "MC", "OP" )
        $ MC_STRONG -= 1

    # Decide what action will be done by the opponent
    $ rdm = renpy.random.randint(0, 20 )
    # 5 chance to have a strong hit. If there's a strong
    # hit possible. Else it's a regular one.
    if rdm > 15 and OP_STRONG > 0:
        call isHit( True, "OP", "MC" )
        $ OP_STRONG -= 1
    else:
        call isHit( False, "OP", "MC" )

    # If both still have HP, next loop.
    if MC_HP > 0 and OP_HP > 0:
        jump combatMainLoop
    # Else if the MC is still alive, he won.
    elif MC_HP > 0:
        return "MC"
    # Else it's the opponent who won.
    else:
        return "Opponent"


# A called label can have parameters, which permit to
# have a single label that can handle more than a single
# case. The values will be stored in the order they are
# passed.
# Here, first parameter in /strong/, second in /hitBy/,
# third in /target/.
label isHit( strong, hitBy, target ):
    # Get the weapon that hit.
    $ weapon = getattr( store, hitBy + "_weapon" )
    # Compute its damages
    if weapon == "dagger":
        $ damage = 2
    elif weapon == "sword":
        $ damage = 5
    elif weapon == "2 hands sword":
        $ damage = 8
    else:
        # It's a bug, do no damages.
        $ damage = 0

   # It's a strong attack, it double the damages
    if strong is True:
       $ damage *= 2

   # Compute the new HP
    $ HP = getattr( store, target + "_HP" )
    if HP < 0:
       $ HP = 0
   # And assign them
    $ setattr( store, target + "_HP", HP )

   # Finished, return to the combat loop.
    return


screen combatUI():

    frame:
        # MC HP in the top left corner.
        text "MC: [MC_HP]":
            xalign 0.0 yalign 0.0
        # Opponent HP in the top right corner.
        text "Opponent: [OP_HP]":
            xalign 1.0 yalign 0.0

        # MC possible actions in the middle
        vbox:
            xalign 0.5 yalign 0.5
            textbutton "Take a potion":
                # The action is to return from the screen,
                # while returning the value "heal".
                action Return( "heal" )
            textbutton "Regular hit":
                action Return( "hit" )
            textbutton "Strong hit":
                action Return( "strong" )
                # The button will be sensitive (and so clickable)
                # Only if the MC still have the possibility to do
                # a strong hit.
                sensitive MC_STRONG > 0
Already solved some small errors, only now the player can only heal itself but their attacks do not lower life to the enemy and the enemy does not attack either.

One question if I wanted to put that when the player wins a fight, a scene comes up and the player wins money, would it be to put this code at the end?

Code:
if win:
    jump win

else:
    jump loss


label win:
    #event for winning.
    $ money += 200
    jump map

label loss:
    #event for losing.
    $ money -= 100
    jump map