adonthell-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[Adonthell-devel] Item spec


From: Kai Sterker
Subject: [Adonthell-devel] Item spec
Date: Sun, 12 Jan 2003 16:40:43 +0100

This mail gonna be long and detailed; you have been warned ;).

I was thinking about coding the lowlevel character/item/inventory stuff.
Guess it will be losely based on the code already in the 0.4 branch
(src/tools/item). But before doing the actual work, I wanted to get a
little spec done. That'll make the coding easier, and also allow some
thoughts and improvements before any code has been written. So please
correct me if something seems wrong or simply not good enough!


Assuming the item architecture will be as suggested earlier, then the
first question is what basic item attributes are there?

Basic item attributes:

* name (string)
  The item's name. Like "Short sword", "Aegnar's Hammer" or "Oil lamp".

  An item's name needs not be unique, but could still be used to check
  whether the player posesses a certain item. Whether it is Oil lamp A 
  or B shouldn't matter much.

* type (list of strings)
  The 'categories' an item belongs to. Like "Weapon", "Sword", "Temple
  property" or "Banec's equipment". Obviously, an item can belong to
  several categories at a time.

  The 'type' attribute can be used to check whether the player carries
  an item of a certain category. Some categories would apply to all
  items of a kind, others only to items with a certain origin or owner.

  Of course, it makes no sense to have categories that are not used
  throughout the game. For convenience, it should be possible to add or
  remove categories throughout an items lifetime. (Like a burned out
  torch could have the "Lightsource" type removed, but still act as 
  "Club" or "Weapon")

* quantity (int)
  For items that are stackable, it's important to know how many are in a
  stack. Of course we also need a flag to tell whether an item is
  stackable or not. Usually, small, uniform items like gems or arrows
  should be stackagble, while large ones shouldn't.

  There should also be a max_quantity value. Stacking zillions of gems
  seems a bit unrealistic.

  Further, an item needs to be destroyed when it's quantity reaches 0.

* charge (int)
  For items that require some sort of 'fuel' to work, like lamps, or
  that have a limited number of uses, like potions. Not sure whether
  we need a special flag for those items.

  What we need is a charge_type though, to specify what item(s) may 
  be used to recharge the item. Here the empty string ("") might be
  used to indicate that the item is not rechargeble.

  When the charge reaches 0, there are different possibilities. An
  item that is not rechargable might be destroyed (candle) or 
  transformed into another item (potion -> empty bottle). Or it just
  remains unusable until recharged.

  We probably need a max_charge value too.

  As a matter of fact, items can not be chargable and stackable at a
  time, as stackable items share the same attributes. So using up one
  item of the stack would use up all the others.

* value (int or float)
  How much the item is worth compared to other items. This attribute
  does not define the items price when buying or selling, but it is the 
  base for calculating the price. Other attributes, like the player's 
  barter skill or a merchant-specific modifier will play a role here.


Okay, the above list contains all attributes that should be useful to
any kind of item system. There are a few more of which I am not so sure.

* description (string)
  A detailed description of an item. Especially useful for unique items
  of course. But other items could as well have a little bit of
  background information attached to them.

  With the description could go an identified flag, which would indicate
  whether the player knows what the item is and does. Furthermore,
  unidentified items would probably not be usable.

* weight (int of float)
  The weight of an item. If we want to limit the carrying capacity of
  characters, such an attribute might be useful. But we may also rely 
  entirely on the number of slots a character has. Still, others may
  want to use weight instead.

  One question related to weight is how chargable and stackable items
  are treated. Some chargables may lose weight when being used up,
  others won't. Maybe there could be a weight_per_charge modifier value.
  
  The actual weight would then be: weight + weight_per_charge * charge

  The weight of an item stack can of course be calculated by multiplying
  weight and quantity.


As far as an item editor is concerned, it only needs to be able to set
or change the above values, plus do a little sanity checking.
  

Well, so much for basic item attributes. Anything I have forgotten?
Anything that should be changed?

What remains, the interface to the Python item implementation, isn't so
easy. So what follows here is less sane and detailed as the above.
Mainly suggestions and thoughts rather than proper ideas.

First of all, lets look how the C++ and Python side of items can be
integrated:

A If a specific item on python side would simply extend the C++
  item_base class, then it would see the attributes and methods of it's
  base class, but it wouldn't be visible on C++ side, where we'd only
  work with item_base instances. However, would we really need to access
  such extended attributes on C++ side? Or, the other way round,
  shouldn't all attributes that are required on C++ side be also kept on
  C++ side?

B Instead of extending the C++ item_base class we could also add the
  topmost python class as a member to the item_base class (via our
  py_object class). That way, the C++ side would see the python methods
  and attributes. However, the python side would not see the base_item
  class, as it doesn't inherit from it. Of course one could pass a
  reference to the item_base instance to each method of the python
  class.

C Another way is combining the two alternative. Python item classes
  inherit from the item_base class and the item_base class gets a
  'pointer' to the (topmost) python extension class. That way, the C++
  side could access all attributes and methods of the python class (via
  the py_object interface). And on python side, transparent access to
  the underlying item_base instance is possible.

Now lets have a look how different things would work with method A, B
and C respectively.

* Creating a new item on C++ side: 

  A: Tricky. Create the desired item as PyObject (via the py_object
     class), then aquire a pointer to the item_base class. Would
     probably require the complement to the python::pass_instance
     method.

  B: Instanciate an object of the item_base class. At construction time,
     it'll automatically instanciate the py_object member to the
     desired item. (Means the item_base constructor requires the name of
     the desired Python item class.)
   
  C: Tricky as well. If we do it as in (B) we end up with two item_base
     instances. The one we created on C++ side, and the one that is
     created when instanciating the py_object.

     So it must be done in the fashion of (A). Afterwards, we can assign
     the py_object we created first to the py_object member of the
     item_base class we retrieved via a hypothetic python::get_instance.


* Creating a new item on Python side:

  A: Easy. Instanciate the desired item type. Done.

  B: Easy. Instanciate an item_base object. Rest is like (B) above.

  C: Not so easy. Instanciate the desired item type. Then pass this
     instance to a new py_object instance. Finally assign this to the
     py_object member of the item. (Requires a version of py_object 
     that can be given an existing Python instance instead of creating
     a new one)


* Loading an item:
  This would probably be done by first creating an empty item, then
  calling it's load method.

  A: From creating the item we still have the py_object containing 
     the actual item on Python side. We can call it's 'load' method.
     That way it can load it's own attributes, then call 'load' of
     the parent class. At the end, the whole item is loaded,

  B: We can call item_base::load to load the basic attributes. From
     that method we can also call 'load' of the py_object member.    

  C: Much like (A).

  Saving an item will work analog.
  

* Destroying an item on C++ side:

  A: We probably have to keep the py_object instance of the item
     around. When deleting that, everything would be free'd.

  B: The item_base destructor will also destroy the py_object, and thus
     the complete item.

  C: As long as 'thisown' of the item_base shadow class is 0, we can do
     it same way as (B). (Otherwise, the item_base instance would get 
     free'd twice.)
 

* Destroying an item on Python side:

  A: As simple as 'del some_item'.

  B: As long as we have a reference to the item_base we can simply
     delete that. Otherwise it won't be possible.

  C: Needs to be 'del item_base.some_item'. Otherwise like (C) above.


Well, when looking at the above, I'd say that (A) is no good at all, as
we'd end up with two objects for each item (an item_base and a
py_object). Remain (B) and (C). Seeing that the implementation of (C) is
more complex, I guess (B) is the way to go. Of course, further
suggestions or even completely different alternatives are welcome.


The final part of this mail is about the actions an item must allow.
Those make up the methods that a python item class may provide for
item-dependant reaction to a character performing an action with the
item. All that the C++ item_base class needs to do is to check for the
presence of the desired 'action'. If it is provided by the item, it
needs to be executed. Otherwise, the item_base class can indicate that 
the item does not support that action.


First of all I'll describe the possible actions, then I'll go into some
implementation details.

* Pick up
  Picking up an item usually involves moving it from one inventory to
  another. This shouldn't require any special actions from the item
  itself.

  Items can be picked up from the map, from containers or characters.
  That means buying, stealing or simply being given an item are treated
  as 'picking up' when it comes to putting that item into the player's
  inventory.
  
  In case the character picking up that item has no more room in the
  inventory it should be dropped onto the ground.

* Drop
  Unlike picking up, there is a bit more to dropping than just switching
  inventories. If an item is important for the plot, the character might
  not be able to drop it so easily. (He may only be able to hand it to 
  a certain character.)

  Equipped items need to be unequipped before dropping them.

  Dropping includes placing items on the map, into containers or giving
  them to other characters. That includes selling, robbery and simply
  giving items away.

* Equip
  Equipping means moving an item from the inventory onto the character.
  Of course the character must be able to 'wear' that item; i.e. have a
  free slot or the required attributes. In which slot an item fits could
  be defined by the item type.

  If an item has any constant effects, they'll have to be applied.

* Unequip
  Unequipping moves an item from the character to his inventory. If the
  inventory is full, the item should be dropped to the ground instead.

  If an item had any effects, they'll have to be removed.

* Use
  What happens here depends much on the item being used. A lightsource
  might be turned on or off, a potion might be consumed. That means the
  use method needs to be implemented on python side.


As far as I can tell, those five 'actions' cover everything we'll ever
need. Note that these methods affect items, but would be implemented on
character level. I.e. you'd have 'character.use (item)', not 'item.use
(character)'.

So lets have a look at the parameters and conditions required for
each method.

* bool pick_up (item_base *item)
  Before an item can be picked up, it must have been dropped (i.e.
  removed from the inventory it has been in). As an item can be picked
  up in different ways (from the map, from containers, buying, stealing,
  etc. ...) different code for 'dropping' is required for each case,
  which can't go into the pick_up method.

  When picking up an item, at least three instances would be involved:
  The character recieving the item, the character's inventory, and the 
  item itself.

  First 'character.pick_up (item)' will be called. Then the character's
  inventory needs to be checked for free space. In case of stackable
  items, only part of the stack that has been picked up might fit.

  If there's enough room in the inventory, we can call the python side
  'pick_up' method if the item has one. This can do item-specific tests
  or trigger some event, like setting off alarms or traps and the like.

  If the python side method exists and returns true, the item can be
  added to the inventory at last.

  (For example, if a certain character would refuse to carry a certain
  item, this would be implemented on python side.)

* item_base* drop (item_base *item)
  drop will remove the desired item from the character and return the
  item, or NULL if it can't be dropped for some reason. Before an item
  can be dropped, we'll have to retrieve a pointer to that item. This
  can be done by a method like
  
      vector<item_base*> get_item (string item, int flag)
  
  'flag' would indicate whether 'item' is an item name or item type. It
  would return a list of items with matching name or type.

  Once we have the pointer to the desired item, we need to check whether
  it is equipped. If that is the case, it needs to be unequipped, if
  possible. If it can be unequipped, the python 'drop' method would be
  called (if available) to do item-specific stuff. If that one returns
  true, the item can finally be removed from the inventory and the
  method can return.

* bool equip (item_base *item)
  As it is yet unclear how characters equip items, the exact procedure
  isn't known yet. However, first of all the python 'equip' method will
  be called to check whether the item can be equipped by the given
  character and to apply any required modifications.

* bool unequip (item_base *item)
  Much like 'equip'. First we call python 'unequip' if it is present, to
  check whether the item can be unequipped and to remove all
  modifications. Then we actually unequip it.

* bool use (item_base *item)
  Nothing needs be done on C++ side. Just call the python 'use' method
  (if available) to do what needs to be done.


What currently isn't covered are things concerning specific item types,
like weapons or armour. Their implementation will depend upon how the
role playing system is implemented. But it should be easy to add
whatever will become neccessary on top of the basic item implementation.


Since all of the above was pretty dry, here's a demo item: a torch.

    import adonthell

    # -- we use the event system to implement using up charges. That's
    #    why we have to inherit from 'event_list'.
    class torch (adonthell.event_list):
        __init__(self):
            # -- flag to indicate whether the torch is on or off
            self.is_burning = 0

        # -- depending on the current state, the torch will be either
        #    turned on or off. Parameters are the item_base instance 
        #    (since we're using method (B)) and the character using the 
        #    item. Latter isn't required for the torch, but other items
        #    will need him. 
        use (self, item, user):
            # -- turn it on
            if self.is_burning == 0:
                # -- does it have enough charge?
                if item.charge == 0: return
                
                # -- register time event so it is consumed while burning
                #    One charge lasts 5 minutes of game time
                self.consume_event = adonthell.time_event ("5m")

                # -- callback to execute whenever a charge has been used
                #    up.
                self.consume_event.set_callback (self.on_burn, ...)

Um, well, here's the first problem. We would have to pass 'item' as
parameter to the callback, because that contains the number of charges.
But if we pass item, the event cannot be saved and restored, as only
integer and string arguments are allowed for that. There are a couple of
possible workarounds:

Either we do use method (C) instead of (B). Then we can access the
'charge' member from the callback without passing any argument.

Or we do something like 'self.charge = item.charge' and use that copy
within the callback. That's very dirty however, and will surely cause
other problems. For example the item weight wouldn't be calculated
correctly.

Or we could pass the character name and item name to the callback. Then
we could restore the character and try to get the item back. However,
how can we make sure we get the right torch if the character carries
multiple? That might be possible by setting a unique item type for the
time being, but there is one more problem: what if the character has
dropped the torch meanwhile? In that case, there's no way for the
callback to retrieve it from the character! 

So it seems that even though method (C) is more complex, we'll have to
use it to avoid even more serious problems. So lets switch to method (C)
and start over with the item:

    import adonthell

    # -- now we inherit from item_base too
    class torch (adonthell.item_base, adonthell.event_list):
        __init__(self):
            self.is_burning = 0

        # -- so no need to pass the item_base instance, as we can
        #    easily access it
        use (self, user):
            # -- turn it on
            if self.is_burning == 0:
                # -- does it have enough charge?
                if self.charge == 0: return
                
                # -- register time event so it is consumed while burning
                #    One charge lasts 5 minutes of game time
                self.consume_event = adonthell.time_event ("5m")

                # -- callback to execute whenever a charge has been used
                #    up.
                self.consume_event.set_callback (self.on_burn)

                # -- repeat event as often as item has charges
                self.consume_event.set_repeat (self.charge)

                # -- register consume event with event_list
                self.add (consume_event)
                self.is_burning = 1
 
                # -- while it's burning, make it a lightsource
                self.add_type ("Lightsource")

                # -- start any graphical effects, etc
                ...
 
            # -- switch it off
            else:
                self.is_burning = 0
                del self.consume_event

                # -- not burning, so no lightsource
                self.remove_type ("Lightsource")

                # -- remove any graphical effects, etc
                ...

    on_burn (self):
        # -- decrease item's charge
        self.charge--

        # -- if no charges left, turn off torch
        if self.charge == 0:
            # -- the 'use' method will do that for us
            self.use (None)
    
    put_state (self, file):
        # -- save state of parent class
        adonthell.item_base.put_state (self, file)

        # -- save event list
        adonthell.event_list.put_state (self, file)

        # -- save own parameters
        file.put_bool (is_burning)

    get_state (self, file):
        # -- load state of parent class
        adonthell.item_base.get_state (self, file)

        # -- load event list
        adonthell.event_list.get_state (self, file)

        # -- load own parameters
        is_burning = file.get_bool ()

That should have been it. Now we have a torch that can be turned on and
off. While burning, it will use up it's charges, and when it runs out of
charges, it will be turned off automatically. After that, it can't be
turned on again. It can be saved and loaded any time too, without losing
it's current state. And it can be dropped or passed along while burning
without problems. The only flaw is that it will not use up charges when
burning less than 5 minutes. This could be fixed with a little effort
though.

The only thing that is currently missing is the actual graphical effect
of a burning torch.


One more word about the item editor. Practically all items defined on
Python side will have some attributes not defined on C++ side. The torch
for example has the 'is_burning' flag. How can an item editor deal with
that? The only solution is to read those attributes at runtime and
create a dynamic interface for editing each item. For example, we could
add some sort of comments to the top of each item source file that the 
editor can parse and recognize. If an item does not inherit from
item_base directly, the editor would also have to check the parent class
for its attributes and so on. 


With that I'll come to an end. Sorry if it's a lot of info, and not all
of it explained in detail. If nobody sees any huge flaws in the above, I
would go about implementing (and documenting) that stuff. Sounds okay?

Kai




reply via email to

[Prev in Thread] Current Thread [Next in Thread]