Vbox Images and Array Variables

Discuss how to use the Ren'Py engine to create visual novels and story-based games. New releases are announced in this section.
Forum rules
This is the right place for Ren'Py help. Please ask one question per thread, use a descriptive subject like 'NotFound error in option.rpy' , and include all the relevant information - especially any relevant code and traceback messages. Use the code tag to format scripts.
Message
Author
halitsu
Newbie
Posts: 10
Joined: Thu Nov 16, 2017 3:56 am
Contact:

Vbox Images and Array Variables

#1 Post by halitsu »

I have some code

Code: Select all

            if contactlist[pContact0].points > 79:
                image "[contactlist[pContact0].contactStates[4].picture]"
                text "[contactlist[pContact0].contactStates[4].name]" style "plain"
                text "[contactlist[pContact0].contactStates[4].desc]" style "plainsmall"
It doesn't work because ren'py doesn't like the variable 'pContact0' to be the array index, and it wants me to put plain number in there. The problem is that I want to display pages of contacts in the game and I want to just reuse the same vboxes for the new page. I would simply change what image, name, and description are displayed based on what page you're on. I tried putting it all in a python block, and the 'if' statement worked, but then the python block didn't understand what 'image' and 'text' were. I would really rather not hard code a nested series of conditional statements for each page, but obviously I will if it's not possible otherwise.

Please advise

-Thank you for your time
Halitsu
Last edited by halitsu on Sat Apr 14, 2018 12:07 am, edited 1 time in total.

kivik
Miko-Class Veteran
Posts: 786
Joined: Fri Jun 24, 2016 5:58 pm
Contact:

Re: Vbox Images and Array Variables

#2 Post by kivik »

Can I ask what variable type pContact0 is? If it's string then perhaps you just need to change contactlist into a dict?

Also I think you've got some problems in your code anyway, assuming you're doing a screen:
- image doesn't go inside screens, I think you want to use add if you want to add an image to a screen.
- you've also included a $ there
- I have a feeling you can't nest square brackets inside the square brackets - this is from experience, I think Ren'py gets confused, but it may be a mistake from my part. If that's the case, you may just want to set a temporary variable with contactlist[pContact0].contactStates[4] and then just reference tmpVar.picture, tmpVar.name etc.

Finally, if you're in a screen, you can actually just pass contactlist[pContact0] as a variable to the screen:

Code: Select all

call screen contactscreen(contactlist[pContact0]

...

screen contactscreen(contact):
Then you can just access contact.points, contact.contactStates[4].picture etc.

Hope that helps!

halitsu
Newbie
Posts: 10
Joined: Thu Nov 16, 2017 3:56 am
Contact:

Re: Vbox Images and Array Variables

#3 Post by halitsu »

Oops, yea that extra $ was a typo.

Your suggestion of passing the variable to the screen is good to know, but unfortunately for my purposes not practical. The code I provided is repeated 25 times with different images, and the images will change while still in the screen, which is why I need it to look up the array index from a variable

As for the [] within the [], it works fine if I give it a number like: [contactlist[0].contactStates[0].picture] works just fine.

kivik
Miko-Class Veteran
Posts: 786
Joined: Fri Jun 24, 2016 5:58 pm
Contact:

Re: Vbox Images and Array Variables

#4 Post by kivik »

Did you get it working then?

Also if you've got it repeated 25 times, I think there may be a way of optimising that. Generally speaking you can normalise / refract your code if it repeats itself. Can you share the whole screen code for us to have a look?

User avatar
Remix
Eileen-Class Veteran
Posts: 1628
Joined: Tue May 30, 2017 6:10 am
Completed: None... yet (as I'm still looking for an artist)
Projects: An un-named anime based trainer game
Contact:

Re: Vbox Images and Array Variables

#5 Post by Remix »

I would pre-evaluate the object to a temporary variable inside the loop (which I presume works fine)...

Code: Select all

            if contactlist[pContact0].points > 79:
                $ this_contact = contactlist[pContact0].contactStates[4]
                image "[this_contact.picture]"
                # etc
Frameworks & Scriptlets:

User avatar
xavimat
Eileen-Class Veteran
Posts: 1461
Joined: Sat Feb 25, 2012 8:45 pm
Completed: Yeshua, Jesus Life, Cops&Robbers
Projects: Fear&Love
Organization: Pilgrim Creations
Github: xavi-mat
itch: pilgrimcreations
Location: Spain
Discord: xavimat
Contact:

Re: Vbox Images and Array Variables

#6 Post by xavimat »

If the variable results in a string, I think you can simple delete the quotes and the first []

Code: Select all

image contactlist[pContact0].contactStates[4].picture
text contactlist[pContact0].contactStates[4].name
I'm not sure for the image, but the text should work fine.
Comunidad Ren'Py en español: ¡Únete a nuestro Discord!
Rhaier Kingdom A Ren'Py Multiplayer Adventure Visual Novel.
Cops&Robbers A two-player experiment | Fear&Love Why can't we say I love you?
Honest Critique (Avatar made with Chibi Maker by ~gen8)

User avatar
Remix
Eileen-Class Veteran
Posts: 1628
Joined: Tue May 30, 2017 6:10 am
Completed: None... yet (as I'm still looking for an artist)
Projects: An un-named anime based trainer game
Contact:

Re: Vbox Images and Array Variables

#7 Post by Remix »

You might also note, it is simple to reuse screens inside screens, so you could basically define a contact_card screen that accepted parameters and just repeat 'use' that inside a loop:
A basic dictionary based example:

Code: Select all

default contacts = {
    0 : ['257a','Amber','Redhead Flirt'],
    1 : ['223a','Kat','Brunette Minx'],
    2 : ['180','Tracy','Blonde Dancer'],
}

default state_map = {
    4 : range(80, 95),
    5 : range(95, 101),
}

screen contact_card( id, state=4 ):
    $ this_contact = contacts.get( id, False )
    if this_contact:
        vbox:
            image "images/[this_contact[0]].png"
            text "[this_contact[1]]"
            text "[this_contact[2]]"

screen contacts( ids=[], state_level=95 ):

    $ state = [ k for k in state_map if state_level in state_map[k] ]
    $ state = 0 if not len(state) else state[0]

    viewport:
        mousewheel True
        scrollbars "vertical"
        vbox:
            for id in ids:
                use contact_card( id, state )

label start:

    show screen contacts( [0,1] )
    "..."
Frameworks & Scriptlets:

kivik
Miko-Class Veteran
Posts: 786
Joined: Fri Jun 24, 2016 5:58 pm
Contact:

Re: Vbox Images and Array Variables

#8 Post by kivik »

Remix wrote: Sat Apr 14, 2018 6:25 am You might also note, it is simple to reuse screens inside screens, so you could basically define a contact_card screen that accepted parameters and just repeat 'use' that inside a loop:
Wow! This will be super useful thanks!

halitsu
Newbie
Posts: 10
Joined: Thu Nov 16, 2017 3:56 am
Contact:

Re: Vbox Images and Array Variables

#9 Post by halitsu »

Remix wrote: Sat Apr 14, 2018 6:25 am You might also note, it is simple to reuse screens inside screens, so you could basically define a contact_card screen that accepted parameters and just repeat 'use' that inside a loop:
A basic dictionary based example:

Code: Select all

default contacts = {
    0 : ['257a','Amber','Redhead Flirt'],
    1 : ['223a','Kat','Brunette Minx'],
    2 : ['180','Tracy','Blonde Dancer'],
}

default state_map = {
    4 : range(80, 95),
    5 : range(95, 101),
}

screen contact_card( id, state=4 ):
    $ this_contact = contacts.get( id, False )
    if this_contact:
        vbox:
            image "images/[this_contact[0]].png"
            text "[this_contact[1]]"
            text "[this_contact[2]]"

screen contacts( ids=[], state_level=95 ):

    $ state = [ k for k in state_map if state_level in state_map[k] ]
    $ state = 0 if not len(state) else state[0]

    viewport:
        mousewheel True
        scrollbars "vertical"
        vbox:
            for id in ids:
                use contact_card( id, state )

label start:

    show screen contacts( [0,1] )
    "..."
This is amazing. I actually changed to a vpgrid instead of just a viewport, but the two python lines for "state = [ k for k...." can you expand on how that works? Why is it "k"?

I have 5 different pictures for an unknown total number of characters. I'm using a hotspot to open the menu so I have action ShowMenu ('status',contacts), and it all works, I'm just not sure how to take that and apply it to showing a different picture depending on a variable each character has.

User avatar
Remix
Eileen-Class Veteran
Posts: 1628
Joined: Tue May 30, 2017 6:10 am
Completed: None... yet (as I'm still looking for an artist)
Projects: An un-named anime based trainer game
Contact:

Re: Vbox Images and Array Variables

#10 Post by Remix »

halitsu wrote: Mon Apr 30, 2018 7:44 pm This is amazing. I actually changed to a vpgrid instead of just a viewport, but the two python lines for "state = [ k for k...." can you expand on how that works? Why is it "k"?
I doubt you need them (they were just a bit of code 'accidentally' left in from some other question)... unless the other question was yours too

They basically map the state_level through a state_map and return the key ( 4 : range(80, 95), 5 : range(95, 101) -- ask for 81, get 4 as result etc ) - Python 3+ has a simplified version of the same concept with keys and values reversed by using ranges in keys.

To expand though: It is termed list comprehension and is quite highly used in Python (also dict comprehension). Basically:

Code: Select all

# Python, not Ren'py
a = [1,2,3,4,5]
b = [k for k in a if k%2] # if the item divided by 2 leaves a remainder (odd numbers)
>>> [1,3,5]
c = [ "image_{0}".format(str(k)) for k in b ]
>>> ['image_1', 'image_3', 'image_5']
I tend to use k as a disposable local variable name, so tend to use it in comprehensions etc. Any variable name would do.
I have 5 different pictures for an unknown total number of characters. I'm using a hotspot to open the menu so I have action ShowMenu ('status',contacts), and it all works, I'm just not sure how to take that and apply it to showing a different picture depending on a variable each character has.
It really depends how all the variables are stored, whether each character has individual state etc...

Personally I'd mesh the Stats in the character objects ( see Adding Stats to Characters ) then just store the *actually met/encountered* characters in a list, then just iterate and let the object control the response...
It would then let you define all the state responses (different pic/name/description) etc in the characters then map a property or two to return the correct one based on state or whatever...

How are yours stored?
Frameworks & Scriptlets:

halitsu
Newbie
Posts: 10
Joined: Thu Nov 16, 2017 3:56 am
Contact:

Re: Vbox Images and Array Variables

#11 Post by halitsu »

First off, thank you so much for your help, you're quite good at explaining things.

Currently, each npc has a '"name"_points variable stored on its own in the init. A character Wilson has wilson_points, for example, and the current display has a list of if/elifs
if points>79 display pic 5
elif points>59 display pic 4
etc

This worked well enough for us until now. (we're only two people and the project will definitely need a fairly major overhaul as we both learn new and better ways of using ren'py and it's applicable python)

One thing I should note, however, the contacts are displayed in a 'status' screen, not in the main game window.

We wanted to have the contacts listed in order, and only if unlocked. Unfortunately, my expertise are in C# and Java, and I'm very used to that type of functionality, so my code is probably bad. I'll just paste what we had, and what we have now, that might be simpler

Old Screen Code:
https://pastebin.com/Y5TFJhM9
Old Contactlist Code:
https://pastebin.com/nnEA5DT8

New Screen Code:
https://pastebin.com/326jN9vd
New Contactlist Code:
https://pastebin.com/4NQmGdw7

The old code 'worked' in that it sorted the contacts properly, and arrow buttons would 'shift' the contacts to see more, but it would make a new screen each time, so if you wanted to exit the status screen, you had to click the "X" as many times as you scrolled the contacts, each time it would show the last view (the contacts would shift in reverse of what you did until it finally closed)

User avatar
Remix
Eileen-Class Veteran
Posts: 1628
Joined: Tue May 30, 2017 6:10 am
Completed: None... yet (as I'm still looking for an artist)
Projects: An un-named anime based trainer game
Contact:

Re: Vbox Images and Array Variables

#12 Post by Remix »

I can definitely tell you have some programming experience from those code parts. Always nice to see code that is well laid out and easily readable. :)

Just gone midnight here so I do not really have time to run up a full reply though should be able to tomorrow (ummm today after sleep) ...
Frameworks & Scriptlets:

halitsu
Newbie
Posts: 10
Joined: Thu Nov 16, 2017 3:56 am
Contact:

Re: Vbox Images and Array Variables

#13 Post by halitsu »

Remix wrote: Thu May 03, 2018 7:15 pm I can definitely tell you have some programming experience from those code parts. Always nice to see code that is well laid out and easily readable. :)

Just gone midnight here so I do not really have time to run up a full reply though should be able to tomorrow (ummm today after sleep) ...
You are wonderful, thank you so much, no hurry.

User avatar
Remix
Eileen-Class Veteran
Posts: 1628
Joined: Tue May 30, 2017 6:10 am
Completed: None... yet (as I'm still looking for an artist)
Projects: An un-named anime based trainer game
Contact:

Re: Vbox Images and Array Variables

#14 Post by Remix »

Being pestered to go out drinking later so this might just be a "show some code and run away" post...

Some comments after the code block...

The initial class (that could/should just go in its own .rpy file to keep it out of the way) is slightly different to the cookbook one in that it includes a __post__init__ conditional method that we use to automatically add characters to a contacts list (because we are lazy and let the code do the work)

Code: Select all

          ######################################
          #         The Python Class           #
          ######################################

           # this could go in a separate .rpy

init python:

    class BaseStatsObject(object):
        """
        Base class for defaulting stats and integrating with the store.

        Designed to be extended by just overloading the constants


        Example of extended class

        class EnemyStats(BaseStatsObject):

            # Set the store.{prefix}.character_id value
            STORE_PREFIX = "enemy_stats"

            # Boolean toggle for validation - defaults both True
            VALIDATE_VALUES = True
            COERCE_VALUES = False

            STAT_DEFAULTS = {
                'element' : 'earth',
                'hp' : 50,
                'mp' : 40,
                'rarity' : 0.075,
            }

        """

        STORE_PREFIX = "character_stats"
        VALIDATE_VALUES = True
        COERCE_VALUES = True

        STAT_DEFAULTS = {}

        def __init__(self, id, **kwargs):
            """
            Initialize values from store or kwargs or default

            @param id: A unique id to use in the store. Generally set to 
            the Character reference to allow cross object lookups

            @param **kwargs: Setup values that are not default. Must use
            only keys defined in STAT_DEFAULTS
            """

            if not isinstance(id, basestring):
                id = str(id) # should raise if not stringable

            self.__dict__['_id'] = id

            # For optional methods to be ran at certain events
            for k, v in {
                'pre_init'   : "__pre_init__",
                'post_init'  : "__post_init__",
                'pre_alter'  : "__pre_alter__",
                'post_alter' : "__post_alter__" }.items():

                self.__dict__[k] = kwargs.pop(k, v)

            self.run_optional_method( 'pre_init', id, **kwargs )

            store_name = "{prefix}.{suffix}".format(
                prefix = type(self).STORE_PREFIX,
                suffix = self.__dict__['_id'] )

            setattr(store, store_name, {})

            self.__dict__['_store'] = getattr(store, store_name)

            defaults = type(self).STAT_DEFAULTS

            for key in defaults:

                if key not in self.__dict__['_store']:

                    if key not in kwargs:
                        # use default value
                        setattr(self, key, defaults[key])
                        continue

                    setattr(self, key, kwargs[key])

            unknown_kwargs = [k for k in kwargs if k not in defaults]

            if len(unknown_kwargs):
                raise AttributeError, "Supplied argument(s) '{0}' not " \
                                      "recognised as default".format(
                                        ", ".join(unknown_kwargs))

            self.run_optional_method( 'post_init', id, **kwargs )


        def run_optional_method(self, method_type='post_init', *args, **kwargs):
            """
            Run a method of the object if it exists
            """
            try:
                getattr( self, self.__dict__[ method_type ] )( *args, **kwargs )
            except:
                pass


        def get_validated_value(self, key, value):

            if not type(self).VALIDATE_VALUES:
                return value

            default_type = type(type(self).STAT_DEFAULTS.get(key, None))

            if isinstance(value, default_type):
                return value

            if type(self).COERCE_VALUES:
                try:
                    return default_type(value)
                except:
                    pass

            raise TypeError, "Supplied value '{0}' for key '{1}' does not " \
                             "match the default '{2}'".format(
                                value, 
                                key,
                                default_type)


        def __setattr__(self, key, value):

            self.run_optional_method( 'pre_alter', key, value )

            self.__dict__[key] = value

            if key in type(self).STAT_DEFAULTS:

                value = self.get_validated_value(key, value)

                self.__dict__['_store'][key] = value

            self.run_optional_method( 'post_alter', key, value )


        def __getattr__(self, key):

            try:
                return self.__dict__['_store'][key]
            except:
                if key in self.__dict__:
                    return self.__dict__[key]
                else:
                    try:
                        # try the character object
                        value = getattr(getattr(character, self._id), key)
                        if key != 'name':
                            return value
                        return renpy.substitutions.substitute(value)[0]
                    except:
                        pass

            # we jump to getattribute (even though it *could* recurse)
            # in order to access @properties 
            return super(BaseStatsObject, self).__getattribute__(key)


        def __getattribute__(self, key):

            if key in type(self).STAT_DEFAULTS:
                try:
                    return self.__dict__['_store'][key]
                except:
                    pass

            return super(BaseStatsObject, self).__getattribute__(key)
    

        def __setstate__(self, data):
            self.__dict__.update(data)


        def __getstate__(self):
            return self.__dict__


        def __delattr__(self, key):
            del self.__dict__[key]


Now the actual usage of the class to let our characters have some attributes that we can use in our script and screens

Code: Select all

          ######################################
          #     An Extending Python Class      #
          ######################################


init python:

    class CharacterStats(BaseStatsObject):

        # Set the store.{prefix}.character_id value
        # STORE_PREFIX = "character_stats"

        # Boolean toggle for validation - defaults both True
        # VALIDATE_VALUES = False
        # COERCE_VALUES = False

        STAT_DEFAULTS = {
            'points' : 0,
            'has_met' : False,
            'has_number' : False,
            'phone_pic_format' : "phoneportrait{name}{state}.png",
            'states' : [
                        "Weird girl.", 
                        "Not bad.", 
                        "Kinda cute.", 
                        "Very cute.", 
                        "<3" ],
            'info' : "A kid from the other side of town.",
            'short_info' : "Just some kid.",
        }

        def __post_init__(self, *args, **kwargs):
            if not 'all_characters' in globals():
                globals()['all_characters'] = []
            globals()['all_characters'].append( self )

        @property
        def state(self):
            """
            Return self.state as:

            the floor of self.points divided by 20
            confined to the range 0 to 4
            """
            return int( min( 4, max( 0, self.points // 20 ) ) )

        @property
        def phone_pic(self):
            """
            Return the phone picture to use
            """
            format_dict = { 
                k : str( getattr(self, k) ).lower() \
                for k in [ 'name', 'state', 'outfit', 'expression' ] 
                if hasattr(self, k) }

            return self.phone_pic_format.format( **format_dict )

        @property
        def state_name(self):
            """
            Return name relevant to state
            """
            return self.states[ self.state ]


          ######################################
          #   Ren'py Character & Stats Setup   #
          ######################################


define character.ap = Character("April")
default ap = CharacterStats("ap")

define character.au = Character("Audric")
default au = CharacterStats("au", 
    states = [ "Weird kid.", "Not a kid.", "Interesting guy.", 
               "Dependable.", "Good friend." ] )

define character.la = Character("Lamont")
default la = CharacterStats("la", 
    states = [ "???", "Freaky.", "Mysterious.", 
               "Interesting.", "Worthwhile." ] )

define character.ly = Character("Lyric")
default ly = CharacterStats("ly", 
    states = [ "Intimidating", "Kinda fun.", "Cute.", 
               "Friend.", "Awesome chick!" ] )

define character.no = Character("Nola")
default no = CharacterStats("no") # same state names as default


          ######################################
          #        Basic Contact Screens       #
          ######################################

screen list_contacts():
    
    $ met_characters = sorted( 
        [ k for k in all_characters
          if hasattr(k, 'has_met') 
          and k.has_met ], 
        key = lambda c: c.name )

    vbox:
        area (0,0, 150, 0.7)
        text "Contacts"
        null height 10

        for char in met_characters:
            
            text "[char.name]"
            
            if not char.has_number:
                # they know this character, yet do not have phone number...
                #
                # add Transform( char.phone_pic, zoom = 0.35 )
                #
                text "No number known"
                text "[char.phone_pic]"
            else:
                # known and phone-able... status screen available
                imagebutton:
                    idle "images/331.png"#Transform( char.phone_pic, zoom=0.35 )
                    action Function( renpy.show_screen, 'show_contact', char )
                text "[char.short_info]"

screen show_contact( char ):

    vbox:
        area (180,0, config.screen_width - 200, 0.7)
        text "[char.name] ([char.state_name])"
        hbox:
            add Image( "images/331.png" )
            text "[char.info]"
            $ stats = "\n".join( [ "{0} = {1}".format(k,getattr(char,k)).replace('{','{{').replace('[','[[') for k in type(char).STAT_DEFAULTS] )
            text "[stats]"


          ######################################
          # Basic Test Label to check it works #
          ######################################

label start:

    show screen list_contacts

    ap "Hello Player"
    $ ap.has_met = True

    ap "Oh you added me to your phone. Did you want my number?"
    $ ap.has_number = True 

    ap "Things are going well"
    $ ap.points += 20

    ap "Oh look, there's Audric"
    $ au.has_met = True 


    "End"

Some super brief explanations:

methods with @property just before them basically map the method name as an attribute getter... So our

Code: Select all

        @property
        def state(self):
            """
            Return self.state as:

            the floor of self.points divided by 20
            confined to the range 0 to 4
            """
            return int( min( 4, max( 0, self.points // 20 ) ) )
basically means if we do -- object_reference.state -- we will get a value that is calculated from the object_reference.points

The property -- phone_pic -- basically takes our ( 'phone_pic_format' : "phoneportrait{name}{state}.png" ) and maps the string through our object, so it might return "phoneportraitaudric3.png" for instance

The property state_name I just added so we can easily use it in interpolated Ren'py strings.

Characters:
Quite easy to set up as most have similar defaults - hopefully enough example code to allow you to extend it as needed

Screens:
list_contacts - first we sort and filter our all_characters into those we have already met
then we just output them in a nicely messy vbox using char.attribute_name for any references
You might want to tidy up that screen

show_contact - just a "more info" version of the list... also needs tidying

Note: I didn't actually test the images as I was too lazy to go through and rename some files. I'm sure you will be able to fix any issues anyway.

Hope that all makes sense and offers a base to perhaps start from. It is certainly similar to how I would tackle the problem.
Should be back later in weekend if needed for any questions.

Edit: __getattr__ in the main class was falling through to __get__ on non __dict__ lookups... changed to __getattribute__ instead so as to support save/load pickling correctly
Also fixed a minor point in @property phone_pic to avoid the error that is now visible with the fix above (namely object.outfit does not exist)

If adjusting your code, maybe just change the base class and the def phone_pic method rather than the lot ...
Last edited by Remix on Sat May 26, 2018 6:46 pm, edited 1 time in total.
Frameworks & Scriptlets:

KingsCard
Newbie
Posts: 14
Joined: Sat Feb 17, 2018 8:58 am
Contact:

Re: Vbox Images and Array Variables

#15 Post by KingsCard »

Remix wrote: Fri May 04, 2018 8:56 am [...]
Thank you Remix ! This code is fantastic ! :)

Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot], Bing [Bot]