diff --git a/CHANGELOG.md b/CHANGELOG.md index 11ba6f0975..6feaed180f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -172,6 +172,8 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10 now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal) - Reworked text2html parser to avoid problems with stateful color tags (inspectorCaracal) - Simplified `EvMenu.options_formatter` hook to use `EvColumn` and f-strings (inspectorcaracal) +- Allow `# CODE`, `# HEADER` etc as well as `#CODE`/`#HEADER` in batchcode + files - this works better with black linting. ## Evennia 0.9.5 diff --git a/evennia/commands/command.py b/evennia/commands/command.py index 4c9c24d403..6a2a794201 100644 --- a/evennia/commands/command.py +++ b/evennia/commands/command.py @@ -4,19 +4,17 @@ The base Command class. All commands in Evennia inherit from the 'Command' class in this module. """ -import re -import math import inspect +import math +import re from django.conf import settings from django.urls import reverse from django.utils.text import slugify - from evennia.locks.lockhandler import LockHandler -from evennia.utils.utils import is_iter, fill, lazy_property, make_iter -from evennia.utils.evtable import EvTable from evennia.utils.ansi import ANSIString - +from evennia.utils.evtable import EvTable +from evennia.utils.utils import fill, is_iter, lazy_property, make_iter CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES diff --git a/evennia/contrib/tutorials/evadventure/__init__.py b/evennia/contrib/tutorials/evadventure/__init__.py index e69de29bb2..9b9f13a136 100644 --- a/evennia/contrib/tutorials/evadventure/__init__.py +++ b/evennia/contrib/tutorials/evadventure/__init__.py @@ -0,0 +1,7 @@ +""" +EvAdventure - a complete game in Evennia. + +This is an implementation of, and reference code to, the game created in the +documentation's beginner tutorial. + +""" diff --git a/evennia/contrib/tutorials/evadventure/build_techdemo.py b/evennia/contrib/tutorials/evadventure/build_techdemo.py index eeca33a738..a05085e21f 100644 --- a/evennia/contrib/tutorials/evadventure/build_techdemo.py +++ b/evennia/contrib/tutorials/evadventure/build_techdemo.py @@ -15,45 +15,69 @@ You can also build/rebuild individiaul #CODE blocks in the `batchcode/interactiv """ -#HEADER +# HEADER # this is loaded at the top of every #CODE block -from evennia import create_object, search_object -from evennia import DefaultExit +from evennia import DefaultExit, create_object, search_object from evennia.contrib.tutorials import evadventure -from evennia.contrib.tutorials.evadventure.objects import ( - EvAdventureObject, EvAdventureRunestone, EvAdventureRunestone, EvAdventureConsumable, - EvAdventureObjectFiller) -from evennia.contrib.tutorials.evadventure.rooms import EvAdventureRoom -from evennia.contrib.tutorials.evadventure.combat_turnbasedA import EvAdventureCombatHandler from evennia.contrib.tutorials.evadventure import npcs +from evennia.contrib.tutorials.evadventure.combat_turnbased import EvAdventureCombatHandler +from evennia.contrib.tutorials.evadventure.objects import ( + EvAdventureConsumable, + EvAdventureObject, + EvAdventureObjectFiller, + EvAdventureRunestone, +) +from evennia.contrib.tutorials.evadventure.rooms import EvAdventureRoom -#CODE +# CODE # Hub room evtechdemo#00 # for other test areas to link back to. Connects in turn back to Limbo. -limbo = search_object("Limbo") -hub_room = create_object(EvAdventureRoom, key="Techdemo Hub", aliases=("evtechdemo#00",), - attributes=[("desc", "Central hub for EvAdventure tech demo.")]) -create_object(DefaultExit, key="EvAdventure Techdemo", aliases=("techdemo",), - location=limbo, destination=hub_room) -create_object(DefaultExit, key="Back to Limbo", aliases=("limbo", "back"), - location=hub_room, destination=limbo) +limbo = search_object("Limbo")[0] +hub_room = create_object( + EvAdventureRoom, + key="Techdemo Hub", + aliases=("evtechdemo#00",), + attributes=[("desc", "Central hub for EvAdventure tech demo.")], +) +create_object( + DefaultExit, + key="EvAdventure Techdemo", + aliases=("techdemo", "demo", "evadventure"), + location=limbo, + destination=hub_room, +) +create_object( + DefaultExit, + key="Back to Limbo", + aliases=("limbo", "back"), + location=hub_room, + destination=limbo, +) -#CODE +# CODE # A combat room evtechdemo#01 # with a static enemy combat_room = create_object(EvAdventureRoom, key="Combat Arena", aliases=("evtechdemo#01",)) -combat_room_enemy = create_object(npcs.EvadventureMob, key="Training Dummy") +combat_room_enemy = create_object( + npcs.EvadventureMob, key="Training Dummy", aliases=("dummy",), location=combat_room +) # link to/back to hub -hub_room = search_object("evtechdemo#00") -create_object(DefaultExit, key="Back to Hub", aliases=("back", "hub"), - location=combat_room, destination=hub_room) -create_object(DefaultExit, key="combat test", aliases=("combat"), - location=combat_room, destination=hub_room) +hub_room = search_object("evtechdemo#00")[0] +create_object( + DefaultExit, key="combat test", aliases=("combat",), location=hub_room, destination=combat_room +) +create_object( + DefaultExit, + key="Back to Hub", + aliases=("back", "hub"), + location=combat_room, + destination=hub_room, +) diff --git a/evennia/contrib/tutorials/evadventure/characters.py b/evennia/contrib/tutorials/evadventure/characters.py index e3fdd0801c..36e3933c52 100644 --- a/evennia/contrib/tutorials/evadventure/characters.py +++ b/evennia/contrib/tutorials/evadventure/characters.py @@ -5,10 +5,11 @@ Base Character and NPCs. from evennia.objects.objects import DefaultCharacter, DefaultObject from evennia.typeclasses.attributes import AttributeProperty -from evennia.utils.utils import lazy_property, int2str -from .objects import EvAdventureObject +from evennia.utils.utils import int2str, lazy_property + from . import rules from .enums import Ability, WieldLocation +from .objects import EvAdventureObject class EquipmentError(TypeError): @@ -290,11 +291,12 @@ class EquipmentHandler: in the list after all). """ - return [obj for obj in slots[WieldLocation.BACKPACK] - if obj.inventory_use_slot in ( - WieldLocation.WEAPON_HAND, - WieldLocation.TWO_HANDS, - WieldLocation.SHIELD_HAND)] + return [ + obj + for obj in 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): """ @@ -307,11 +309,11 @@ class EquipmentHandler: in the list after all). """ - return [obj for obj in slots[WieldLocation.BACKPACK] - if obj.inventory_use_slot in ( - WieldLocation.BODY, - WieldLocation.HEAD - )] + return [ + obj + for obj in slots[WieldLocation.BACKPACK] + if obj.inventory_use_slot in (WieldLocation.BODY, WieldLocation.HEAD) + ] def get_usable_objects_from_backpack(self): """ @@ -327,7 +329,7 @@ class EquipmentHandler: class LivingMixin: """ - Helpers shared between all living things. + Mixin class to use for all living things. """ @@ -488,64 +490,3 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter): Called when character dies. """ - - -class EvAdventureNPC(LivingMixin, DefaultCharacter): - """ - This is the base class for all non-player entities, including monsters. These - generally don't advance in level but uses a simplified, abstract measure of how - dangerous or competent they are - the 'hit dice' (HD). - - HD indicates how much health they have and how hard they hit. In _Knave_, HD also - defaults to being the bonus for all abilities. HP is 4 x Hit die (this can then be - customized per-entity of course). - - Morale is set explicitly per-NPC, usually between 7 and 9. - - Monsters don't use equipment in the way PCs do, instead they have a fixed armor - value, and their Abilities are dynamically generated from the HD (hit_dice). - - If wanting monsters or NPCs that can level and work the same as PCs, base them off the - EvAdventureCharacter class instead. - - """ - - hit_dice = AttributeProperty(default=1) - armor = AttributeProperty(default=11) - morale = AttributeProperty(default=9) - hp = AttributeProperty(default=8) - - @property - def strength(self): - return self.hit_dice - - @property - def dexterity(self): - return self.hit_dice - - @property - def constitution(self): - return self.hit_dice - - @property - def intelligence(self): - return self.hit_dice - - @property - def wisdom(self): - return self.hit_dice - - @property - def charisma(self): - return self.hit_dice - - @property - def hp_max(self): - return self.hit_dice * 4 - - def at_object_creation(self): - """ - Start with max health. - - """ - self.hp = self.hp_max diff --git a/evennia/contrib/tutorials/evadventure/combat_turnbased.py b/evennia/contrib/tutorials/evadventure/combat_turnbased.py index 81016b65bb..52eff1417a 100644 --- a/evennia/contrib/tutorials/evadventure/combat_turnbased.py +++ b/evennia/contrib/tutorials/evadventure/combat_turnbased.py @@ -99,15 +99,16 @@ Choose who to block: """ -from datetime import datetime from collections import defaultdict +from datetime import datetime + from evennia.scripts.scripts import DefaultScript from evennia.typeclasses.attributes import AttributeProperty +from evennia.utils import dbserialize, delay, evmenu, evtable from evennia.utils.utils import make_iter -from evennia.utils import evtable, dbserialize, delay, evmenu -from .enums import Ability -from . import rules +from . import rules +from .enums import Ability COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler" COMBAT_HANDLER_INTERVAL = 60 @@ -242,7 +243,7 @@ class CombatActionAttack(CombatAction): # figure out disadvantage (gained by enemy stunts/actions) disadvantage = bool(self.combathandler.disadvantage_matrix[attacker].pop(defender, False)) - is_hit, quality = rules.EvAdventureRollEngine.opposed_saving_throw( + is_hit, quality = rules.dice.opposed_saving_throw( attacker, defender, attack_type=attacker.weapon.attack_type, @@ -295,9 +296,9 @@ class CombatActionStunt(CombatAction): # quality doesn't matter for stunts, they are either successful or not attacker = self.combatant - advantage, disadvantage = False + advantage, disadvantage = False, False - is_success, _ = rules.EvAdventureRollEngine.opposed_saving_throw( + is_success, _ = rules.dice.opposed_saving_throw( attacker, defender, attack_type=self.attack_type, @@ -333,6 +334,7 @@ class CombatActionUseItem(CombatAction): combat_post_use """ + key = "Use Item" desc = "[U]se item" aliases = ("u", "item", "use item") @@ -406,7 +408,7 @@ class CombatActionBlock(CombatAction): advantage = bool(self.advantage_matrix[combatant].pop(fleeing_target, False)) disadvantage = bool(self.disadvantage_matrix[combatant].pop(fleeing_target, False)) - is_success, _ = rules.EvAdventureRollEngine.opposed_saving_throw( + is_success, _ = rules.dice.opposed_saving_throw( combatant, fleeing_target, attack_type=self.attack_type, @@ -427,12 +429,23 @@ class CombatActionSwapWieldedWeaponOrSpell(CombatAction): Swap Wielded weapon or spell. """ + key = "Swap weapon/rune/shield" desc = "Swap currently wielded weapon, shield or spell-rune." - aliases = ("s", "swap", "draw", "swap weapon", "draw weapon", - "swap rune", "draw rune", "swap spell", "draw spell") - help_text = ("Draw a new weapon or spell-rune from your inventory, " - "replacing your current loadout") + aliases = ( + "s", + "swap", + "draw", + "swap weapon", + "draw weapon", + "swap rune", + "draw rune", + "swap spell", + "draw spell", + ) + help_text = ( + "Draw a new weapon or spell-rune from your inventory, replacing your current loadout" + ) next_menu_node = "node_select_wield_from_inventory" @@ -448,6 +461,7 @@ class CombatActionUseItem(CombatAction): Use an item from inventory. """ + key = "Use an item from backpack" desc = "Use an item from your inventory." aliases = ("u", "use", "use item") @@ -543,6 +557,29 @@ class EvAdventureCombatHandler(DefaultScript): self._end_turn() self._start_turn() + def _init_menu(self, combatant, session=None): + """ + Make sure combatant is in the menu. This is safe to call on a combatant already in a menu. + + """ + if not combatant.ndb._evmenu: + # re-joining the menu is useful during testing + evmenu.EvMenu( + combatant, + { + "node_wait_start": node_wait_start, + "node_select_target": node_select_target, + "node_select_action": node_select_action, + "node_wait_turn": node_wait_turn, + }, + startnode="node_wait_turn", + auto_quit=True, + persistent=True, + cmdset_mergetype="Union", + session=session, + combathandler=self, # makes this available as combatant.ndb._evmenu.combathandler + ) + def _reset_menu(self): """ Move menu to the action-selection node. @@ -577,10 +614,12 @@ class EvAdventureCombatHandler(DefaultScript): # set -1 for unit tests warning_time = 15 self._warn_time_task = delay( - self.interval - warning_time, self._warn_time, warning_time) + self.interval - warning_time, self._warn_time, warning_time + ) for combatant in self.combatants: # cycle combat menu + self._init_menu(combatant) combatant.ndb._evmenu.goto("node_select_action", "") def _end_turn(self): @@ -633,12 +672,12 @@ class EvAdventureCombatHandler(DefaultScript): for combatant in self.combatants: new_advantage_matrix[combatant] = { target: set_at_turn - for target, set_at_turn in advantage_matrix.items() + for target, set_at_turn in advantage_matrix[combatant].items() if set_at_turn > oldest_stunt_age } new_disadvantage_matrix[combatant] = { target: set_at_turn - for target, set_at_turn in disadvantage_matrix.items() + for target, set_at_turn in disadvantage_matrix[combatant].items() if set_at_turn > oldest_stunt_age } @@ -676,23 +715,7 @@ class EvAdventureCombatHandler(DefaultScript): action_class.key: action_class(self, combatant) for action_class in self.default_action_classes + custom_action_classes } - - # start evmenu (menu node definitions at the end of this module) - - evmenu.EvMenu( - combatant, - { - "node_wait_start": node_wait_start, - "node_select_target": node_select_target, - "node_select_action": node_select_action, - "node_wait_turn": node_wait_turn, - }, - startnode="node_wait_turn", - auto_quit=False, - persistent=True, - session=session, - combathandler=self # makes this available as combatant.ndb._evmenu.combathandler - ) + self._init_menu(combatant, session=session) def remove_combatant(self, combatant): """ @@ -823,9 +846,9 @@ class EvAdventureCombatHandler(DefaultScript): """ weapon_dmg_roll = attacker.weapon.damage_roll - dmg = rules.EvAdventureRollEngine.roll(weapon_dmg_roll) + dmg = rules.dice.roll(weapon_dmg_roll) if critical: - dmg += rules.EvAdventureRollEngine.roll(weapon_dmg_roll) + dmg += rules.dice.roll(weapon_dmg_roll) defender.hp -= dmg @@ -834,7 +857,7 @@ class EvAdventureCombatHandler(DefaultScript): if defender.hp <= 0: # roll on death table. This may or may not kill you - rules.EvAdventureRollEngine.roll_death(self) + rules.dice.roll_death(self) # tell everyone self.msg(defender.defeat_message(attacker, dmg)) @@ -870,8 +893,7 @@ class EvAdventureCombatHandler(DefaultScript): """ # get the instantiated action for this combatant action = self.combatant_actions[combatant].get( - action_key, - CombatActionDoNothing(self, combatant) + action_key, CombatActionDoNothing(self, combatant) ) # store the action in the queue @@ -912,13 +934,13 @@ def _register_action(caller, raw_string, **kwargs): Register action with handler. """ - action_key = kwargs.get["action_key"] + action_key = kwargs.pop("action_key") action_args = kwargs["action_args"] action_kwargs = kwargs["action_kwargs"] - action_target = kwargs.get("action_target") - combat_handler = caller._evmenu.combathandler - combat_handler.register_action( - caller, action_key, action_target, *action_args, **action_kwargs) + action_target = kwargs.pop("action_target", None) + combat_handler = caller.ndb._evmenu.combathandler + print("action_args", action_args, "action_kwargs", action_kwargs) + combat_handler.register_action(caller, action_key, action_target, *action_args, **action_kwargs) # move into waiting return "node_wait_turn" @@ -930,41 +952,22 @@ def node_select_target(caller, raw_string, **kwargs): with all other actions. """ - action_key = kwargs.get("action_key") - action_args = kwargs.get("action_args") - action_kwargs = kwargs.get("action_kwargs") combat = caller.ndb._evmenu.combathandler text = "Select target for |w{action_key}|n." # make the apply-self option always the first one, give it key 0 kwargs["action_target"] = caller - options = [ - { - "key": "0", - "desc": "(yourself)", - "goto": (_register_action, kwargs) - } - ] + options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}] # filter out ourselves and then make options for everyone else combatants = [combatant for combatant in combat.combatants if combatant is not caller] - for combatant in combatants: - # automatic menu numbering starts from 1 + for inum, combatant in enumerate(combatants): kwargs["action_target"] = combatant options.append( - { - "desc": combatant.key, - "goto": (_register_action, kwargs) - } + {"key": str(inum + 1), "desc": combatant.key, "goto": (_register_action, kwargs)} ) # add ability to cancel - options.append( - { - "key": "_default", - "desc": "(No input to Abort and go back)", - "goto": "node_select_action" - } - ) + options.append({"key": "_default", "goto": "node_select_action"}) return text, options @@ -981,8 +984,10 @@ def node_select_wield_from_inventory(caller, raw_string, **kwargs): """ combat = caller.ndb._evmenu.combathandler loadout = caller.inventory.display_loadout() - text = (f"{loadout}\nSelect weapon, spell or shield to draw. It will swap out " - "anything already in the same hand (you can't change armor or helmet in combat).") + text = ( + f"{loadout}\nSelect weapon, spell or shield to draw. It will swap out " + "anything already in the same hand (you can't change armor or helmet in combat)." + ) # get a list of all suitable weapons/spells/shields options = [] @@ -997,21 +1002,12 @@ def node_select_wield_from_inventory(caller, raw_string, **kwargs): ) else: # normally working item - kwargs['action_args'] = (obj,) - options.append( - { - "desc": str(obj), - "goto": (_register_action, kwargs) - } - ) + kwargs["action_args"] = (obj,) + options.append({"desc": str(obj), "goto": (_register_action, kwargs)}) # add ability to cancel options.append( - { - "key": "_default", - "desc": "(No input to Abort and go back)", - "goto": "node_select_action" - } + {"key": "_default", "desc": "(No input to Abort and go back)", "goto": "node_select_action"} ) return text, options @@ -1038,21 +1034,12 @@ def node_select_use_item_from_inventory(caller, raw_string, **kwargs): ) else: # normally working item - kwargs['action_args'] = (obj,) - options.append( - { - "desc": str(obj), - "goto": (_register_action, kwargs) - } - ) + kwargs["action_args"] = (obj,) + options.append({"desc": str(obj), "goto": (_register_action, kwargs)}) # add ability to cancel options.append( - { - "key": "_default", - "desc": "(No input to Abort and go back)", - "goto": "node_select_action" - } + {"key": "_default", "desc": "(No input to Abort and go back)", "goto": "node_select_action"} ) return text, options @@ -1063,8 +1050,8 @@ def _action_unavailable(caller, raw_string, **kwargs): Selecting an unavailable action. """ - action_key = kwargs.get["action_key"] - caller.msg(f"Action '{action_key}' is currently not available.") + action_key = kwargs["action_key"] + caller.msg(f"|rAction |w{action_key}|r is currently not available.|n") # go back to previous node return @@ -1093,12 +1080,7 @@ def node_select_action(caller, raw_string, **kwargs): { "key": key, "desc": desc, - "goto": ( - _action_unavailable, - { - "action_key": action.key - } - ) + "goto": (_action_unavailable, {"action_key": action.key}), } ) elif action.next_menu_node is None: @@ -1113,7 +1095,7 @@ def node_select_action(caller, raw_string, **kwargs): { "action_key": action.key, "action_args": (), - "action_kwargs": kwargs, + "action_kwargs": {}, "action_target": None, }, ), @@ -1130,7 +1112,8 @@ def node_select_action(caller, raw_string, **kwargs): { "action_key": action.key, "action_args": (), - "action_kwargs": kwargs, + "action_kwargs": {}, + "action_target": None, }, ), } @@ -1139,8 +1122,7 @@ def node_select_action(caller, raw_string, **kwargs): options.append( { "key": "_default", - "desc": "(No input to Abort and go back)", - "goto": "node_select_action" + "goto": "node_select_action", } ) @@ -1160,7 +1142,7 @@ def node_wait_turn(caller, raw_string, **kwargs): options = { "key": "_default", "desc": "(next round will start automatically)", - "goto": "node_wait_turn" + "goto": "node_wait_turn", } return text, options @@ -1177,7 +1159,7 @@ def node_wait_start(caller, raw_string, **kwargs): options = { "key": "_default", "desc": "(combat will start automatically)", - "goto": "node_wait_start" + "goto": "node_wait_start", } return text, options @@ -1218,10 +1200,13 @@ def join_combat(caller, *targets, session=None): combathandler = location.scripts.add(EvAdventureCombatHandler, autostart=False) created = True + if not hasattr(caller, "hp"): + raise CombatFailure("You have no hp and so can't attack anyone.") + # it's safe to add a combatant to the same combat more than once combathandler.add_combatant(caller, session=session) for target in targets: - combathandler.add_combatant(target, session=session) + combathandler.add_combatant(target) if created: combathandler.start_combat() diff --git a/evennia/contrib/tutorials/evadventure/commands.py b/evennia/contrib/tutorials/evadventure/commands.py index affde6daf6..8d0cdb4f04 100644 --- a/evennia/contrib/tutorials/evadventure/commands.py +++ b/evennia/contrib/tutorials/evadventure/commands.py @@ -1,11 +1,11 @@ """ -EvAdventure commands and cmdsets. - +nextEvAdventure commands and cmdsets. """ from evennia import Command, default_cmds -from . combat_turnbased import join_combat + +from .combat_turnbased import CombatFailure, join_combat class EvAdventureCommand(Command): @@ -17,6 +17,7 @@ class EvAdventureCommand(Command): where whitespace around the argument(s) are stripped. """ + def parse(self): self.args = self.args.strip() @@ -38,6 +39,9 @@ class CmdAttackTurnBased(EvAdventureCommand): """ + key = "attack" + aliases = ("hit",) + def parse(self): super().parse() self.targets = [name.strip() for name in self.args.split(",")] @@ -49,10 +53,15 @@ class CmdAttackTurnBased(EvAdventureCommand): target_objs = [] for target in self.targets: target_obj = self.caller.search(target) - if target_obj: + if not target_obj: # show a warning but don't abort continue target_objs.append(target_obj) if target_objs: - join_combat(self.caller, *target_objs, session=self.session) + try: + join_combat(self.caller, *target_objs, session=self.session) + except CombatFailure as err: + self.caller.msg(f"|r{err}|n") + else: + self.caller.msg("|rFound noone to attack.|n") diff --git a/evennia/contrib/tutorials/evadventure/npcs.py b/evennia/contrib/tutorials/evadventure/npcs.py index 008c8aa299..df4fcb63e6 100644 --- a/evennia/contrib/tutorials/evadventure/npcs.py +++ b/evennia/contrib/tutorials/evadventure/npcs.py @@ -3,15 +3,72 @@ EvAdventure NPCs. This includes both friends and enemies, only separated by thei """ -from .characters import EvAdventureCharacter +from evennia import DefaultCharacter +from evennia.typeclasses.attributes import AttributeProperty -class EvAdventureNPC(EvAdventureCharacter): +from .characters import LivingMixin + + +class EvAdventureNPC(LivingMixin, DefaultCharacter): """ - Base typeclass for NPCs. They have the features of a Character except - they have tooling for AI and for acting as quest-gives and shop-keepers. + This is the base class for all non-player entities, including monsters. These + generally don't advance in level but uses a simplified, abstract measure of how + dangerous or competent they are - the 'hit dice' (HD). + + HD indicates how much health they have and how hard they hit. In _Knave_, HD also + defaults to being the bonus for all abilities. HP is 4 x Hit die (this can then be + customized per-entity of course). + + Morale is set explicitly per-NPC, usually between 7 and 9. + + Monsters don't use equipment in the way PCs do, instead they have a fixed armor + value, and their Abilities are dynamically generated from the HD (hit_dice). + + If wanting monsters or NPCs that can level and work the same as PCs, base them off the + EvAdventureCharacter class instead. """ + hit_dice = AttributeProperty(default=1) + armor = AttributeProperty(default=11) + morale = AttributeProperty(default=9) + hp = AttributeProperty(default=8) + + @property + def strength(self): + return self.hit_dice + + @property + def dexterity(self): + return self.hit_dice + + @property + def constitution(self): + return self.hit_dice + + @property + def intelligence(self): + return self.hit_dice + + @property + def wisdom(self): + return self.hit_dice + + @property + def charisma(self): + return self.hit_dice + + @property + def hp_max(self): + return self.hit_dice * 4 + + def at_object_creation(self): + """ + Start with max health. + + """ + self.hp = self.hp_max + class EvAdventureShopKeeper(EvAdventureNPC): """ diff --git a/evennia/contrib/tutorials/evadventure/objects.py b/evennia/contrib/tutorials/evadventure/objects.py index f1f532c282..c35bbd0d90 100644 --- a/evennia/contrib/tutorials/evadventure/objects.py +++ b/evennia/contrib/tutorials/evadventure/objects.py @@ -10,7 +10,7 @@ Tags. from evennia.objects.objects import DefaultObject from evennia.typeclasses.attributes import AttributeProperty -from .enums import WieldLocation, Ability +from .enums import Ability, WieldLocation class EvAdventureObject(DefaultObject): @@ -44,6 +44,7 @@ class EvAdventureObjectFiller(EvAdventureObject): meaning it's unusable. """ + quality = AttributeProperty(0) @@ -53,6 +54,7 @@ class EvAdventureConsumable(EvAdventureObject): have a limited usage in this way. """ + inventory_use_slot = AttributeProperty(WieldLocation.BACKPACK) size = AttributeProperty(0.25) uses = AttributeProperty(1) @@ -91,6 +93,7 @@ class EvAdventureRunestone(EvAdventureWeapon): they are quite powerful (and scales with caster level). """ + inventory_use_slot = AttributeProperty(WieldLocation.TWO_HANDS) attack_type = AttributeProperty(Ability.INT) diff --git a/evennia/contrib/tutorials/evadventure/rules.py b/evennia/contrib/tutorials/evadventure/rules.py index 595e572c06..1a4c06f612 100644 --- a/evennia/contrib/tutorials/evadventure/rules.py +++ b/evennia/contrib/tutorials/evadventure/rules.py @@ -23,14 +23,13 @@ This module presents several singletons to import """ from random import randint + from evennia.utils.evform import EvForm from evennia.utils.evtable import EvTable -from .enums import Ability -from .random_tables import ( - character_generation as chargen_table, - death_and_dismemberment as death_table, -) +from .enums import Ability +from .random_tables import character_generation as chargen_table +from .random_tables import death_and_dismemberment as death_table # Basic rolls diff --git a/evennia/contrib/tutorials/evadventure/tests/mixins.py b/evennia/contrib/tutorials/evadventure/tests/mixins.py index 7a23d27eb2..1bc4da2cde 100644 --- a/evennia/contrib/tutorials/evadventure/tests/mixins.py +++ b/evennia/contrib/tutorials/evadventure/tests/mixins.py @@ -19,8 +19,9 @@ class EvAdventureMixin: def setUp(self): super().setUp() self.location = create.create_object(EvAdventureRoom, key="testroom") - self.character = create.create_object(EvAdventureCharacter, key="testchar", - location=self.location) + self.character = create.create_object( + EvAdventureCharacter, key="testchar", location=self.location + ) self.helmet = create.create_object( EvAdventureObject, key="helmet", diff --git a/evennia/contrib/tutorials/evadventure/tests/test_combat.py b/evennia/contrib/tutorials/evadventure/tests/test_combat.py index 195c6222a5..bbba30890a 100644 --- a/evennia/contrib/tutorials/evadventure/tests/test_combat.py +++ b/evennia/contrib/tutorials/evadventure/tests/test_combat.py @@ -16,6 +16,7 @@ class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest): Test the turn-based combat-handler implementation. """ + maxDiff = None @patch( @@ -52,10 +53,7 @@ class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest): self.combathandler.register_action(self.combatant, action.key) - self.assertEqual( - self.combathandler.action_queue[self.combatant], - (action, (), {}) - ) + self.assertEqual(self.combathandler.action_queue[self.combatant], (action, (), {})) action.use = MagicMock() diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 89935982f7..9d7d6488db 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -13,7 +13,6 @@ from collections import defaultdict import inflect from django.conf import settings from django.utils.translation import gettext as _ - from evennia.commands import cmdset from evennia.commands.cmdsethandler import CmdSetHandler from evennia.objects.manager import ObjectManager @@ -22,15 +21,9 @@ from evennia.scripts.scripthandler import ScriptHandler from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.models import TypeclassBase from evennia.utils import ansi, create, funcparser, logger, search -from evennia.utils.utils import ( - class_from_module, - is_iter, - lazy_property, - list_to_string, - make_iter, - to_str, - variable_from_module, -) +from evennia.utils.utils import (class_from_module, is_iter, lazy_property, + list_to_string, make_iter, to_str, + variable_from_module) _INFLECT = inflect.engine() _MULTISESSION_MODE = settings.MULTISESSION_MODE diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index a718d9b01b..012a9276a9 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -1,10 +1,11 @@ -from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase -from evennia import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit -from evennia.typeclasses.attributes import AttributeProperty -from evennia.typeclasses.tags import TagProperty, AliasProperty, PermissionProperty +from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom from evennia.objects.models import ObjectDB from evennia.objects.objects import DefaultObject +from evennia.typeclasses.attributes import AttributeProperty +from evennia.typeclasses.tags import (AliasProperty, PermissionProperty, + TagProperty) from evennia.utils import create +from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase class DefaultObjectTest(BaseEvenniaTest): diff --git a/evennia/scripts/scripthandler.py b/evennia/scripts/scripthandler.py index 905a1b952f..2c3e2fc78a 100644 --- a/evennia/scripts/scripthandler.py +++ b/evennia/scripts/scripthandler.py @@ -126,7 +126,7 @@ class ScriptHandler(object): """ return ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=key) - def delete(self, key=None): + def remove(self, key=None): """ Forcibly delete a script from this object. @@ -149,7 +149,8 @@ class ScriptHandler(object): num += 1 return num - # alias to delete + # legacy aliases to remove + delete = remove stop = delete def all(self): diff --git a/evennia/scripts/scripts.py b/evennia/scripts/scripts.py index aac89ce719..7abd589c34 100644 --- a/evennia/scripts/scripts.py +++ b/evennia/scripts/scripts.py @@ -5,13 +5,13 @@ ability to run timers. """ +from django.utils.translation import gettext as _ +from evennia.scripts.manager import ScriptManager +from evennia.scripts.models import ScriptDB +from evennia.typeclasses.models import TypeclassBase +from evennia.utils import create, logger from twisted.internet.defer import Deferred, maybeDeferred from twisted.internet.task import LoopingCall -from django.utils.translation import gettext as _ -from evennia.typeclasses.models import TypeclassBase -from evennia.scripts.models import ScriptDB -from evennia.scripts.manager import ScriptManager -from evennia.utils import create, logger __all__ = ["DefaultScript", "DoNothing", "Store"] @@ -366,7 +366,11 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase): return # call hook - self.at_repeat() + try: + self.at_repeat() + except Exception: + logger.log_trace() + raise # check repeats if self.ndb._task: