source: trunk/game/scripts/objects/base.py @ 599

Revision 596, 17.6 KB checked in by beliar, 9 years ago (diff)

Patch by Beliar.

  • Items can now be dropped and picked up again
  • Added generic image for items (Made by Q_x, he did the other items too).
  • 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 is_x class member set to True 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.is_openable = True
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 DynamicObject (object):
57    """A base class that only supports dynamic attributes functionality"""
58    def __init__ (self, name="Dynamic object", real_name=None, image=None, **kwargs):
59        """Initialise minimalistic set of data
60           @type name: String
61           @param name: Object display name
62           @type image: String or None
63           @param name: Filename of image to use in inventory"""
64        self.name = name
65        self.real_name = real_name or name
66        self.image = image
67
68    def prepareStateForSaving(self, state):
69        """Prepares state for saving
70        @type state: dictionary
71        @param state: State of the object 
72        """
73        pass
74   
75    def restoreState(self, state):
76        """Restores a state from a saved state
77        @type state: dictionary
78        @param state: Saved state 
79        """
80        self.__dict__.update(state)
81
82    def __getstate__(self):
83        odict = self.__dict__.copy()
84        self.prepareStateForSaving(odict)
85        return odict
86   
87    def __setstate__(self, state):
88        self.restoreState(state)
89
90    def trueAttr(self, attr):
91        """Shortcut function to check if the current object has a member named
92           is_%attr and if that attribute evaluates to True"""
93        return hasattr(self,'is_%s' % attr) and getattr(self, 'is_%s' % attr)
94   
95    def getStateForSaving(self):
96        """Returns state for saving
97        """
98        state = {}
99        state["Name"] = self.name
100        state["RealName"] = self.real_name
101        state["Image"] = self.image
102        return state
103
104class GameObject (DynamicObject):
105    """A base class to be inherited by all game objects. This must be the
106       first class (left to right) inherited by any game object."""
107    def __init__ (self, ID, gfx = None, xpos = 0.0, ypos = 0.0, map_id = None, 
108                  blocking=True, name="Generic object", real_name="Generic object", text="Item description",
109                  desc="Detailed description", **kwargs):
110        """Set the basic values that are shared by all game objects.
111           @type ID: String
112           @param ID: Unique object identifier. Must be present.
113           @type gfx: Dictionary
114           @param gfx: Dictionary with graphics for the different contexts       
115           @type coords 2-item tuple
116           @param coords: Initial coordinates of the object.
117           @type map_id: String
118           @param map_id: Identifier of the map where the object is located
119           @type blocking: Boolean
120           @param blocking: Whether the object blocks character movement
121           @type name: String
122           @param name: The display name of this object (e.g. 'Dirty crate')
123           @type text: String
124           @param text: A longer description of the item
125           @type desc: String
126           @param desc: A long description of the item that is displayed when it is examined
127           """
128        DynamicObject.__init__(self, name, real_name, **kwargs)
129        self.ID = ID
130        self.gfx = gfx or {}
131        self.X = xpos
132        self.Y = ypos
133        self.map_id = map_id
134        self.blocking = True
135        self.text = text
136        self.desc = desc
137       
138    def _getCoords(self):
139        """Get-er property function"""
140        return (self.X, self.Y)
141   
142    def _setCoords(self, coords):
143        """Set-er property function"""
144        self.X, self.Y = float(coords[0]), float (coords[1])
145       
146    coords = property (_getCoords, _setCoords, 
147        doc = "Property allowing you to get and set the object's \
148                coordinates via tuples")
149   
150    def __repr__(self):
151        """A debugging string representation of the object"""
152        return "<%s:%s>" % (self.name, self.ID)
153
154    def getStateForSaving(self):
155        """Returns state for saving
156        """
157        state = super(GameObject, self).getStateForSaving()
158        state["ObjectModel"] = self.gfx
159        state["Text"] = self.text
160        state["Desc"] = self.desc
161        state["Position"] = list(self.coords)
162        return state
163
164
165class Scriptable (object):
166    """Allows objects to have predefined scripts executed on certain events"""
167    def __init__ (self, scripts = None, **kwargs):
168        """Init operation for scriptable objects
169           @type scripts: Dictionary
170           @param scripts: Dictionary where the event strings are keys. The
171           values are 3-item tuples (function, positional_args, keyword_args)"""
172        self.is_scriptable = True
173        self.scripts = scripts or {}
174       
175    def runScript (self, event):
176        """Runs the script for the given event"""
177        if event in self.scripts and self.scripts[event]:
178            func, args, kwargs = self.scripts[event]
179            func (*args, **kwargs)
180           
181    def setScript (self, event, func, args = None , kwargs = None):
182        """Sets a script to be executed for the given event."""
183        args = args or {}
184        kwargs = kwargs or {}
185        self.scripts[event] = (func, args, kwargs)
186
187class Openable(DynamicObject, Scriptable):
188    """Adds open() and .close() capabilities to game objects
189       The current state is tracked by the .is_open variable"""
190    def __init__(self, is_open = True, **kwargs):
191        """Init operation for openable objects
192           @type is_open: Boolean
193           @param is_open: Keyword boolean argument sets the initial state."""
194        DynamicObject.__init__(self, **kwargs)
195        Scriptable.__init__(self, **kwargs)
196        self.is_openable = True
197        self.is_open = is_open
198   
199    def open(self):
200        """Opens the object, and runs an 'onOpen' script, if present"""
201        self.is_open = True
202        try:
203            if self.trueAttr ('scriptable'):
204                self.runScript('onOpen')
205        except AttributeError :
206            pass
207           
208    def close(self):
209        """Opens the object, and runs an 'onClose' script, if present"""
210        self.is_open = False
211        try:
212            if self.trueAttr ('scriptable'):
213                self.runScript('onClose')
214        except AttributeError :
215            pass
216       
217class Lockable (Openable):
218    """Allows objects to be locked"""
219    def __init__ (self, locked = False, is_open = True, **kwargs):
220        """Init operation for lockable objects
221           @type locked: Boolean
222           @param locked: Keyword boolen argument sets the initial locked state.
223           @type is_open: Boolean
224           @param is_open: Keyword boolean argument sets the initial open state.
225                           It is ignored if locked is True -- locked objects
226                           are always closed."""
227        self.is_lockable = True
228        self.locked = locked
229        if locked :
230            is_open = False
231        Openable.__init__( self, is_open, **kwargs )
232       
233    def unlock (self):
234        """Handles unlocking functionality"""
235        self.locked = False     
236       
237    def lock (self):
238        """Handles  locking functionality"""
239        self.close()
240        self.locked = True
241       
242    def open (self, *args, **kwargs):
243        """Adds a check to see if the object is unlocked before running the
244           .open() function of the parent class"""
245        if self.locked:
246            raise ValueError ("Open failed: object locked")
247        super (Lockable, self).open(*args, **kwargs)
248       
249class Carryable (DynamicObject):
250    """Allows objects to be stored in containers"""
251    def __init__ (self, weight=0.0, bulk=0.0, **kwargs):
252        DynamicObject.__init__(self, **kwargs)
253        self.is_carryable = True
254        self.in_container = None
255        self.on_map = None
256        self.agent = None
257        self.weight = weight
258        self.bulk = bulk
259
260    def getInventoryThumbnail(self):
261        """Returns the inventory thumbnail of the object"""
262        # TODO: Implement properly after the objects database is in place
263        if self.image == None:
264            return "gui/inv_images/inv_litem.png"
265        else:
266            return self.image
267   
268class Container (DynamicObject, Scriptable):
269    """Gives objects the capability to hold other objects"""
270    class TooBig(Exception):
271        """Exception to be raised when the object is too big
272        to fit into container"""
273        pass
274   
275    class SlotBusy(Exception):
276        """Exception to be raised when the requested slot is occupied"""
277        pass
278
279    def __init__ (self, capacity = 0, items = None, **kwargs):
280        DynamicObject.__init__(self, **kwargs)
281        Scriptable.__init__(self, **kwargs)
282        self.is_container = True
283        self.items = {}
284        self.capacity = capacity
285        if items:
286            for item in items:
287                self.placeItem(item)
288       
289    def placeItem (self, item, index=None):
290        """Adds the provided carriable item to the inventory.
291           Runs an 'onStoreItem' script, if present"""   
292        if not item.trueAttr ('carryable'):
293            raise TypeError ('%s is not carriable!' % item)
294        if self.capacity and self.getContentsBulk()+item.bulk > self.capacity:
295            raise self.TooBig ('%s is too big to fit into %s' % (item, self))
296        item.in_container = self
297        if index == None:
298            self._placeAtVacant(item)
299        else:
300            if index in self.items :
301                raise self.SlotBusy('Slot %d is busy in %s' % (index, 
302                                                               self.name))
303            self.items[index] = item
304
305        # Run any scripts associated with storing an item in the container
306        try:
307            if self.trueAttr ('scriptable'):
308                self.runScript('onPlaceItem')
309        except AttributeError :
310            pass
311
312    def _placeAtVacant(self, item):
313        """Places an item at a vacant slot"""
314        vacant = None
315        for i in range(len(self.items)):
316            if i not in self.items :
317                vacant = i
318        if vacant == None :
319            vacant = len(self.items)
320        self.items[vacant] = item
321   
322    def takeItem (self, item):
323        """Takes the listed item out of the inventory.
324           Runs an 'onTakeItem' script"""       
325        if not item in self.items.values():
326            raise ValueError ('I do not contain this item: %s' % item)
327        del self.items[self.items.keys()[self.items.values().index(item)]]
328
329        # Run any scripts associated with popping an item out of the container
330        try:
331            if self.trueAttr ('scriptable'):
332                self.runScript('onTakeItem')
333        except AttributeError :
334            pass
335   
336    def replaceItem(self, old_item, new_item):
337        """Replaces the old item with the new one
338        @param old_item: Old item which is removed
339        @type old_item: Carryable
340        @param new_item: New item which is added
341        @type new_item: Carryable
342        """
343        old_index = self.indexOf(old_item.ID)
344        self.removeItem(old_item)
345        self.placeItem(new_item, old_index)
346       
347    def removeItem(self, item):
348        """Removes an item from the container, basically the same as 'takeItem'
349        but does run a different script. This should be used when an item is
350        destroyed rather than moved out.
351        Runs 'onRemoveItem' script
352        """
353        if not item in self.items.values():
354            raise ValueError ('I do not contain this item: %s' % item)
355        del self.items[self.items.keys()[self.items.values().index(item)]]
356
357        # Run any scripts associated with popping an item out of the container
358        try:
359            if self.trueAttr ('scriptable'):
360                self.runScript('onRemoveItem')
361        except AttributeError :
362            pass
363
364    def count (self, item_id = ""):
365        """Returns the number of items"""
366        if item_id:
367            ret_count = 0
368            for index in self.items :
369                if self.items[index].item_id == item_id:
370                    ret_count += 1
371            return ret_count
372        return len(self.items)   
373   
374    def getContentsBulk(self):
375        """Bulk of the container contents"""
376        return sum((item.bulk for item in self.items.values()))
377
378    def getItemAt(self, index):
379        return self.items[index]
380   
381    def indexOf(self, ID):
382        """Returns the index of the item with the passed ID"""
383        for index in self.items :
384            if self.items[index].ID == ID:
385                return index
386        return None
387
388    def findItemByID(self, ID):
389        """Returns the item with the passed ID"""
390        for i in self.items :
391            if self.items[i].ID == ID:
392                return self.items[i]
393        return None
394
395    def findItemByItemID(self, item_id):
396        """Returns the item with the passed item_id"""
397        for index in self.items :
398            if self.items[index].item_id == item_id:
399                return self.items[index]
400        return None
401
402    def findItem(self, **kwargs):
403        """Find an item in container by attributes. All params are optional.
404           @type name: String
405           @param name: If the name is non-unique, return first matching object
406           @type kind: String
407           @param kind: One of the possible object types
408           @return: The item matching criteria or None if none was found"""
409        for index in self.items :
410            if "name" in kwargs and self.items[index].name != kwargs["name"]:
411                continue
412            if "ID" in kwargs and self.items[index].ID != kwargs["ID"]:
413                continue
414            if "kind" in kwargs and not self.items[index].trueAttr(kwargs["kind"]):
415                continue
416            if "item_id" in kwargs and self.items[index].item_id != kwargs["item_id"]:
417                continue
418            return self.items[index]
419        return None   
420   
421    def serializeItems(self):
422        """Returns the items as a list"""
423        items = []
424        for index, item in self.items.iteritems():
425            item_dict = item.getStateForSaving()
426            item_dict["index"] = index
427            item_dict["type"] = type(item).__name__
428            items.append(item_dict)
429        return items
430   
431    def getStateForSaving(self):
432        """Returns state for saving
433        """
434        ret_state = {}
435        ret_state["Items"] = self.serializeItems()
436        return ret_state
437       
438class Living (object):
439    """Objects that 'live'"""
440    def __init__ (self, **kwargs):
441        self.is_living = True
442
443    def die(self):
444        """Kills the object"""
445        self.is_living = False   
446
447class CharStats (object):
448    """Provides the object with character statistics"""
449    def __init__ (self, **kwargs):
450        self.is_charstats = True
451       
452class Wearable (object):
453    """Objects than can be weared"""
454    def __init__ (self, slots, **kwargs):
455        """Allows the object to be worn somewhere on the body (e.g. pants)"""
456        self.is_wearable = True
457        if isinstance(slots, tuple) :
458            self.slots = slots
459        else :
460            self.slots = (slots,)
461   
462class Usable (object):
463    """Allows the object to be used in some way (e.g. a Zippo lighter
464       to make a fire)"""
465    def __init__ (self, actions = None, **kwargs):
466        self.is_usable = True
467        self.actions = actions or {}   
468       
469class Weapon (object):
470    """Allows the object to be used as a weapon"""
471    def __init__ (self, **kwargs):
472        self.is_weapon = True
473       
474class Destructable (object):
475    """Allows the object to be destroyed"""
476    def __init__ (self, **kwargs):
477        self.is_destructable = True
478       
479class Trappable (object):
480    """Provides trap slots to the object"""
481    def __init__ (self, **kwargs):
482        self.is_trappable = True
Note: See TracBrowser for help on using the repository browser.