Perhaps you want your love interest and friends to all have height, weight, blood type and age...
Perhaps you want your companions to have attack level, defence score, hit points and mana points...
The most basic way is to default a unique named variable for each stat you have and hold it independent of the character in question:
Code: Select all
define k = Character("Kaori")
default kaori_height = 156
default kaori_weight = 48
default kaori_blood = "A"
default kaori_age = 19
Where it gets somewhat messier is with hoards of characters or hoards of stats per character or "eeek" hoards of characters each with hoards of stats.
pesky person reading this over your shoulder wrote:Isn't there an easier way? A nicer way, some way of doing something like:
Code: Select all
define k = Character("Kaori", height=156, weight=48)
label a:
"[k.height] : [k.weight]"
Odd you should mention that, yes he did in This Post which outlines the concept.person behind the pesky person behind you, also rudely reading over your shoulder wrote:Didn't the great PyTom once mention something about using the character namespace alongside another object and using that object to store statistics?
Yup, you can do that, implement a DataObject of your own and shoe-horn it in like shown.another of pesky person's friends... rudely a reading along with the others wrote:Ok, so job's a goodun, problem sorted, all done. We just do that.
After that you can rollback or save and then come back and read the rest of this thread...
Well, the first problem is: How did you write that without touching your keyboard? You sure it wasn't one of that huge crowd reading over your shoulder?You wrote:So, what's the problem then?
Anyway, getting back on track...
Ren'py will save/pickle/serialize/whatever an object if the reference to the object changes its value after the initialization phase... If we say 'kaori_age = 19' then say 'kaori_age += 1' the value of kaori_age changes from being integer 19 to integer 20, which is a change so is saved. If we say 'k = DataObject( some params )' and then change an internal value of that object the reference k is still pointing at DataObject, rather than the internal attribute, which does not get recognized as a change and therefore is not saved.
...The Ren'py Ducumentation wrote: What Is Saved
The python state consists of the variables in the store that have changed since the game began, and all objects reachable from those variables. Note that it's the change to the variables that matters - changes to fields in objects will not cause those objects to be saved.
*cough* I was teaching...An impatient member of the 'over the shoulder reading' crowd wrote:Enough with the explanations already. No more dithering. Just show us the code !!!
Anyway:
The Base Class ( just leave this bit alone... extend it with a child class (shown below) )
Best advice: Just save this in a separate file called something like 01base_stats.rpy
Code: Select all
######################################
# The Python Class #
######################################
# this should 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
"""
if not isinstance(id, basestring):
id = str(id) # should raise if not stringable
self.__dict__['_id'] = id
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)
# We use:
# Store value
# else kwargs value
# else default value
for key, value in kwargs.items():
if key not in self.__dict__['_store']:
setattr(self, key, value)
for key, value in type(self).STAT_DEFAULTS.items():
if key not in self.__dict__['_store']:
setattr(self, key, value)
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):
"""
Return a value after validating where applicable
"""
if not type(self).VALIDATE_VALUES:
return value
if not key in self.__dict__:
return value
default_type = type( self.__dict__[key] )
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 existing '{2}'".format(
value,
key,
default_type)
def __setattr__(self, key, value):
value = self.get_validated_value(key, value)
self.__dict__[key] = value
# Anything not recognized as an attribute of object
# is placed into the store
if key not in dir(object):
self.__dict__['_store'][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
# substitute the name (for interpolation/translations)
return renpy.substitutions.substitute(value)[0]
except:
pass
return super(BaseStatsObject, self).__getattr__(key)
def __getattribute__(self, key):
# Check if the attribute is an @property first
v = object.__getattribute__(self, key)
if hasattr(v, '__get__'):
return v.__get__(None, self)
# Try the store if the attribute is not in base object
if key not in dir(object):
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]
Extending the Class and using it
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 = {
'gender' : 'f',
'age' : 22,
'location' : 'school',
'affection' : 1,
'obedience' : 0.01,
}
######################################
# Ren'py Character & Stats Setup #
######################################
default test_name = "Amber"
define character.a = Character("[test_name]", who_color="#F9A")
default a = CharacterStats("a", location='lounge', affection=15, age=21)
define character.e = Character("Eileen")
default e = CharacterStats("e", location='study', affection=5)
######################################
# Basic Test Label to check it works #
######################################
label start:
"Start"
e "Expected: Eileen 22 study 5 0.01
\nReceived: [e.name] [e.age] [e.location] [e.affection] [e.obedience]"
$ e.location = 'hall'
$ e.obedience += 0.01
$ e.affection = "10" # coerce to integer (if set)
e "Expected: Eileen 22 hall 10 0.02
\nReceived: [e.name] [e.age] [e.location] [e.affection] [e.obedience]"
$ e.affection += 15.0 # coerce to integer
$ e.location = 'classroom'
$ e.obedience += 0.02
e "Expected: Eileen 22 classroom 25 0.04
\nReceived: [e.name] [e.age] [e.location] [e.affection] [e.obedience]"
"Middle"
a "Expected: Amber 21 lounge 15 0.01
\nReceived: [a.name] [a.age] [a.location] [a.affection] [a.obedience]"
$ a.affection += 1
$ a.location = 'on the sofa (dancing)'
a "Expected: Amber 21 on the sofa (dancing) 16 0.01
\nReceived: [a.name] [a.age] [a.location] [a.affection] [a.obedience]"
"The clock strikes midnight... Happy Birthday Amber"
$ a.age += 1
$ test_name = "Dancing Queen Amber"
a "Expected: Dancing Queen Amber 22 on the sofa (dancing) 16 0.01
\nReceived: [a.name] [a.age] [a.location] [a.affection] [a.obedience]"
$ amber_color = a.who_args.get('color', 'Failed')
"Expected '#F9A' and got '[amber_color]'"
"End"
define character.a = Character( .....
# that uses the character namespace for that object
# then we default our data object globally ( using the same id ('a' in this case) makes sense )
default a = CharacterStats("a",
# and note the first argument "a" is also the same ( which ties the objects together nicely and allows us to drop the namespace when calling attribute from the Character object ( like a.name rather than character.a.name ) )
Well, I've tested a few saves, those loaded fine, rollback/forward also ran perfectly.The cutest one of the 'shoulder perchers' wrote:Brief scan looks good. Does it actually work with saving, loading, rollback and all that?
Thanks. It is very basic though, just takes the type of the default value and validates/coerces (if the constant is set) any kwargs or value change that occurs.The cute one again, now they found their voice wrote:Oh, I see it also has validation and coercion. Nice touch
Oh drat, Friday and friends calling to invite me out. ( Yup I do have friends, ones who do not read over my shoulder and even some that aren't imaginary )Same one wrote:Just one more question...
Feel free to post any questions... Back later in the weekend.
Addendum and Changes
Validation is now done against the type of the first declared value, meaning you can pass in a parameter of a different type to the default one and it will use that type for validation instead.
All attributes that are not part of python's base object are stored in the store, meaning you can pass in an arbitrary parameter and it will be stored. For example:
default a = CharacterStats("a", non_default_variable_name=17)
will accept the non_default_variable_name parameter and save it for you.
In order to support @property attributes/methods I reworked the getattr/getattribute pair (along with adding calls to __pre_init__ and __post_init__ methods if they exist).
The @property methods could be quite useful if you are looking to bind a value to a particular range, e.g:
Code: Select all
init python:
class CharacterStats(BaseStatsObject):
STAT_DEFAULTS = {
'affection' : 35
}
@property
def affection(self):
value = self.__dict__['affection']
if not ( 0 <= value <= 100 ):
value = max( 0, min( 100, value ) )
self.affection = value
return self.__dict__['affection']
You could also overload the __setattr__ method, put known key values into range and then call the super if wanted
A script.rpy with all the above code in one file.
After testing, I suggest moving the base class to its own file to keep your code tidy.