From 2f28c8982dc3299e9eda29af0c429dbdd1a4b28c Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 24 Jul 2022 22:30:13 +0200 Subject: [PATCH] Made simple map, start shops --- .../contrib/tutorials/evadventure/enums.py | 17 +++ .../contrib/tutorials/evadventure/objects.py | 132 ++++++++++++++---- .../contrib/tutorials/evadventure/rooms.py | 56 +++++++- .../contrib/tutorials/evadventure/shops.py | 36 +++++ evennia/objects/objects.py | 39 ++++-- 5 files changed, 242 insertions(+), 38 deletions(-) create mode 100644 evennia/contrib/tutorials/evadventure/shops.py diff --git a/evennia/contrib/tutorials/evadventure/enums.py b/evennia/contrib/tutorials/evadventure/enums.py index 928aec3ed2..9253c79976 100644 --- a/evennia/contrib/tutorials/evadventure/enums.py +++ b/evennia/contrib/tutorials/evadventure/enums.py @@ -69,3 +69,20 @@ class WieldLocation(Enum): # combat-related OPTIMAL_DISTANCE = "optimal_distance" SUBOPTIMAL_DISTANCE = "suboptimal_distance" + + +class ObjType(Enum): + """ + Object types + + """ + + WEAPON = "weapon" + ARMOR = "armor" + SHIELD = "shield" + HELMET = "helmet" + CONSUMABLE = "consumable" + GEAR = "gear" + MAGIC = "magic" + QUEST = "quest" + TREASURE = "treasure" diff --git a/evennia/contrib/tutorials/evadventure/objects.py b/evennia/contrib/tutorials/evadventure/objects.py index 1e31c1600f..afd08d181e 100644 --- a/evennia/contrib/tutorials/evadventure/objects.py +++ b/evennia/contrib/tutorials/evadventure/objects.py @@ -3,14 +3,26 @@ All items in the game inherit from a base object. The properties (what you can d with an object, such as wear, wield, eat, drink, kill etc) are all controlled by Tags. +Every object has one of a few `obj_type`-category tags: +- weapon +- armor +- shield +- helmet +- consumable (potions, torches etc) +- magic (runestones, magic items) +- quest (quest-items) +- treasure (valuable to sell) +It's possible for an item to have more than one tag, such as a golden helmet (helmet+treasure) or +rune sword (weapon+quest). """ +from evennia import AttributeProperty, TagProperty from evennia.objects.objects import DefaultObject -from evennia.typeclasses.attributes import AttributeProperty +from evennia.utils.utils import make_iter -from .enums import Ability, WieldLocation +from .enums import Ability, ObjType, WieldLocation class EvAdventureObject(DefaultObject): @@ -23,15 +35,23 @@ class EvAdventureObject(DefaultObject): inventory_use_slot = AttributeProperty(WieldLocation.BACKPACK) # how many inventory slots it uses (can be a fraction) size = AttributeProperty(1) - armor = AttributeProperty(0) - # items that are usable (like potions) have a value larger than 0. Wieldable items - # like weapons, armor etc are not 'usable' in this respect. - uses = AttributeProperty(0) - # when 0, item is destroyed and is unusable - quality = AttributeProperty(1) value = AttributeProperty(0) - help_text = AttributeProperty("") + # can also be an iterable, for adding multiple obj-type tags + obj_type = ObjType.TREASURE.value + + def at_object_creation(self): + for obj_type in make_iter(self.obj_type): + self.tags.add(obj_type, category="obj_type") + + def has_obj_type(self, objtype): + """ + Check if object is of a particular type. + + typeobj_enum (enum.ObjType): A type to check, like enums.TypeObj.TREASURE. + + """ + return objtype.value in make_iter(self.obj_type) def get_help(self): """ @@ -93,9 +113,30 @@ class EvAdventureObjectFiller(EvAdventureObject): """ + obj_type = ObjType.QUEST.value # can't be sold quality = AttributeProperty(0) +class EvAdventureQuest(EvAdventureObject): + """ + A quest object. These cannot be sold and only be used for quest resolution. + + """ + + obj_type = ObjType.QUEST.value + value = AttributeProperty(0) + + +class EvAdventureTreasure(EvAdventureObject): + """ + A 'treasure' is mainly useful to sell for coin. + + """ + + obj_type = ObjType.TREASURE.value + value = AttributeProperty(100) + + class EvAdventureConsumable(EvAdventureObject): """ Item that can be 'used up', like a potion or food. Weapons, armor etc does not @@ -103,7 +144,7 @@ class EvAdventureConsumable(EvAdventureObject): """ - inventory_use_slot = AttributeProperty(WieldLocation.BACKPACK) + obj_type = ObjType.CONSUMABLE.value size = AttributeProperty(0.25) uses = AttributeProperty(1) @@ -134,25 +175,13 @@ class EvAdventureConsumable(EvAdventureObject): self.delete() -class EvAdventureWeapon(EvAdventureObject): - """ - Base weapon class for all EvAdventure weapons. - - """ - - inventory_use_slot = AttributeProperty(WieldLocation.WEAPON_HAND) - - attack_type = AttributeProperty(Ability.STR) - defense_type = AttributeProperty(Ability.ARMOR) - damage_roll = AttributeProperty("1d6") - - class WeaponEmptyHand: """ - This is used when you wield no weapons. We won't create any db-object for it. + This is a dummy-class loaded when you wield no weapons. We won't create any db-object for it. """ + obj_type = ObjType.WEAPON.value key = "Empty Fists" inventory_use_slot = WieldLocation.WEAPON_HAND attack_type = Ability.STR @@ -164,6 +193,23 @@ class WeaponEmptyHand: return "" +class EvAdventureWeapon(EvAdventureObject): + """ + Base weapon class for all EvAdventure weapons. + + """ + + obj_type = ObjType.WEAPON.value + inventory_use_slot = AttributeProperty(WieldLocation.WEAPON_HAND) + quality = AttributeProperty(3) + + # what ability used to attack with this weapon + attack_type = AttributeProperty(Ability.STR) + # what defense stat of the enemy it must defeat + defense_type = AttributeProperty(Ability.ARMOR) + damage_roll = AttributeProperty("1d6") + + class EvAdventureRunestone(EvAdventureWeapon): """ Base class for magic runestones. In _Knave_, every spell is represented by a rune stone @@ -173,8 +219,44 @@ class EvAdventureRunestone(EvAdventureWeapon): """ + obj_type = (ObjType.WEAPON.value, ObjType.MAGIC.value) inventory_use_slot = AttributeProperty(WieldLocation.TWO_HANDS) + quality = AttributeProperty(3) attack_type = AttributeProperty(Ability.INT) - defense_type = AttributeProperty(Ability.CON) + defense_type = AttributeProperty(Ability.DEX) damage_roll = AttributeProperty("1d8") + + +class EvAdventureArmor(EvAdventureObject): + """ + Base class for all wearable Armors. + + """ + + obj_type = ObjType.ARMOR.value + inventory_use_slot = AttributeProperty(WieldLocation.BODY) + armor = AttributeProperty(11) + quality = AttributeProperty(3) + + +class EvAdventureShield(EvAdventureArmor): + """ + Base class for all Shields. + + """ + + obj_type = ObjType.SHIELD.value + inventory_use_slot = AttributeProperty(WieldLocation.SHIELD_HAND) + armor = AttributeProperty(1) + + +class EvAdventureHelmet(EvAdventureArmor): + """ + Base class for all Helmets. + + """ + + obj_type = ObjType.HELMET.value + inventory_use_slot = AttributeProperty(WieldLocation.HEAD) + armor = AttributeProperty(1) diff --git a/evennia/contrib/tutorials/evadventure/rooms.py b/evennia/contrib/tutorials/evadventure/rooms.py index 4bed2437db..63ea6240c0 100644 --- a/evennia/contrib/tutorials/evadventure/rooms.py +++ b/evennia/contrib/tutorials/evadventure/rooms.py @@ -5,7 +5,28 @@ EvAdventure rooms. """ -from evennia import AttributeProperty, DefaultRoom, TagProperty +from copy import deepcopy + +from evennia import AttributeProperty, DefaultCharacter, DefaultRoom, TagProperty +from evennia.utils.utils import inherits_from + +_MAP_GRID = [ + [" ", " ", " ", " ", " "], + [" ", " ", " ", " ", " "], + [" ", " ", "@", " ", " "], + [" ", " ", " ", " ", " "], + [" ", " ", " ", " ", " "], +] +_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, "\\"), +} class EvAdventureRoom(DefaultRoom): @@ -18,8 +39,39 @@ class EvAdventureRoom(DefaultRoom): allow_pvp = False allow_death = False + def format_appearance(self, appearance, looker, **kwargs): + """Don't left-strip the appearance string""" + return appearance.rstrip() -class EvAdventurePvPRoom(DefaultRoom): + def get_display_header(self, looker, **kwargs): + """ + Display the current location as a mini-map. + """ + if not inherits_from(looker, DefaultCharacter): + # we don't need a map for npcs/mobs + return "" + + # build a map + map_grid = deepcopy(_MAP_GRID) + dx0, dy0 = 2, 2 + map_grid[dy0][dx0] = "|w@|n" + for exi in self.exits: + dx, dy, symbol = _EXIT_GRID_SHIFT.get(exi.key, (None, None, None)) + if symbol is None: + # we have a non-cardinal direction to go to - mark us blue to indicate this + map_grid[dy0][dx0] = "|b>|n" + continue + map_grid[dy0 + dy][dx0 + dx] = symbol + if exi.destination != self: + map_grid[dy0 + dy + dy][dx0 + dx + dx] = "X" + + # Note that on the grid, dy is really going *downwards* (origo is + # in the top left), so we need to reverse the order at the end to mirror it + # vertically and have it come out right. + return " " + "\n ".join("".join(line) for line in reversed(map_grid)) + + +class EvAdventurePvPRoom(EvAdventureRoom): """ Room where PvP can happen, but noone gets killed. diff --git a/evennia/contrib/tutorials/evadventure/shops.py b/evennia/contrib/tutorials/evadventure/shops.py new file mode 100644 index 0000000000..0026710e68 --- /dev/null +++ b/evennia/contrib/tutorials/evadventure/shops.py @@ -0,0 +1,36 @@ +""" +EvAdventure Shop system. + + +A shop is run by an NPC. It can provide one or more of several possible services: + +- Buy from a pre-set list of (possibly randomized) items. Cost is based on the item's value, + adjusted by how stingy the shopkeeper is. When bought this way, the item is + generated on the fly and passed to the player character's inventory. Inventory files are + a list of prototypes, normally from a prototype-file. A random selection of items from each + inventory file is available. +- Sell items to the shop for a certain percent of their value. One could imagine being able + to buy back items again, but we will instead _destroy_ sold items, so as to remove them + from circulation. In-game we can say it's because the merchants collect the best stuff + to sell to collectors in the big city later. Each merchant buys a certain subset of items + based on their tags. +- Buy a service. For a cost, a certain action is performed for the character; this applies + immediately when bought. The most notable services are healing and converting coin to XP. +- Buy rumors - this is echoed to the player for a price. Different merchants could have + different rumors (or randomized ones). +- Quest - gain or hand in a quest for a merchant. + +All shops are menu-driven. One starts talking to the npc and will then end up in their shop +interface. + +""" + +from evennia.utils.evmenu import EvMenu + + +def start_npc_menu(caller, shopkeeper, **kwargs): + """ + Access function - start the NPC interaction/shop interface. + + + """ diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index d147e4e65f..f467b6d2e9 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1367,6 +1367,20 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): """ return "" + def format_appearance(self, appearance, looker, **kwargs): + """ + Final processing of the entire appearance string. Called by `return_appearance`. + + Args: + appearance (str): The compiled appearance string. + looker (Object): Object doing the looking. + **kwargs: Arbitrary data for use when overriding. + Returns: + str: The final formatted output. + + """ + return appearance.strip() + def return_appearance(self, looker, **kwargs): """ Main callback used by 'look' for the object to describe itself. @@ -1398,17 +1412,20 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): if not looker: return "" - # populate the appearance_template string. It's a good idea to strip it and - # let the client add any extra spaces instead. - return self.appearance_template.format( - name=self.get_display_name(looker, **kwargs), - desc=self.get_display_desc(looker, **kwargs), - header=self.get_display_header(looker, **kwargs), - footer=self.get_display_footer(looker, **kwargs), - exits=self.get_display_exits(looker, **kwargs), - characters=self.get_display_characters(looker, **kwargs), - things=self.get_display_things(looker, **kwargs), - ).strip() + # populate the appearance_template string. + return self.format_appearance( + self.appearance_template.format( + name=self.get_display_name(looker, **kwargs), + desc=self.get_display_desc(looker, **kwargs), + header=self.get_display_header(looker, **kwargs), + footer=self.get_display_footer(looker, **kwargs), + exits=self.get_display_exits(looker, **kwargs), + characters=self.get_display_characters(looker, **kwargs), + things=self.get_display_things(looker, **kwargs), + ), + looker, + **kwargs, + ) # # Hook methods