First design of shop menu nodes for plugging in

This commit is contained in:
Griatch 2022-08-03 01:04:07 +02:00
parent e6e632c13a
commit 4b856b84f7
5 changed files with 361 additions and 39 deletions

View file

@ -1,5 +1,15 @@
"""
nextEvAdventure commands and cmdsets.
EvAdventure commands and cmdsets. We don't need that many stand-alone new
commands since a lot of functionality is managed in menus. These commands
are in additional to normal Evennia commands and should be added
to the CharacterCmdSet
New commands:
attack/hit <target>[,...]
inventory
wield/wear <item>
unwield/remove <item>
give <item> to <target>
"""
@ -65,3 +75,30 @@ class CmdAttackTurnBased(EvAdventureCommand):
self.caller.msg(f"|r{err}|n")
else:
self.caller.msg("|rFound noone to attack.|n")
class CmdInventory(EvAdventureCommand):
"""
View your inventory
Usage:
inventory
"""
key = "inventory"
aliases = ("i", "inv")
def func(self):
self.caller.msg(self.caller.equipment.display_loadout())
class CmdWield(EvAdventureCommand):
"""
Wield a weapon/shield or wear armor.
Usage:
wield <item>
wear <item>
"""

View file

@ -55,7 +55,7 @@ class EquipmentHandler:
"""
self.obj.attributes.add(self.save_attribute, self.slots, category="inventory")
def _count_slots(self):
def count_slots(self):
"""
Count slot usage. This is fetched from the .size Attribute of the
object. The size can also be partial slots.
@ -94,7 +94,7 @@ class EquipmentHandler:
"""
size = getattr(obj, "size", 0)
max_slots = self.max_slots
current_slot_usage = self._count_slots()
current_slot_usage = self.count_slots()
if current_slot_usage + size > max_slots:
slots_left = max_slots - current_slot_usage
raise EquipmentError(
@ -104,6 +104,26 @@ class EquipmentHandler:
)
return True
def all(self):
"""
Get all objects in inventory, regardless of location.
Returns:
list: A flat list of item tuples `[(item, WieldLocation),...]`
starting with the wielded ones, backpack content last.
"""
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]]
@property
def armor(self):
"""
@ -333,3 +353,22 @@ 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):
"""
Get a string of stats about the object.
"""
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}"""

View file

@ -217,9 +217,9 @@ class EvAdventureShopKeeper(EvAdventureTalkativeNPC):
"""
# how much extra the shopkeeper adds on top of the item cost
upsell_factor = AttributePropert(1.0, autocreate=False)
upsell_factor = AttributeProperty(1.0, autocreate=False)
# how much of the raw cost the shopkeep is willing to pay when buying from character
miser_factor = Attribute(0.5, autocreate=False)
miser_factor = AttributeProperty(0.5, autocreate=False)
# prototypes of common wares
common_ware_prototypes = AttributeProperty([], autocreate=False)
@ -241,7 +241,7 @@ class EvAdventureMob(EvAdventureNPC):
"""
# chance (%) that this enemy will loot you when defeating you
loot_chance = AttributeProperty(75)
loot_chance = AttributeProperty(75, autocreate=False)
def ai_combat_next_action(self, combathandler):
"""

View file

@ -37,16 +37,16 @@ node name will be the name of the option capitalized, with underscores replaced
"""
from dataclasses import dataclass
from random import choice
from evennia.prototypes.prototypes import search_prototype
from evennia.prototypes.spawner import flatten_prototype
from evennia.utils.evmenu import EvMenu, list_node
from evennia.prototypes.spawner import flatten_prototype, spawn
from evennia.utils.evmenu import list_node
from evennia.utils.logger import log_err, log_trace
from evennia.utils.utils import make_iter
from .enums import Ability, ObjType, WieldLocation
from .npcs import EvAdventureShopKeeper
from .enums import ObjType, WieldLocation
from .equipment import EquipmentError
# ------------------------------------ Buying from an NPC
@dataclass
@ -170,8 +170,9 @@ class BuyItem:
obj_type = _get_attr_value("obj_type", prototype, optional=False)
size = _get_attr_value("size", prototype, optional=False)
use_slot = _get_attr_value("use_slot", prototype, optional=False)
value = int(_get_attr_value("value", prototype, optional=False)
* shopkeeper.upsell_factor)
value = int(
_get_attr_value("value", prototype, optional=False) * shopkeeper.upsell_factor
)
except (KeyError, IndexError):
# not a buyable item
log_trace("Not a buyable item")
@ -194,12 +195,12 @@ class BuyItem:
prototype=prototype,
)
def get_sdesc(self):
def __str__(self):
"""
Get the short description to show in buy list.
"""
return self.key
return f"{self.key} [|y{self.value}|n coins]"
def get_detail(self):
"""
@ -207,11 +208,48 @@ class BuyItem:
"""
return f"""
|c{self.key}|n
{self.desc}
|c{self.key}|n Cost: |y{self.value}|n coins
Slots: {self.size} Used from: {self.use_slot.value}
{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}"""
def to_obj(self):
"""
Convert this into an actual database object that we can trade. This either means
using the stored `.prototype` to spawn a new instance of the object, or to
use the `.obj` reference to get the already existing object.
"""
if self.obj:
return self.obj
return spawn(self.prototype)
def _get_or_create_buymap(caller, shopkeep):
"""
Helper that fetches or creates the mapping of `{"short description": BuyItem, ...}`
we need for the buy menu. We cache it on the `_evmenu` object on the caller.
"""
if not caller.ndb._evmenu.buymap:
# buymap not in cache - build it and store in memory on _evmenu object - this way
# it will be removed automatically when the menu closes. We will need to reset this
# when the shopkeep buys new things.
# items carried by the shopkeep are sellable (these are items already created, such as
# things sold to the shopkeep earlier). We
obj_wares = [BuyItem.create_from_obj(obj) for obj in list(shopkeep.contents)]
prototype_wares = [
BuyItem.create_from_prototype(prototype)
for prototype in shopkeep.common_ware_prototypes
]
wares = obj_wares + prototype_wares
caller.ndb._evmenu.buymap = {str(ware): ware for ware in wares if ware}
return caller.ndb._evmenu.buymap
# Helper functions for building the shop listings and select a ware to buy
@ -224,19 +262,61 @@ def _get_all_wares_to_buy(caller, raw_string, **kwargs):
"""
shopkeep = kwargs["npc"]
# items carried by the shopkeep are sellable (these are items already created, such as
# things sold to the shopkeep earlier). We
wares = [BuyItem.create_from_obj(obj) for obj in list(shopkeep.contents)] + [
BuyItem.create_from_prototype(prototype) for prototype in shopkeep.common_ware_prototypes
]
# clean out any ByItems that failed to create for some reason
wares = [ware for ware in wares if ware]
buymap = _get_or_create_buymap(caller, shopkeep)
return [ware_desc for ware_desc in buymap]
# shop menu nodes to use for building a Shopkeeper npc
def _select_ware_to_buy(caller, selected_ware_desc, **kwargs):
"""
This helper is used by `EvMenu.list_node` to operate on what the user selected.
We return `item` in the kwargs to the `node_select_buy` node.
"""
shopkeep = kwargs["npc"]
buymap = _get_or_create_buymap(caller, shopkeep)
kwargs["item"] = buymap[selected_ware_desc]
return "node_confirm_buy", kwargs
@list_node(_get_all_wares_to_buy, select=_select_ware_to_buy, pagesize=10)
def _back_to_previous_node(caller, raw_string, **kwargs):
"""
Back to previous node is achieved by returning a node of None.
"""
return None, kwargs
def _buy_ware(caller, raw_string, **kwargs):
"""
Complete the purchase of a ware. At this point the money is deducted
and the item is either spawned from a prototype or simply moved from
the sellers inventory to that of the buyer.
We will have kwargs `item` and `npc` passed along to refer to the BuyItem we bought
and the shopkeep selling it.
"""
item = kwargs["item"] # a BuyItem instance
shopkeep = kwargs["npc"]
# exchange money
caller.coins -= item.value
shopkeep += item.value
# get the item - if not enough room, dump it on the ground
obj = item.to_obj()
try:
caller.equipment.add(obj)
except EquipmentError as err:
obj.location = caller.location
caller.msg(err)
caller.msg(f"|w{obj.key} ends up on the ground.|n")
caller.msg("|gYou bought |w{obj.key}|g for |y{item.value}|g coins.|n")
@list_node(_get_all_wares_to_buy, select=_select_ware_to_buy, pagesize=40)
def node_start_buy(caller, raw_string, **kwargs):
"""
Menu node for the caller to buy items from the shopkeep. This assumes `**kwargs` contains
@ -248,3 +328,170 @@ def node_start_buy(caller, raw_string, **kwargs):
only spawn when bought).
"""
coins = caller.coins
used_slots = caller.equipment.count_slots()
max_slots = caller.equipment.max_slots
text = (
f'"Seeing something you like?" [you have |y{coins}|n coins, '
f"using |b{used_slots}/{max_slots}|n slots]"
)
# this will be in addition to the options generated by the list-node
extra_options = [{"key": ("[c]ancel", "b", "c", "cancel"), "goto": "node_start"}]
return text, extra_options
def node_confirm_buy(caller, raw_string, **kwargs):
"""
Menu node reached when a user selects an item in the buy menu. The `item` passed
along in `**kwargs` is the selected item (see `_select_ware_to_buy`, where this is injected).
"""
# this was injected in _select_ware_to_buy. This is an BuyItem instance.
item = kwargs["item"]
coins = caller.coins
used_slots = caller.equipment.count_slots()
max_slots = caller.equipment.max_slots
text = item.get_detail()
text += f"\n\n[You have |y{coins}|n coins] and are using |b{used_slots}/{max_slots}|n slots]"
options = []
if caller.coins >= item.value and item.size <= (max_slots - used_slots):
options.append({"desc": f"Buy [{item.value} coins]", "goto": (_buy_ware, kwargs)})
options.append({"desc": "Cancel", "goto": (_back_to_previous_node, kwargs)})
return text, options
# node tree to inject for buying things
node_tree_buy = {"node_start_buy": node_start_buy, "node_confirm_buy": node_confirm_buy}
# ------------------------------------------------- Selling to an NPC
def _get_or_create_sellmap(self, caller, shopkeep):
if not caller.ndb._evmenu.sellmap:
# no sellmap, build one anew
sellmap = {}
for obj, wieldlocation in caller.equipment.all():
key = obj.key
value = int(obj.value * shopkeep.miser_factor)
if value > 0 and obj.obj_type is not ObjType.QUEST:
sellmap[f"|w{key}|n [{wieldlocation.value}] - sell price |y{value}|n coins"] = (
obj,
value,
)
caller.ndb._evmenu.sellmap = sellmap
sellmap = caller.ndb._evmenu.sellmap
return sellmap
def _get_all_wares_to_sell(caller, raw_string, **kwargs):
"""
Get all wares available to sell from caller's inventory. We need to build a
mapping between the descriptors and the items.
"""
shopkeep = kwargs["npc"]
sellmap = _get_or_create_sellmap(caller, shopkeep)
return [ware_desc for ware_desc in sellmap]
def _sell_ware(caller, raw_string, **kwargs):
"""
Complete the sale of a ware. This is were money is gained and the item is removed.
We will have kwargs `item`, `value` and `npc` passed along to refer to the inventory item we
sold, its (adjusted) sales cost and the shopkeep buying it.
"""
item = kwargs["item"]
value = kwargs["value"]
shopkeep = kwargs["npc"]
# move item to shopkeep
obj = caller.equipment.remove(item)
obj.location = shopkeep
# exchange money - shopkeep always have money to pay, so we don't deduct from them
caller.coins += value
caller.msg("|gYou sold |w{obj.key}|g for |y{value}|g coins.|n")
def _select_ware_to_sell(caller, selected_ware_desc, **kwargs):
"""
Selected one ware to sell. Figure out which one it is using the sellmap.
Store the result as "item" kwarg.
"""
shopkeep = kwargs["npc"]
sellmap = _get_or_create_sellmap(caller, shopkeep)
kwargs["item"], kwargs["value"] = sellmap[selected_ware_desc]
return "node_examine_sell", kwargs
@list_node(_get_all_wares_to_sell, select=_select_ware_to_sell, pagesize=20)
def node_start_sell(caller, raw_string, **kwargs):
"""
The start-level node for selling items from the user's inventory. This assumes
`**kwargs` contains a kwarg `npc` referencing the npc/shopkeep being talked to.
Items available to sell are all items in the player's equipment handler, including
things in their hands.
"""
coins = caller.coins
used_slots = caller.equipment.count_slots()
max_slots = caller.equipment.max_slots
text = (
f'"Anything you want to sell?" [you have |y{coins}|n coins, '
f"using |b{used_slots}/{max_slots}|n slots]"
)
# this will be in addition to the options generated by the list-node
extra_options = [{"key": ("[c]ancel", "b", "c", "cancel"), "goto": "node_start"}]
return text, extra_options
def node_confirm_sell(caller, raw_string, **kwargs):
"""
In this node we confirm the sell by first investigating the item we are about to sell.
We have `item` and `value` available in kwargs here, added by `_select_ware_to_sell` earler.
"""
item = kwargs["item"]
value = kwargs["value"]
coins = caller.coins
used_slots = caller.equipment.count_slots()
max_slots = caller.equipment.max_slots
text = caller.equipment.get_obj_stats(item)
text += f"\n\n[You have |y{coins}|n coins] and are using |b{used_slots}/{max_slots}|n slots]"
options = (
{"desc": f"Sell [{value} coins]", "goto": (_sell_ware, kwargs)},
{"desc": "Cancel", "goto": (_back_to_previous_node, kwargs)},
)
return text, options
# node tree to inject for selling things
node_tree_sell = {"node_start_sell": node_start_sell, "node_confirm_sell": node_confirm_sell}
# Full shopkeep node tree - inject into ShopKeep NPC menu to add buy/sell submenus
node_tree_shopkeep = {**node_tree_buy, **node_tree_sell}

View file

@ -269,24 +269,23 @@ they will be tested in sequence.
"""
import re
import inspect
import re
from ast import literal_eval
from fnmatch import fnmatch
from inspect import getargspec, isfunction
from math import ceil
from inspect import isfunction, getargspec
from django.conf import settings
from evennia import Command, CmdSet
from evennia.utils import logger
from evennia.utils.evtable import EvTable, EvColumn
from evennia.utils.ansi import strip_ansi
from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
from evennia.commands import cmdhandler
# i18n
from django.utils.translation import gettext as _
from evennia import CmdSet, Command
from evennia.commands import cmdhandler
from evennia.utils import logger
from evennia.utils.ansi import strip_ansi
from evennia.utils.evtable import EvColumn, EvTable
from evennia.utils.utils import crop, dedent, is_iter, m_len, make_iter, mod_import, pad, to_str
# read from protocol NAWS later?
_MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
@ -1394,9 +1393,9 @@ def list_node(option_generator, select=None, pagesize=10):
# we assume a string was given, we inject the result into the kwargs
# to pass on to the next node
kwargs["selection"] = selection
return str(select)
return str(select), kwargs
# this means the previous node will be re-run with these same kwargs
return None
return None, kwargs
def _list_node(caller, raw_string, **kwargs):