Ren'Py [Solved]Need help with Python code

TempTales-Paul

Newbie
Game Developer
Nov 11, 2024
52
104
33
[edit :
Right after posting this, I had an idea and it solved my problem.
Sorry ^^]


Hi everyone,

A question for Python programmers.
I'm having trouble with some Python code for my Renpy game. In the game, the female character has outfits that are divided into tops, bottoms, and lingerie.

I'm trying to code a function that will modify the character's stats based on the outfit she's wearing.
The “wardrobe” system works very well. I can change and remove outfits without any problems. The stats vary correctly and it works in 90% of cases.
But I have one case (and so I imagine there could be others) where when I reset the outfits, there is a ghost +1 added to a stat.
This happens when I put on outfits that affect the same variable, one positively and one negatively.

In addition, it is important to note that the values that are modified go through another function that generates a visual effect when a stat is modified.
Furthermore, some stats have a clamp when they fall below 0.
I also tried to program the outfits (top and bottom) that are worn at the same time so that their stats are combined before being applied to the character.
Otherwise, if I applied one and then the other, the clamps could obviously create undesirable side effects. (For example, if the stat was 0, and I put on an outfit that removes 1 and then one that adds 2, I wanted it to be 0+(-1+2) and not 1-1+2, which would not give the same result with the clamp at 0).
And finally, the function is supposed to store the actual applied value (and not the theoretical value) to take the clamps into account.

Here is my code:

default top_current_effect = {}
default bottom_current_effect = {}

default top_effects = {
"blue_pull": {},
"red_pull": {"lust": -1, "slut": 1},
"green_pull": {"study": 1, "submission": -2},
"croptop_pink": {"lust": 2, "karma": -1},
"croptop_marine": {"lust": 2, "slut": -2},
"croptop_white": {"lust": 2, "study": -1},
"croptop_blue": {"lust": 1},
}

default bottom_effects = {
"pants_beige": {},
"pants_yellow": {"study": 1, "slut": -2},
"pants_white": {"submission": 1, "lust": -1},
"pants_blue": {"slut": -2, "karma": 1},
"short_blue": {"lust": 2},
"short_red": {"slut": 1},
"short_white": {"submission": 1},
}

init python:
def _apply_stat_change(stat, delta):
if delta == 0:
return 0
if stat == "study":
return update_study(delta)
elif stat == "lust":
return update_lust(delta)
elif stat == "submission":
return update_submission(delta)
elif stat == "karma":
return update_karma(delta)
else:
import renpy.store as S
cur = getattr(S, stat, 0)
setattr(S, stat, cur + delta)
return delta


# TOP + BOTTOM

def apply_top_bottom_effects(new_top, new_bottom):
global selected_top, selected_bottom
global top_current_effect, bottom_current_effect
changes = []

for stat, applied_value in top_current_effect.items():
if applied_value != 0:
removed = _apply_stat_change(stat, -applied_value)
changes.append(f"REMOVE TOP {stat}: {applied_value} (applied={removed})")
top_current_effect = {}

for stat, applied_value in bottom_current_effect.items():
if applied_value != 0:
removed = _apply_stat_change(stat, -applied_value)
changes.append(f"REMOVE BOTTOM {stat}: {applied_value} (applied={removed})")
bottom_current_effect = {}

eff_top = top_effects.get(new_top, {})
for stat, value in eff_top.items():
applied = _apply_stat_change(stat, value)
if applied != 0:
top_current_effect[stat] = applied
changes.append(f"APPLY TOP {stat}: {value} (applied={applied})")

eff_bottom = bottom_effects.get(new_bottom, {})
for stat, value in eff_bottom.items():
applied = _apply_stat_change(stat, value)
if applied != 0:
bottom_current_effect[stat] = applied
changes.append(f"APPLY BOTTOM {stat}: {value} (applied={applied})")

selected_top = new_top
selected_bottom = new_bottom
if changes:
renpy.notify("\n".join(changes))

#####Stats function



init python:
def update_karma(change):
global karma
before = karma
karma += change
applied = karma - before
renpy.hide("karmaup")
renpy.hide("karmadown")
if applied > 0:
renpy.show("karmaup", at_list=[move_up_and_disappear])
elif applied < 0:
renpy.show("karmadown", at_list=[move_down_and_disappear])
return applied


def update_submission(change):
global submission
old_val = submission
submission += change

if submission < 0:
submission = 0

applied = submission - old_val

renpy.hide("submissionup")
renpy.hide("submissiondown")

if applied > 0:
renpy.show("submissionup", at_list=[move_up_and_disappear])
elif applied < 0:
renpy.show("submissiondown", at_list=[move_down_and_disappear])

return applied

def update_lust(change):
global lust
before = lust
lust += change
if lust < 0:
lust = 0
applied = lust - before
renpy.hide("lustup")
renpy.hide("lustdown")
if applied > 0:
renpy.show("lustup", at_list=[move_up_and_disappear])
elif applied < 0:
renpy.show("lustdown", at_list=[move_down_and_disappear])
return applied

def update_study(change):
global study
before = study
study += change
if study < 0:
study = 0
applied = study - before
renpy.hide("studyup")
renpy.hide("studydown")
if applied > 0:
renpy.show("studyup", at_list=[move_up_and_disappear])
elif applied < 0:
renpy.show("studydown", at_list=[move_down_and_disappear])
return applied

-----------------------
The problem is when I do croptop_pink + pants_white (this works, I get the right values returned).
Then when I change the pants to pants_yellow for example but keep the top, the game creates a ghost +1 on the lust stat :/

I added notify to verify that the game is taking the correct values into account, and they are returning the correct values.

I'm far from being a Python expert (I'm a real noob). I rely heavily on AI for coding, but I have to admit that I can't find the problem here (and I've made quite a few versions of this function :/).
Anyway, if anyone can help me, that would be really cool.

Best regards and thank you for your time,
Paul
 
Last edited:

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
12,725
20,887
1,026
[edit :
Right after posting this, I had an idea and it solved my problem.
Sorry ^^]
Noted, but still there's people who will face a similar problem, find your post, and be frustrated to only find a question without the help they expected.
And there's few things that have to be addressed.


Python:
default top_current_effect = {}
default bottom_current_effect = {}

default top_effects = { [...] }

default bottom_effects = { [...] }
This is good, but you'll hit a wall the moment you'll have a clothing piece that cover both; there's croptop, so at one time there will be dresses.
It's better to group all the clothes in a single dict and have add a notion of used slots. Since you limit to top and bottom, a basic approach would be enough:
Python:
define allClothes = {
    "pull_red": {"lust": -1, "slut": 1},
    "pants_yellow": {"study": 1, "slut": -2},
    "dress_blue": { [whatever] },
    [...]
    }

define areTop = [ "pull_red", "dress_blue", [...] ]
define areBottom = [ "pants_yellow", "dress_blue", [...] ]

default currentEffects = {}
default currentlyWorn = []

init python:

    # I know that it don't apply the stats, it's just an usage demonstration.
    def wearIt( clothe ):
        if clothe in areTop :
            selected_top = clothe
        # NOT /elif/ here, because a clothe can be both.
        if clothe in areBottom:
            selected_bottom = clothe
Firstly, note that I used define instead of default.
The difference is important because you'll probably add some clothes in the future, or can be unsatisfied by the current stats and want to adjust the balance of your game by changing some.

default create a variable that will be included in the save file. This mean that the day you'll add a clothes, or update the stats, by default players who load a save file will not benefits from the addition or update. Either they'll need to restarts the game, or you'll have to rely on the "after_load" label to correct "areTop" and "areBottom" for them.
But define create a variable that will not be included in the save file. Since they are dict and list, this will apply even if you change one of the entry during the play. Therefore, all players will systematically benefits from any addition or update that you'll do.

Secondly, not that I replaced your "red_pull" by "pull_red". You use "croptop_[color]", "pants_[color]" and "short_[color]", but "[color]_pull". This lack of constancy is an open door to breaking errors.
You'll pass months, perhaps years, working on your game, and because of the lack of constancy, at one time you'll write "pull_[color]" or "[color]_pants". Ren'Py will not find it because it's not the correct name, and boom. By following the same structure for the clothe names, you radically limits the risk that this happen.

Side effect, this constancy can also help you identify the clothe without the need for additional lists. By example:
Python:
def wearIt( clothe ):
    if clothe.startswith( "pull_" ):
        selected_top = clothe
    elif clothe.startswith( "pants_" ):
        selected_bottom = clothe
    elif clothe.startswith( "dress_" ):
        selected_top = clothe
        selected_bottom = clothe
    else:
        raise Exception( "Something goes wrong, unknown clothing piece {}".format( clothe ) )
Also note that there's no global in my code.
We are in an init python block, so it's not needed. The variables defined in the game will be parts of the global scope for all code, whatever inline, a function or a method, that is defined in a rpy file, whatever if it's in an init python block or in a python block.


Python:
init python:
    def _apply_stat_change(stat, delta):
        [...]
        else:
            import renpy.store as S
            cur = getattr(S, stat, 0)
            setattr(S, stat, cur + delta)
This is purely useless over complicated code.
Once again we re in an init python block, therefore in a rpy file. Like all other variables, the "store" store is already available and don't need to be imported.
Python:
init python:
    def _apply_stat_change(stat, delta):
        [...]
        else:
            setattr(store, stat, getattr(store, stat, 0) + delta)
More globally, you don't need to have so many lines in your function:
Python:
    def _apply_stat_change(stat, delta):

        if not delta == 0:
            if hasattr( store, "update_{}".format( stat ) ):
                return getattr( store, "update_{}".format( stat ) )( delta )
            else:
                setattr(store, stat, getattr(store, stat, 0) + delta)
                return delta

        return 0  # case where /delta/ is 0
Functions and methods are variables that store code, therefore they can be addressed through hasattr(), getattr() and setattr() like any other variable.


Python:
    def apply_top_bottom_effects(new_top, new_bottom):
        [...]
Once again, in the future you can have clothes like dresses, and the code can not handle them.

Python:
    #  Only one parameter because players will change the clothes one by one, and 
    # they'll want to be able to compare the stats this finely.
    def apply_effects( new_clothe ):

        # remove the clothe(s) that conflict.
        for clothe in currentlyWorn:
            # A clothe using the /top/ slot will replace a clothe using the /top/ clothe.
            if new_clothe in areTop and clothe in areTop:
                removeClothe( clothe, "TOP" )
            #  A clothe using the /bottom/ slot will replace a clothe using the /bottom/ clothe.
            # Once again, /if/ and not /elif/ because a clothe can use both.
            if new_clothe in areBottom and clothe in areBottom:
                removeClothe( clothe, "BOTTOM" )

        effect = allClothes.get( new_clothe, {} )
        for stat, value in effect.items():
            applied = _apply_stat_change(stat, value)
            if applied != 0:
                currentEffects[stat] = applied

                if clothe in areTop and clothe in areBottom:
                    target = "ALL"
                elif clothe in areTop:
                    target = "TOP"
                else:
                    target = "BOTTOM"
                changes.append(f"APPLY {target} {stat}: {value} (applied={applied})")

        #  Add the clothe to the list of worn clothes.
        currentlyWorn.append( clothe )

        if changes:
            renpy.notify("\n".join(changes))


    #  Some times the player, or yourself, will possibly just want to remove a piece of clothing,
    # therefore this need to be a dedicated function.
    def removeClothe( clothe, slot ):

        effect = allClothes.get( clothe, {} )

        for stat, applied_value in effect.items:
            if applied_value != 0:
                removed = _apply_stat_change(stat, -applied_value)
                changes.append( f"REMOVE {slot} {stat}: {applied_value} (applied={removed})")

        # Remove the clothe
        currentlyWorn.remove( clothe )

Python:
init python:
    def update_karma(change):
        [...]

    def update_submission(change):
        [...]

    def update_lust(change):
        [...]

    def update_study(change):
        [...]
They are all the same (modulo "karma"), this can be a single function:
Python:
    def update_stats( stat, delta )
       
        value = getattr( store, stat )
        #  "karma" is the only stat that can be negative.
        if not stat == "karma" and value + delta < 0:
            setattr( store, stat, 0 )
            #  The stat return to 0, so you removed its current value.
            delta = -value
        else:
            setattr( store, stat, value + delta )

        renpy.hide( "{}up".format( stat ) )
        renpy.hide( "{}down".format( stat ) )
        if delta > 0:
            renpy.show( "{}up".format( stat ), at_list=[move_up_and_disappear])
        elif delta < 0:
            renpy.show( "{}down".format( stat ) , at_list=[move_down_and_disappear])
        return delta
This also imply that "_apply_stat_change" become totally useless. This new "update_stats" function do everything it does.

BUT there's still an issue with this code, the values are not computed in real time, but with each clothe change. Therefore if a stat is at 0, then the player wear a -1 bottom, the stat will stay at 0. If he later wear a +1 top, the stat will rise to +1, while it should stat at 0 (-1 + 1 = 0).
And it's more than surely the problem you encountered.

Therefore you need to keep track of the current scheme for each stats, and redo the whole computation every time a value change.

So, here's the final version, harmonized to take count of all changes, corrected to correctly compute the stats values, and without the debug parts:
/!\ Wrote in the fly, there's possibly some typo /!\
Python:
define allClothes = {
    "pull_red": {"lust": -1, "slut": 1},
    "pants_yellow": {"study": 1, "slut": -2},
    "dress_blue": { [whatever] },
    [...]
    }

define areTop = [ "pull_red", "dress_blue", [...] ]
define areBottom = [ "pants_yellow", "dress_blue", [...] ]

default currentlyWorn = []

#  You need two value by stats. The first one will be the current value, once the modifier are
# applied. And the second value is the default value without any modifier.
default lust = 0
default lustBase = 0
default submission = 0
default submissionBase = 0
[...]

init python:

    def wearClothe( new_clothe ):

        #  Remove the clothe(s) that conflict.
        for clothe in currentlyWorn:

            if new_clothe in areTop and clothe in areTop:
                removeClothe( clothe, "TOP" )

            if new_clothe in areBottom and clothe in areBottom:
                removeClothe( clothe, "BOTTOM" )

        #  This should also works:
        #for stat, value in allClothes.get( new_clothe, {} ).items():
        #  But this is better to understand:
        effect = allClothes.get( new_clothe, {} )

        #  Apply the stats for the new clothe.
        for stat, value in effect.items():

            #  Compute the new value for the stat.
            computeStats( stat )

            #  And you notify the player about the change.
            notifyChange( stat, value )

        #  Add the clothe to the list of worn clothes.
        currentlyWorn.append( clothe )


    #  Some times the player, or yourself, will possibly just want to remove a piece of clothing,
    # therefore this need to be a dedicated function.
    def removeClothe( clothe ):

        #  This should also works:
        #for stat, value in allClothes.get( clothe, {} ).items():
        #  But this is better to understand:
        effect = allClothes.get( clothe, {} )

        for stat, applied_value in effect.items:

            #  Compute the new value for the stat.
            computeStats( stat )

            #  And you notify the player.
            notifyChange( stat, value )
        
        # Remove the clothe from the list of worn clothes.
        currentlyWorn.remove( clothe )

                
    def computeStats( stat )
      
        #  Get the value for this stat when there's no clothe modifiers.
        value = getattr( store, "{}Base".format( stat ) )

        #  For each worn clothe... 
        for clothe in currentlyWorn:

            effect = allClothes.get( clothe, {} )

            # ...that change this effect...
            if stat in effect:

                # ...update the value.
                value += effect[stat]
    
        # Only now do you limit the change at 0, if it apply.
        if not stat == "karma" and value < 0:
            setattr( store, stat, 0 )
        else:
            setattr( store, stat, value )


    def notifyChange( stats, delta )

        renpy.hide( "{}up".format( stat ) )
        renpy.hide( "{}down".format( stat ) )

        if delta > 0:
            renpy.show( "{}up".format( stat ), at_list=[move_up_and_disappear])
        elif delta < 0:
            renpy.show( "{}down".format( stat ) , at_list=[move_down_and_disappear])


The problem is when I do croptop_pink + pants_white (this works, I get the right values returned).
Then when I change the pants to pants_yellow for example but keep the top, the game creates a ghost +1 on the lust stat :/
As I said above, it's probably a conflict because you compute the stat one change after the other, starting with the current value, that can't be negative.
You wear clothes starting naked:
  • 0 + 2 = 2
  • 2 - 1 = 1
You remove the same clothes, and so apply the opposite values to the current value:
  • 1 - 2 = -1 = 0
  • 0 + 1 = 1

If you compute the stat by applying all changes to the default value, then limit to 0, you'll never encounter this issue.
 

TempTales-Paul

Newbie
Game Developer
Nov 11, 2024
52
104
33
Thank you very much!
I had indeed found the problem, which was that the stats weren't adding up at the same time, and I had corrected it.
But the rest of your explanations are very interesting, and I will use them.
Thank you for your time :)
 
  • Like
Reactions: osanaiko