Merge branch 'main' into evadventure_work
This commit is contained in:
commit
541716d979
6 changed files with 437 additions and 1 deletions
|
|
@ -1,5 +1,13 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Main branch
|
||||||
|
|
||||||
|
- Contrib: Container typeclass with new commands for storing and retrieving
|
||||||
|
things inside them (InspectorCaracal)
|
||||||
|
- Fix: The `AttributeHandler.all()` now actually accepts `category=` as
|
||||||
|
keyword arg, like our docs already claimed it should (Volund)
|
||||||
|
|
||||||
|
|
||||||
## Evennia 1.3.0
|
## Evennia 1.3.0
|
||||||
|
|
||||||
Apr 29, 2023
|
Apr 29, 2023
|
||||||
|
|
|
||||||
55
evennia/contrib/game_systems/containers/README.md
Normal file
55
evennia/contrib/game_systems/containers/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Containers
|
||||||
|
Contribution by InspectorCaracal (2023)
|
||||||
|
|
||||||
|
Adds the ability to put objects into other container objects by providing a container typeclass and extending certain base commands.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To install, import and add the `ContainerCmdSet` to `CharacterCmdSet` in your `default_cmdsets.py` file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from evennia.contrib.game_systems.containers import ContainerCmdSet
|
||||||
|
|
||||||
|
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||||
|
# ...
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
# ...
|
||||||
|
self.add(ContainerCmdSet)
|
||||||
|
```
|
||||||
|
|
||||||
|
This will replace the default `look` and `get` commands with the container-friendly versions provided by the contrib as well as add a new `put` command.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The contrib includes a `ContribContainer` typeclass which has all of the set-up necessary to be used as a container. To use, all you need to do is create an object in-game with that typeclass - it will automatically inherit anything you implemented in your base Object typeclass as well.
|
||||||
|
|
||||||
|
create bag:game_systems.containers.ContribContainer
|
||||||
|
|
||||||
|
The contrib's `ContribContainer` comes with a capacity limit of a maximum number of items it can hold. This can be changed per individual object.
|
||||||
|
|
||||||
|
In code:
|
||||||
|
```py
|
||||||
|
obj.capacity = 5
|
||||||
|
```
|
||||||
|
In game:
|
||||||
|
|
||||||
|
set box/capacity = 5
|
||||||
|
|
||||||
|
You can also make any other objects usable as containers by setting the `get_from` lock type on it.
|
||||||
|
|
||||||
|
lock mysterious box = get_from:true()
|
||||||
|
|
||||||
|
## Extending
|
||||||
|
|
||||||
|
The `ContribContainer` class is intended to be usable as-is, but you can also inherit from it for your own container classes to extend its functionality. Aside from having the container lock pre-set on object creation, it comes with three main additions:
|
||||||
|
|
||||||
|
### `capacity` property
|
||||||
|
|
||||||
|
`ContribContainer.capacity` is an `AttributeProperty` - meaning you can access it in code with `obj.capacity` and also set it in game with `set obj/capacity = 5` - which represents the capacity of the container as an integer. You can override this with a more complex representation of capacity on your own container classes.
|
||||||
|
|
||||||
|
### `at_pre_get_from` and `at_pre_put_in` methods
|
||||||
|
|
||||||
|
These two methods on `ContribContainer` are called as extra checks when attempting to either get an object from, or put an object in, a container. The contrib's `ContribContainer.at_pre_get_from` doesn't do any additional validation by default, while `ContribContainer.at_pre_put_in` does a simple capacity check.
|
||||||
|
|
||||||
|
You can override these methods on your own child class to do any additional capacity or access checks.
|
||||||
1
evennia/contrib/game_systems/containers/__init__.py
Normal file
1
evennia/contrib/game_systems/containers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from .containers import ContainerCmdSet, ContribContainer # noqa
|
||||||
307
evennia/contrib/game_systems/containers/containers.py
Normal file
307
evennia/contrib/game_systems/containers/containers.py
Normal file
|
|
@ -0,0 +1,307 @@
|
||||||
|
"""
|
||||||
|
Containers
|
||||||
|
|
||||||
|
Contribution by InspectorCaracal (2023)
|
||||||
|
|
||||||
|
Adds the ability to put objects into other container objects by providing a container typeclass and extending certain base commands.
|
||||||
|
|
||||||
|
To install, import and add the `ContainerCmdSet` to `CharacterCmdSet` in your `default_cmdsets.py` file:
|
||||||
|
|
||||||
|
from evennia.contrib.game_systems.containers import ContainerCmdSet
|
||||||
|
|
||||||
|
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||||
|
# ...
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
# ...
|
||||||
|
self.add(ContainerCmdSet)
|
||||||
|
|
||||||
|
The ContainerCmdSet includes:
|
||||||
|
|
||||||
|
- a modified `look` command to look at or inside objects
|
||||||
|
- a modified `get` command to get objects from your location or inside objects
|
||||||
|
- a new `put` command to put objects from your inventory into other objects
|
||||||
|
|
||||||
|
Create objects with the `ContribContainer` typeclass to easily create containers,
|
||||||
|
or implement the same locks/hooks in your own typeclasses.
|
||||||
|
|
||||||
|
`ContribContainer` implements the following new methods:
|
||||||
|
|
||||||
|
at_pre_get_from(getter, target, **kwargs) - called with the pre-get hooks
|
||||||
|
at_pre_put_in(putter, target, **kwargs) - called with the pre-put hooks
|
||||||
|
"""
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from evennia import AttributeProperty, CmdSet, DefaultObject
|
||||||
|
from evennia.commands.default.general import CmdLook, CmdGet, CmdDrop
|
||||||
|
from evennia.utils import class_from_module
|
||||||
|
|
||||||
|
# establish the right inheritance for container objects
|
||||||
|
_BASE_OBJECT_TYPECLASS = class_from_module(settings.BASE_OBJECT_TYPECLASS, DefaultObject)
|
||||||
|
|
||||||
|
|
||||||
|
class ContribContainer(_BASE_OBJECT_TYPECLASS):
|
||||||
|
"""
|
||||||
|
A type of Object which can be used as a container.
|
||||||
|
|
||||||
|
It implements a very basic "size" limitation that is just a flat number of objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This defines how many objects the container can hold.
|
||||||
|
capacity = AttributeProperty(default=20)
|
||||||
|
|
||||||
|
def at_object_creation(self):
|
||||||
|
"""
|
||||||
|
Extends the base object `at_object_creation` method by setting the "get_from" lock to "true",
|
||||||
|
allowing other objects to be put inside and removed from this object.
|
||||||
|
|
||||||
|
By default, a lock type not being explicitly set will fail access checks, so objects without
|
||||||
|
the new "get_from" access lock will fail the access checks and continue behaving as
|
||||||
|
non-container objects.
|
||||||
|
"""
|
||||||
|
super().at_object_creation()
|
||||||
|
self.locks.add("get_from:true()")
|
||||||
|
|
||||||
|
def at_pre_get_from(self, getter, target, **kwargs):
|
||||||
|
"""
|
||||||
|
This will be called when something attempts to get another object FROM this object,
|
||||||
|
rather than when getting this object itself.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
getter (Object): The actor attempting to take something from this object.
|
||||||
|
target (Object): The thing this object contains that is being removed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
boolean: Whether the object `target` should be gotten or not.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
If this method returns False/None, the getting is cancelled before it is even started.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def at_pre_put_in(self, putter, target, **kwargs):
|
||||||
|
"""
|
||||||
|
This will be called when something attempts to put another object into this object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
putter (Object): The actor attempting to put something in this object.
|
||||||
|
target (Object): The thing being put into this object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
boolean: Whether the object `target` should be put down or not.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
If this method returns False/None, the putting is cancelled before it is even started.
|
||||||
|
To add more complex capacity checks, modify this method on your child typeclass.
|
||||||
|
"""
|
||||||
|
# check if we're already at capacity
|
||||||
|
if len(self.contents) >= self.capacity:
|
||||||
|
singular, _ = self.get_numbered_name(1, putter)
|
||||||
|
putter.msg(f"You can't fit anything else in {singular}.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class CmdContainerLook(CmdLook):
|
||||||
|
"""
|
||||||
|
look at location or object
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
look
|
||||||
|
look <obj>
|
||||||
|
look <obj> in <container>
|
||||||
|
look *<account>
|
||||||
|
|
||||||
|
Observes your location or objects in your vicinity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
rhs_split = (" in ",)
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
"""
|
||||||
|
Handle the looking.
|
||||||
|
"""
|
||||||
|
caller = self.caller
|
||||||
|
# by default, we don't look in anything
|
||||||
|
container = None
|
||||||
|
|
||||||
|
if not self.args:
|
||||||
|
target = caller.location
|
||||||
|
if not target:
|
||||||
|
self.msg("You have no location to look at!")
|
||||||
|
return
|
||||||
|
elif self.rhs:
|
||||||
|
# we are looking in something, find that first
|
||||||
|
container = caller.search(self.rhs)
|
||||||
|
if not container:
|
||||||
|
return
|
||||||
|
|
||||||
|
target = caller.search(self.lhs, location=container)
|
||||||
|
if not target:
|
||||||
|
return
|
||||||
|
|
||||||
|
desc = caller.at_look(target)
|
||||||
|
# add the type=look to the outputfunc to make it
|
||||||
|
# easy to separate this output in client.
|
||||||
|
self.msg(text=(desc, {"type": "look"}), options=None)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdContainerGet(CmdGet):
|
||||||
|
"""
|
||||||
|
pick up something
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
get <obj>
|
||||||
|
get <obj> from <container>
|
||||||
|
|
||||||
|
Picks up an object from your location or a container and puts it in
|
||||||
|
your inventory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
rhs_split = (" from ",)
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
caller = self.caller
|
||||||
|
# by default, we get from the caller's location
|
||||||
|
location = caller.location
|
||||||
|
|
||||||
|
if not self.args:
|
||||||
|
self.msg("Get what?")
|
||||||
|
return
|
||||||
|
|
||||||
|
# check for a container as the location to get from
|
||||||
|
if self.rhs:
|
||||||
|
location = caller.search(self.rhs)
|
||||||
|
if not location:
|
||||||
|
return
|
||||||
|
# check access lock
|
||||||
|
if not location.access(caller, "get_from"):
|
||||||
|
# supports custom error messages on individual containers
|
||||||
|
if location.db.get_from_err_msg:
|
||||||
|
self.msg(location.db.get_from_err_msg)
|
||||||
|
else:
|
||||||
|
self.msg("You can't get things from that.")
|
||||||
|
return
|
||||||
|
|
||||||
|
obj = caller.search(self.lhs, location=location)
|
||||||
|
if not obj:
|
||||||
|
return
|
||||||
|
if caller == obj:
|
||||||
|
self.msg("You can't get yourself.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# check if this object can be gotten
|
||||||
|
if not obj.access(caller, "get") or not obj.at_pre_get(caller):
|
||||||
|
if obj.db.get_err_msg:
|
||||||
|
self.msg(obj.db.get_err_msg)
|
||||||
|
else:
|
||||||
|
self.msg("You can't get that.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# calling possible at_pre_get_from hook on location
|
||||||
|
if hasattr(location, "at_pre_get_from") and not location.at_pre_get_from(caller, obj):
|
||||||
|
self.msg("You can't get that.")
|
||||||
|
return
|
||||||
|
|
||||||
|
success = obj.move_to(caller, quiet=True, move_type="get")
|
||||||
|
if not success:
|
||||||
|
self.msg("This can't be picked up.")
|
||||||
|
else:
|
||||||
|
singular, _ = obj.get_numbered_name(1, caller)
|
||||||
|
if location == caller.location:
|
||||||
|
# we're picking it up from the area
|
||||||
|
caller.location.msg_contents(f"$You() $conj(pick) up {singular}.", from_obj=caller)
|
||||||
|
else:
|
||||||
|
# we're getting it from somewhere else
|
||||||
|
container_name, _ = location.get_numbered_name(1, caller)
|
||||||
|
caller.location.msg_contents(
|
||||||
|
f"$You() $conj(get) {singular} from {container_name}.", from_obj=caller
|
||||||
|
)
|
||||||
|
# calling at_get hook method
|
||||||
|
obj.at_get(caller)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdPut(CmdDrop):
|
||||||
|
"""
|
||||||
|
put an object into something else
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
put <obj> in <container>
|
||||||
|
|
||||||
|
Lets you put an object from your inventory into another
|
||||||
|
object in the vicinity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "put"
|
||||||
|
rhs_split = ("=", " in ", " on ")
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
caller = self.caller
|
||||||
|
if not self.args:
|
||||||
|
self.msg("Put what in where?")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.rhs:
|
||||||
|
super().func()
|
||||||
|
return
|
||||||
|
|
||||||
|
obj = caller.search(
|
||||||
|
self.lhs,
|
||||||
|
location=caller,
|
||||||
|
nofound_string=f"You aren't carrying {self.args}.",
|
||||||
|
multimatch_string=f"You carry more than one {self.args}:",
|
||||||
|
)
|
||||||
|
if not obj:
|
||||||
|
return
|
||||||
|
|
||||||
|
container = caller.search(self.rhs)
|
||||||
|
if not container:
|
||||||
|
return
|
||||||
|
|
||||||
|
# check access lock
|
||||||
|
if not container.access(caller, "get_from"):
|
||||||
|
# supports custom error messages on individual containers
|
||||||
|
if container.db.put_err_msg:
|
||||||
|
self.msg(container.db.put_err_msg)
|
||||||
|
else:
|
||||||
|
self.msg("You can't put things in that.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Call the object script's at_pre_drop() method.
|
||||||
|
if not obj.at_pre_drop(caller):
|
||||||
|
self.msg("You can't put that down.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Call the container's possible at_pre_put_in method.
|
||||||
|
if hasattr(container, "at_pre_put_in") and not container.at_pre_put_in(caller, obj):
|
||||||
|
self.msg("You can't put that there.")
|
||||||
|
return
|
||||||
|
|
||||||
|
success = obj.move_to(container, quiet=True, move_type="drop")
|
||||||
|
if not success:
|
||||||
|
self.msg("This couldn't be dropped.")
|
||||||
|
else:
|
||||||
|
obj_name, _ = obj.get_numbered_name(1, caller)
|
||||||
|
container_name, _ = container.get_numbered_name(1, caller)
|
||||||
|
caller.location.msg_contents(
|
||||||
|
f"$You() $conj(put) {obj_name} in {container_name}.", from_obj=caller
|
||||||
|
)
|
||||||
|
# Call the object script's at_drop() method.
|
||||||
|
obj.at_drop(caller)
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerCmdSet(CmdSet):
|
||||||
|
"""
|
||||||
|
Extends the basic `look` and `get` commands to support containers,
|
||||||
|
and adds an additional `put` command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "Container CmdSet"
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
super().at_cmdset_creation()
|
||||||
|
|
||||||
|
self.add(CmdContainerLook)
|
||||||
|
self.add(CmdContainerGet)
|
||||||
|
self.add(CmdPut)
|
||||||
62
evennia/contrib/game_systems/containers/tests.py
Normal file
62
evennia/contrib/game_systems/containers/tests.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
from evennia import create_object
|
||||||
|
from evennia.utils.test_resources import BaseEvenniaTest, BaseEvenniaCommandTest # noqa
|
||||||
|
from .containers import ContribContainer, CmdContainerGet, CmdContainerLook, CmdPut
|
||||||
|
|
||||||
|
|
||||||
|
class TestContainer(BaseEvenniaTest):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
# create a container to test with
|
||||||
|
self.container = create_object(key="Box", typeclass=ContribContainer, location=self.room1)
|
||||||
|
|
||||||
|
def test_capacity(self):
|
||||||
|
# limit capacity to 1
|
||||||
|
self.container.capacity = 1
|
||||||
|
self.assertTrue(self.container.at_pre_put_in(self.char1, self.obj1))
|
||||||
|
# put Obj2 in container to hit max capacity
|
||||||
|
self.obj2.location = self.container
|
||||||
|
self.assertFalse(self.container.at_pre_put_in(self.char1, self.obj1))
|
||||||
|
|
||||||
|
|
||||||
|
class TestContainerCmds(BaseEvenniaCommandTest):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
# create a container to test with
|
||||||
|
self.container = create_object(key="Box", typeclass=ContribContainer, location=self.room1)
|
||||||
|
|
||||||
|
def test_look_in(self):
|
||||||
|
# make sure the object is in the container so we can look at it
|
||||||
|
self.obj1.location = self.container
|
||||||
|
self.call(CmdContainerLook(), "obj in box", "Obj")
|
||||||
|
# move it into a non-container object and look at it there too
|
||||||
|
self.obj1.location = self.obj2
|
||||||
|
self.call(CmdContainerLook(), "obj in obj2", "Obj")
|
||||||
|
|
||||||
|
def test_get_and_put(self):
|
||||||
|
# get normally
|
||||||
|
self.call(CmdContainerGet(), "Obj", "You pick up an Obj.")
|
||||||
|
# put in the container
|
||||||
|
self.call(CmdPut(), "obj in box", "You put an Obj in a Box.")
|
||||||
|
# get from the container
|
||||||
|
self.call(CmdContainerGet(), "obj from box", "You get an Obj from a Box.")
|
||||||
|
|
||||||
|
def test_locked_get_put(self):
|
||||||
|
# lock container
|
||||||
|
self.container.locks.add("get_from:false()")
|
||||||
|
# move object to container to try getting
|
||||||
|
self.obj1.location = self.container
|
||||||
|
self.call(CmdContainerGet(), "obj from box", "You can't get things from that.")
|
||||||
|
# move object to character to try putting
|
||||||
|
self.obj1.location = self.char1
|
||||||
|
self.call(CmdPut(), "obj in box", "You can't put things in that.")
|
||||||
|
|
||||||
|
def test_at_capacity_put(self):
|
||||||
|
# set container capacity
|
||||||
|
self.container.capacity = 1
|
||||||
|
# move object to container to fill capacity
|
||||||
|
self.obj2.location = self.container
|
||||||
|
# move object to character to try putting
|
||||||
|
self.obj1.location = self.char1
|
||||||
|
self.call(CmdPut(), "obj in box", "You can't fit anything else in a Box.")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1392,11 +1392,12 @@ class AttributeHandler:
|
||||||
"""
|
"""
|
||||||
self.backend.clear_attributes(category, accessing_obj, default_access)
|
self.backend.clear_attributes(category, accessing_obj, default_access)
|
||||||
|
|
||||||
def all(self, accessing_obj=None, default_access=True):
|
def all(self, category=None, accessing_obj=None, default_access=True):
|
||||||
"""
|
"""
|
||||||
Return all Attribute objects on this object, regardless of category.
|
Return all Attribute objects on this object, regardless of category.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
category (str, optional): A given category to limit results to.
|
||||||
accessing_obj (object, optional): Check the `attrread`
|
accessing_obj (object, optional): Check the `attrread`
|
||||||
lock on each attribute before returning them. If not
|
lock on each attribute before returning them. If not
|
||||||
given, this check is skipped.
|
given, this check is skipped.
|
||||||
|
|
@ -1410,6 +1411,8 @@ class AttributeHandler:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
attrs = self.backend.get_all_attributes()
|
attrs = self.backend.get_all_attributes()
|
||||||
|
if category:
|
||||||
|
attrs = [attr for attr in attrs if attr.category == category]
|
||||||
|
|
||||||
if accessing_obj:
|
if accessing_obj:
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue