Start adding dungeon logic
This commit is contained in:
parent
091a13674d
commit
944420e92e
4 changed files with 623 additions and 26 deletions
262
evennia/contrib/tutorials/evadventure/dungeon.py
Normal file
262
evennia/contrib/tutorials/evadventure/dungeon.py
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
"""
|
||||||
|
Dungeon system
|
||||||
|
|
||||||
|
This creates a procedurally generated dungeon.
|
||||||
|
|
||||||
|
The dungone originates in an entrance room with exits that spawn a new dungeon connection every X
|
||||||
|
minutes. As long as characters go through the same exit within that time, they will all end up in
|
||||||
|
the same dungeon 'branch', otherwise they will go into separate, un-connected dungeon 'branches'.
|
||||||
|
They can always go back to the start room, but this will become a one-way exit back.
|
||||||
|
|
||||||
|
When moving through the dungeon, a new room is not generated until characters
|
||||||
|
decided to go in that direction. Each room is tagged with the specific 'instance'
|
||||||
|
id of that particular branch of dungon. When no characters remain in the branch,
|
||||||
|
the branch is deleted.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from math import sqrt
|
||||||
|
from random import randint, random, shuffle
|
||||||
|
|
||||||
|
from evennia import AttributeProperty, DefaultExit, DefaultScript
|
||||||
|
from evennia.utils import create
|
||||||
|
from evennia.utils.utils import inherits_from
|
||||||
|
|
||||||
|
from .rooms import EvAdventureDungeonRoom
|
||||||
|
|
||||||
|
# aliases for cardinal directions
|
||||||
|
_EXIT_ALIASES = {
|
||||||
|
"north": ("n",),
|
||||||
|
"east": ("w",),
|
||||||
|
"south": ("s",),
|
||||||
|
"west": ("w",),
|
||||||
|
"northeast": ("ne",),
|
||||||
|
"southeast": ("se",),
|
||||||
|
"southwest": ("sw",),
|
||||||
|
"northwest": ("nw",),
|
||||||
|
}
|
||||||
|
# finding the reverse cardinal direction
|
||||||
|
_EXIT_REVERSE_MAPPING = {
|
||||||
|
"north": "south",
|
||||||
|
"east": "west",
|
||||||
|
"south": "north",
|
||||||
|
"west": "east",
|
||||||
|
"northeast": "southwest",
|
||||||
|
"southeast": "northwest",
|
||||||
|
"southwest": "northeast",
|
||||||
|
"northwest": "southeast",
|
||||||
|
}
|
||||||
|
|
||||||
|
# how xy coordinate shifts by going in direction
|
||||||
|
_EXIT_GRID_SHIFT = {
|
||||||
|
"north": (0, 1),
|
||||||
|
"east": (1, 0),
|
||||||
|
"south": (0, -1),
|
||||||
|
"west": (-1, 0),
|
||||||
|
"northeast": (1, 1),
|
||||||
|
"southeast": (1, -1),
|
||||||
|
"southwest": (-1, -1),
|
||||||
|
"northwest": (-1, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Dungeon orchestrator and rooms
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class EvAdventureDungeonExit(DefaultExit):
|
||||||
|
"""
|
||||||
|
Dungeon exit. This will not create the target room until it's traversed.
|
||||||
|
It must be created referencing the dungeon_orchestrator it belongs to.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
dungeon_orchestrator = AttributeProperty(None, autocreate=False)
|
||||||
|
|
||||||
|
def at_traverse(self, traversing_object, target_location, **kwargs):
|
||||||
|
"""
|
||||||
|
Called when traversing. `target_location` will be None if the
|
||||||
|
target was not yet created.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not target_location:
|
||||||
|
self.destination = target_location = self.dungeon_orchestrator.new_room(self)
|
||||||
|
super().at_traverse(traversing_object, target_location, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class EvAdventureDungeonOrchestrator(DefaultScript):
|
||||||
|
"""
|
||||||
|
One script is created per dungeon 'branch' created. The orchestrator is
|
||||||
|
responsible for determining what is created next when a character enters an
|
||||||
|
exit within the dungeon.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this determines how branching the dungeon will be
|
||||||
|
max_unexplored_exits = 5
|
||||||
|
max_new_exits_per_room = 3
|
||||||
|
|
||||||
|
rooms = AttributeProperty(list())
|
||||||
|
n_unvisited_exits = AttributeProperty(list())
|
||||||
|
highest_depth = AttributeProperty(0)
|
||||||
|
|
||||||
|
# (x,y): room
|
||||||
|
xy_grid = AttributeProperty(dict())
|
||||||
|
|
||||||
|
def register_exit_traversed(self, exit):
|
||||||
|
"""
|
||||||
|
Tell the system the given exit was traversed. This allows us to track how many unvisited
|
||||||
|
paths we have so as to not have it grow exponentially.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if exit.id in self.unvisited_exits:
|
||||||
|
self.unvisited_exits.remove(exit.id)
|
||||||
|
|
||||||
|
def create_out_exit(self, location, exit_direction="north"):
|
||||||
|
"""
|
||||||
|
Create outgoing exit from a room. The target room is not yet created.
|
||||||
|
|
||||||
|
"""
|
||||||
|
out_exit, _ = EvAdventureDungeonExit.create(
|
||||||
|
key=exit_direction, location=location, aliases=_EXIT_ALIASES[exit_direction]
|
||||||
|
)
|
||||||
|
self.unvisited_exits.append(out_exit.id)
|
||||||
|
|
||||||
|
def _generate_room(self, depth, coords):
|
||||||
|
# TODO - determine what type of room to create here based on location and depth
|
||||||
|
room_typeclass = EvAdventureDungeonRoom
|
||||||
|
new_room = create.create_object(
|
||||||
|
room_typeclass,
|
||||||
|
key="Dungeon room",
|
||||||
|
tags=((self.key,),),
|
||||||
|
attributes=(("xy_coord", coords, "dungeon_xygrid"),),
|
||||||
|
)
|
||||||
|
return new_room
|
||||||
|
|
||||||
|
def new_room(self, from_exit):
|
||||||
|
"""
|
||||||
|
Create a new Dungeon room leading from the provided exit.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# figure out coordinate of old room and figure out what coord the
|
||||||
|
# new one would get
|
||||||
|
source_location = from_exit.location
|
||||||
|
x, y = source_location.get("xy_coord", category="dungeon_xygrid", default=(0, 0))
|
||||||
|
dx, dy = _EXIT_GRID_SHIFT.get(from_exit.key, (1, 0))
|
||||||
|
new_x, new_y = (x + dx, y + dy)
|
||||||
|
|
||||||
|
# the dungeon's depth acts as a measure of the current difficulty level. This is the radial
|
||||||
|
# distance from the (0, 0) (the entrance). The Orchestrator also tracks the highest
|
||||||
|
# depth achieved.
|
||||||
|
depth = int(sqrt(new_x**2 + new_y**2))
|
||||||
|
|
||||||
|
new_room = self._generate_room(depth, (new_x, new_y))
|
||||||
|
|
||||||
|
self.xy_grid[(new_x, new_y)] = new_room
|
||||||
|
|
||||||
|
# always make a return exit back to where we came from
|
||||||
|
back_exit_key = (_EXIT_REVERSE_MAPPING.get(from_exit.key, "back"),)
|
||||||
|
EvAdventureDungeonExit(
|
||||||
|
key=back_exit_key,
|
||||||
|
aliases=_EXIT_ALIASES.get(back_exit_key, ()),
|
||||||
|
location=new_room,
|
||||||
|
destination=from_exit.location,
|
||||||
|
attributes=(("desc", "A dark passage."),),
|
||||||
|
)
|
||||||
|
|
||||||
|
# figure out what other exits should be here, if any
|
||||||
|
n_unexplored = len(self.unvisited_exits)
|
||||||
|
if n_unexplored >= self.max_unexplored_exits:
|
||||||
|
# no more exits to open - this is a dead end.
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
n_exits = randint(1, min(self.max_new_exits_per_room, n_unexplored))
|
||||||
|
back_exit = from_exit.key
|
||||||
|
available_directions = [
|
||||||
|
direction for direction in _EXIT_ALIASES if direction != back_exit
|
||||||
|
]
|
||||||
|
# randomize order of exits
|
||||||
|
shuffle(available_directions)
|
||||||
|
for _ in range(n_exits):
|
||||||
|
while available_directions:
|
||||||
|
# get a random direction and check so there isn't a room already
|
||||||
|
# created in that direction
|
||||||
|
direction = available_directions.pop(0)
|
||||||
|
dx, dy = _EXIT_GRID_SHIFT(direction)
|
||||||
|
target_coord = (new_x + dx, new_y + dy)
|
||||||
|
if target_coord not in self.xy_grid:
|
||||||
|
# no room there - make an exit to it
|
||||||
|
self.create_out_exit(new_room, direction)
|
||||||
|
break
|
||||||
|
|
||||||
|
self.highest_depth = max(self.highest_depth, depth)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Start room
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class EvAdventureStartRoomExit(DefaultExit):
|
||||||
|
"""
|
||||||
|
Traversing this exit will either lead to an existing dungeon branch or create
|
||||||
|
a new one.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
dungeon_orchestrator = AttributeProperty(None, autocreate=False)
|
||||||
|
|
||||||
|
def reset_exit(self):
|
||||||
|
"""
|
||||||
|
Flush the exit, so next traversal creates a new dungeon branch.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.dungeon_orchestrator = self.destination = None
|
||||||
|
|
||||||
|
def at_traverse(self, traversing_object, target_location, **kwargs):
|
||||||
|
"""
|
||||||
|
When traversing create a new orchestrator if one is not already assigned.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if target_location is None or self.dungeon_orchestrator is None:
|
||||||
|
self.dungeon_orchestrator, _ = EvAdventureDungeonOrchestrator.create(
|
||||||
|
f"dungeon_orchestrator_{datetime.utcnow()}",
|
||||||
|
)
|
||||||
|
target_location = self.destination = self.dungeon_orchestrator.new_room(self)
|
||||||
|
|
||||||
|
super().at_traverse(traversing_object, target_location, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class EvAdventureStartRoomResetter(DefaultScript):
|
||||||
|
"""
|
||||||
|
Simple ticker-script. Introduces a chance of the room's exits cycling every interval.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def at_repeat(self):
|
||||||
|
"""
|
||||||
|
Called every time the script repeats.
|
||||||
|
|
||||||
|
"""
|
||||||
|
room = self.obj
|
||||||
|
for exi in room.exits:
|
||||||
|
if inherits_from(exi, EvAdventureStartRoomExit) and random() < 0.5:
|
||||||
|
exi.reset_exit()
|
||||||
|
|
||||||
|
|
||||||
|
class EvAdventureDungeonRoomStart(EvAdventureDungeonRoom):
|
||||||
|
"""
|
||||||
|
Exits leading out of the start room, (except one leading outside) will lead to a different
|
||||||
|
dungeon-branch, and after a certain time, the given exit will instead spawn a new branch. This
|
||||||
|
room is responsible for cycling these exits regularly.
|
||||||
|
|
||||||
|
The actual exits should be created in the build script.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
recycle_time = 5 * 60 # seconds
|
||||||
|
|
||||||
|
def at_object_creation(self):
|
||||||
|
self.scripts.add(EvAdventureStartRoomResetter, interval=self.recycle_time, autostart=True)
|
||||||
335
evennia/contrib/tutorials/evadventure/equipment.py
Normal file
335
evennia/contrib/tutorials/evadventure/equipment.py
Normal file
|
|
@ -0,0 +1,335 @@
|
||||||
|
"""
|
||||||
|
Knave has a system of Slots for its inventory.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .enums import Ability, WieldLocation
|
||||||
|
from .objects import WeaponEmptyHand
|
||||||
|
|
||||||
|
|
||||||
|
class EquipmentError(TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EquipmentHandler:
|
||||||
|
"""
|
||||||
|
_Knave_ puts a lot of emphasis on the inventory. You have CON_DEFENSE inventory
|
||||||
|
slots. Some things, like torches can fit multiple in one slot, other (like
|
||||||
|
big weapons and armor) use more than one slot. The items carried and wielded has a big impact
|
||||||
|
on character customization - even magic requires carrying a runestone per spell.
|
||||||
|
|
||||||
|
The inventory also doubles as a measure of negative effects. Getting soaked in mud
|
||||||
|
or slime could gunk up some of your inventory slots and make the items there unusuable
|
||||||
|
until you clean them.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
save_attribute = "inventory_slots"
|
||||||
|
|
||||||
|
def __init__(self, obj):
|
||||||
|
self.obj = obj
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
"""
|
||||||
|
Load or create a new slot storage.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.slots = self.obj.attributes.get(
|
||||||
|
self.save_attribute,
|
||||||
|
category="inventory",
|
||||||
|
default={
|
||||||
|
WieldLocation.WEAPON_HAND: None,
|
||||||
|
WieldLocation.SHIELD_HAND: None,
|
||||||
|
WieldLocation.TWO_HANDS: None,
|
||||||
|
WieldLocation.BODY: None,
|
||||||
|
WieldLocation.HEAD: None,
|
||||||
|
WieldLocation.BACKPACK: [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _save(self):
|
||||||
|
"""
|
||||||
|
Save slot to storage.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.obj.attributes.add(self.save_attribute, self.slots, category="inventory")
|
||||||
|
|
||||||
|
def _count_slots(self):
|
||||||
|
"""
|
||||||
|
Count slot usage. This is fetched from the .size Attribute of the
|
||||||
|
object. The size can also be partial slots.
|
||||||
|
|
||||||
|
"""
|
||||||
|
slots = self.slots
|
||||||
|
wield_usage = sum(
|
||||||
|
getattr(slotobj, "size", 0) or 0
|
||||||
|
for slot, slotobj in slots.items()
|
||||||
|
if slot is not WieldLocation.BACKPACK
|
||||||
|
)
|
||||||
|
backpack_usage = sum(
|
||||||
|
getattr(slotobj, "size", 0) or 0 for slotobj in slots[WieldLocation.BACKPACK]
|
||||||
|
)
|
||||||
|
return wield_usage + backpack_usage
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_slots(self):
|
||||||
|
"""
|
||||||
|
The max amount of equipment slots ('carrying capacity') is based on
|
||||||
|
the constitution defense.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return getattr(self.obj, Ability.CON.value, 1) + 10
|
||||||
|
|
||||||
|
def validate_slot_usage(self, obj):
|
||||||
|
"""
|
||||||
|
Check if obj can fit in equipment, based on its size.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj (EvAdventureObject): The object to add.
|
||||||
|
|
||||||
|
Raise:
|
||||||
|
EquipmentError: If there's not enough room.
|
||||||
|
|
||||||
|
"""
|
||||||
|
size = getattr(obj, "size", 0)
|
||||||
|
max_slots = self.max_slots
|
||||||
|
current_slot_usage = self._count_slots()
|
||||||
|
if current_slot_usage + size > max_slots:
|
||||||
|
slots_left = max_slots - current_slot_usage
|
||||||
|
raise EquipmentError(
|
||||||
|
f"Equipment full ($int2str({slots_left}) slots "
|
||||||
|
f"remaining, {obj.key} needs $int2str({size}) "
|
||||||
|
f"$pluralize(slot, {size}))."
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def armor(self):
|
||||||
|
"""
|
||||||
|
Armor provided by actually worn equipment/shield. For body armor
|
||||||
|
this is a base value, like 12, for shield/helmet, it's a bonus, like +1.
|
||||||
|
We treat values and bonuses equal and just add them up. This value
|
||||||
|
can thus be 0, the 'unarmored' default should be handled by the calling
|
||||||
|
method.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Armor from equipment. Note that this is the +bonus of Armor, not the
|
||||||
|
'defense' (to get that one adds 10).
|
||||||
|
|
||||||
|
"""
|
||||||
|
slots = self.slots
|
||||||
|
return sum(
|
||||||
|
(
|
||||||
|
# armor is listed using its defense, so we remove 10 from it
|
||||||
|
# (11 is base no-armor value in Knave)
|
||||||
|
getattr(slots[WieldLocation.BODY], "armor", 11) - 10,
|
||||||
|
# shields and helmets are listed by their bonus to armor
|
||||||
|
getattr(slots[WieldLocation.SHIELD_HAND], "armor", 0),
|
||||||
|
getattr(slots[WieldLocation.HEAD], "armor", 0),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def weapon(self):
|
||||||
|
"""
|
||||||
|
Conveniently get the currently active weapon or rune stone.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
obj or None: The weapon. None if unarmored.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# first checks two-handed wield, then one-handed; the two
|
||||||
|
# should never appear simultaneously anyhow (checked in `use` method).
|
||||||
|
slots = self.slots
|
||||||
|
weapon = slots[WieldLocation.TWO_HANDS]
|
||||||
|
if not weapon:
|
||||||
|
weapon = slots[WieldLocation.WEAPON_HAND]
|
||||||
|
if not weapon:
|
||||||
|
weapon = WeaponEmptyHand()
|
||||||
|
return weapon
|
||||||
|
|
||||||
|
def display_loadout(self):
|
||||||
|
"""
|
||||||
|
Get a visual representation of your current loadout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The current loadout.
|
||||||
|
|
||||||
|
"""
|
||||||
|
slots = self.slots
|
||||||
|
weapon_str = "You are fighting with your bare fists"
|
||||||
|
shield_str = " and have no shield."
|
||||||
|
armor_str = "You wear no armor"
|
||||||
|
helmet_str = " and no helmet."
|
||||||
|
|
||||||
|
two_hands = slots[WieldLocation.TWO_HANDS]
|
||||||
|
if two_hands:
|
||||||
|
weapon_str = f"You wield {two_hands} with both hands"
|
||||||
|
shield_str = " (you can't hold a shield at the same time)."
|
||||||
|
else:
|
||||||
|
one_hands = slots[WieldLocation.WEAPON_HAND]
|
||||||
|
if one_hands:
|
||||||
|
weapon_str = f"You are wielding {one_hands} in one hand."
|
||||||
|
shield = slots[WieldLocation.SHIELD_HAND]
|
||||||
|
if shield:
|
||||||
|
shield_str = f"You have {shield} in your off hand."
|
||||||
|
|
||||||
|
armor = slots[WieldLocation.BODY]
|
||||||
|
if armor:
|
||||||
|
armor_str = f"You are wearing {armor}"
|
||||||
|
|
||||||
|
helmet = slots[WieldLocation.BODY]
|
||||||
|
if helmet:
|
||||||
|
helmet_str = f" and {helmet} on your head."
|
||||||
|
|
||||||
|
return f"{weapon_str}{shield_str}\n{armor_str}{helmet_str}"
|
||||||
|
|
||||||
|
def use(self, obj):
|
||||||
|
"""
|
||||||
|
Make use of item - this makes use of the object's wield slot to decide where
|
||||||
|
it goes. If it doesn't have any, it goes into backpack.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj (EvAdventureObject): Thing to use.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
EquipmentError: If there's no room in inventory. It will contains the details
|
||||||
|
of the error, suitable to echo to user.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
If using an item already in the backpack, it should first be `removed` from the
|
||||||
|
backpack, before applying here - otherwise, it will be added a second time!
|
||||||
|
|
||||||
|
this will cleanly move any 'colliding' items to the backpack to
|
||||||
|
make the use possible (such as moving sword + shield to backpack when wielding
|
||||||
|
a two-handed weapon). If wanting to warn the user about this, it needs to happen
|
||||||
|
before this call.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# first check if we have room for this
|
||||||
|
self.validate_slot_usage(obj)
|
||||||
|
|
||||||
|
slots = self.slots
|
||||||
|
use_slot = getattr(obj, "inventory_use_slot", WieldLocation.BACKPACK)
|
||||||
|
|
||||||
|
if use_slot is WieldLocation.TWO_HANDS:
|
||||||
|
# two-handed weapons can't co-exist with weapon/shield-hand used items
|
||||||
|
slots[WieldLocation.WEAPON_HAND] = slots[WieldLocation.SHIELD_HAND] = None
|
||||||
|
slots[use_slot] = obj
|
||||||
|
elif use_slot in (WieldLocation.WEAPON_HAND, WieldLocation.SHIELD_HAND):
|
||||||
|
# can't keep a two-handed weapon if adding a one-handede weapon or shield
|
||||||
|
slots[WieldLocation.TWO_HANDS] = None
|
||||||
|
slots[use_slot] = obj
|
||||||
|
elif use_slot is WieldLocation.BACKPACK:
|
||||||
|
# backpack has multiple slots.
|
||||||
|
slots[use_slot].append(obj)
|
||||||
|
else:
|
||||||
|
# for others (body, head), just replace whatever's there
|
||||||
|
slots[use_slot] = obj
|
||||||
|
|
||||||
|
# store new state
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
def add(self, obj):
|
||||||
|
"""
|
||||||
|
Put something in the backpack specifically (even if it could be wield/worn).
|
||||||
|
|
||||||
|
"""
|
||||||
|
# check if we have room
|
||||||
|
self.validate_slot_usage(obj)
|
||||||
|
self.slots[WieldLocation.BACKPACK].append(obj)
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
def can_remove(self, leaving_object):
|
||||||
|
"""
|
||||||
|
Called to check if the object can be removed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return True # TODO - some things may not be so easy, like mud
|
||||||
|
|
||||||
|
def remove(self, obj_or_slot):
|
||||||
|
"""
|
||||||
|
Remove specific object or objects from a slot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj_or_slot (EvAdventureObject or WieldLocation): The specific object or
|
||||||
|
location to empty. If this is WieldLocation.BACKPACK, all items
|
||||||
|
in the backpack will be emptied and returned!
|
||||||
|
Returns:
|
||||||
|
list: A list of 0, 1 or more objects emptied from the inventory.
|
||||||
|
|
||||||
|
"""
|
||||||
|
slots = self.slots
|
||||||
|
ret = []
|
||||||
|
if isinstance(obj_or_slot, WieldLocation):
|
||||||
|
if obj_or_slot is WieldLocation.BACKPACK:
|
||||||
|
# empty entire backpack
|
||||||
|
ret.extend(slots[obj_or_slot])
|
||||||
|
slots[obj_or_slot] = []
|
||||||
|
else:
|
||||||
|
ret.append(slots[obj_or_slot])
|
||||||
|
slots[obj_or_slot] = None
|
||||||
|
elif obj_or_slot in self.slots.values():
|
||||||
|
# obj in use/wear slot
|
||||||
|
for slot, objslot in slots.items():
|
||||||
|
if objslot is obj_or_slot:
|
||||||
|
slots[slot] = None
|
||||||
|
ret.append(objslot)
|
||||||
|
elif obj_or_slot in slots[WieldLocation.BACKPACK]:
|
||||||
|
# obj in backpack slot
|
||||||
|
try:
|
||||||
|
slots[WieldLocation.BACKPACK].remove(obj_or_slot)
|
||||||
|
ret.append(obj_or_slot)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if ret:
|
||||||
|
self._save()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def get_wieldable_objects_from_backpack(self):
|
||||||
|
"""
|
||||||
|
Get all wieldable weapons (or spell runes) from backpack. This is useful in order to
|
||||||
|
have a list to select from when swapping your wielded loadout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of objects with a suitable `inventory_use_slot`. We don't check
|
||||||
|
quality, so this may include broken items (we may want to visually show them
|
||||||
|
in the list after all).
|
||||||
|
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
obj
|
||||||
|
for obj in self.slots[WieldLocation.BACKPACK]
|
||||||
|
if obj.inventory_use_slot
|
||||||
|
in (WieldLocation.WEAPON_HAND, WieldLocation.TWO_HANDS, WieldLocation.SHIELD_HAND)
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_wearable_objects_from_backpack(self):
|
||||||
|
"""
|
||||||
|
Get all wearable items (armor or helmets) from backpack. This is useful in order to
|
||||||
|
have a list to select from when swapping your worn loadout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of objects with a suitable `inventory_use_slot`. We don't check
|
||||||
|
quality, so this may include broken items (we may want to visually show them
|
||||||
|
in the list after all).
|
||||||
|
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
obj
|
||||||
|
for obj in self.slots[WieldLocation.BACKPACK]
|
||||||
|
if obj.inventory_use_slot in (WieldLocation.BODY, WieldLocation.HEAD)
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_usable_objects_from_backpack(self):
|
||||||
|
"""
|
||||||
|
Get all 'usable' items (like potions) from backpack. This is useful for getting a
|
||||||
|
list to select from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of objects that are usable.
|
||||||
|
|
||||||
|
"""
|
||||||
|
character = self.obj
|
||||||
|
return [obj for obj in self.slots[WieldLocation.BACKPACK] if obj.at_pre_use(character)]
|
||||||
|
|
@ -532,7 +532,7 @@ class EvAdventureCharacterGeneration:
|
||||||
|
|
||||||
for item in self.backpack:
|
for item in self.backpack:
|
||||||
# TODO create here
|
# TODO create here
|
||||||
character.equipment.store(item)
|
character.equipment.add(item)
|
||||||
|
|
||||||
|
|
||||||
# character improvement
|
# character improvement
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,14 @@ Test the rules and chargen.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unittest.mock import patch, MagicMock, call
|
from unittest.mock import MagicMock, call, patch
|
||||||
from parameterized import parameterized
|
|
||||||
from evennia.utils.test_resources import BaseEvenniaTest
|
|
||||||
|
|
||||||
|
from anything import Something
|
||||||
|
from evennia.utils.test_resources import BaseEvenniaTest
|
||||||
|
from parameterized import parameterized
|
||||||
|
|
||||||
|
from .. import characters, enums, equipment, random_tables, rules
|
||||||
from .mixins import EvAdventureMixin
|
from .mixins import EvAdventureMixin
|
||||||
from .. import rules
|
|
||||||
from .. import enums
|
|
||||||
from .. import random_tables
|
|
||||||
from .. import characters
|
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureRollEngineTest(BaseEvenniaTest):
|
class EvAdventureRollEngineTest(BaseEvenniaTest):
|
||||||
|
|
@ -86,23 +85,24 @@ class EvAdventureRollEngineTest(BaseEvenniaTest):
|
||||||
character.dexterity = 1
|
character.dexterity = 1
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.roll_engine.saving_throw(character, bonus_type=enums.Ability.STR), (False, None)
|
self.roll_engine.saving_throw(character, bonus_type=enums.Ability.STR),
|
||||||
|
(False, None, Something),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.roll_engine.saving_throw(character, bonus_type=enums.Ability.DEX, modifier=1),
|
self.roll_engine.saving_throw(character, bonus_type=enums.Ability.DEX, modifier=1),
|
||||||
(False, None),
|
(False, None, Something),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.roll_engine.saving_throw(
|
self.roll_engine.saving_throw(
|
||||||
character, advantage=True, bonus_type=enums.Ability.DEX, modifier=6
|
character, advantage=True, bonus_type=enums.Ability.DEX, modifier=6
|
||||||
),
|
),
|
||||||
(False, None),
|
(False, None, Something),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.roll_engine.saving_throw(
|
self.roll_engine.saving_throw(
|
||||||
character, disadvantage=True, bonus_type=enums.Ability.DEX, modifier=7
|
character, disadvantage=True, bonus_type=enums.Ability.DEX, modifier=7
|
||||||
),
|
),
|
||||||
(True, None),
|
(True, None, Something),
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_randint.return_value = 1
|
mock_randint.return_value = 1
|
||||||
|
|
@ -110,7 +110,7 @@ class EvAdventureRollEngineTest(BaseEvenniaTest):
|
||||||
self.roll_engine.saving_throw(
|
self.roll_engine.saving_throw(
|
||||||
character, disadvantage=True, bonus_type=enums.Ability.STR, modifier=2
|
character, disadvantage=True, bonus_type=enums.Ability.STR, modifier=2
|
||||||
),
|
),
|
||||||
(False, enums.Ability.CRITICAL_FAILURE),
|
(False, enums.Ability.CRITICAL_FAILURE, Something),
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_randint.return_value = 20
|
mock_randint.return_value = 20
|
||||||
|
|
@ -118,7 +118,7 @@ class EvAdventureRollEngineTest(BaseEvenniaTest):
|
||||||
self.roll_engine.saving_throw(
|
self.roll_engine.saving_throw(
|
||||||
character, disadvantage=True, bonus_type=enums.Ability.STR, modifier=2
|
character, disadvantage=True, bonus_type=enums.Ability.STR, modifier=2
|
||||||
),
|
),
|
||||||
(True, enums.Ability.CRITICAL_SUCCESS),
|
(True, enums.Ability.CRITICAL_SUCCESS, Something),
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("evennia.contrib.tutorials.evadventure.rules.randint")
|
@patch("evennia.contrib.tutorials.evadventure.rules.randint")
|
||||||
|
|
@ -133,7 +133,7 @@ class EvAdventureRollEngineTest(BaseEvenniaTest):
|
||||||
self.roll_engine.opposed_saving_throw(
|
self.roll_engine.opposed_saving_throw(
|
||||||
attacker, defender, attack_type=enums.Ability.STR, defense_type=enums.Ability.ARMOR
|
attacker, defender, attack_type=enums.Ability.STR, defense_type=enums.Ability.ARMOR
|
||||||
),
|
),
|
||||||
(False, None),
|
(False, None, Something),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.roll_engine.opposed_saving_throw(
|
self.roll_engine.opposed_saving_throw(
|
||||||
|
|
@ -143,7 +143,7 @@ class EvAdventureRollEngineTest(BaseEvenniaTest):
|
||||||
defense_type=enums.Ability.ARMOR,
|
defense_type=enums.Ability.ARMOR,
|
||||||
modifier=2,
|
modifier=2,
|
||||||
),
|
),
|
||||||
(True, None),
|
(True, None, Something),
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("evennia.contrib.tutorials.evadventure.rules.randint")
|
@patch("evennia.contrib.tutorials.evadventure.rules.randint")
|
||||||
|
|
@ -224,7 +224,7 @@ class EvAdventureRollEngineTest(BaseEvenniaTest):
|
||||||
# death
|
# death
|
||||||
mock_randint.return_value = 1
|
mock_randint.return_value = 1
|
||||||
self.roll_engine.roll_death(character)
|
self.roll_engine.roll_death(character)
|
||||||
character.handle_death.assert_called()
|
character.at_death.assert_called()
|
||||||
# strength loss
|
# strength loss
|
||||||
mock_randint.return_value = 3
|
mock_randint.return_value = 3
|
||||||
self.roll_engine.roll_death(character)
|
self.roll_engine.roll_death(character)
|
||||||
|
|
@ -310,7 +310,7 @@ class EvAdventureCharacterGenerationTest(BaseEvenniaTest):
|
||||||
self.assertTrue(character.db.desc.startswith("Herbalist"))
|
self.assertTrue(character.db.desc.startswith("Herbalist"))
|
||||||
self.assertEqual(character.armor, "gambeson")
|
self.assertEqual(character.armor, "gambeson")
|
||||||
|
|
||||||
character.equipment.store.assert_called()
|
character.equipment.add.assert_called()
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureEquipmentTest(EvAdventureMixin, BaseEvenniaTest):
|
class EvAdventureEquipmentTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
|
|
@ -350,7 +350,7 @@ class EvAdventureEquipmentTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
if is_ok:
|
if is_ok:
|
||||||
self.assertTrue(self.character.equipment.validate_slot_usage(obj))
|
self.assertTrue(self.character.equipment.validate_slot_usage(obj))
|
||||||
else:
|
else:
|
||||||
with self.assertRaises(characters.EquipmentError):
|
with self.assertRaises(equipment.EquipmentError):
|
||||||
self.character.equipment.validate_slot_usage(obj)
|
self.character.equipment.validate_slot_usage(obj)
|
||||||
|
|
||||||
@parameterized.expand(
|
@parameterized.expand(
|
||||||
|
|
@ -375,8 +375,8 @@ class EvAdventureEquipmentTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
else:
|
else:
|
||||||
self.assertEqual(self.character.equipment.slots[where], obj)
|
self.assertEqual(self.character.equipment.slots[where], obj)
|
||||||
|
|
||||||
def test_store(self):
|
def test_add(self):
|
||||||
self.character.equipment.store(self.weapon)
|
self.character.equipment.add(self.weapon)
|
||||||
self.assertEqual(self.character.equipment.slots[enums.WieldLocation.WEAPON_HAND], None)
|
self.assertEqual(self.character.equipment.slots[enums.WieldLocation.WEAPON_HAND], None)
|
||||||
self.assertTrue(self.weapon in self.character.equipment.slots[enums.WieldLocation.BACKPACK])
|
self.assertTrue(self.weapon in self.character.equipment.slots[enums.WieldLocation.BACKPACK])
|
||||||
|
|
||||||
|
|
@ -408,7 +408,7 @@ class EvAdventureEquipmentTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
def test_remove__with_obj(self):
|
def test_remove__with_obj(self):
|
||||||
self.character.equipment.use(self.shield)
|
self.character.equipment.use(self.shield)
|
||||||
self.character.equipment.use(self.item)
|
self.character.equipment.use(self.item)
|
||||||
self.character.equipment.store(self.weapon)
|
self.character.equipment.add(self.weapon)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.character.equipment.slots[enums.WieldLocation.SHIELD_HAND], self.shield
|
self.character.equipment.slots[enums.WieldLocation.SHIELD_HAND], self.shield
|
||||||
|
|
@ -428,7 +428,7 @@ class EvAdventureEquipmentTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
def test_remove__with_slot(self):
|
def test_remove__with_slot(self):
|
||||||
self.character.equipment.use(self.shield)
|
self.character.equipment.use(self.shield)
|
||||||
self.character.equipment.use(self.item)
|
self.character.equipment.use(self.item)
|
||||||
self.character.equipment.store(self.helmet)
|
self.character.equipment.add(self.helmet)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.character.equipment.slots[enums.WieldLocation.SHIELD_HAND], self.shield
|
self.character.equipment.slots[enums.WieldLocation.SHIELD_HAND], self.shield
|
||||||
|
|
@ -449,11 +449,11 @@ class EvAdventureEquipmentTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
|
|
||||||
def test_properties(self):
|
def test_properties(self):
|
||||||
self.character.equipment.use(self.armor)
|
self.character.equipment.use(self.armor)
|
||||||
self.assertEqual(self.character.equipment.armor, 11)
|
self.assertEqual(self.character.equipment.armor, 1)
|
||||||
self.character.equipment.use(self.shield)
|
self.character.equipment.use(self.shield)
|
||||||
self.assertEqual(self.character.equipment.armor, 12)
|
self.assertEqual(self.character.equipment.armor, 2)
|
||||||
self.character.equipment.use(self.helmet)
|
self.character.equipment.use(self.helmet)
|
||||||
self.assertEqual(self.character.equipment.armor, 13)
|
self.assertEqual(self.character.equipment.armor, 3)
|
||||||
|
|
||||||
self.character.equipment.use(self.weapon)
|
self.character.equipment.use(self.weapon)
|
||||||
self.assertEqual(self.character.equipment.weapon, self.weapon)
|
self.assertEqual(self.character.equipment.weapon, self.weapon)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue