Seronis' Book of Tweaks, Tricks, and Toys (latest counts: 4,1,0)

A place for Ren'Py tutorials and reusable Ren'Py code.
Forum rules
Do not post questions here!

This forum is for example code you want to show other people. Ren'Py questions should be asked in the Ren'Py Questions and Announcements forum.
Post Reply
Message
Author
seronis
Newbie
Posts: 7
Joined: Fri Aug 05, 2022 9:57 pm
Deviantart: seronis
Github: seronis
itch: seronis
Contact:

Seronis' Book of Tweaks, Tricks, and Toys (latest counts: 4,1,0)

#1 Post by seronis »

Im not a professional but I see many people in the Ren'Py discord server having problems because they try to use a Cookbook item that is out of date. Figure that I might start posting some things that might be simple but will at least work with Ren'Py 8.1. I will post all the things in this thread as I get around to it and keep an Index in this main post to quickly go to the relevant sub topic.

Tweaks are small changes to the renpy starter project (or any project) for more usability.
Tricks are individual functions or some other small bit of code to simplify tasks
Toys are mini projects that are usable on their own or meant to show practical way of using features

* SERONIS' TWEAK INDEX (click to move to sub topic)

1. Opening the game menu will go to the last used submenu instead of always 'save'

2. Allow save/load menus to have any number of save slot pages instead of just 9

3. Alternate / replacement choice menu screen with timing and activation features.

4. Have the Quick Menu bar show up only while the mouse is in range, then auto hide.

SERONIS' TRICK INDEX

1. Using min() and max() to create a clamp() function to force a value into specific range.

SERONIS' TOY INDEX

(coming soon)


* Suggested Advice
I dont use renpys starter project because of all the annoying inter dependencies that make editing anything annoying if you dont want lots of side effects. I **very** highly recommend you to try out Feniks alternate starter project at:

https://github.com/shawna-p/EasyRenPyGui

It also reorganizes the basic file structure. Just try it out


* Contact Info
I will almost never be looking for replies here so if there are problems with anything find me on the renpy discord with username Nanashi or Seronis
Last edited by seronis on Fri May 12, 2023 11:06 am, edited 15 times in total.

seronis
Newbie
Posts: 7
Joined: Fri Aug 05, 2022 9:57 pm
Deviantart: seronis
Github: seronis
itch: seronis
Contact:

Re: Seronis' Book of Tweaks

#2 Post by seronis »

1. Opening the game menu will go to the last used submenu instead of always 'save'


In screens.rpy scroll down until you find the definitions for the save() and load() screens. Above the "screen save():" add the following short snippet:

Code: Select all

## Enables right click to return to the last used sub menu
default persistent.last_menu = "preferences"
default _game_menu_screen = "lastmenu_screen"

screen lastmenu_screen():
    on "show" action ShowMenu(persistent.last_menu)
By default when you press right click or the escape key renpy will call the ShowMenu() action with the value of _game_menu_screen as the target screen. You could change this value during gameplay if you wanted and renpy would load your newly chosen screen next time you right click or Escape. But any time ShowMenu() is used to display a screen renpy will roll the game back to before you showed the screen when leaving it. This means if you edit that value while the menu is open renpy will forget you edited it when you leave and next time will blindly go to the save screen again.

I fix this by creating a new menu screen to show that acts as a middleman and named it lastmenu_screen. When shown it immediately uses a new persistent variable to load your last used screen. The screens themselves need 2 lines added to each of them. You can add these lines immediately after the first line of the screen definitions so:

Code: Select all

screen save():
    on "replace" action SetVariable("persistent.last_menu", "save")
    on "show" action SetVariable("persistent.last_menu", "save")
    
screen load():
    on "replace" action SetVariable("persistent.last_menu", "load")
    on "show" action SetVariable("persistent.last_menu", "load")
    
screen history():
    on "replace" action SetVariable("persistent.last_menu", "history")
    on "show" action SetVariable("persistent.last_menu", "history")
    
screen preferences():
    on "replace" action SetVariable("persistent.last_menu", "preferences")
    on "show" action SetVariable("persistent.last_menu", "preferences")
just dont delete the rest of those screens by mistake when making the additions. if you have custom other screens they can be supported by adding the same two lines of code. just make sure the value you set is the exact matching spelling for the screen. typos will cause crashes here
Last edited by seronis on Mon May 08, 2023 2:58 pm, edited 2 times in total.

seronis
Newbie
Posts: 7
Joined: Fri Aug 05, 2022 9:57 pm
Deviantart: seronis
Github: seronis
itch: seronis
Contact:

Re: Seronis' Book of Tweaks

#3 Post by seronis »

2. Allow save/load menus to have any number of save slot pages instead of just 9

Renpys save/load system in the starter project has a page for Quick Saves, Auto Saves, and 9 numbered pages you can easily click on. Technically renpy allows you to access an infinite number (well 4 billion) of pages but the UI only provides clickable numbers for pages 1 - 9. The code that controls this is in the file_slots() screen in screens.rpy and looks similar to:

Code: Select all

                hbox xalign 0.5 spacing gui.page_spacing:

                    textbutton _("<") action FilePagePrevious()

                    if config.has_autosave:
                        textbutton _("{#auto_page}A") action FilePage("auto")

                    if config.has_quicksave:
                        textbutton _("{#quick_page}Q") action FilePage("quick")

                    for page in range(1, 10):
                        textbutton "[page]" action FilePage(page)

                    textbutton _(">") action FilePageNext()
Of course it isn't the whole screen but it is the portion that controls the clickable page buttons. The hbox ensures its centered horizontally and uses the gui.page_spacing configuration variable to set spacing to 0. This means there are zero dead pixels between the buttons and your mouse will always be considered hovering over the nearest page number button. Visually you still see space between them because the page_button style is pulling in the padding property. You can edit that value in gui.rpy if you want. I dislike having to switch between files so I tend to delete the gui.some_setting variables and just put the values directly in the screen or the screens style definitions nearby.

Now our goal is to not be restricted to just 9 numbered pages. Since the for statement in the above code uses hard numbers we will have to switch to using variables so that it will dynamically display exactly what we want. We will create one variable that determines what the first page is and a 2nd variable to determine how many pages to offer at once. You might want to only show 4 numbers at a time on mobile, or possibly 16 or more on a desktop release. If its a variable then its adjustable. While we're at it im moving the QuickSaves and AutoSaves buttons outside the arrows since I want them visible at all times.

Code: Select all

                hbox xalign 0.5 spacing 0:

                    if config.has_autosave:
                        textbutton _("{#auto_page}A") action FilePage("auto")

                    if config.has_quicksave:
                        textbutton _("{#quick_page}Q") action FilePage("quick")

                    textbutton _("<") action FilePagePrevious(auto=False, quick=False)

                    for page in range(persistent.page_first, persistent.page_first+persistent.page_count):
                        textbutton "[page]" action FilePage(page)

                    textbutton _(">") action FilePageNext(auto=False, quick=False)
and just before the file_slots() screen definition we add those two variables like:

Code: Select all

default persistent.page_first = 1
default persistent.page_count = 10

screen file_slots(title):
At this point you can confirm our changes work by noticing a 10th slot can be directly clicked. I also added two arguments to the arrow buttons so that they dont activate the quick or auto slots. You need to manually click those. From this point on the arrows are only related to numbered slots.

Next changes will define the maximum number of pages we support and enable the arrows to wrap between the first and last. First add the following new default variable to track the total allowed pages and then adjust the actions on the arrows as follows:

Code: Select all

default persistent.page_total = 25

...

                    textbutton _("<") action FilePagePrevious(max=persistent.page_total,wrap=True,auto=False, quick=False)

                    for page in range(persistent.page_first, persistent.page_first+persistent.page_count):
                        textbutton "[page]" action FilePage(page)

                    textbutton _(">") action FilePageNext(max=persistent.page_total,wrap=True,auto=False, quick=False)

...
At this point its time to update which slots are displayed.

Code: Select all

                    textbutton _("«") action [ SetVariable("persistent.page_first", persistent.page_first-persistent.page_count),
                        Function(clamp_page_first) ]
                    textbutton _("<") action FilePagePrevious(max=persistent.page_total,wrap=True,auto=False,quick=False)
                    
                    for idx in range(persistent.page_first, persistent.page_first+persistent.page_count):
                        $ page = (idx+persistent.page_total) if (idx<1) else (idx-persistent.page_total) if (idx>persistent.page_total) else idx
                        textbutton "[page]" action FilePage(page):
                            xsize 80 text_xalign 0.5

                    textbutton _(">") action FilePageNext(max=persistent.page_total,wrap=True,auto=False,quick=False)
                    textbutton _("»") action [ SetVariable("persistent.page_first", persistent.page_first+persistent.page_count),
                        Function(clamp_page_first) ]
I added double arrow buttons that will scroll which pages of save slots are on the button bar. The both work the same way by increasing or decreasing the index of the first page by the count of how many you choose to display at a time. Then they call a function that fixes the value if its out of legal range. That function is:

Code: Select all

init python:
    def clamp_page_first():
        if persistent.page_first > persistent.page_total:
            persistent.page_first -= persistent.page_total 
        if persistent.page_first < 1:
            persistent.page_first += persistent.page_total 
        return
and I choose to place it just before the file_slots() screen definition just after the 3 defaulted persistent variables you already added. The for loop that displays the buttons also had a line added that makes sure when viewing the end of the list of saves it will wrap back to the beginning instead of access slots out of range. And in case you chose to allow 100 slots or more for the page_total value, i added xsize and alignment properties to make room for larger values to display cleanly.

This probably sums up the feature. I dont think I forgot anything but if there are problems find me in the renpy discord.
Last edited by seronis on Tue May 09, 2023 12:29 am, edited 1 time in total.

seronis
Newbie
Posts: 7
Joined: Fri Aug 05, 2022 9:57 pm
Deviantart: seronis
Github: seronis
itch: seronis
Contact:

Re: Seronis' Book of Tweaks

#4 Post by seronis »

3. Alternate / replacement choice menu screen with timing and activation features.

Code: Select all

screen choice(items, noqm=False, timeout=0):
    default qm_restore = store.quick_menu
    default tCounter = 0
    default chosen = items[0].action
    style_prefix "choice"

    viewport id "choice_vp":
        mousewheel True draggable True

        vbox:
            for i in items:
                if i.kwargs.get("select"):
                    $ chosen = i.action
                $ visAfter = i.kwargs.get("visAfter",0)
                if visAfter <= tCounter:
                    $ hideAfter = i.kwargs.get("hideAfter", False)
                    if not hideAfter or hideAfter >= tCounter:
                        textbutton i.caption action i.action:
                            selected i.kwargs.get("select",False)
                            sensitive i.kwargs.get("enabled", True)

    vbar value YScrollValue('choice_vp') unscrollable "hide"

    timer 0.25 repeat True action SetScreenVariable("tCounter",tCounter+0.25)

    if timeout >= 1:
        timer timeout action chosen
    if noqm:
        on "show" action SetVariable("store.quick_menu", False)
        on "hide" action SetVariable("store.quick_menu", qm_restore)

Code: Select all

label start:
    menu( timeout=42.0, noqm=True ): 
        "Hinted choice" (select=True):
            pass
        "Think Fast" (hideAfter=0.5):
            pass
        "Peek" (visAfter=1.0, hideAfter=2.0):
            pass
        "Ah" (visAfter=2.0, hideAfter=3.0):
            pass
        "BOOOM!!" (visAfter=3.0):
            pass
        "<wait for it ...>" (enabled=False, hideAfter=3.0):
            pass
        
First off this menu will display scroll bars if there happens to be too many choices and wont show them if not needed. So it LOOKS like a normal choice menu.

If you happen to set the noqm=True option that means 'no quick menu'. So it will hide the quick menu while making choices and will reshow it afterwards if it was visible before. It wont show the quickmenu if it was already hidden beforehand.

If you supply the timeout=value variable the entire choice menu will dismiss after the given number of seconds selecting the first option by default. The select=True flag will just make the button look like you are suggesting it as a good option and if a timeout was specified the 'select'ed choice will be what is chosen. enabled=False will have the button still be visible but in the disabled status and thus unclickable.

visafter and hideafter control buttons whose visibility you want tied to how long the choice menu has been visible. Due to the order the variables are checked its possible to have the vis value lower than the hide value and a menu option waill wait to show up before hiding again. It is not possible to use the variables in the opposite order and try to et the choice to disappear temporarily. If you try that it just wont show up at all.

All in all if you supply no additional arguments it means it will act exactly like the original choice menu so it works as a full replacement
Last edited by seronis on Mon Jul 24, 2023 9:53 pm, edited 1 time in total.

seronis
Newbie
Posts: 7
Joined: Fri Aug 05, 2022 9:57 pm
Deviantart: seronis
Github: seronis
itch: seronis
Contact:

Re: Seronis' Book of Tweaks, Tricks, and Toys

#5 Post by seronis »

4. Have the Quick Menu bar show up only while the mouse is in range, then auto hide

Code: Select all

default persistent.qm_helpseen = False

screen quick_menu():
    default hidden = True
    zorder 100

    if quick_menu:
        if hidden == True:
            mousearea style "qm_show" hovered SetScreenVariable("hidden", False)
            if persistent.qm_helpseen == False:
                textbutton "< Move cursor here for Quick Menu >" style 'qm_show':
                    hovered SetVariable('persistent.qm_helpseen', True) action NullAction()
        if hidden == False:
            mousearea style "qm_hide" unhovered SetScreenVariable("hidden", True)

            hbox:
                style_prefix "quick"

                textbutton _("Back") action Rollback()
                textbutton _("History") action ShowMenu('history')
                textbutton _("Skip") action Skip() alternate Skip(fast=True, confirm=True)
                textbutton _("Auto") action Preference("auto-forward", "toggle")
                textbutton _("Q.Save") action QuickSave() alternate ShowMenu('save')
                textbutton _("Q.Load") action QuickLoad() alternate ShowMenu('load')
                textbutton _("Prefs") action ShowMenu('preferences')

style qm_show:
    xsize 0.33 xcenter 0.5
    ysize 0.03 yanchor 1.0 ypos 1.0

style qm_hide:
    xsize 0.67 xcenter 0.5
    ysize 0.07 yanchor 1.0 ypos 1.0
The above code sets up mousearea's that will detect if the cursor has moved to the bottom center of the screen. Once there the quick menu is displayed and the area expanded a bit so it wont disappear with just a little movement.

Also the FIRST time the menu is hidden the player will see a hint telling them to move the cursor there. Once they have done this the hint is no longer needed so will never be displayed again

seronis
Newbie
Posts: 7
Joined: Fri Aug 05, 2022 9:57 pm
Deviantart: seronis
Github: seronis
itch: seronis
Contact:

Re: Seronis' Book of Tweaks, Tricks, and Toys (latest counts: 4,0,0)

#6 Post by seronis »

Trick 1. Using min() and max() to create a clamp() function to force a value into specific range.

First:
The min() function takes two arguments and will return the smaller of the two. This can be used to set an upper boundary for a value. For example:

Code: Select all

default health = 90
default maxHealth = 100

# our healing spell happens to heal 50 points at a time
health = health + 50
at this point our health would be 140 which is illegal. so instead we do

Code: Select all

health = min( maxHealth, health + 50 )
and our players health will be increased by 50 but not allowed to excede the given maxHealth. Note that if we had done

Code: Select all

health = min( health + 50, maxHealth )
we would have got the exact same results as min() returns the smaller of the two and doesnt care about order

Second:

The max() function takes two arguments and will return the larger of the two. This can be used to set a lower boundary for a value. For example:

Code: Select all

default health = 90
default maxHealth = 100
default minHealth = -100

# A 'crushing embrace' spell will do up to 1000 damage
health = health - 1000
In this game we consider a characters body to be as damaged as possible when its at negative their max health. Healing that much damage will always put them in a state the adventurer could be resurrected so we want to limit how badly a spell can hurt. So this time we will use max() via either of:

Code: Select all

health = max( minHealth, health-1000 )
#or
health = max( health-1000, minHealth )
Either of those will ensure the adventurer character might be dead but only a little =-)

Finally:

There might be parts of your code where you need to apply an adjustment but all the factors leading up to the adjustment means it could be positive or negative so you need to make sure the result value obeys both limits. This time we will define our own function

Code: Select all

init python:
    def clamp( value, adj=0, _min=value, _max=value ):
        return max( _min, min( _max, value + adj ))

default minHealth = -100
default maxHealth =  100
default health = 90

label start:
   #healing spell
   $ health = clamp( health, +50, _max=maxHealth )
   
   #crushing doom spell
   $ health = clamp( health, -1000, _min=minHealth )
   
   #walk into wildmagic zone, affects health by up to 100 healing or damage
   $ effect = renpy.random.randint(1,100)-renpy.random.randint(1,100)
   $ health = clamp( health, effect, _min=minHealth, _max=maxHealth )
   #on the other hand our cleric is immune to curse damage
   $ cleric.health = clamp( cleric.health, effect, _max=maxHealth )
the clamp function above was written so that you cant lower a value without supplying a minimum range, and you cant increase value without supplying the upper range. so the cleric is immune to damage since we didnt assign _min a value while calling clamp

The point is not for you to use this exact function but to teach how using min() and max() either alone or together can enforce limits

Post Reply

Who is online

Users browsing this forum: No registered users