mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
First design of shop menu nodes for plugging in
This commit is contained in:
parent
e6e632c13a
commit
4b856b84f7
5 changed files with 361 additions and 39 deletions
|
|
@ -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>
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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}"""
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue