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 ...