comparison src/parpg/objects/base.py @ 0:1fd2201f5c36

Initial commit of parpg-core.
author M. George Hansen <technopolitica@gmail.com>
date Sat, 14 May 2011 01:12:35 -0700
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:1fd2201f5c36
1 # This file is part of PARPG.
2
3 # PARPG is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
7
8 # PARPG is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12
13 # You should have received a copy of the GNU General Public License
14 # along with PARPG. If not, see <http://www.gnu.org/licenses/>.
15
16 """Containes classes defining the base properties of all interactable in-game
17 objects (such as Carryable, Openable, etc. These are generally independent
18 classes, which can be combined in almost any way and order.
19
20 Some rules that should be followed when CREATING base property classes:
21
22 1. If you want to support some custom initialization arguments,
23 always define them as keyword ones. Only GameObject would use
24 positional arguments.
25 2. In __init__() **ALWAYS** call the parent's __init__(**kwargs), preferably
26 *at the end* of your __init__() (makes it easier to follow)
27 3. There should always be an attributes.append(x) call on __init__
28 (where X is the name of the class)
29
30 EXAMPLE:
31
32 class Openable(object):
33 def __init__ (self, is_open = True, **kwargs):
34 self.attribbutes.append("openable")
35 self.is_open = is_open
36 super(Openable,self).__init__ (**kwargs)
37
38
39 Some rules are to be followed when USING the base classes to make composed
40 ones:
41
42 1. The first parent should always be the base GameObject class
43 2. Base classes other than GameObject can be inherited in any order
44 3. The __init__ functoin of the composed class should always invoke the
45 parent's __init__() *before* it starts customizing any variables.
46
47 EXAMPLE:
48
49 class TinCan (GameObject, Container, Scriptable, Destructable, Carryable):
50 def __init__ (self, *args, **kwargs):
51 super(TinCan,self).__init__ (*args, **kwargs)
52 self.name = 'Tin Can'"""
53
54 class BaseObject(object):
55 """A base class that supports dynamic attributes functionality"""
56 def __init__ (self):
57 if not self.__dict__.has_key("attributes"):
58 self.attributes = []
59
60 def trueAttr(self, attr):
61 """Method that checks if the instance has an attribute"""
62 return attr in self.attributes
63
64 def getStateForSaving(self):
65 """Returns state for saving
66 """
67 state = {}
68 state["attributes"] = self.attributes
69 return state
70
71 class DynamicObject (BaseObject):
72 """Class with basic attributes"""
73 def __init__ (self, name="Dynamic object", real_name=None, image=None, **kwargs):
74 """Initialise minimalistic set of data
75 @type name: String
76 @param name: Object display name
77 @type image: String or None
78 @param name: Filename of image to use in inventory"""
79 BaseObject.__init__(self)
80 self.name = name
81 self.real_name = real_name or name
82 self.image = image
83
84 def prepareStateForSaving(self, state):
85 """Prepares state for saving
86 @type state: dictionary
87 @param state: State of the object
88 """
89 pass
90
91 def restoreState(self, state):
92 """Restores a state from a saved state
93 @type state: dictionary
94 @param state: Saved state
95 """
96 self.__dict__.update(state)
97
98 def __getstate__(self):
99 odict = self.__dict__.copy()
100 self.prepareStateForSaving(odict)
101 return odict
102
103 def __setstate__(self, state):
104 self.restoreState(state)
105
106 def getStateForSaving(self):
107 """Returns state for saving
108 """
109 state = BaseObject.getStateForSaving(self)
110 state["Name"] = self.name
111 state["RealName"] = self.real_name
112 state["Image"] = self.image
113 return state
114
115 class GameObject (DynamicObject):
116 """A base class to be inherited by all game objects. This must be the
117 first class (left to right) inherited by any game object."""
118 def __init__ (self, ID, gfx = None, xpos = 0.0, ypos = 0.0, map_id = None,
119 blocking=True, name="Generic object", real_name="Generic object", text="Item description",
120 desc="Detailed description", **kwargs):
121 """Set the basic values that are shared by all game objects.
122 @type ID: String
123 @param ID: Unique object identifier. Must be present.
124 @type gfx: Dictionary
125 @param gfx: Dictionary with graphics for the different contexts
126 @type coords 2-item tuple
127 @param coords: Initial coordinates of the object.
128 @type map_id: String
129 @param map_id: Identifier of the map where the object is located
130 @type blocking: Boolean
131 @param blocking: Whether the object blocks character movement
132 @type name: String
133 @param name: The display name of this object (e.g. 'Dirty crate')
134 @type text: String
135 @param text: A longer description of the item
136 @type desc: String
137 @param desc: A long description of the item that is displayed when it is examined
138 """
139 DynamicObject.__init__(self, name, real_name, **kwargs)
140 self.ID = ID
141 self.gfx = gfx or {}
142 self.X = xpos
143 self.Y = ypos
144 self.map_id = map_id
145 self.blocking = True
146 self.text = text
147 self.desc = desc
148
149 def _getCoords(self):
150 """Get-er property function"""
151 return (self.X, self.Y)
152
153 def _setCoords(self, coords):
154 """Set-er property function"""
155 self.X, self.Y = float(coords[0]), float (coords[1])
156
157 coords = property (_getCoords, _setCoords,
158 doc = "Property allowing you to get and set the object's \
159 coordinates via tuples")
160
161 def __repr__(self):
162 """A debugging string representation of the object"""
163 return "<%s:%s>" % (self.name, self.ID)
164
165 def getStateForSaving(self):
166 """Returns state for saving
167 """
168 state = super(GameObject, self).getStateForSaving()
169 state["ObjectModel"] = self.gfx
170 state["Text"] = self.text
171 state["Desc"] = self.desc
172 state["Position"] = list(self.coords)
173 return state
174
175
176 class Scriptable (BaseObject):
177 """Allows objects to have predefined parpg executed on certain events"""
178 def __init__ (self, parpg = None, **kwargs):
179 """Init operation for scriptable objects
180 @type parpg: Dictionary
181 @param parpg: Dictionary where the event strings are keys. The
182 values are 3-item tuples (function, positional_args, keyword_args)"""
183 BaseObject.__init__(self)
184 self.attributes.append("scriptable")
185 self.parpg = parpg or {}
186
187 def runScript (self, event):
188 """Runs the script for the given event"""
189 if event in self.parpg and self.parpg[event]:
190 func, args, kwargs = self.parpg[event]
191 func (*args, **kwargs)
192
193 def setScript (self, event, func, args = None , kwargs = None):
194 """Sets a script to be executed for the given event."""
195 args = args or {}
196 kwargs = kwargs or {}
197 self.parpg[event] = (func, args, kwargs)
198
199 class Openable(DynamicObject, Scriptable):
200 """Adds open() and .close() capabilities to game objects
201 The current state is tracked by the .is_open variable"""
202 def __init__(self, is_open = True, **kwargs):
203 """Init operation for openable objects
204 @type is_open: Boolean
205 @param is_open: Keyword boolean argument sets the initial state."""
206 DynamicObject.__init__(self, **kwargs)
207 Scriptable.__init__(self, **kwargs)
208 self.attributes.append("openable")
209 self.is_open = is_open
210
211 def open(self):
212 """Opens the object, and runs an 'onOpen' script, if present"""
213 self.is_open = True
214 try:
215 if self.trueAttr ('scriptable'):
216 self.runScript('onOpen')
217 except AttributeError :
218 pass
219
220 def close(self):
221 """Opens the object, and runs an 'onClose' script, if present"""
222 self.is_open = False
223 try:
224 if self.trueAttr ('scriptable'):
225 self.runScript('onClose')
226 except AttributeError :
227 pass
228
229 class Lockable (Openable):
230 """Allows objects to be locked"""
231 def __init__ (self, locked = False, is_open = True, **kwargs):
232 """Init operation for lockable objects
233 @type locked: Boolean
234 @param locked: Keyword boolen argument sets the initial locked state.
235 @type is_open: Boolean
236 @param is_open: Keyword boolean argument sets the initial open state.
237 It is ignored if locked is True -- locked objects
238 are always closed."""
239 self.attributes.append("lockable")
240 self.locked = locked
241 if locked :
242 is_open = False
243 Openable.__init__( self, is_open, **kwargs )
244
245 def unlock (self):
246 """Handles unlocking functionality"""
247 self.locked = False
248
249 def lock (self):
250 """Handles locking functionality"""
251 self.close()
252 self.locked = True
253
254 def open (self, *args, **kwargs):
255 """Adds a check to see if the object is unlocked before running the
256 .open() function of the parent class"""
257 if self.locked:
258 raise ValueError ("Open failed: object locked")
259 super (Lockable, self).open(*args, **kwargs)
260
261 class Carryable (DynamicObject):
262 """Allows objects to be stored in containers"""
263 def __init__ (self, weight=0.0, bulk=0.0, **kwargs):
264 DynamicObject.__init__(self, **kwargs)
265 self.attributes.append("carryable")
266 self.in_container = None
267 self.on_map = None
268 self.agent = None
269 self.weight = weight
270 self.bulk = bulk
271
272 def getInventoryThumbnail(self):
273 """Returns the inventory thumbnail of the object"""
274 # TODO: Implement properly after the objects database is in place
275 if self.image == None:
276 return "gui/inv_images/inv_litem.png"
277 else:
278 return self.image
279
280 class Container (DynamicObject, Scriptable):
281 """Gives objects the capability to hold other objects"""
282 class TooBig(Exception):
283 """Exception to be raised when the object is too big
284 to fit into container"""
285 pass
286
287 class SlotBusy(Exception):
288 """Exception to be raised when the requested slot is occupied"""
289 pass
290
291 class ItemSelf(Exception):
292 """Exception to be raised when trying to add the container as an item"""
293 pass
294
295 def __init__ (self, capacity = 0, items = None, **kwargs):
296 DynamicObject.__init__(self, **kwargs)
297 Scriptable.__init__(self, **kwargs)
298 self.attributes.append("container")
299 self.items = {}
300 self.capacity = capacity
301 if items:
302 for item in items:
303 self.placeItem(item)
304
305 def placeItem (self, item, index=None):
306 """Adds the provided carryable item to the inventory.
307 Runs an 'onStoreItem' script, if present"""
308 if item is self:
309 raise self.ItemSelf("Paradox: Can't contain myself")
310 if not item.trueAttr ('carryable'):
311 raise TypeError ('%s is not carryable!' % item)
312 if self.capacity and self.getContentsBulk()+item.bulk > self.capacity:
313 raise self.TooBig ('%s is too big to fit into %s' % (item, self))
314 item.in_container = self
315 if index == None:
316 self._placeAtVacant(item)
317 else:
318 if index in self.items :
319 raise self.SlotBusy('Slot %d is busy in %s' % (index,
320 self.name))
321 self.items[index] = item
322
323 # Run any parpg associated with storing an item in the container
324 try:
325 if self.trueAttr ('scriptable'):
326 self.runScript('onPlaceItem')
327 except AttributeError :
328 pass
329
330 def _placeAtVacant(self, item):
331 """Places an item at a vacant slot"""
332 vacant = None
333 for i in range(len(self.items)):
334 if i not in self.items :
335 vacant = i
336 if vacant == None :
337 vacant = len(self.items)
338 self.items[vacant] = item
339
340 def takeItem (self, item):
341 """Takes the listed item out of the inventory.
342 Runs an 'onTakeItem' script"""
343 if not item in self.items.values():
344 raise ValueError ('I do not contain this item: %s' % item)
345 del self.items[self.items.keys()[self.items.values().index(item)]]
346
347 # Run any parpg associated with popping an item out of the container
348 try:
349 if self.trueAttr ('scriptable'):
350 self.runScript('onTakeItem')
351 except AttributeError :
352 pass
353
354 def replaceItem(self, old_item, new_item):
355 """Replaces the old item with the new one
356 @param old_item: Old item which is removed
357 @type old_item: Carryable
358 @param new_item: New item which is added
359 @type new_item: Carryable
360 """
361 old_index = self.indexOf(old_item.ID)
362 self.removeItem(old_item)
363 self.placeItem(new_item, old_index)
364
365 def removeItem(self, item):
366 """Removes an item from the container, basically the same as 'takeItem'
367 but does run a different script. This should be used when an item is
368 destroyed rather than moved out.
369 Runs 'onRemoveItem' script
370 """
371 if not item in self.items.values():
372 raise ValueError ('I do not contain this item: %s' % item)
373 del self.items[self.items.keys()[self.items.values().index(item)]]
374
375 # Run any parpg associated with popping an item out of the container
376 try:
377 if self.trueAttr ('scriptable'):
378 self.runScript('onRemoveItem')
379 except AttributeError :
380 pass
381
382 def count (self, item_type = ""):
383 """Returns the number of items"""
384 if item_type:
385 ret_count = 0
386 for index in self.items :
387 if self.items[index].item_type == item_type:
388 ret_count += 1
389 return ret_count
390 return len(self.items)
391
392 def getContentsBulk(self):
393 """Bulk of the container contents"""
394 return sum((item.bulk for item in self.items.values()))
395
396 def getItemAt(self, index):
397 return self.items[index]
398
399 def indexOf(self, ID):
400 """Returns the index of the item with the passed ID"""
401 for index in self.items :
402 if self.items[index].ID == ID:
403 return index
404 return None
405
406 def findItemByID(self, ID):
407 """Returns the item with the passed ID"""
408 for i in self.items :
409 if self.items[i].ID == ID:
410 return self.items[i]
411 return None
412
413 def findItemByItemType(self, item_type):
414 """Returns the item with the passed item_type"""
415 for index in self.items :
416 if self.items[index].item_type == item_type:
417 return self.items[index]
418 return None
419
420 def findItem(self, **kwargs):
421 """Find an item in container by attributes. All params are optional.
422 @type name: String
423 @param name: If the name is non-unique, return first matching object
424 @type kind: String
425 @param kind: One of the possible object types
426 @return: The item matching criteria or None if none was found"""
427 for index in self.items :
428 if "name" in kwargs and self.items[index].name != kwargs["name"]:
429 continue
430 if "ID" in kwargs and self.items[index].ID != kwargs["ID"]:
431 continue
432 if "kind" in kwargs and not self.items[index].trueAttr(kwargs["kind"]):
433 continue
434 if "item_type" in kwargs and self.items[index].item_type != kwargs["item_type"]:
435 continue
436 return self.items[index]
437 return None
438
439 def serializeItems(self):
440 """Returns the items as a list"""
441 items = []
442 for index, item in self.items.iteritems():
443 item_dict = item.getStateForSaving()
444 item_dict["index"] = index
445 item_dict["type"] = item.item_type
446 items.append(item_dict)
447 return items
448
449 def getStateForSaving(self):
450 """Returns state for saving
451 """
452 ret_state = DynamicObject.getStateForSaving(self)
453 ret_state["Items"] = self.serializeItems()
454 return ret_state
455
456 class Living (BaseObject):
457 """Objects that 'live'"""
458 def __init__ (self, **kwargs):
459 BaseObject.__init__(self)
460 self.attributes.append("living")
461 self.lives = True
462
463 def die(self):
464 """Kills the object"""
465 self.lives = False
466
467 class CharStats (BaseObject):
468 """Provides the object with character statistics"""
469 def __init__ (self, **kwargs):
470 BaseObject.__init__(self)
471 self.attributes.append("charstats")
472
473 class Wearable (BaseObject):
474 """Objects than can be weared"""
475 def __init__ (self, slots, **kwargs):
476 """Allows the object to be worn somewhere on the body (e.g. pants)"""
477 BaseObject.__init__(self)
478 self.attributes.append("wearable")
479 if isinstance(slots, tuple) :
480 self.slots = slots
481 else :
482 self.slots = (slots,)
483
484 class Usable (BaseObject):
485 """Allows the object to be used in some way (e.g. a Zippo lighter
486 to make a fire)"""
487 def __init__ (self, actions = None, **kwargs):
488 BaseObject.__init__(self)
489 self.attributes.append("usable")
490 self.actions = actions or {}
491
492 class Weapon (BaseObject):
493 """Allows the object to be used as a weapon"""
494 def __init__ (self, **kwargs):
495 BaseObject.__init__(self)
496 self.attributes.append("weapon")
497
498 class Destructable (BaseObject):
499 """Allows the object to be destroyed"""
500 def __init__ (self, **kwargs):
501 BaseObject.__init__(self)
502 self.attributes.append("destructable")
503
504 class Trapable (BaseObject):
505 """Provides trap slots to the object"""
506 def __init__ (self, **kwargs):
507 BaseObject.__init__(self)
508 self.attributes.append("trapable")