Mercurial > parpg-source
comparison objects/base.py @ 0:7a89ea5404b1
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:7a89ea5404b1 |
---|---|
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") |