source: branches/active/character_customization/game/parpg/objects/base.py @ 736

Revision 736, 18.6 KB checked in by aspidites, 8 years ago (diff)

Patch by Aspidites:

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