Add all needed evadventure commands

This commit is contained in:
Griatch 2022-08-04 20:22:42 +02:00
parent e1439104e0
commit 2848e31f4b
8 changed files with 490 additions and 61 deletions

View file

@ -2,9 +2,10 @@
General Character commands usually available to all characters
"""
import re
from django.conf import settings
from evennia.utils import utils
from evennia.typeclasses.attributes import NickTemplateInvalid
from evennia.utils import utils
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -501,7 +502,7 @@ class CmdGive(COMMAND_DEFAULT_CLASS):
Usage:
give <inventory obj> <to||=> <target>
Gives an items from your inventory to another character,
Gives an item from your inventory to another person,
placing it in their inventory.
"""
@ -538,7 +539,7 @@ class CmdGive(COMMAND_DEFAULT_CLASS):
return
# give object
success = to_give.move_to(target, quiet=True, move_type="get")
success = to_give.move_to(target, quiet=True, move_type="give")
if not success:
caller.msg("This could not be given.")
else:

View file

@ -5,11 +5,14 @@ Character class.
from evennia.objects.objects import DefaultCharacter
from evennia.typeclasses.attributes import AttributeProperty
from evennia.utils.evmenu import ask_yes_no
from evennia.utils.logger import log_trace
from evennia.utils.utils import lazy_property
from . import rules
from .equipment import EquipmentHandler
from .equipment import EquipmentError, EquipmentHandler
from .quests import EvAdventureQuestHandler
from .utils import get_obj_stats
class LivingMixin:
@ -79,7 +82,7 @@ class LivingMixin:
"""
pass
def at_loot(self, looted):
def at_do_loot(self, looted):
"""
Called when looting another entity.
@ -87,9 +90,9 @@ class LivingMixin:
looted: The thing to loot.
"""
looted.get_loot()
looted.at_looted()
def get_loot(self, looter):
def at_looted(self, looter):
"""
Called when being looted (after defeat).
@ -186,12 +189,14 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
Args:
moved_object (Object): Object to move into this one (that is, into inventory).
source_location (Object): Source location moved from.
**kwargs: Passed from move operation; unused here.
**kwargs: Passed from move operation; the `move_type` is useful; if someone is giving
us something (`move_type=='give'`) we want to ask first.
Returns:
bool: If move should be allowed or not.
"""
# this will raise EquipmentError if inventory is full
return self.equipment.validate_slot_usage(moved_object)
def at_object_receive(self, moved_object, source_location, **kwargs):
@ -205,15 +210,18 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
**kwargs: Passed from move operation; unused here.
"""
self.equipment.add(moved_object)
try:
self.equipment.add(moved_object)
except EquipmentError as err:
log_trace(f"at_object_receive error: {err}")
def at_pre_object_leave(self, leaving_object, destination, **kwargs):
"""
Hook called when dropping an item. We don't allow to drop weilded/worn items
(need to unwield/remove them first).
(need to unwield/remove them first). Return False to
"""
return self.equipment.can_remove(leaving_object)
return True
def at_object_leave(self, moved_object, destination, **kwargs):
"""
@ -264,7 +272,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
# don't allow looting in pvp
return not self.location.allow_pvp
def get_loot(self, looter):
def at_looted(self, looter):
"""
Called when being looted.

View file

@ -765,7 +765,7 @@ class EvAdventureCombatHandler(DefaultScript):
for enemy in defeated_enemies:
try:
if ally.pre_loot(enemy):
enemy.get_loot(ally)
enemy.at_looted(ally)
ally.post_loot(enemy)
except Exception:
logger.log_trace()

View file

@ -9,13 +9,35 @@ New commands:
inventory
wield/wear <item>
unwield/remove <item>
give <item> to <target>
give <item or coin> to <character>
talk <npc>
To install, add the `EvAdventureCmdSet` from this module to the default character cmdset:
```python
# in mygame/commands/default_cmds.py
from evennia.contrib.tutorials.evadventure.commands import EvAdventureCmdSet # <---
# ...
class CharacterCmdSet(CmdSet):
def at_cmdset_creation(self):
# ...
self.add(EvAdventureCmdSet) # <-----
```
"""
from evennia import Command, default_cmds
from evennia import CmdSet, Command, InterruptCommand
from evennia.utils.evmenu import EvMenu
from evennia.utils.utils import inherits_from
from .combat_turnbased import CombatFailure, join_combat
from .enums import WieldLocation
from .equipment import EquipmentError
from .npcs import EvAdventureTalkativeNPC
from .utils import get_obj_stats
class EvAdventureCommand(Command):
@ -93,12 +115,343 @@ class CmdInventory(EvAdventureCommand):
self.caller.msg(self.caller.equipment.display_loadout())
class CmdWield(EvAdventureCommand):
class CmdWieldOrWear(EvAdventureCommand):
"""
Wield a weapon/shield or wear armor.
Wield a weapon/shield, or wear a piece of armor or a helmet.
Usage:
wield <item>
wear <item>
The item will automatically end up in the suitable spot, replacing whatever
was there previously.
"""
key = "wield"
aliases = ("wear",)
out_txts = {
WieldLocation.BACKPACK: "You shuffle the position of {key} around in your backpack.",
WieldLocation.TWO_HANDS: "You hold {key} with both hands.",
WieldLocation.WEAPON_HAND: "You hold {key} in your strongest hand, ready for action.",
WieldLocation.SHIELD_HAND: "You hold {key} in your off hand, ready to protect you.",
WieldLocation.BODY: "You strap {key} on yourself.",
WieldLocation.HEAD: "You put {key} on your head.",
}
def func(self):
# find the item among those in equipment
item = self.caller.search(self.args, candidates=self.caller.equipment.all(only_objs=True))
if not item:
# An 'item not found' error will already have been reported; we add another line
# here for clarity.
self.caller.msg("You must carry the item you want to wield or wear.")
return
use_slot = getattr(item, "inventory_use_slot", WieldLocation.BACKPACK)
# check what is currently in this slot
current = self.caller.equipment.slots[use_slot]
if current == item:
self.caller.msg(f"You are already using {item.key} here.")
return
# move it to the right slot based on the type of object
self.caller.equipment.use(item)
# inform the user of the change (and potential swap)
if current:
self.caller.msg(f"Returning {current.key} to the backpack.")
self.caller.msg(self.out_txts[use_slot].format(key=item.key))
class CmdRemove(EvAdventureCommand):
"""
Remove a remove a weapon/shield, armor or helmet.
Usage:
remove <item>
unwield <item>
unwear <item>
To remove an item from the backpack, use |wdrop|n instead.
"""
key = "remove"
aliases = ("unwield", "unwear")
def func(self):
caller = self.caller
# find the item among those in equipment
item = caller.search(self.args, candidates=caller.equipment.all(only_objs=True))
if not item:
# An 'item not found' error will already have been reported
return
current_slot = caller.equipment.get_current_slot(item)
if current_slot is WieldLocation.BACKPACK:
# we don't allow dropping this way since it may be unexepected by users who forgot just
# where their item currently is.
caller.msg(
f"You already stashed away {item.key} in your backpack. Use 'drop' if "
"you want to get rid of it."
)
return
caller.equipment.remove(item)
caller.equipment.add(item)
caller.msg(f"You stash {item.key} in your backpack.")
# give / accept menu
def _rescind_gift(caller, raw_string, **kwargs):
"""
Called when giver rescinds their gift in `node_give` below.
It means they entered 'cancel' on the gift screen.
"""
# kill the gift menu for the receiver immediately
receiver = kwargs["receiver"]
receiver.ndb._evmenu.close_menu()
receiver.msg("The offer was rescinded.")
return "node_end"
def node_give(caller, raw_string, **kwargs):
"""
This will show to the giver until receiver accepts/declines. It allows them
to rescind their offer.
The `caller` here is the one giving the item. We also make sure to feed
the 'item' and 'receiver' into the Evmenu.
"""
item = kwargs["item"]
receiver = kwargs["receiver"]
text = f"""
You are offering {item.key} to {receiver.get_display_name(looker=caller)}.
|wWaiting for them to accept or reject the offer ...|n
""".strip()
options = {
"key": ("cancel", "abort"),
"desc": "Rescind your offer.",
"goto": (_rescind_gift, kwargs),
}
return text, options
def _accept_or_reject_gift(caller, raw_string, **kwargs):
"""
Called when receiver enters yes/no in `node_receive` below. We first need to
figure out which.
"""
item = kwargs["item"]
giver = kwargs["giver"]
if raw_string.lower() in ("yes", "y"):
# they accepted - move the item!
item = giver.equipment.remove(item)
if item:
try:
# this will also add them to the equipment backpack, if possible
item.move_to(caller, quiet=True, move_type="give")
except EquipmentError:
caller.location.msg_contents(
f"$You({giver.key.key}) $conj(try) to give "
f"{item.key} to $You({caller.key}), but they can't accept it since their "
"inventory is full.",
mapping={giver.key: giver, caller.key: caller},
)
else:
caller.location.msg_contents(
f"$You({giver.key}) $conj(give) {item.key} to $You({caller.key}), "
"and they accepted the offer.",
mapping={giver.key: giver, caller.key: caller},
)
giver.ndb._evmenu.close_menu()
return "node_end"
def node_receive(caller, raw_string, **kwargs):
"""
Will show to the receiver and allow them to accept/decline the offer for
as long as the giver didn't rescind it.
The `caller` here is the one receiving the item. We also make sure to feed
the 'item' and 'giver' into the EvMenu.
"""
item = kwargs["item"]
giver = kwargs["giver"]
text = f"""
{giver.get_display_name()} is offering you {item.key}:
{get_obj_stats(item)}
[Your inventory usage: {caller.equipment.get_slot_usage_string()}]
|wDo you want to accept the given item? Y/[N]
"""
options = ({"key": "_default", "goto": (_accept_or_reject_gift, kwargs)},)
return text, options
def node_end(caller, raw_string, **kwargs):
return "", None
class CmdGive(EvAdventureCommand):
"""
Give item or money to another person. Items need to be accepted before
they change hands. Money changes hands immediately with no wait.
Usage:
give <item> to <receiver>
give <number of coins> [coins] to receiver
If item name includes ' to ', surround it in quotes.
Examples:
give apple to ranger
give "road to happiness" to sad ranger
give 10 coins to ranger
give 12 to ranger
"""
key = "give"
def parse(self):
"""
Parsing is a little more complex for this command.
"""
super().parse()
args = self.args
if " to " not in args:
self.caller.msg(
"Usage: give <item> to <recevier>. Specify e.g. '10 coins' to pay money. "
"Use quotes around the item name it if includes the substring ' to '. "
)
raise InterruptCommand
self.item_name = ""
self.coins = 0
# make sure we can use '...' to include items with ' to ' in the name
if args.startswith('"') and args.count('"') > 1:
end_ind = args[1:].index('"') + 1
item_name = args[:end_ind]
_, receiver_name = args.split(" to ", 1)
elif args.startswith("'") and args.count("'") > 1:
end_ind = args[1:].index("'") + 1
item_name = args[:end_ind]
_, receiver_name = args.split(" to ", 1)
else:
item_name, receiver_name = args.split(" to ", 1)
# a coin count rather than a normal name
if " coins" in item_name:
item_name = item_name[:-6]
if item_name.isnumeric():
self.coins = max(0, int(item_name))
self.item_name = item_name
self.receiver_name = receiver_name
def func(self):
caller = self.caller
receiver = caller.search(self.receiver_name)
if not receiver:
return
# giving of coins is always accepted
if self.coins:
current_coins = caller.coins
if caller.coins < current_coins:
caller.msg("You only have |y{current_coins}|n to give.")
return
# do transaction
caller.coins -= self.coins
receiver.coins += self.coins
caller.location.msg_contents(
f"$You() $conj(give) $You(receiver.key) {self.coins} coins."
)
return
# giving of items require acceptance before it happens
item = caller.search(self.item_name, candidates=caller.equipment.all(only_objs=True))
if not item:
return
# testing hook
if not item.at_pre_give(caller, receiver):
return
# before we start menus, we must check so either part is not already in a menu,
# that would be annoying otherwise
if receiver.ndb._evmenu:
caller.msg(
f"{receiver.get_display_name(looker=caller)} seems busy talking to someone else."
)
return
if caller.ndb._evmenu:
caller.msg("Close the current menu first.")
return
# this starts evmenus for both parties
EvMenu(
receiver, {"node_receive": node_receive, "node_end": node_end}, item=item, giver=caller
)
EvMenu(caller, {"node_give": node_give, "node_end": node_end}, item=item, receiver=receiver)
class CmdTalk(EvAdventureCommand):
"""
Start a conversations with shop keepers and other NPCs in the world.
Args:
talk <npc>
"""
key = "talk"
def func(self):
target = self.search(self.args)
if not target:
return
if not inherits_from(target, EvAdventureTalkativeNPC):
self.caller.msg(
f"{target.get_display_name(looker=self.caller)} does not seem very talkative."
)
return
target.at_talk(self.caller)
class EvAdventureCmdSet(CmdSet):
"""
Groups all commands in one cmdset which can be added in one go to the DefaultCharacter cmdset.
"""
key = "evadventure"
def at_cmdset_creation(self):
self.add(CmdAttackTurnBased())
self.add(CmdInventory())
self.add(CmdWieldOrWear())
self.add(CmdRemove())
self.add(CmdGive())
self.add(CmdTalk())

View file

@ -3,8 +3,10 @@ Knave has a system of Slots for its inventory.
"""
from evennia.utils.utils import inherits_from
from .enums import Ability, WieldLocation
from .objects import WeaponEmptyHand
from .objects import EvAdventureObject, WeaponEmptyHand
class EquipmentError(TypeError):
@ -81,6 +83,16 @@ class EquipmentHandler:
"""
return getattr(self.obj, Ability.CON.value, 1) + 10
def get_slot_usage_string(self):
"""
Get a slot usage/max string for display.
Returns:
str: The usage string.
"""
return f"|b{self.count_slots()}/{self.max_slots}|n"
def validate_slot_usage(self, obj):
"""
Check if obj can fit in equipment, based on its size.
@ -92,7 +104,10 @@ class EquipmentHandler:
EquipmentError: If there's not enough room.
"""
size = getattr(obj, "size", 0)
if not inherits_from(obj, EvAdventureObject):
raise EquipmentError(f"{obj.key} is not something that can be equipped.")
size = obj.size
max_slots = self.max_slots
current_slot_usage = self.count_slots()
if current_slot_usage + size > max_slots:
@ -104,25 +119,21 @@ class EquipmentHandler:
)
return True
def all(self):
def get_current_slot(self, obj):
"""
Get all objects in inventory, regardless of location.
Check which slot-type the given object is in.
Args:
obj (EvAdventureObject): The object to check.
Returns:
list: A flat list of item tuples `[(item, WieldLocation),...]`
starting with the wielded ones, backpack content last.
WieldLocation: A location the object is in. None if the object
is not in the inventory at all.
"""
slots = self.slots
lst = [
(slots[WieldLocation.WEAPON_HAND], WieldLocation.WEAPON_HAND),
(slots[WieldLocation.SHIELD_HAND], WieldLocation.SHIELD_HAND),
(slots[WieldLocation.TWO_HANDS], WieldLocation.TWO_HANDS),
(slots[WieldLocation.BODY], WieldLocation.BODY),
(slots[WieldLocation.HEAD], WieldLocation.HEAD),
] + [(item, WieldLocation.BACKPACK) for item in slots[WieldLocation.BACKPACK]]
# remove any None-results from empty slots
return [tup for tup in lst if item[0]]
for equipment_item, slot in self.all():
if obj == equipment_item:
return slot
@property
def armor(self):
@ -205,10 +216,10 @@ class EquipmentHandler:
return f"{weapon_str}{shield_str}\n{armor_str}{helmet_str}"
def use(self, obj):
def move(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.
Moves item to the place it things it should be in - 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.
@ -238,7 +249,7 @@ class EquipmentHandler:
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
# can't keep a two-handed weapon if adding a one-handed weapon or shield
slots[WieldLocation.TWO_HANDS] = None
slots[use_slot] = obj
elif use_slot is WieldLocation.BACKPACK:
@ -255,19 +266,19 @@ class EquipmentHandler:
"""
Put something in the backpack specifically (even if it could be wield/worn).
Args:
obj (EvAdventureObject): The object to add.
Notes:
This will not change the object's `.location`, this must be done
by the calling code.
"""
# 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.
@ -279,6 +290,10 @@ class EquipmentHandler:
Returns:
list: A list of 0, 1 or more objects emptied from the inventory.
Notes:
This will not change the object's `.location`, this must be done separately
by the calling code.
"""
slots = self.slots
ret = []
@ -354,21 +369,28 @@ class EquipmentHandler:
character = self.obj
return [obj for obj in self.slots[WieldLocation.BACKPACK] if obj.at_pre_use(character)]
def get_obj_stats(self, obj):
def all(self, only_objs=False):
"""
Get a string of stats about the object.
Get all objects in inventory, regardless of location.
Keyword Args:
only_objs (bool): Only return a flat list of objects, not tuples.
Returns:
list: A list of item tuples `[(item, WieldLocation),...]`
starting with the wielded ones, backpack content last. If `only_objs` is set,
this will just be a flat list of objects.
"""
objmap = dict(self.all())
carried = objmap.get(obj)
carried = f"Worn: [{carried.value}]" if carried else ""
return f"""
|c{self.key}|n Value: |y{self.value}|n coins {carried}
{self.desc}
Slots: |w{self.size}|n Used from: |w{self.use_slot.value}|n
Quality: |w{self.quality}|n Uses: |wself.uses|n
Attacks using: |w{self.attack_type.value}|n against |w{self.defense_type.value}|n
Damage roll: |w{self.damage_roll}"""
slots = self.slots
lst = [
(slots[WieldLocation.WEAPON_HAND], WieldLocation.WEAPON_HAND),
(slots[WieldLocation.SHIELD_HAND], WieldLocation.SHIELD_HAND),
(slots[WieldLocation.TWO_HANDS], WieldLocation.TWO_HANDS),
(slots[WieldLocation.BODY], WieldLocation.BODY),
(slots[WieldLocation.HEAD], WieldLocation.HEAD),
] + [(item, WieldLocation.BACKPACK) for item in slots[WieldLocation.BACKPACK]]
# remove any None-results from empty slots
if only_objs:
return [tup[0] for tup in lst if tup[0]]
return [tup for tup in lst if tup[0]]

View file

@ -280,7 +280,7 @@ class EvAdventureMob(EvAdventureNPC):
"""
self.at_death()
def at_loot(self, looted):
def at_do_loot(self, looted):
"""
Called when mob gets to loot a PC.

View file

@ -2,3 +2,47 @@
Various utilities.
"""
_OBJ_STATS = """
|c{key}|n Value: approx. |y{value}|n coins {carried}
{desc}
Slots: |w{size}|n Used from: |w{use_slot_name}|n
Quality: |w{quality}|n Uses: |wuses|n
Attacks using: |w{attack_type_name}|n against |w{defense_type_value}|n
Damage roll: |w{damage_roll}""".strip()
def get_obj_stats(obj, owner=None):
"""
Get a string of stats about the object.
Args:
obj (EvAdventureObject): The object to get stats for.
owner (EvAdventureCharacter, optional): If given, it allows us to
also get information about if the item is currently worn/wielded.
Returns:
str: A stat string to show about the object.
"""
carried = ""
if owner:
objmap = dict(owner.equipment.all())
carried = objmap.get(obj)
carried = f"Worn: [{carried.value}]" if carried else ""
return _OBJ_STATS.format(
key=obj.key,
value=obj.value,
carried=carried,
desc=obj.db.desc,
size=obj.size,
use_slot=obj.use_slot.value,
quality=obj.quality,
uses=obj.uses,
attack_type=obj.attack_type.value,
defense_type=obj.defense_type.value,
damage_roll=obj.damage_roll,
)

View file

@ -1744,10 +1744,11 @@ def ask_yes_no(
**kwargs,
):
"""
A helper question for asking a simple yes/no question. This will cause
A helper function for asking a simple yes/no question. This will cause
the system to pause and wait for input from the player.
Args:
caller (Object): The entity being asked.
prompt (str): The yes/no question to ask. This takes an optional formatting
marker `{options}` which will be filled with 'Y/N', '[Y]/N' or
'Y/[N]' depending on the setting of `default`. If `allow_abort` is set,