From 7ddb5162ab0d1cd7411b98123b183d4e37bc3781 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Thu, 9 Nov 2017 22:36:11 -0800 Subject: [PATCH 01/58] Added tb_magic.py - only basic input parsing --- evennia/contrib/turnbattle/tb_magic.py | 961 +++++++++++++++++++++++++ 1 file changed, 961 insertions(+) create mode 100644 evennia/contrib/turnbattle/tb_magic.py diff --git a/evennia/contrib/turnbattle/tb_magic.py b/evennia/contrib/turnbattle/tb_magic.py new file mode 100644 index 0000000000..cc639737c0 --- /dev/null +++ b/evennia/contrib/turnbattle/tb_magic.py @@ -0,0 +1,961 @@ +""" +Simple turn-based combat system + +Contrib - Tim Ashley Jenkins 2017 + +This is a framework for a simple turn-based combat system, similar +to those used in D&D-style tabletop role playing games. It allows +any character to start a fight in a room, at which point initiative +is rolled and a turn order is established. Each participant in combat +has a limited time to decide their action for that turn (30 seconds by +default), and combat progresses through the turn order, looping through +the participants until the fight ends. + +Only simple rolls for attacking are implemented here, but this system +is easily extensible and can be used as the foundation for implementing +the rules from your turn-based tabletop game of choice or making your +own battle system. + +To install and test, import this module's TBMagicCharacter object into +your game's character.py module: + + from evennia.contrib.turnbattle.tb_magic import TBMagicCharacter + +And change your game's character typeclass to inherit from TBMagicCharacter +instead of the default: + + class Character(TBMagicCharacter): + +Next, import this module into your default_cmdsets.py module: + + from evennia.contrib.turnbattle import tb_magic + +And add the battle command set to your default command set: + + # + # any commands you add below will overload the default ones. + # + self.add(tb_magic.BattleCmdSet()) + +This module is meant to be heavily expanded on, so you may want to copy it +to your game's 'world' folder and modify it there rather than importing it +in your game and using it as-is. +""" + +from random import randint +from evennia import DefaultCharacter, Command, default_cmds, DefaultScript +from evennia.commands.default.muxcommand import MuxCommand +from evennia.commands.default.help import CmdHelp + +""" +---------------------------------------------------------------------------- +OPTIONS +---------------------------------------------------------------------------- +""" + +TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds +ACTIONS_PER_TURN = 1 # Number of actions allowed per turn + +""" +---------------------------------------------------------------------------- +COMBAT FUNCTIONS START HERE +---------------------------------------------------------------------------- +""" + +def roll_init(character): + """ + Rolls a number between 1-1000 to determine initiative. + + Args: + character (obj): The character to determine initiative for + + Returns: + initiative (int): The character's place in initiative - higher + numbers go first. + + Notes: + By default, does not reference the character and simply returns + a random integer from 1 to 1000. + + Since the character is passed to this function, you can easily reference + a character's stats to determine an initiative roll - for example, if your + character has a 'dexterity' attribute, you can use it to give that character + an advantage in turn order, like so: + + return (randint(1,20)) + character.db.dexterity + + This way, characters with a higher dexterity will go first more often. + """ + return randint(1, 1000) + + +def get_attack(attacker, defender): + """ + Returns a value for an attack roll. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + + Returns: + attack_value (int): Attack roll value, compared against a defense value + to determine whether an attack hits or misses. + + Notes: + By default, returns a random integer from 1 to 100 without using any + properties from either the attacker or defender. + + This can easily be expanded to return a value based on characters stats, + equipment, and abilities. This is why the attacker and defender are passed + to this function, even though nothing from either one are used in this example. + """ + # For this example, just return a random integer up to 100. + attack_value = randint(1, 100) + return attack_value + + +def get_defense(attacker, defender): + """ + Returns a value for defense, which an attack roll must equal or exceed in order + for an attack to hit. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + + Returns: + defense_value (int): Defense value, compared against an attack roll + to determine whether an attack hits or misses. + + Notes: + By default, returns 50, not taking any properties of the defender or + attacker into account. + + As above, this can be expanded upon based on character stats and equipment. + """ + # For this example, just return 50, for about a 50/50 chance of hit. + defense_value = 50 + return defense_value + + +def get_damage(attacker, defender): + """ + Returns a value for damage to be deducted from the defender's HP after abilities + successful hit. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being damaged + + Returns: + damage_value (int): Damage value, which is to be deducted from the defending + character's HP. + + Notes: + By default, returns a random integer from 15 to 25 without using any + properties from either the attacker or defender. + + Again, this can be expanded upon. + """ + # For this example, just generate a number between 15 and 25. + damage_value = randint(15, 25) + return damage_value + + +def apply_damage(defender, damage): + """ + Applies damage to a target, reducing their HP by the damage amount to a + minimum of 0. + + Args: + defender (obj): Character taking damage + damage (int): Amount of damage being taken + """ + defender.db.hp -= damage # Reduce defender's HP by the damage dealt. + # If this reduces it to 0 or less, set HP to 0. + if defender.db.hp <= 0: + defender.db.hp = 0 + +def at_defeat(defeated): + """ + Announces the defeat of a fighter in combat. + + Args: + defeated (obj): Fighter that's been defeated. + + Notes: + All this does is announce a defeat message by default, but if you + want anything else to happen to defeated fighters (like putting them + into a dying state or something similar) then this is the place to + do it. + """ + defeated.location.msg_contents("%s has been defeated!" % defeated) + +def resolve_attack(attacker, defender, attack_value=None, defense_value=None): + """ + Resolves an attack and outputs the result. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + + Notes: + Even though the attack and defense values are calculated + extremely simply, they are separated out into their own functions + so that they are easier to expand upon. + """ + # Get an attack roll from the attacker. + if not attack_value: + attack_value = get_attack(attacker, defender) + # Get a defense value from the defender. + if not defense_value: + defense_value = get_defense(attacker, defender) + # If the attack value is lower than the defense value, miss. Otherwise, hit. + if attack_value < defense_value: + attacker.location.msg_contents("%s's attack misses %s!" % (attacker, defender)) + else: + damage_value = get_damage(attacker, defender) # Calculate damage value. + # Announce damage dealt and apply damage. + attacker.location.msg_contents("%s hits %s for %i damage!" % (attacker, defender, damage_value)) + apply_damage(defender, damage_value) + # If defender HP is reduced to 0 or less, call at_defeat. + if defender.db.hp <= 0: + at_defeat(defender) + +def combat_cleanup(character): + """ + Cleans up all the temporary combat-related attributes on a character. + + Args: + character (obj): Character to have their combat attributes removed + + Notes: + Any attribute whose key begins with 'combat_' is temporary and no + longer needed once a fight ends. + """ + for attr in character.attributes.all(): + if attr.key[:7] == "combat_": # If the attribute name starts with 'combat_'... + character.attributes.remove(key=attr.key) # ...then delete it! + + +def is_in_combat(character): + """ + Returns true if the given character is in combat. + + Args: + character (obj): Character to determine if is in combat or not + + Returns: + (bool): True if in combat or False if not in combat + """ + return bool(character.db.combat_turnhandler) + + +def is_turn(character): + """ + Returns true if it's currently the given character's turn in combat. + + Args: + character (obj): Character to determine if it is their turn or not + + Returns: + (bool): True if it is their turn or False otherwise + """ + turnhandler = character.db.combat_turnhandler + currentchar = turnhandler.db.fighters[turnhandler.db.turn] + return bool(character == currentchar) + + +def spend_action(character, actions, action_name=None): + """ + Spends a character's available combat actions and checks for end of turn. + + Args: + character (obj): Character spending the action + actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions + + Kwargs: + action_name (str or None): If a string is given, sets character's last action in + combat to provided string + """ + if not is_in_combat(character): + return + if action_name: + character.db.combat_lastaction = action_name + if actions == 'all': # If spending all actions + character.db.combat_actionsleft = 0 # Set actions to 0 + else: + character.db.combat_actionsleft -= actions # Use up actions. + if character.db.combat_actionsleft < 0: + character.db.combat_actionsleft = 0 # Can't have fewer than 0 actions + character.db.combat_turnhandler.turn_end_check(character) # Signal potential end of turn. + + +""" +---------------------------------------------------------------------------- +CHARACTER TYPECLASS +---------------------------------------------------------------------------- +""" + + +class TBMagicCharacter(DefaultCharacter): + """ + A character able to participate in turn-based combat. Has attributes for current + and maximum HP, and access to combat commands. + """ + + def at_object_creation(self): + """ + Called once, when this object is first created. This is the + normal hook to overload for most object types. + """ + self.db.max_hp = 100 # Set maximum HP to 100 + self.db.hp = self.db.max_hp # Set current HP to maximum + self.db.spells_known = [] # Set empty spells known list + """ + Adds attributes for a character's current and maximum HP. + We're just going to set this value at '100' by default. + + You may want to expand this to include various 'stats' that + can be changed at creation and factor into combat calculations. + """ + + def at_before_move(self, destination): + """ + Called just before starting to move this object to + destination. + + Args: + destination (Object): The object we are moving to + + Returns: + shouldmove (bool): If we should move or not. + + Notes: + If this method returns False/None, the move is cancelled + before it is even started. + + """ + # Keep the character from moving if at 0 HP or in combat. + if is_in_combat(self): + self.msg("You can't exit a room while in combat!") + return False # Returning false keeps the character from moving. + if self.db.HP <= 0: + self.msg("You can't move, you've been defeated!") + return False + return True + +""" +---------------------------------------------------------------------------- +SCRIPTS START HERE +---------------------------------------------------------------------------- +""" + + +class TBMagicTurnHandler(DefaultScript): + """ + This is the script that handles the progression of combat through turns. + On creation (when a fight is started) it adds all combat-ready characters + to its roster and then sorts them into a turn order. There can only be one + fight going on in a single room at a time, so the script is assigned to a + room as its object. + + Fights persist until only one participant is left with any HP or all + remaining participants choose to end the combat with the 'disengage' command. + """ + + def at_script_creation(self): + """ + Called once, when the script is created. + """ + self.key = "Combat Turn Handler" + self.interval = 5 # Once every 5 seconds + self.persistent = True + self.db.fighters = [] + + # Add all fighters in the room with at least 1 HP to the combat." + for thing in self.obj.contents: + if thing.db.hp: + self.db.fighters.append(thing) + + # Initialize each fighter for combat + for fighter in self.db.fighters: + self.initialize_for_combat(fighter) + + # Add a reference to this script to the room + self.obj.db.combat_turnhandler = self + + # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. + # The initiative roll is determined by the roll_init function and can be customized easily. + ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True) + self.db.fighters = ordered_by_roll + + # Announce the turn order. + self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters)) + + # Start first fighter's turn. + self.start_turn(self.db.fighters[0]) + + # Set up the current turn and turn timeout delay. + self.db.turn = 0 + self.db.timer = TURN_TIMEOUT # Set timer to turn timeout specified in options + + def at_stop(self): + """ + Called at script termination. + """ + for fighter in self.db.fighters: + combat_cleanup(fighter) # Clean up the combat attributes for every fighter. + self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location + + def at_repeat(self): + """ + Called once every self.interval seconds. + """ + currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order. + self.db.timer -= self.interval # Count down the timer. + + if self.db.timer <= 0: + # Force current character to disengage if timer runs out. + self.obj.msg_contents("%s's turn timed out!" % currentchar) + spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions. + return + elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left + # Warn the current character if they're about to time out. + currentchar.msg("WARNING: About to time out!") + self.db.timeout_warning_given = True + + def initialize_for_combat(self, character): + """ + Prepares a character for combat when starting or entering a fight. + + Args: + character (obj): Character to initialize for combat. + """ + combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. + character.db.combat_actionsleft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 + character.db.combat_turnhandler = self # Add a reference to this turn handler script to the character + character.db.combat_lastaction = "null" # Track last action taken in combat + + def start_turn(self, character): + """ + Readies a character for the start of their turn by replenishing their + available actions and notifying them that their turn has come up. + + Args: + character (obj): Character to be readied. + + Notes: + Here, you only get one action per turn, but you might want to allow more than + one per turn, or even grant a number of actions based on a character's + attributes. You can even add multiple different kinds of actions, I.E. actions + separated for movement, by adding "character.db.combat_movesleft = 3" or + something similar. + """ + character.db.combat_actionsleft = ACTIONS_PER_TURN # Replenish actions + # Prompt the character for their turn and give some information. + character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) + + def next_turn(self): + """ + Advances to the next character in the turn order. + """ + + # Check to see if every character disengaged as their last action. If so, end combat. + disengage_check = True + for fighter in self.db.fighters: + if fighter.db.combat_lastaction != "disengage": # If a character has done anything but disengage + disengage_check = False + if disengage_check: # All characters have disengaged + self.obj.msg_contents("All fighters have disengaged! Combat is over!") + self.stop() # Stop this script and end combat. + return + + # Check to see if only one character is left standing. If so, end combat. + defeated_characters = 0 + for fighter in self.db.fighters: + if fighter.db.HP == 0: + defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated) + if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated + for fighter in self.db.fighters: + if fighter.db.HP != 0: + LastStanding = fighter # Pick the one fighter left with HP remaining + self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding) + self.stop() # Stop this script and end combat. + return + + # Cycle to the next turn. + currentchar = self.db.fighters[self.db.turn] + self.db.turn += 1 # Go to the next in the turn order. + if self.db.turn > len(self.db.fighters) - 1: + self.db.turn = 0 # Go back to the first in the turn order once you reach the end. + newchar = self.db.fighters[self.db.turn] # Note the new character + self.db.timer = TURN_TIMEOUT + self.time_until_next_repeat() # Reset the timer. + self.db.timeout_warning_given = False # Reset the timeout warning. + self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) + self.start_turn(newchar) # Start the new character's turn. + + def turn_end_check(self, character): + """ + Tests to see if a character's turn is over, and cycles to the next turn if it is. + + Args: + character (obj): Character to test for end of turn + """ + if not character.db.combat_actionsleft: # Character has no actions remaining + self.next_turn() + return + + def join_fight(self, character): + """ + Adds a new character to a fight already in progress. + + Args: + character (obj): Character to be added to the fight. + """ + # Inserts the fighter to the turn order, right behind whoever's turn it currently is. + self.db.fighters.insert(self.db.turn, character) + # Tick the turn counter forward one to compensate. + self.db.turn += 1 + # Initialize the character like you do at the start. + self.initialize_for_combat(character) + + +""" +---------------------------------------------------------------------------- +COMMANDS START HERE +---------------------------------------------------------------------------- +""" + + +class CmdFight(Command): + """ + Starts a fight with everyone in the same room as you. + + Usage: + fight + + When you start a fight, everyone in the room who is able to + fight is added to combat, and a turn order is randomly rolled. + When it's your turn, you can attack other characters. + """ + key = "fight" + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + here = self.caller.location + fighters = [] + + if not self.caller.db.hp: # If you don't have any hp + self.caller.msg("You can't start a fight if you've been defeated!") + return + if is_in_combat(self.caller): # Already in a fight + self.caller.msg("You're already in a fight!") + return + for thing in here.contents: # Test everything in the room to add it to the fight. + if thing.db.HP: # If the object has HP... + fighters.append(thing) # ...then add it to the fight. + if len(fighters) <= 1: # If you're the only able fighter in the room + self.caller.msg("There's nobody here to fight!") + return + if here.db.combat_turnhandler: # If there's already a fight going on... + here.msg_contents("%s joins the fight!" % self.caller) + here.db.combat_turnhandler.join_fight(self.caller) # Join the fight! + return + here.msg_contents("%s starts a fight!" % self.caller) + # Add a turn handler script to the room, which starts combat. + here.scripts.add("contrib.turnbattle.tb_magic.TBMagicTurnHandler") + # Remember you'll have to change the path to the script if you copy this code to your own modules! + + +class CmdAttack(Command): + """ + Attacks another character. + + Usage: + attack + + When in a fight, you may attack another character. The attack has + a chance to hit, and if successful, will deal damage. + """ + + key = "attack" + help_category = "combat" + + def func(self): + "This performs the actual command." + "Set the attacker to the caller and the defender to the target." + + if not is_in_combat(self.caller): # If not in combat, can't attack. + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # If it's not your turn, can't attack. + self.caller.msg("You can only do that on your turn.") + return + + if not self.caller.db.hp: # Can't attack if you have no HP. + self.caller.msg("You can't attack, you've been defeated.") + return + + attacker = self.caller + defender = self.caller.search(self.args) + + if not defender: # No valid target given. + return + + if not defender.db.hp: # Target object has no HP left or to begin with + self.caller.msg("You can't fight that!") + return + + if attacker == defender: # Target and attacker are the same + self.caller.msg("You can't attack yourself!") + return + + "If everything checks out, call the attack resolving function." + resolve_attack(attacker, defender) + spend_action(self.caller, 1, action_name="attack") # Use up one action. + + +class CmdPass(Command): + """ + Passes on your turn. + + Usage: + pass + + When in a fight, you can use this command to end your turn early, even + if there are still any actions you can take. + """ + + key = "pass" + aliases = ["wait", "hold"] + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + if not is_in_combat(self.caller): # Can only pass a turn in combat. + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # Can only pass if it's your turn. + self.caller.msg("You can only do that on your turn.") + return + + self.caller.location.msg_contents("%s takes no further action, passing the turn." % self.caller) + spend_action(self.caller, 'all', action_name="pass") # Spend all remaining actions. + + +class CmdDisengage(Command): + """ + Passes your turn and attempts to end combat. + + Usage: + disengage + + Ends your turn early and signals that you're trying to end + the fight. If all participants in a fight disengage, the + fight ends. + """ + + key = "disengage" + aliases = ["spare"] + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + if not is_in_combat(self.caller): # If you're not in combat + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # If it's not your turn + self.caller.msg("You can only do that on your turn.") + return + + self.caller.location.msg_contents("%s disengages, ready to stop fighting." % self.caller) + spend_action(self.caller, 'all', action_name="disengage") # Spend all remaining actions. + """ + The action_name kwarg sets the character's last action to "disengage", which is checked by + the turn handler script to see if all fighters have disengaged. + """ + +class CmdLearnSpell(Command): + """ + Learn a magic spell + """ + + key = "learnspell" + help_category = "magic" + + def func(self): + """ + This performs the actual command. + """ + spell_list = sorted(SPELLS.keys()) + args = self.args.lower() + args = args.strip(" ") + caller = self.caller + spell_to_learn = [] + + if not args or len(args) < 3: + caller.msg("Usage: learnspell ") + return + + for spell in spell_list: # Match inputs to spells + if args in spell.lower(): + spell_to_learn.append(spell) + + if spell_to_learn == []: # No spells matched + caller.msg("There is no spell with that name.") + return + if len(spell_to_learn) > 1: # More than one match + matched_spells = ', '.join(spell_to_learn) + caller.msg("Which spell do you mean: %s?" % matched_spells) + return + + if len(spell_to_learn) == 1: # If one match, extract the string + spell_to_learn = spell_to_learn[0] + + if spell_to_learn not in self.caller.db.spells_known: + caller.db.spells_known.append(spell_to_learn) + caller.msg("You learn the spell '%s'!" % spell_to_learn) + return + if spell_to_learn in self.caller.db.spells_known: + caller.msg("You already know the spell '%s'!" % spell_to_learn) + +class CmdCast(MuxCommand): + """ + Cast a magic spell! + """ + + key = "cast" + help_category = "magic" + + def func(self): + """ + This performs the actual command. + """ + caller = self.caller + + syntax_err = "Usage: cast [= ]" + if not self.lhs or len(self.lhs) < 3: # No spell name given + self.caller.msg(syntax_err) + return + + spellname = self.lhs.lower() + spell_to_cast = [] + spell_targets = [] + + if not self.rhs: + spell_targets = [] + elif self.rhs.lower() in ['me', 'self', 'myself']: + spell_targets = [caller] + elif len(self.rhs) > 2: + spell_targets = self.rhslist + + for spell in caller.db.spells_known: # Match inputs to spells + if self.lhs in spell.lower(): + spell_to_cast.append(spell) + + if spell_to_cast == []: # No spells matched + caller.msg("You don't know a spell of that name.") + return + if len(spell_to_cast) > 1: # More than one match + matched_spells = ', '.join(spell_to_cast) + caller.msg("Which spell do you mean: %s?" % matched_spells) + return + + if len(spell_to_cast) == 1: # If one match, extract the string + spell_to_cast = spell_to_cast[0] + + if spell_to_cast not in SPELLS: # Spell isn't defined + caller.msg("ERROR: Spell %s is undefined" % spell_to_cast) + return + + # Time to extract some info from the chosen spell! + spelldata = SPELLS[spell_to_cast] + + # Add in some default data if optional parameters aren't specified + if "combat_spell" not in spelldata: + spelldata.update({"combat_spell":True}) + if "noncombat_spell" not in spelldata: + spelldata.update({"noncombat_spell":True}) + if "max_targets" not in spelldata: + spelldata.update({"max_targets":1}) + + # If spell takes no targets, give error message and return + if len(spell_targets) > 0 and spelldata["target"] == "none": + caller.msg("The spell '%s' isn't cast on a target.") + return + + # If no target is given and spell requires a target, give error message + if spelldata["target"] not in ["self", "none"]: + if len(spell_targets) == 0: + caller.msg("The spell %s requires a target." % spell_to_cast) + return + + # If more targets given than maximum, give error message + if len(spell_targets) > spelldata["max_targets"]: + targplural = "target" + if spelldata["max_targets"] > 1: + targplural = "targets" + caller.msg("The spell %s can only be cast on %i %s." % (spell_to_cast, spelldata["max_targets"], targplural)) + return + + # Set up our candidates for targets + target_candidates = [] + + if spelldata["target"] in ["any", "other"]: + target_candidates = caller.location.contents + caller.contents + + if spelldata["target"] == "anyobj": + prefilter_candidates = caller.location.contents + caller.contents + for thing in prefilter_candidates: + if not thing.attributes.has("max_hp"): # Has no max HP, isn't a fighter + target_candidates.append(thing) + + if spelldata["target"] in ["anychar", "otherchar"]: + prefilter_candidates = caller.location.contents + for thing in prefilter_candidates: + if thing.attributes.has("max_hp"): # Has max HP, is a fighter + target_candidates.append(thing) + + # Now, match each entry in spell_targets to an object + matched_targets = [] + for target in spell_targets: + match = caller.search(target, candidates=target_candidates) + matched_targets.append(match) + spell_targets = matched_targets + + # If no target is given and the spell's target is 'self', set target to self + if len(spell_targets) == 0 and spelldata["target"] == "self": + spell_targets = [caller] + + # Give error message if trying to cast an "other" target spell on yourself + if spelldata["target"] in ["other", "otherchar"]: + if caller in spell_targets: + caller.msg("You can't cast %s on yourself." % spell_to_cast) + return + + # Return if "None" in target list, indicating failed match + if None in spell_targets: + return + + # Give error message if repeats in target list + if len(spell_targets) != len(set(spell_targets)): + caller.msg("You can't specify the same target more than once!") + return + + caller.msg("You cast %s! Fwooosh!" % spell_to_cast) + + +class CmdRest(Command): + """ + Recovers damage. + + Usage: + rest + + Resting recovers your HP to its maximum, but you can only + rest if you're not in a fight. + """ + + key = "rest" + help_category = "combat" + + def func(self): + "This performs the actual command." + + if is_in_combat(self.caller): # If you're in combat + self.caller.msg("You can't rest while you're in combat.") + return + + self.caller.db.hp = self.caller.db.max_hp # Set current HP to maximum + self.caller.location.msg_contents("%s rests to recover HP." % self.caller) + """ + You'll probably want to replace this with your own system for recovering HP. + """ + + +class CmdCombatHelp(CmdHelp): + """ + View help or a list of topics + + Usage: + help + help list + help all + + This will search for help on commands and other + topics related to the game. + """ + # Just like the default help command, but will give quick + # tips on combat when used in a fight with no arguments. + + def func(self): + if is_in_combat(self.caller) and not self.args: # In combat and entered 'help' alone + self.caller.msg("Available combat commands:|/" + + "|wAttack:|n Attack a target, attempting to deal damage.|/" + + "|wPass:|n Pass your turn without further action.|/" + + "|wDisengage:|n End your turn and attempt to end combat.|/") + else: + super(CmdCombatHelp, self).func() # Call the default help command + + +class BattleCmdSet(default_cmds.CharacterCmdSet): + """ + This command set includes all the commmands used in the battle system. + """ + key = "DefaultCharacter" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + self.add(CmdFight()) + self.add(CmdAttack()) + self.add(CmdRest()) + self.add(CmdPass()) + self.add(CmdDisengage()) + self.add(CmdCombatHelp()) + self.add(CmdLearnSpell()) + self.add(CmdCast()) + +""" +Required values for spells: + + cost (int): MP cost of casting the spell + target (str): Valid targets for the spell. Can be any of: + "none" - No target needed + "self" - Self only + "any" - Any object + "anyobj" - Any object that isn't a character + "anychar" - Any character + "other" - Any object excluding the caster + "otherchar" - Any character excluding the caster + spellfunc (callable): Function that performs the action of the spell. + Must take the following arguments: caster (obj), targets (list), + and cost(int). + +Optional values for spells: + + combat_spell (bool): If the spell can be cast in combat. True by default. + noncombat_spell (bool): If the spell can be cast out of combat. True by default. + max_targets (int): Maximum number of objects that can be targeted by the spell. + 1 by default - unused if target is "none" or "self" + +Any other values specified besides the above will be passed as kwargs to the spellfunc. + +""" + +SPELLS = { +"magic missile":{"spellfunc":None, "target":"otherchar", "cost":3, "noncombat_spell":False, "max_targets":3}, +"cure wounds":{"spellfunc":None, "target":"anychar", "cost":5}, +} \ No newline at end of file From 5fe3cd186ddf5832b5f50dab5c538018ac625561 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 12 Nov 2017 18:23:22 -0800 Subject: [PATCH 02/58] Added functional 'cure wounds' spell Also added more spell verification in the 'cast' command, accounting for spell's MP cost and whether it can be used in combat --- evennia/contrib/turnbattle/tb_magic.py | 83 ++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_magic.py b/evennia/contrib/turnbattle/tb_magic.py index cc639737c0..7b59393e23 100644 --- a/evennia/contrib/turnbattle/tb_magic.py +++ b/evennia/contrib/turnbattle/tb_magic.py @@ -312,6 +312,8 @@ class TBMagicCharacter(DefaultCharacter): self.db.max_hp = 100 # Set maximum HP to 100 self.db.hp = self.db.max_hp # Set current HP to maximum self.db.spells_known = [] # Set empty spells known list + self.db.max_mp = 20 # Set maximum MP to 20 + self.db.mp = self.db.max_mp # Set current MP to maximum """ Adds attributes for a character's current and maximum HP. We're just going to set this value at '100' by default. @@ -733,6 +735,13 @@ class CmdLearnSpell(Command): class CmdCast(MuxCommand): """ Cast a magic spell! + + Notes: This is a quite long command, since it has to cope with all + the different circumstances in which you may or may not be able + to cast a spell. None of the spell's effects are handled by the + command - all the command does is verify that the player's input + is valid for the spell being cast and then call the spell's + function. """ key = "cast" @@ -789,8 +798,29 @@ class CmdCast(MuxCommand): spelldata.update({"noncombat_spell":True}) if "max_targets" not in spelldata: spelldata.update({"max_targets":1}) + + # Store any superfluous options as kwargs to pass to the spell function + kwargs = {} + spelldata_opts = ["spellfunc", "target", "cost", "combat_spell", "noncombat_spell", "max_targets"] + for key in spelldata: + if key not in spelldata_opts: + kwargs.update({key:spelldata[key]}) + + # If caster doesn't have enough MP to cover the spell's cost, give error and return + if spelldata["cost"] > caller.db.mp: + caller.msg("You don't have enough MP to cast '%s'." % spell_to_cast) + + # If in combat and the spell isn't a combat spell, give error message and return + if spelldata["combat_spell"] == False and is_in_combat(caller): + caller.msg("You can't use the spell '%s' in combat." % spell_to_cast) + return + + # If not in combat and the spell isn't a non-combat spell, error ms and return. + if spelldata["noncombat_spell"] == False and is_in_combat(caller) == False: + caller.msg("You can't use the spell '%s' outside of combat." % spell_to_cast) + return - # If spell takes no targets, give error message and return + # If spell takes no targets and one is given, give error message and return if len(spell_targets) > 0 and spelldata["target"] == "none": caller.msg("The spell '%s' isn't cast on a target.") return @@ -798,7 +828,7 @@ class CmdCast(MuxCommand): # If no target is given and spell requires a target, give error message if spelldata["target"] not in ["self", "none"]: if len(spell_targets) == 0: - caller.msg("The spell %s requires a target." % spell_to_cast) + caller.msg("The spell '%s' requires a target." % spell_to_cast) return # If more targets given than maximum, give error message @@ -806,28 +836,32 @@ class CmdCast(MuxCommand): targplural = "target" if spelldata["max_targets"] > 1: targplural = "targets" - caller.msg("The spell %s can only be cast on %i %s." % (spell_to_cast, spelldata["max_targets"], targplural)) + caller.msg("The spell '%s' can only be cast on %i %s." % (spell_to_cast, spelldata["max_targets"], targplural)) return # Set up our candidates for targets target_candidates = [] + # If spell targets 'any' or 'other', any object in caster's inventory or location + # can be targeted by the spell. if spelldata["target"] in ["any", "other"]: target_candidates = caller.location.contents + caller.contents + # If spell targets 'anyobj', only non-character objects can be targeted. if spelldata["target"] == "anyobj": prefilter_candidates = caller.location.contents + caller.contents for thing in prefilter_candidates: if not thing.attributes.has("max_hp"): # Has no max HP, isn't a fighter target_candidates.append(thing) + # If spell targets 'anychar' or 'otherchar', only characters can be targeted. if spelldata["target"] in ["anychar", "otherchar"]: prefilter_candidates = caller.location.contents for thing in prefilter_candidates: if thing.attributes.has("max_hp"): # Has max HP, is a fighter target_candidates.append(thing) - # Now, match each entry in spell_targets to an object + # Now, match each entry in spell_targets to an object in the search candidates matched_targets = [] for target in spell_targets: match = caller.search(target, candidates=target_candidates) @@ -841,11 +875,12 @@ class CmdCast(MuxCommand): # Give error message if trying to cast an "other" target spell on yourself if spelldata["target"] in ["other", "otherchar"]: if caller in spell_targets: - caller.msg("You can't cast %s on yourself." % spell_to_cast) + caller.msg("You can't cast '%s' on yourself." % spell_to_cast) return # Return if "None" in target list, indicating failed match if None in spell_targets: + # No need to give an error message, as 'search' gives one by default. return # Give error message if repeats in target list @@ -853,7 +888,8 @@ class CmdCast(MuxCommand): caller.msg("You can't specify the same target more than once!") return - caller.msg("You cast %s! Fwooosh!" % spell_to_cast) + # Finally, we can cast the spell itself + spelldata["spellfunc"](caller, spell_to_cast, spell_targets, spelldata["cost"], **kwargs) class CmdRest(Command): @@ -927,7 +963,31 @@ class BattleCmdSet(default_cmds.CharacterCmdSet): self.add(CmdCombatHelp()) self.add(CmdLearnSpell()) self.add(CmdCast()) - + +""" +SPELL FUNCTIONS START HERE +""" + +def spell_cure_wounds(caster, spell_name, targets, cost, **kwargs): + """ + Spell that restores HP to a target. + """ + spell_msg = "%s casts %s!" % (caster, spell_name) + + for character in targets: + to_heal = randint(20, 40) # Restore 20 to 40 hp + if character.db.hp + to_heal > character.db.max_hp: + to_heal = character.db.max_hp - character.db.hp # Cap healing to max HP + character.db.hp += to_heal + spell_msg += " %s regains %i HP!" % (character, to_heal) + + caster.db.mp -= cost # Deduct MP cost + + caster.location.msg_contents(spell_msg) # Message the room with spell results + + if is_in_combat(caster): # Spend action if in combat + spend_action(caster, 1, action_name="cast") + """ Required values for spells: @@ -941,8 +1001,8 @@ Required values for spells: "other" - Any object excluding the caster "otherchar" - Any character excluding the caster spellfunc (callable): Function that performs the action of the spell. - Must take the following arguments: caster (obj), targets (list), - and cost(int). + Must take the following arguments: caster (obj), spell_name (str), targets (list), + and cost(int), as well as **kwargs. Optional values for spells: @@ -957,5 +1017,6 @@ Any other values specified besides the above will be passed as kwargs to the spe SPELLS = { "magic missile":{"spellfunc":None, "target":"otherchar", "cost":3, "noncombat_spell":False, "max_targets":3}, -"cure wounds":{"spellfunc":None, "target":"anychar", "cost":5}, -} \ No newline at end of file +"cure wounds":{"spellfunc":spell_cure_wounds, "target":"anychar", "cost":5, "its_a_kwarg":"wow"}, +} + From 83579a2e06ce040ddc9fdd6002b0584a5a159c28 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 12 Nov 2017 21:11:28 -0800 Subject: [PATCH 03/58] Added attack spells, more healing spell variants --- evennia/contrib/turnbattle/tb_magic.py | 134 ++++++++++++++++++++++--- 1 file changed, 120 insertions(+), 14 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_magic.py b/evennia/contrib/turnbattle/tb_magic.py index 7b59393e23..bb451b1e96 100644 --- a/evennia/contrib/turnbattle/tb_magic.py +++ b/evennia/contrib/turnbattle/tb_magic.py @@ -753,10 +753,16 @@ class CmdCast(MuxCommand): """ caller = self.caller - syntax_err = "Usage: cast [= ]" if not self.lhs or len(self.lhs) < 3: # No spell name given - self.caller.msg(syntax_err) - return + caller.msg("Usage: cast = , , ...") + if not caller.db.spells_known: + caller.msg("You don't know any spells.") + return + else: + caller.db.spells_known = sorted(caller.db.spells_known) + spells_known_msg = "You know the following spells:|/" + "|/".join(caller.db.spells_known) + caller.msg(spells_known_msg) # List the spells the player knows + return spellname = self.lhs.lower() spell_to_cast = [] @@ -809,6 +815,7 @@ class CmdCast(MuxCommand): # If caster doesn't have enough MP to cover the spell's cost, give error and return if spelldata["cost"] > caller.db.mp: caller.msg("You don't have enough MP to cast '%s'." % spell_to_cast) + return # If in combat and the spell isn't a combat spell, give error message and return if spelldata["combat_spell"] == False and is_in_combat(caller): @@ -894,13 +901,13 @@ class CmdCast(MuxCommand): class CmdRest(Command): """ - Recovers damage. + Recovers damage and restores MP. Usage: rest - Resting recovers your HP to its maximum, but you can only - rest if you're not in a fight. + Resting recovers your HP and MP to their maximum, but you can + only rest if you're not in a fight. """ key = "rest" @@ -914,9 +921,10 @@ class CmdRest(Command): return self.caller.db.hp = self.caller.db.max_hp # Set current HP to maximum - self.caller.location.msg_contents("%s rests to recover HP." % self.caller) + self.caller.db.mp = self.caller.db.max_mp # Set current MP to maximum + self.caller.location.msg_contents("%s rests to recover HP and MP." % self.caller) """ - You'll probably want to replace this with your own system for recovering HP. + You'll probably want to replace this with your own system for recovering HP and MP. """ @@ -974,8 +982,16 @@ def spell_cure_wounds(caster, spell_name, targets, cost, **kwargs): """ spell_msg = "%s casts %s!" % (caster, spell_name) + min_healing = 20 + max_healing = 40 + + # Retrieve healing range from kwargs, if present + if "healing_range" in kwargs: + min_healing = kwargs["healing_range"][0] + max_healing = kwargs["healing_range"][1] + for character in targets: - to_heal = randint(20, 40) # Restore 20 to 40 hp + to_heal = randint(min_healing, max_healing) # Restore 20 to 40 hp if character.db.hp + to_heal > character.db.max_hp: to_heal = character.db.max_hp - character.db.hp # Cap healing to max HP character.db.hp += to_heal @@ -986,7 +1002,91 @@ def spell_cure_wounds(caster, spell_name, targets, cost, **kwargs): caster.location.msg_contents(spell_msg) # Message the room with spell results if is_in_combat(caster): # Spend action if in combat - spend_action(caster, 1, action_name="cast") + spend_action(caster, 1, action_name="cast") + +def spell_attack(caster, spell_name, targets, cost, **kwargs): + """ + Spell that deals damage in combat. Similar to resolve_attack. + """ + spell_msg = "%s casts %s!" % (caster, spell_name) + + atkname_single = "The spell" + atkname_plural = "spells" + min_damage = 10 + max_damage = 20 + accuracy = 0 + attack_count = 1 + + # Retrieve some variables from kwargs, if present + if "attack_name" in kwargs: + atkname_single = kwargs["attack_name"][0] + atkname_plural = kwargs["attack_name"][1] + if "damage_range" in kwargs: + min_damage = kwargs["damage_range"][0] + max_damage = kwargs["damage_range"][1] + if "accuracy" in kwargs: + accuracy = kwargs["accuracy"] + if "attack_count" in kwargs: + attack_count = kwargs["attack_count"] + + to_attack = [] + print targets + # If there are more attacks than targets given, attack first target multiple times + if len(targets) < attack_count: + to_attack = to_attack + targets + extra_attacks = attack_count - len(targets) + for n in range(extra_attacks): + to_attack.insert(0, targets[0]) + else: + to_attack = targets + + print to_attack + print targets + + # Set up dictionaries to track number of hits and total damage + total_hits = {} + total_damage = {} + for fighter in targets: + total_hits.update({fighter:0}) + total_damage.update({fighter:0}) + + # Resolve attack for each target + for fighter in to_attack: + attack_value = randint(1, 100) + accuracy # Spell attack roll + defense_value = get_defense(caster, fighter) + if attack_value >= defense_value: + spell_dmg = randint(min_damage, max_damage) # Get spell damage + total_hits[fighter] += 1 + total_damage[fighter] += spell_dmg + + print total_hits + print total_damage + print targets + + for fighter in targets: + # Construct combat message + if total_hits[fighter] == 0: + spell_msg += " The spell misses %s!" % fighter + elif total_hits[fighter] > 0: + attack_count_str = atkname_single + " hits" + if total_hits[fighter] > 1: + attack_count_str = "%i %s hit" % (total_hits[fighter], atkname_plural) + spell_msg += " %s %s for %i damage!" % (attack_count_str, fighter, total_damage[fighter]) + + caster.db.mp -= cost # Deduct MP cost + + caster.location.msg_contents(spell_msg) # Message the room with spell results + + for fighter in targets: + # Apply damage + apply_damage(fighter, total_damage[fighter]) + # If fighter HP is reduced to 0 or less, call at_defeat. + if fighter.db.hp <= 0: + at_defeat(fighter) + + if is_in_combat(caster): # Spend action if in combat + spend_action(caster, 1, action_name="cast") + """ Required values for spells: @@ -1002,7 +1102,7 @@ Required values for spells: "otherchar" - Any character excluding the caster spellfunc (callable): Function that performs the action of the spell. Must take the following arguments: caster (obj), spell_name (str), targets (list), - and cost(int), as well as **kwargs. + and cost (int), as well as **kwargs. Optional values for spells: @@ -1012,11 +1112,17 @@ Optional values for spells: 1 by default - unused if target is "none" or "self" Any other values specified besides the above will be passed as kwargs to the spellfunc. - +You can use kwargs to effectively re-use the same function for different but similar +spells. """ SPELLS = { -"magic missile":{"spellfunc":None, "target":"otherchar", "cost":3, "noncombat_spell":False, "max_targets":3}, -"cure wounds":{"spellfunc":spell_cure_wounds, "target":"anychar", "cost":5, "its_a_kwarg":"wow"}, +"magic missile":{"spellfunc":spell_attack, "target":"otherchar", "cost":3, "noncombat_spell":False, "max_targets":3, + "attack_name":("A bolt", "bolts"), "damage_range":(4, 7), "accuracy":999, "attack_count":3}, +"flame shot":{"spellfunc":spell_attack, "target":"otherchar", "cost":3, "noncombat_spell":False, + "attack_name":("A jet of flame", "jets of flame"), "damage_range":(25, 35)}, +"cure wounds":{"spellfunc":spell_cure_wounds, "target":"anychar", "cost":5}, +"mass cure wounds":{"spellfunc":spell_cure_wounds, "target":"anychar", "cost":10, "max_targets": 5}, +"full heal":{"spellfunc":spell_cure_wounds, "target":"anychar", "cost":12, "healing_range":(100, 100)} } From 858494eebbae228317c87e8d45f5973bbced7b3b Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Wed, 15 Nov 2017 14:14:16 -0800 Subject: [PATCH 04/58] Formatting and documentation --- evennia/contrib/turnbattle/tb_magic.py | 206 +++++++++++++++++++------ 1 file changed, 160 insertions(+), 46 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_magic.py b/evennia/contrib/turnbattle/tb_magic.py index bb451b1e96..c2222645ac 100644 --- a/evennia/contrib/turnbattle/tb_magic.py +++ b/evennia/contrib/turnbattle/tb_magic.py @@ -1,20 +1,38 @@ """ -Simple turn-based combat system +Simple turn-based combat system with spell casting Contrib - Tim Ashley Jenkins 2017 -This is a framework for a simple turn-based combat system, similar -to those used in D&D-style tabletop role playing games. It allows -any character to start a fight in a room, at which point initiative -is rolled and a turn order is established. Each participant in combat -has a limited time to decide their action for that turn (30 seconds by -default), and combat progresses through the turn order, looping through -the participants until the fight ends. +This is a version of the 'turnbattle' contrib that includes a basic, +expandable framework for a 'magic system', whereby players can spend +a limited resource (MP) to achieve a wide variety of effects, both in +and out of combat. This does not have to strictly be a system for +magic - it can easily be re-flavored to any other sort of resource +based mechanic, like psionic powers, special moves and stamina, and +so forth. -Only simple rolls for attacking are implemented here, but this system -is easily extensible and can be used as the foundation for implementing -the rules from your turn-based tabletop game of choice or making your -own battle system. +In this system, spells are learned by name with the 'learnspell' +command, and then used with the 'cast' command. Spells can be cast in or +out of combat - some spells can only be cast in combat, some can only be +cast outside of combat, and some can be cast any time. However, if you +are in combat, you can only cast a spell on your turn, and doing so will +typically use an action (as specified in the spell's funciton). + +Spells are defined at the end of the module in a database that's a +dictionary of dictionaries - each spell is matched by name to a function, +along with various parameters that restrict when the spell can be used and +what the spell can be cast on. Included is a small variety of spells that +damage opponents and heal HP, as well as one that creates an object. + +Because a spell can call any function, a spell can be made to do just +about anything at all. The SPELLS dictionary at the bottom of the module +even allows kwargs to be passed to the spell function, so that the same +function can be re-used for multiple similar spells. + +Spells in this system work on a very basic resource: MP, which is spent +when casting spells and restored by resting. It shouldn't be too difficult +to modify this system to use spell slots, some physical fuel or resource, +or whatever else your game requires. To install and test, import this module's TBMagicCharacter object into your game's character.py module: @@ -43,7 +61,7 @@ in your game and using it as-is. """ from random import randint -from evennia import DefaultCharacter, Command, default_cmds, DefaultScript +from evennia import DefaultCharacter, Command, default_cmds, DefaultScript, create_object from evennia.commands.default.muxcommand import MuxCommand from evennia.commands.default.help import CmdHelp @@ -690,7 +708,12 @@ class CmdDisengage(Command): class CmdLearnSpell(Command): """ - Learn a magic spell + Learn a magic spell. + + Usage: + learnspell + + Adds a spell by name to your list of spells known. """ key = "learnspell" @@ -706,7 +729,7 @@ class CmdLearnSpell(Command): caller = self.caller spell_to_learn = [] - if not args or len(args) < 3: + if not args or len(args) < 3: # No spell given caller.msg("Usage: learnspell ") return @@ -725,23 +748,29 @@ class CmdLearnSpell(Command): if len(spell_to_learn) == 1: # If one match, extract the string spell_to_learn = spell_to_learn[0] - if spell_to_learn not in self.caller.db.spells_known: - caller.db.spells_known.append(spell_to_learn) + if spell_to_learn not in self.caller.db.spells_known: # If the spell isn't known... + caller.db.spells_known.append(spell_to_learn) # ...then add the spell to the character caller.msg("You learn the spell '%s'!" % spell_to_learn) return - if spell_to_learn in self.caller.db.spells_known: - caller.msg("You already know the spell '%s'!" % spell_to_learn) + if spell_to_learn in self.caller.db.spells_known: # Already has the spell specified + caller.msg("You already know the spell '%s'!" % spell_to_learn) + """ + You will almost definitely want to replace this with your own system + for learning spells, perhaps tied to character advancement or finding + items in the game world that spells can be learned from. + """ class CmdCast(MuxCommand): """ - Cast a magic spell! + Cast a magic spell that you know, provided you have the MP + to spend on its casting. - Notes: This is a quite long command, since it has to cope with all - the different circumstances in which you may or may not be able - to cast a spell. None of the spell's effects are handled by the - command - all the command does is verify that the player's input - is valid for the spell being cast and then call the spell's - function. + Usage: + cast [= , , etc...] + + Some spells can be cast on multiple targets, some can be cast + on only yourself, and some don't need a target specified at all. + Typing 'cast' by itself will give you a list of spells you know. """ key = "cast" @@ -829,7 +858,7 @@ class CmdCast(MuxCommand): # If spell takes no targets and one is given, give error message and return if len(spell_targets) > 0 and spelldata["target"] == "none": - caller.msg("The spell '%s' isn't cast on a target.") + caller.msg("The spell '%s' isn't cast on a target." % spell_to_cast) return # If no target is given and spell requires a target, give error message @@ -895,8 +924,16 @@ class CmdCast(MuxCommand): caller.msg("You can't specify the same target more than once!") return - # Finally, we can cast the spell itself + # Finally, we can cast the spell itself. Note that MP is not deducted here! spelldata["spellfunc"](caller, spell_to_cast, spell_targets, spelldata["cost"], **kwargs) + """ + Note: This is a quite long command, since it has to cope with all + the different circumstances in which you may or may not be able + to cast a spell. None of the spell's effects are handled by the + command - all the command does is verify that the player's input + is valid for the spell being cast and then call the spell's + function. + """ class CmdRest(Command): @@ -973,12 +1010,32 @@ class BattleCmdSet(default_cmds.CharacterCmdSet): self.add(CmdCast()) """ +---------------------------------------------------------------------------- SPELL FUNCTIONS START HERE +---------------------------------------------------------------------------- + +These are the functions that are called by the 'cast' command to perform the +effects of various spells. Which spells execute which functions and what +parameters are passed to them are specified at the bottom of the module, in +the 'SPELLS' dictionary. + +All of these functions take the same arguments: + caster (obj): Character casting the spell + spell_name (str): Name of the spell being cast + targets (list): List of objects targeted by the spell + cost (int): MP cost of casting the spell + +These functions also all accept **kwargs, and how these are used is specified +in the docstring for each function. """ -def spell_cure_wounds(caster, spell_name, targets, cost, **kwargs): +def spell_healing(caster, spell_name, targets, cost, **kwargs): """ - Spell that restores HP to a target. + Spell that restores HP to a target or targets. + + kwargs: + healing_range (tuple): Minimum and maximum amount healed to + each target. (20, 40) by default. """ spell_msg = "%s casts %s!" % (caster, spell_name) @@ -1007,6 +1064,20 @@ def spell_cure_wounds(caster, spell_name, targets, cost, **kwargs): def spell_attack(caster, spell_name, targets, cost, **kwargs): """ Spell that deals damage in combat. Similar to resolve_attack. + + kwargs: + attack_name (tuple): Single and plural describing the sort of + attack or projectile that strikes each enemy. + damage_range (tuple): Minimum and maximum damage dealt by the + spell. (10, 20) by default. + accuracy (int): Modifier to the spell's attack roll, determining + an increased or decreased chance to hit. 0 by default. + attack_count (int): How many individual attacks are made as part + of the spell. If the number of attacks exceeds the number of + targets, the first target specified will be attacked more + than once. Just 1 by default - if the attack_count is less + than the number targets given, each target will only be + attacked once. """ spell_msg = "%s casts %s!" % (caster, spell_name) @@ -1030,7 +1101,6 @@ def spell_attack(caster, spell_name, targets, cost, **kwargs): attack_count = kwargs["attack_count"] to_attack = [] - print targets # If there are more attacks than targets given, attack first target multiple times if len(targets) < attack_count: to_attack = to_attack + targets @@ -1038,10 +1108,8 @@ def spell_attack(caster, spell_name, targets, cost, **kwargs): for n in range(extra_attacks): to_attack.insert(0, targets[0]) else: - to_attack = targets + to_attack = to_attack + targets - print to_attack - print targets # Set up dictionaries to track number of hits and total damage total_hits = {} @@ -1058,10 +1126,6 @@ def spell_attack(caster, spell_name, targets, cost, **kwargs): spell_dmg = randint(min_damage, max_damage) # Get spell damage total_hits[fighter] += 1 total_damage[fighter] += spell_dmg - - print total_hits - print total_damage - print targets for fighter in targets: # Construct combat message @@ -1087,11 +1151,54 @@ def spell_attack(caster, spell_name, targets, cost, **kwargs): if is_in_combat(caster): # Spend action if in combat spend_action(caster, 1, action_name="cast") +def spell_conjure(caster, spell_name, targets, cost, **kwargs): + """ + Spell that creates an object. + + kwargs: + obj_key (str): Key of the created object. + obj_desc (str): Desc of the created object. + obj_typeclass (str): Typeclass path of the object. + + If you want to make more use of this particular spell funciton, + you may want to modify it to use the spawner (in evennia.utils.spawner) + instead of creating objects directly. + """ + + obj_key = "a nondescript object" + obj_desc = "A perfectly generic object." + obj_typeclass = "evennia.objects.objects.DefaultObject" + + # Retrieve some variables from kwargs, if present + if "obj_key" in kwargs: + obj_key = kwargs["obj_key"] + if "obj_desc" in kwargs: + obj_desc = kwargs["obj_desc"] + if "obj_typeclass" in kwargs: + obj_typeclass = kwargs["obj_typeclass"] + + conjured_obj = create_object(obj_typeclass, key=obj_key, location=caster.location) # Create object + conjured_obj.db.desc = obj_desc # Add object desc + + caster.db.mp -= cost # Deduct MP cost + + caster.location.msg_contents("%s casts %s, and %s appears!" % (caster, spell_name, conjured_obj)) """ +---------------------------------------------------------------------------- +SPELL DEFINITIONS START HERE +---------------------------------------------------------------------------- +In this section, each spell is matched to a function, and given parameters +that determine its MP cost, valid type and number of targets, and what +function casting the spell executes. + +This data is given as a dictionary of dictionaries - the key of each entry +is the spell's name, and the value is a dictionary of various options and +parameters, some of which are required and others which are optional. + Required values for spells: - cost (int): MP cost of casting the spell + cost (int): MP cost of casting the spell target (str): Valid targets for the spell. Can be any of: "none" - No target needed "self" - Self only @@ -1101,8 +1208,8 @@ Required values for spells: "other" - Any object excluding the caster "otherchar" - Any character excluding the caster spellfunc (callable): Function that performs the action of the spell. - Must take the following arguments: caster (obj), spell_name (str), targets (list), - and cost (int), as well as **kwargs. + Must take the following arguments: caster (obj), spell_name (str), + targets (list), and cost (int), as well as **kwargs. Optional values for spells: @@ -1115,14 +1222,21 @@ Any other values specified besides the above will be passed as kwargs to the spe You can use kwargs to effectively re-use the same function for different but similar spells. """ - + SPELLS = { "magic missile":{"spellfunc":spell_attack, "target":"otherchar", "cost":3, "noncombat_spell":False, "max_targets":3, "attack_name":("A bolt", "bolts"), "damage_range":(4, 7), "accuracy":999, "attack_count":3}, + "flame shot":{"spellfunc":spell_attack, "target":"otherchar", "cost":3, "noncombat_spell":False, - "attack_name":("A jet of flame", "jets of flame"), "damage_range":(25, 35)}, -"cure wounds":{"spellfunc":spell_cure_wounds, "target":"anychar", "cost":5}, -"mass cure wounds":{"spellfunc":spell_cure_wounds, "target":"anychar", "cost":10, "max_targets": 5}, -"full heal":{"spellfunc":spell_cure_wounds, "target":"anychar", "cost":12, "healing_range":(100, 100)} + "attack_name":("A jet of flame", "jets of flame"), "damage_range":(25, 35)}, + +"cure wounds":{"spellfunc":spell_healing, "target":"anychar", "cost":5}, + +"mass cure wounds":{"spellfunc":spell_healing, "target":"anychar", "cost":10, "max_targets": 5}, + +"full heal":{"spellfunc":spell_healing, "target":"anychar", "cost":12, "healing_range":(100, 100)}, + +"cactus conjuration":{"spellfunc":spell_conjure, "target":"none", "cost":2, "combat_spell":False, + "obj_key":"a cactus", "obj_desc":"An ordinary green cactus with little spines."} } From 7701d5f92bed464223a463d3562d8351fa23e270 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Wed, 15 Nov 2017 14:41:43 -0800 Subject: [PATCH 05/58] Comments and documentation, CmdStatus() added --- evennia/contrib/turnbattle/tb_magic.py | 41 +++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/turnbattle/tb_magic.py b/evennia/contrib/turnbattle/tb_magic.py index c2222645ac..eabf8c0932 100644 --- a/evennia/contrib/turnbattle/tb_magic.py +++ b/evennia/contrib/turnbattle/tb_magic.py @@ -714,6 +714,22 @@ class CmdLearnSpell(Command): learnspell Adds a spell by name to your list of spells known. + + The following spells are provided as examples: + + |wmagic missile|n (3 MP): Fires three missiles that never miss. Can target + up to three different enemies. + + |wflame shot|n (3 MP): Shoots a high-damage jet of flame at one target. + + |wcure wounds|n (5 MP): Heals damage on one target. + + |wmass cure wounds|n (10 MP): Like 'cure wounds', but can heal up to 5 + targets at once. + + |wfull heal|n (12 MP): Heals one target back to full HP. + + |wcactus conjuration|n (2 MP): Creates a cactus. """ key = "learnspell" @@ -925,7 +941,10 @@ class CmdCast(MuxCommand): return # Finally, we can cast the spell itself. Note that MP is not deducted here! - spelldata["spellfunc"](caller, spell_to_cast, spell_targets, spelldata["cost"], **kwargs) + try: + spelldata["spellfunc"](caller, spell_to_cast, spell_targets, spelldata["cost"], **kwargs) + except Exception: + log_trace("Error in callback for spell: %s." % spell_to_cast) """ Note: This is a quite long command, since it has to cope with all the different circumstances in which you may or may not be able @@ -963,7 +982,25 @@ class CmdRest(Command): """ You'll probably want to replace this with your own system for recovering HP and MP. """ + +class CmdStatus(Command): + """ + Gives combat information. + Usage: + status + + Shows your current and maximum HP and your distance from + other targets in combat. + """ + + key = "status" + help_category = "combat" + + def func(self): + "This performs the actual command." + char = self.caller + char.msg("You have %i / %i HP and %i / %i MP." % (char.db.hp, char.db.max_hp, char.db.mp, char.db.max_mp)) class CmdCombatHelp(CmdHelp): """ @@ -1008,6 +1045,7 @@ class BattleCmdSet(default_cmds.CharacterCmdSet): self.add(CmdCombatHelp()) self.add(CmdLearnSpell()) self.add(CmdCast()) + self.add(CmdStatus()) """ ---------------------------------------------------------------------------- @@ -1182,6 +1220,7 @@ def spell_conjure(caster, spell_name, targets, cost, **kwargs): caster.db.mp -= cost # Deduct MP cost + # Message the room to announce the creation of the object caster.location.msg_contents("%s casts %s, and %s appears!" % (caster, spell_name, conjured_obj)) """ From fda565b274428f54364fa25f5822fd898beaf5bb Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Wed, 15 Nov 2017 14:58:25 -0800 Subject: [PATCH 06/58] Final touches --- evennia/contrib/turnbattle/tb_magic.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_magic.py b/evennia/contrib/turnbattle/tb_magic.py index eabf8c0932..6e16cd0d46 100644 --- a/evennia/contrib/turnbattle/tb_magic.py +++ b/evennia/contrib/turnbattle/tb_magic.py @@ -769,7 +769,7 @@ class CmdLearnSpell(Command): caller.msg("You learn the spell '%s'!" % spell_to_learn) return if spell_to_learn in self.caller.db.spells_known: # Already has the spell specified - caller.msg("You already know the spell '%s'!" % spell_to_learn) + caller.msg("You already know the spell '%s'!" % spell_to_learn) """ You will almost definitely want to replace this with your own system for learning spells, perhaps tied to character advancement or finding @@ -1257,9 +1257,13 @@ Optional values for spells: max_targets (int): Maximum number of objects that can be targeted by the spell. 1 by default - unused if target is "none" or "self" -Any other values specified besides the above will be passed as kwargs to the spellfunc. +Any other values specified besides the above will be passed as kwargs to 'spellfunc'. You can use kwargs to effectively re-use the same function for different but similar -spells. +spells - for example, 'magic missile' and 'flame shot' use the same function, but +behave differently, as they have different damage ranges, accuracy, amount of attacks +made as part of the spell, and so forth. If you make your spell functions flexible +enough, you can make a wide variety of spells just by adding more entries to this +dictionary. """ SPELLS = { From 0616e0b21888e5f8f40951f7a0e046db75ea73ec Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Wed, 15 Nov 2017 16:25:08 -0800 Subject: [PATCH 07/58] Create tb_items.py --- evennia/contrib/turnbattle/tb_items.py | 754 +++++++++++++++++++++++++ 1 file changed, 754 insertions(+) create mode 100644 evennia/contrib/turnbattle/tb_items.py diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py new file mode 100644 index 0000000000..d4883e4c99 --- /dev/null +++ b/evennia/contrib/turnbattle/tb_items.py @@ -0,0 +1,754 @@ +""" +Simple turn-based combat system + +Contrib - Tim Ashley Jenkins 2017 + +This is a framework for a simple turn-based combat system, similar +to those used in D&D-style tabletop role playing games. It allows +any character to start a fight in a room, at which point initiative +is rolled and a turn order is established. Each participant in combat +has a limited time to decide their action for that turn (30 seconds by +default), and combat progresses through the turn order, looping through +the participants until the fight ends. + +Only simple rolls for attacking are implemented here, but this system +is easily extensible and can be used as the foundation for implementing +the rules from your turn-based tabletop game of choice or making your +own battle system. + +To install and test, import this module's TBBasicCharacter object into +your game's character.py module: + + from evennia.contrib.turnbattle.tb_basic import TBBasicCharacter + +And change your game's character typeclass to inherit from TBBasicCharacter +instead of the default: + + class Character(TBBasicCharacter): + +Next, import this module into your default_cmdsets.py module: + + from evennia.contrib.turnbattle import tb_basic + +And add the battle command set to your default command set: + + # + # any commands you add below will overload the default ones. + # + self.add(tb_basic.BattleCmdSet()) + +This module is meant to be heavily expanded on, so you may want to copy it +to your game's 'world' folder and modify it there rather than importing it +in your game and using it as-is. +""" + +from random import randint +from evennia import DefaultCharacter, Command, default_cmds, DefaultScript +from evennia.commands.default.help import CmdHelp + +""" +---------------------------------------------------------------------------- +OPTIONS +---------------------------------------------------------------------------- +""" + +TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds +ACTIONS_PER_TURN = 1 # Number of actions allowed per turn + +""" +---------------------------------------------------------------------------- +COMBAT FUNCTIONS START HERE +---------------------------------------------------------------------------- +""" + +def roll_init(character): + """ + Rolls a number between 1-1000 to determine initiative. + + Args: + character (obj): The character to determine initiative for + + Returns: + initiative (int): The character's place in initiative - higher + numbers go first. + + Notes: + By default, does not reference the character and simply returns + a random integer from 1 to 1000. + + Since the character is passed to this function, you can easily reference + a character's stats to determine an initiative roll - for example, if your + character has a 'dexterity' attribute, you can use it to give that character + an advantage in turn order, like so: + + return (randint(1,20)) + character.db.dexterity + + This way, characters with a higher dexterity will go first more often. + """ + return randint(1, 1000) + + +def get_attack(attacker, defender): + """ + Returns a value for an attack roll. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + + Returns: + attack_value (int): Attack roll value, compared against a defense value + to determine whether an attack hits or misses. + + Notes: + By default, returns a random integer from 1 to 100 without using any + properties from either the attacker or defender. + + This can easily be expanded to return a value based on characters stats, + equipment, and abilities. This is why the attacker and defender are passed + to this function, even though nothing from either one are used in this example. + """ + # For this example, just return a random integer up to 100. + attack_value = randint(1, 100) + return attack_value + + +def get_defense(attacker, defender): + """ + Returns a value for defense, which an attack roll must equal or exceed in order + for an attack to hit. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + + Returns: + defense_value (int): Defense value, compared against an attack roll + to determine whether an attack hits or misses. + + Notes: + By default, returns 50, not taking any properties of the defender or + attacker into account. + + As above, this can be expanded upon based on character stats and equipment. + """ + # For this example, just return 50, for about a 50/50 chance of hit. + defense_value = 50 + return defense_value + + +def get_damage(attacker, defender): + """ + Returns a value for damage to be deducted from the defender's HP after abilities + successful hit. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being damaged + + Returns: + damage_value (int): Damage value, which is to be deducted from the defending + character's HP. + + Notes: + By default, returns a random integer from 15 to 25 without using any + properties from either the attacker or defender. + + Again, this can be expanded upon. + """ + # For this example, just generate a number between 15 and 25. + damage_value = randint(15, 25) + return damage_value + + +def apply_damage(defender, damage): + """ + Applies damage to a target, reducing their HP by the damage amount to a + minimum of 0. + + Args: + defender (obj): Character taking damage + damage (int): Amount of damage being taken + """ + defender.db.hp -= damage # Reduce defender's HP by the damage dealt. + # If this reduces it to 0 or less, set HP to 0. + if defender.db.hp <= 0: + defender.db.hp = 0 + +def at_defeat(defeated): + """ + Announces the defeat of a fighter in combat. + + Args: + defeated (obj): Fighter that's been defeated. + + Notes: + All this does is announce a defeat message by default, but if you + want anything else to happen to defeated fighters (like putting them + into a dying state or something similar) then this is the place to + do it. + """ + defeated.location.msg_contents("%s has been defeated!" % defeated) + +def resolve_attack(attacker, defender, attack_value=None, defense_value=None): + """ + Resolves an attack and outputs the result. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + + Notes: + Even though the attack and defense values are calculated + extremely simply, they are separated out into their own functions + so that they are easier to expand upon. + """ + # Get an attack roll from the attacker. + if not attack_value: + attack_value = get_attack(attacker, defender) + # Get a defense value from the defender. + if not defense_value: + defense_value = get_defense(attacker, defender) + # If the attack value is lower than the defense value, miss. Otherwise, hit. + if attack_value < defense_value: + attacker.location.msg_contents("%s's attack misses %s!" % (attacker, defender)) + else: + damage_value = get_damage(attacker, defender) # Calculate damage value. + # Announce damage dealt and apply damage. + attacker.location.msg_contents("%s hits %s for %i damage!" % (attacker, defender, damage_value)) + apply_damage(defender, damage_value) + # If defender HP is reduced to 0 or less, call at_defeat. + if defender.db.hp <= 0: + at_defeat(defender) + +def combat_cleanup(character): + """ + Cleans up all the temporary combat-related attributes on a character. + + Args: + character (obj): Character to have their combat attributes removed + + Notes: + Any attribute whose key begins with 'combat_' is temporary and no + longer needed once a fight ends. + """ + for attr in character.attributes.all(): + if attr.key[:7] == "combat_": # If the attribute name starts with 'combat_'... + character.attributes.remove(key=attr.key) # ...then delete it! + + +def is_in_combat(character): + """ + Returns true if the given character is in combat. + + Args: + character (obj): Character to determine if is in combat or not + + Returns: + (bool): True if in combat or False if not in combat + """ + return bool(character.db.combat_turnhandler) + + +def is_turn(character): + """ + Returns true if it's currently the given character's turn in combat. + + Args: + character (obj): Character to determine if it is their turn or not + + Returns: + (bool): True if it is their turn or False otherwise + """ + turnhandler = character.db.combat_turnhandler + currentchar = turnhandler.db.fighters[turnhandler.db.turn] + return bool(character == currentchar) + + +def spend_action(character, actions, action_name=None): + """ + Spends a character's available combat actions and checks for end of turn. + + Args: + character (obj): Character spending the action + actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions + + Kwargs: + action_name (str or None): If a string is given, sets character's last action in + combat to provided string + """ + if action_name: + character.db.combat_lastaction = action_name + if actions == 'all': # If spending all actions + character.db.combat_actionsleft = 0 # Set actions to 0 + else: + character.db.combat_actionsleft -= actions # Use up actions. + if character.db.combat_actionsleft < 0: + character.db.combat_actionsleft = 0 # Can't have fewer than 0 actions + character.db.combat_turnhandler.turn_end_check(character) # Signal potential end of turn. + + +""" +---------------------------------------------------------------------------- +CHARACTER TYPECLASS +---------------------------------------------------------------------------- +""" + + +class TBBasicCharacter(DefaultCharacter): + """ + A character able to participate in turn-based combat. Has attributes for current + and maximum HP, and access to combat commands. + """ + + def at_object_creation(self): + """ + Called once, when this object is first created. This is the + normal hook to overload for most object types. + """ + self.db.max_hp = 100 # Set maximum HP to 100 + self.db.hp = self.db.max_hp # Set current HP to maximum + """ + Adds attributes for a character's current and maximum HP. + We're just going to set this value at '100' by default. + + You may want to expand this to include various 'stats' that + can be changed at creation and factor into combat calculations. + """ + + def at_before_move(self, destination): + """ + Called just before starting to move this object to + destination. + + Args: + destination (Object): The object we are moving to + + Returns: + shouldmove (bool): If we should move or not. + + Notes: + If this method returns False/None, the move is cancelled + before it is even started. + + """ + # Keep the character from moving if at 0 HP or in combat. + if is_in_combat(self): + self.msg("You can't exit a room while in combat!") + return False # Returning false keeps the character from moving. + if self.db.HP <= 0: + self.msg("You can't move, you've been defeated!") + return False + return True + +""" +---------------------------------------------------------------------------- +SCRIPTS START HERE +---------------------------------------------------------------------------- +""" + + +class TBBasicTurnHandler(DefaultScript): + """ + This is the script that handles the progression of combat through turns. + On creation (when a fight is started) it adds all combat-ready characters + to its roster and then sorts them into a turn order. There can only be one + fight going on in a single room at a time, so the script is assigned to a + room as its object. + + Fights persist until only one participant is left with any HP or all + remaining participants choose to end the combat with the 'disengage' command. + """ + + def at_script_creation(self): + """ + Called once, when the script is created. + """ + self.key = "Combat Turn Handler" + self.interval = 5 # Once every 5 seconds + self.persistent = True + self.db.fighters = [] + + # Add all fighters in the room with at least 1 HP to the combat." + for thing in self.obj.contents: + if thing.db.hp: + self.db.fighters.append(thing) + + # Initialize each fighter for combat + for fighter in self.db.fighters: + self.initialize_for_combat(fighter) + + # Add a reference to this script to the room + self.obj.db.combat_turnhandler = self + + # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. + # The initiative roll is determined by the roll_init function and can be customized easily. + ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True) + self.db.fighters = ordered_by_roll + + # Announce the turn order. + self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters)) + + # Start first fighter's turn. + self.start_turn(self.db.fighters[0]) + + # Set up the current turn and turn timeout delay. + self.db.turn = 0 + self.db.timer = TURN_TIMEOUT # Set timer to turn timeout specified in options + + def at_stop(self): + """ + Called at script termination. + """ + for fighter in self.db.fighters: + combat_cleanup(fighter) # Clean up the combat attributes for every fighter. + self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location + + def at_repeat(self): + """ + Called once every self.interval seconds. + """ + currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order. + self.db.timer -= self.interval # Count down the timer. + + if self.db.timer <= 0: + # Force current character to disengage if timer runs out. + self.obj.msg_contents("%s's turn timed out!" % currentchar) + spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions. + return + elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left + # Warn the current character if they're about to time out. + currentchar.msg("WARNING: About to time out!") + self.db.timeout_warning_given = True + + def initialize_for_combat(self, character): + """ + Prepares a character for combat when starting or entering a fight. + + Args: + character (obj): Character to initialize for combat. + """ + combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. + character.db.combat_actionsleft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 + character.db.combat_turnhandler = self # Add a reference to this turn handler script to the character + character.db.combat_lastaction = "null" # Track last action taken in combat + + def start_turn(self, character): + """ + Readies a character for the start of their turn by replenishing their + available actions and notifying them that their turn has come up. + + Args: + character (obj): Character to be readied. + + Notes: + Here, you only get one action per turn, but you might want to allow more than + one per turn, or even grant a number of actions based on a character's + attributes. You can even add multiple different kinds of actions, I.E. actions + separated for movement, by adding "character.db.combat_movesleft = 3" or + something similar. + """ + character.db.combat_actionsleft = ACTIONS_PER_TURN # Replenish actions + # Prompt the character for their turn and give some information. + character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) + + def next_turn(self): + """ + Advances to the next character in the turn order. + """ + + # Check to see if every character disengaged as their last action. If so, end combat. + disengage_check = True + for fighter in self.db.fighters: + if fighter.db.combat_lastaction != "disengage": # If a character has done anything but disengage + disengage_check = False + if disengage_check: # All characters have disengaged + self.obj.msg_contents("All fighters have disengaged! Combat is over!") + self.stop() # Stop this script and end combat. + return + + # Check to see if only one character is left standing. If so, end combat. + defeated_characters = 0 + for fighter in self.db.fighters: + if fighter.db.HP == 0: + defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated) + if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated + for fighter in self.db.fighters: + if fighter.db.HP != 0: + LastStanding = fighter # Pick the one fighter left with HP remaining + self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding) + self.stop() # Stop this script and end combat. + return + + # Cycle to the next turn. + currentchar = self.db.fighters[self.db.turn] + self.db.turn += 1 # Go to the next in the turn order. + if self.db.turn > len(self.db.fighters) - 1: + self.db.turn = 0 # Go back to the first in the turn order once you reach the end. + newchar = self.db.fighters[self.db.turn] # Note the new character + self.db.timer = TURN_TIMEOUT + self.time_until_next_repeat() # Reset the timer. + self.db.timeout_warning_given = False # Reset the timeout warning. + self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) + self.start_turn(newchar) # Start the new character's turn. + + def turn_end_check(self, character): + """ + Tests to see if a character's turn is over, and cycles to the next turn if it is. + + Args: + character (obj): Character to test for end of turn + """ + if not character.db.combat_actionsleft: # Character has no actions remaining + self.next_turn() + return + + def join_fight(self, character): + """ + Adds a new character to a fight already in progress. + + Args: + character (obj): Character to be added to the fight. + """ + # Inserts the fighter to the turn order, right behind whoever's turn it currently is. + self.db.fighters.insert(self.db.turn, character) + # Tick the turn counter forward one to compensate. + self.db.turn += 1 + # Initialize the character like you do at the start. + self.initialize_for_combat(character) + + +""" +---------------------------------------------------------------------------- +COMMANDS START HERE +---------------------------------------------------------------------------- +""" + + +class CmdFight(Command): + """ + Starts a fight with everyone in the same room as you. + + Usage: + fight + + When you start a fight, everyone in the room who is able to + fight is added to combat, and a turn order is randomly rolled. + When it's your turn, you can attack other characters. + """ + key = "fight" + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + here = self.caller.location + fighters = [] + + if not self.caller.db.hp: # If you don't have any hp + self.caller.msg("You can't start a fight if you've been defeated!") + return + if is_in_combat(self.caller): # Already in a fight + self.caller.msg("You're already in a fight!") + return + for thing in here.contents: # Test everything in the room to add it to the fight. + if thing.db.HP: # If the object has HP... + fighters.append(thing) # ...then add it to the fight. + if len(fighters) <= 1: # If you're the only able fighter in the room + self.caller.msg("There's nobody here to fight!") + return + if here.db.combat_turnhandler: # If there's already a fight going on... + here.msg_contents("%s joins the fight!" % self.caller) + here.db.combat_turnhandler.join_fight(self.caller) # Join the fight! + return + here.msg_contents("%s starts a fight!" % self.caller) + # Add a turn handler script to the room, which starts combat. + here.scripts.add("contrib.turnbattle.tb_basic.TBBasicTurnHandler") + # Remember you'll have to change the path to the script if you copy this code to your own modules! + + +class CmdAttack(Command): + """ + Attacks another character. + + Usage: + attack + + When in a fight, you may attack another character. The attack has + a chance to hit, and if successful, will deal damage. + """ + + key = "attack" + help_category = "combat" + + def func(self): + "This performs the actual command." + "Set the attacker to the caller and the defender to the target." + + if not is_in_combat(self.caller): # If not in combat, can't attack. + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # If it's not your turn, can't attack. + self.caller.msg("You can only do that on your turn.") + return + + if not self.caller.db.hp: # Can't attack if you have no HP. + self.caller.msg("You can't attack, you've been defeated.") + return + + attacker = self.caller + defender = self.caller.search(self.args) + + if not defender: # No valid target given. + return + + if not defender.db.hp: # Target object has no HP left or to begin with + self.caller.msg("You can't fight that!") + return + + if attacker == defender: # Target and attacker are the same + self.caller.msg("You can't attack yourself!") + return + + "If everything checks out, call the attack resolving function." + resolve_attack(attacker, defender) + spend_action(self.caller, 1, action_name="attack") # Use up one action. + + +class CmdPass(Command): + """ + Passes on your turn. + + Usage: + pass + + When in a fight, you can use this command to end your turn early, even + if there are still any actions you can take. + """ + + key = "pass" + aliases = ["wait", "hold"] + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + if not is_in_combat(self.caller): # Can only pass a turn in combat. + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # Can only pass if it's your turn. + self.caller.msg("You can only do that on your turn.") + return + + self.caller.location.msg_contents("%s takes no further action, passing the turn." % self.caller) + spend_action(self.caller, 'all', action_name="pass") # Spend all remaining actions. + + +class CmdDisengage(Command): + """ + Passes your turn and attempts to end combat. + + Usage: + disengage + + Ends your turn early and signals that you're trying to end + the fight. If all participants in a fight disengage, the + fight ends. + """ + + key = "disengage" + aliases = ["spare"] + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + if not is_in_combat(self.caller): # If you're not in combat + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # If it's not your turn + self.caller.msg("You can only do that on your turn.") + return + + self.caller.location.msg_contents("%s disengages, ready to stop fighting." % self.caller) + spend_action(self.caller, 'all', action_name="disengage") # Spend all remaining actions. + """ + The action_name kwarg sets the character's last action to "disengage", which is checked by + the turn handler script to see if all fighters have disengaged. + """ + + +class CmdRest(Command): + """ + Recovers damage. + + Usage: + rest + + Resting recovers your HP to its maximum, but you can only + rest if you're not in a fight. + """ + + key = "rest" + help_category = "combat" + + def func(self): + "This performs the actual command." + + if is_in_combat(self.caller): # If you're in combat + self.caller.msg("You can't rest while you're in combat.") + return + + self.caller.db.hp = self.caller.db.max_hp # Set current HP to maximum + self.caller.location.msg_contents("%s rests to recover HP." % self.caller) + """ + You'll probably want to replace this with your own system for recovering HP. + """ + + +class CmdCombatHelp(CmdHelp): + """ + View help or a list of topics + + Usage: + help + help list + help all + + This will search for help on commands and other + topics related to the game. + """ + # Just like the default help command, but will give quick + # tips on combat when used in a fight with no arguments. + + def func(self): + if is_in_combat(self.caller) and not self.args: # In combat and entered 'help' alone + self.caller.msg("Available combat commands:|/" + + "|wAttack:|n Attack a target, attempting to deal damage.|/" + + "|wPass:|n Pass your turn without further action.|/" + + "|wDisengage:|n End your turn and attempt to end combat.|/") + else: + super(CmdCombatHelp, self).func() # Call the default help command + + +class BattleCmdSet(default_cmds.CharacterCmdSet): + """ + This command set includes all the commmands used in the battle system. + """ + key = "DefaultCharacter" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + self.add(CmdFight()) + self.add(CmdAttack()) + self.add(CmdRest()) + self.add(CmdPass()) + self.add(CmdDisengage()) + self.add(CmdCombatHelp()) From 35340f86c8ed658b2176e0b3ad324b10208c7777 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Wed, 15 Nov 2017 23:12:25 -0800 Subject: [PATCH 08/58] Added 'use' command, item functions, example items --- evennia/contrib/turnbattle/tb_items.py | 268 ++++++++++++++++++++++--- 1 file changed, 245 insertions(+), 23 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index d4883e4c99..70cedd7fda 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -1,41 +1,48 @@ """ -Simple turn-based combat system +Simple turn-based combat system with items and status effects Contrib - Tim Ashley Jenkins 2017 -This is a framework for a simple turn-based combat system, similar -to those used in D&D-style tabletop role playing games. It allows -any character to start a fight in a room, at which point initiative -is rolled and a turn order is established. Each participant in combat -has a limited time to decide their action for that turn (30 seconds by -default), and combat progresses through the turn order, looping through -the participants until the fight ends. +This is a version of the 'turnbattle' combat system that includes status +effects and usable items, which can instill these status effects, cure +them, or do just about anything else. -Only simple rolls for attacking are implemented here, but this system -is easily extensible and can be used as the foundation for implementing -the rules from your turn-based tabletop game of choice or making your -own battle system. +Status effects are stored on characters as a dictionary, where the key +is the name of the status effect and the value is a list of two items: +an integer representing the number of turns left until the status runs +out, and the character upon whose turn the condition timer is ticked +down. Unlike most combat-related attributes, conditions aren't wiped +once combat ends - if out of combat, they tick down in real time +instead. -To install and test, import this module's TBBasicCharacter object into +Items aren't given any sort of special typeclass - instead, whether or +not an object counts as an item is determined by its attributes. To make +an object into an item, it must have the attribute 'item_on_use', with +the value given as a callable - this is the function that will be called +when an item is used. Other properties of the item, such as how many +uses it has, whether it's destroyed when its uses are depleted, and such +can be specified on the item as well, but they are optional. + +To install and test, import this module's TBItemsCharacter object into your game's character.py module: - from evennia.contrib.turnbattle.tb_basic import TBBasicCharacter + from evennia.contrib.turnbattle.tb_items import TBItemsCharacter -And change your game's character typeclass to inherit from TBBasicCharacter +And change your game's character typeclass to inherit from TBItemsCharacter instead of the default: - class Character(TBBasicCharacter): + class Character(TBItemsCharacter): Next, import this module into your default_cmdsets.py module: - from evennia.contrib.turnbattle import tb_basic + from evennia.contrib.turnbattle import tb_items And add the battle command set to your default command set: # # any commands you add below will overload the default ones. # - self.add(tb_basic.BattleCmdSet()) + self.add(tb_items.BattleCmdSet()) This module is meant to be heavily expanded on, so you may want to copy it to your game's 'world' folder and modify it there rather than importing it @@ -44,7 +51,9 @@ in your game and using it as-is. from random import randint from evennia import DefaultCharacter, Command, default_cmds, DefaultScript +from evennia.commands.default.muxcommand import MuxCommand from evennia.commands.default.help import CmdHelp +from evennia.utils.spawner import spawn """ ---------------------------------------------------------------------------- @@ -190,7 +199,7 @@ def at_defeat(defeated): """ defeated.location.msg_contents("%s has been defeated!" % defeated) -def resolve_attack(attacker, defender, attack_value=None, defense_value=None): +def resolve_attack(attacker, defender, attack_value=None, defense_value=None, damage_value=None): """ Resolves an attack and outputs the result. @@ -213,7 +222,8 @@ def resolve_attack(attacker, defender, attack_value=None, defense_value=None): if attack_value < defense_value: attacker.location.msg_contents("%s's attack misses %s!" % (attacker, defender)) else: - damage_value = get_damage(attacker, defender) # Calculate damage value. + if not damage_value: + damage_value = get_damage(attacker, defender) # Calculate damage value. # Announce damage dealt and apply damage. attacker.location.msg_contents("%s hits %s for %i damage!" % (attacker, defender, damage_value)) apply_damage(defender, damage_value) @@ -287,6 +297,33 @@ def spend_action(character, actions, action_name=None): character.db.combat_actionsleft = 0 # Can't have fewer than 0 actions character.db.combat_turnhandler.turn_end_check(character) # Signal potential end of turn. +def spend_item_use(item): + """ + Spends one use on an item with limited uses. If item.db.item_consumable + is 'True', the item is destroyed if it runs out of uses - if it's a string + instead of 'True', it will also spawn a new object as residue, using the + value of item.db.item_consumable as the name of the prototype to spawn. + + + """ + if item.db.item_uses: + item.db.item_uses -= 1 # Spend one use + if item.db.item_uses > 0: # Has uses remaining + # Inform th eplayer + self.caller.msg("%s has %i uses remaining." % (item.key.capitalize(), item.db.item_uses)) + else: # All uses spent + if not item.db.item_consumable: + # If not consumable, just inform the player that the uses are gone + self.caller.msg("%s has no uses remaining." % item.key.capitalize()) + else: # If consumable + if item.db.item_consumable == True: # If the value is 'True', just destroy the item + self.caller.msg("%s has been consumed." % item.key.capitalize()) + item.delete() # Delete the spent item + else: # If a string, use value of item_consumable to spawn an object in its place + residue = spawn({"prototype":item.db.item_consumable})[0] # Spawn the residue + residue.location = item.location # Move the residue to the same place as the item + self.caller.msg("After using %s, you are left with %s." % (item, residue)) + item.delete() # Delete the spent item """ ---------------------------------------------------------------------------- @@ -295,7 +332,7 @@ CHARACTER TYPECLASS """ -class TBBasicCharacter(DefaultCharacter): +class TBItemsCharacter(DefaultCharacter): """ A character able to participate in turn-based combat. Has attributes for current and maximum HP, and access to combat commands. @@ -348,7 +385,7 @@ SCRIPTS START HERE """ -class TBBasicTurnHandler(DefaultScript): +class TBItemsTurnHandler(DefaultScript): """ This is the script that handles the progression of combat through turns. On creation (when a fight is started) it adds all combat-ready characters @@ -563,7 +600,7 @@ class CmdFight(Command): return here.msg_contents("%s starts a fight!" % self.caller) # Add a turn handler script to the room, which starts combat. - here.scripts.add("contrib.turnbattle.tb_basic.TBBasicTurnHandler") + here.scripts.add("contrib.turnbattle.tb_items.TBItemsTurnHandler") # Remember you'll have to change the path to the script if you copy this code to your own modules! @@ -736,6 +773,73 @@ class CmdCombatHelp(CmdHelp): super(CmdCombatHelp, self).func() # Call the default help command +class CmdUse(MuxCommand): + """ + Use an item. + + Usage: + use [= target] + + Items: you just GOTTA use them. + """ + + key = "use" + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + item = self.caller.search(self.lhs, candidates=self.caller.contents) + if not item: + return + + target = None + if self.rhs: + target = self.caller.search(self.rhs) + if not target: + return + + if is_in_combat(self.caller): + if not is_turn(self.caller): + self.caller.msg("You can only use items on your turn.") + return + + if not item.db.item_func: # Object has no item_func, not usable + self.caller.msg("'%s' is not a usable item." % item.key.capitalize()) + return + + if item.attributes.has("item_uses"): # Item has limited uses + if item.db.item_uses <= 0: # Limited uses are spent + self.caller.msg("'%s' has no uses remaining." % item.key.capitalize()) + return + + kwargs = {} + if item.db.item_kwargs: + kwargs = item.db.item_kwargs # Set kwargs to pass to item_func + + # Match item_func string to function + try: + item_func = ITEMFUNCS[item.db.item_func] + except KeyError: + self.caller.msg("ERROR: %s not defined in ITEMFUNCS" % item.db.item_func) + return + + # Call the item function - abort if it returns False, indicating an error. + # Regardless of what the function returns (if anything), it's still executed. + if item_func(item, self.caller, target, **kwargs) == False: + return + + # If we haven't returned yet, we assume the item was used successfully. + + # Spend one use if item has limited uses + spend_item_use(item) + + # Spend an action if in combat + if is_in_combat(self.caller): + spend_action(self.caller, 1, action_name="item") + + class BattleCmdSet(default_cmds.CharacterCmdSet): """ This command set includes all the commmands used in the battle system. @@ -752,3 +856,121 @@ class BattleCmdSet(default_cmds.CharacterCmdSet): self.add(CmdPass()) self.add(CmdDisengage()) self.add(CmdCombatHelp()) + self.add(CmdUse()) + +""" +ITEM FUNCTIONS START HERE +""" + +def itemfunc_heal(item, user, target, **kwargs): + """ + Item function that heals HP. + """ + if not target: + target = user # Target user if none specified + + if not target.attributes.has("max_hp"): # Has no HP to speak of + user.msg("You can't use %s on that." % item) + return False + + if target.db.hp >= target.db.max_hp: + user.msg("%s is already at full health." % target) + return False + + min_healing = 20 + max_healing = 40 + + # Retrieve healing range from kwargs, if present + if "healing_range" in kwargs: + min_healing = kwargs["healing_range"][0] + max_healing = kwargs["healing_range"][1] + + to_heal = randint(min_healing, max_healing) # Restore 20 to 40 hp + if target.db.hp + to_heal > target.db.max_hp: + to_heal = target.db.max_hp - target.db.hp # Cap healing to max HP + target.db.hp += to_heal + + user.location.msg_contents("%s uses %s! %s regains %i HP!" % (user, item, target, to_heal)) + +def itemfunc_attack(item, user, target, **kwargs): + """ + Item function that attacks a target. + """ + if not is_in_combat(user): + user.msg("You can only use that in combat.") + return False + + if not target: + user.msg("You have to specify a target to use %s! (use = )" % item) + return False + + if target == user: + user.msg("You can't attack yourself!") + return False + + if not target.db.hp: # Has no HP + user.msg("You can't use %s on that." % item) + return False + + min_damage = 20 + max_damage = 40 + accuracy = 0 + + # Retrieve values from kwargs, if present + if "damage_range" in kwargs: + min_damage = kwargs["damage_range"][0] + max_damage = kwargs["damage_range"][1] + if "accuracy" in kwargs: + accuracy = kwargs["accuracy"] + + # Roll attack and damage + attack_value = randint(1, 100) + accuracy + damage_value = randint(min_damage, max_damage) + + user.location.msg_contents("%s attacks %s with %s!" % (user, target, item)) + resolve_attack(user, target, attack_value=attack_value, damage_value=damage_value) + +# Match strings to item functions here. We can't store callables on +# prototypes, so we store a string instead, matching that string to +# a callable in this dictionary. +ITEMFUNCS = { + "heal":itemfunc_heal, + "attack":itemfunc_attack +} + +""" +ITEM PROTOTYPES START HERE +""" + +MEDKIT = { + "key" : "a medical kit", + "aliases" : ["medkit"], + "desc" : "A standard medical kit. It can be used a few times to heal wounds.", + "item_func" : "heal", + "item_uses" : 3, + "item_consumable" : True, + "item_kwargs" : {"healing_range":(15, 25)} +} + +GLASS_BOTTLE = { + "key" : "a glass bottle", + "desc" : "An empty glass bottle." +} + +HEALTH_POTION = { + "key" : "a health potion", + "desc" : "A glass bottle full of a mystical potion that heals wounds when used.", + "item_func" : "heal", + "item_uses" : 1, + "item_consumable" : "GLASS_BOTTLE", + "item_kwargs" : {"healing_range":(35, 50)} +} + +BOMB = { + "key" : "a rotund bomb", + "desc" : "A large black sphere with a fuse at the end. Can be used on enemies in combat.", + "item_func" : "attack", + "item_uses" : 1, + "item_consumable" : True, + "item_kwargs" : {"damage_range":(25, 40), "accuracy":25} +} \ No newline at end of file From 5ce18379c0e36db8f8ecf0c15fa3eecdde855152 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Thu, 16 Nov 2017 00:15:20 -0800 Subject: [PATCH 09/58] Proper implementation of spend_item_use() --- evennia/contrib/turnbattle/tb_items.py | 36 ++++++++++++++------------ 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index 70cedd7fda..b99fba599d 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -17,7 +17,7 @@ instead. Items aren't given any sort of special typeclass - instead, whether or not an object counts as an item is determined by its attributes. To make -an object into an item, it must have the attribute 'item_on_use', with +an object into an item, it must have the attribute 'item_func', with the value given as a callable - this is the function that will be called when an item is used. Other properties of the item, such as how many uses it has, whether it's destroyed when its uses are depleted, and such @@ -297,32 +297,30 @@ def spend_action(character, actions, action_name=None): character.db.combat_actionsleft = 0 # Can't have fewer than 0 actions character.db.combat_turnhandler.turn_end_check(character) # Signal potential end of turn. -def spend_item_use(item): +def spend_item_use(item, user): """ Spends one use on an item with limited uses. If item.db.item_consumable is 'True', the item is destroyed if it runs out of uses - if it's a string instead of 'True', it will also spawn a new object as residue, using the value of item.db.item_consumable as the name of the prototype to spawn. - - """ if item.db.item_uses: item.db.item_uses -= 1 # Spend one use if item.db.item_uses > 0: # Has uses remaining - # Inform th eplayer - self.caller.msg("%s has %i uses remaining." % (item.key.capitalize(), item.db.item_uses)) + # Inform the player + user.msg("%s has %i uses remaining." % (item.key.capitalize(), item.db.item_uses)) else: # All uses spent if not item.db.item_consumable: # If not consumable, just inform the player that the uses are gone - self.caller.msg("%s has no uses remaining." % item.key.capitalize()) + user.msg("%s has no uses remaining." % item.key.capitalize()) else: # If consumable if item.db.item_consumable == True: # If the value is 'True', just destroy the item - self.caller.msg("%s has been consumed." % item.key.capitalize()) + user.msg("%s has been consumed." % item.key.capitalize()) item.delete() # Delete the spent item else: # If a string, use value of item_consumable to spawn an object in its place residue = spawn({"prototype":item.db.item_consumable})[0] # Spawn the residue residue.location = item.location # Move the residue to the same place as the item - self.caller.msg("After using %s, you are left with %s." % (item, residue)) + user.msg("After using %s, you are left with %s." % (item, residue)) item.delete() # Delete the spent item """ @@ -790,16 +788,19 @@ class CmdUse(MuxCommand): """ This performs the actual command. """ + # Search for item item = self.caller.search(self.lhs, candidates=self.caller.contents) if not item: return + # Search for target, if any is given target = None if self.rhs: target = self.caller.search(self.rhs) if not target: return - + + # If in combat, can only use items on your turn if is_in_combat(self.caller): if not is_turn(self.caller): self.caller.msg("You can only use items on your turn.") @@ -814,9 +815,10 @@ class CmdUse(MuxCommand): self.caller.msg("'%s' has no uses remaining." % item.key.capitalize()) return + # Set kwargs to pass to item_func kwargs = {} if item.db.item_kwargs: - kwargs = item.db.item_kwargs # Set kwargs to pass to item_func + kwargs = item.db.item_kwargs # Match item_func string to function try: @@ -826,14 +828,14 @@ class CmdUse(MuxCommand): return # Call the item function - abort if it returns False, indicating an error. + # This performs the actual action of using the item. # Regardless of what the function returns (if anything), it's still executed. if item_func(item, self.caller, target, **kwargs) == False: return # If we haven't returned yet, we assume the item was used successfully. - # Spend one use if item has limited uses - spend_item_use(item) + spend_item_use(item, self.caller) # Spend an action if in combat if is_in_combat(self.caller): @@ -871,7 +873,7 @@ def itemfunc_heal(item, user, target, **kwargs): if not target.attributes.has("max_hp"): # Has no HP to speak of user.msg("You can't use %s on that." % item) - return False + return False # Returning false aborts the item use if target.db.hp >= target.db.max_hp: user.msg("%s is already at full health." % target) @@ -898,7 +900,7 @@ def itemfunc_attack(item, user, target, **kwargs): """ if not is_in_combat(user): user.msg("You can only use that in combat.") - return False + return False # Returning false aborts the item use if not target: user.msg("You have to specify a target to use %s! (use = )" % item) @@ -906,7 +908,7 @@ def itemfunc_attack(item, user, target, **kwargs): if target == user: user.msg("You can't attack yourself!") - return False + return False if not target.db.hp: # Has no HP user.msg("You can't use %s on that." % item) @@ -940,6 +942,8 @@ ITEMFUNCS = { """ ITEM PROTOTYPES START HERE + +Copy these to your game's /world/prototypes.py module! """ MEDKIT = { From 91c333e6d3c75d136ad3b684dc0dd2439394e33b Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Fri, 17 Nov 2017 16:46:27 -0800 Subject: [PATCH 10/58] Move some item logic from CmdUse to new func use_item --- evennia/contrib/turnbattle/tb_items.py | 57 +++++++++++++++----------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index b99fba599d..94ab4bacea 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -322,6 +322,36 @@ def spend_item_use(item, user): residue.location = item.location # Move the residue to the same place as the item user.msg("After using %s, you are left with %s." % (item, residue)) item.delete() # Delete the spent item + +def use_item(user, item, target): + """ + Performs the action of using an item. + """ + # Set kwargs to pass to item_func + kwargs = {} + if item.db.item_kwargs: + kwargs = item.db.item_kwargs + + # Match item_func string to function + try: + item_func = ITEMFUNCS[item.db.item_func] + except KeyError: + user.msg("ERROR: %s not defined in ITEMFUNCS" % item.db.item_func) + return + + # Call the item function - abort if it returns False, indicating an error. + # This performs the actual action of using the item. + # Regardless of what the function returns (if anything), it's still executed. + if item_func(item, user, target, **kwargs) == False: + return + + # If we haven't returned yet, we assume the item was used successfully. + # Spend one use if item has limited uses + spend_item_use(item, user) + + # Spend an action if in combat + if is_in_combat(user): + spend_action(user, 1, action_name="item") """ ---------------------------------------------------------------------------- @@ -815,31 +845,8 @@ class CmdUse(MuxCommand): self.caller.msg("'%s' has no uses remaining." % item.key.capitalize()) return - # Set kwargs to pass to item_func - kwargs = {} - if item.db.item_kwargs: - kwargs = item.db.item_kwargs - - # Match item_func string to function - try: - item_func = ITEMFUNCS[item.db.item_func] - except KeyError: - self.caller.msg("ERROR: %s not defined in ITEMFUNCS" % item.db.item_func) - return - - # Call the item function - abort if it returns False, indicating an error. - # This performs the actual action of using the item. - # Regardless of what the function returns (if anything), it's still executed. - if item_func(item, self.caller, target, **kwargs) == False: - return - - # If we haven't returned yet, we assume the item was used successfully. - # Spend one use if item has limited uses - spend_item_use(item, self.caller) - - # Spend an action if in combat - if is_in_combat(self.caller): - spend_action(self.caller, 1, action_name="item") + # If everything checks out, call the use_item function + use_item(self.caller, item, target) class BattleCmdSet(default_cmds.CharacterCmdSet): From e61df0a400e8952c9b3c0c352cd36fda5d3e5804 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Fri, 17 Nov 2017 21:17:35 -0800 Subject: [PATCH 11/58] Start porting in condition code from coolbattles --- evennia/contrib/turnbattle/tb_items.py | 78 +++++++++++++++++++------- 1 file changed, 58 insertions(+), 20 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index 94ab4bacea..8f33e717b1 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -304,24 +304,28 @@ def spend_item_use(item, user): instead of 'True', it will also spawn a new object as residue, using the value of item.db.item_consumable as the name of the prototype to spawn. """ - if item.db.item_uses: - item.db.item_uses -= 1 # Spend one use - if item.db.item_uses > 0: # Has uses remaining - # Inform the player - user.msg("%s has %i uses remaining." % (item.key.capitalize(), item.db.item_uses)) - else: # All uses spent - if not item.db.item_consumable: - # If not consumable, just inform the player that the uses are gone - user.msg("%s has no uses remaining." % item.key.capitalize()) - else: # If consumable - if item.db.item_consumable == True: # If the value is 'True', just destroy the item - user.msg("%s has been consumed." % item.key.capitalize()) - item.delete() # Delete the spent item - else: # If a string, use value of item_consumable to spawn an object in its place - residue = spawn({"prototype":item.db.item_consumable})[0] # Spawn the residue - residue.location = item.location # Move the residue to the same place as the item - user.msg("After using %s, you are left with %s." % (item, residue)) - item.delete() # Delete the spent item + item.db.item_uses -= 1 # Spend one use + + if item.db.item_uses > 0: # Has uses remaining + # Inform the player + user.msg("%s has %i uses remaining." % (item.key.capitalize(), item.db.item_uses)) + + else: # All uses spent + + if not item.db.item_consumable: # Item isn't consumable + # Just inform the player that the uses are gone + user.msg("%s has no uses remaining." % item.key.capitalize()) + + else: # If item is consumable + if item.db.item_consumable == True: # If the value is 'True', just destroy the item + user.msg("%s has been consumed." % item.key.capitalize()) + item.delete() # Delete the spent item + + else: # If a string, use value of item_consumable to spawn an object in its place + residue = spawn({"prototype":item.db.item_consumable})[0] # Spawn the residue + residue.location = item.location # Move the residue to the same place as the item + user.msg("After using %s, you are left with %s." % (item, residue)) + item.delete() # Delete the spent item def use_item(user, item, target): """ @@ -347,11 +351,38 @@ def use_item(user, item, target): # If we haven't returned yet, we assume the item was used successfully. # Spend one use if item has limited uses - spend_item_use(item, user) + if item.db.item_uses: + spend_item_use(item, user) # Spend an action if in combat if is_in_combat(user): spend_action(user, 1, action_name="item") + +def condition_tickdown(character, turnchar): + """ + Ticks down the duration of conditions on a character at the end of a given character's turn. + """ + + for key in character.db.conditions: + # The first value is the remaining turns - the second value is whose turn to count down on. + condition_duration = character.db.conditions[key][0] + condition_turnchar = character.db.conditions[key][1] + # Count down if the given turn character matches the condition's turn character. + if condition_turnchar == turnchar: + character.db.conditions[key][0] -= 1 + if character.db.conditions[key][0] <= 0: + # If the duration is brought down to 0, remove the condition and inform everyone. + character.location.msg_contents("%s no longer has the '%s' condition." % (str(character), str(key))) + del character.db.conditions[key] + +def add_condition(character, turnchar, condition, duration): + """ + Adds a condition to a fighter. + """ + # The first value is the remaining turns - the second value is whose turn to count down on. + character.db.conditions.update({condition:[duration, turnchar]}) + # Tell everyone! + character.location.msg_contents("%s gains the '%s' condition." % (character, condition)) """ ---------------------------------------------------------------------------- @@ -373,6 +404,7 @@ class TBItemsCharacter(DefaultCharacter): """ self.db.max_hp = 100 # Set maximum HP to 100 self.db.hp = self.db.max_hp # Set current HP to maximum + self.db.conditions = {} # Set empty dict for conditions """ Adds attributes for a character's current and maximum HP. We're just going to set this value at '100' by default. @@ -550,6 +582,11 @@ class TBItemsTurnHandler(DefaultScript): self.db.turn += 1 # Go to the next in the turn order. if self.db.turn > len(self.db.fighters) - 1: self.db.turn = 0 # Go back to the first in the turn order once you reach the end. + + # Count down condition timers. + for fighter in self.db.fighters: + condition_tickdown(fighter, newchar) + newchar = self.db.fighters[self.db.turn] # Note the new character self.db.timer = TURN_TIMEOUT + self.time_until_next_repeat() # Reset the timer. self.db.timeout_warning_given = False # Reset the timeout warning. @@ -796,7 +833,8 @@ class CmdCombatHelp(CmdHelp): self.caller.msg("Available combat commands:|/" + "|wAttack:|n Attack a target, attempting to deal damage.|/" + "|wPass:|n Pass your turn without further action.|/" + - "|wDisengage:|n End your turn and attempt to end combat.|/") + "|wDisengage:|n End your turn and attempt to end combat.|/" + + "|wUse:|n Use an item you're carrying.") else: super(CmdCombatHelp, self).func() # Call the default help command From ae060ecc775dea948b5fcac0b4d8c30942e4ee86 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Fri, 17 Nov 2017 21:19:02 -0800 Subject: [PATCH 12/58] Fix weird spacing in use_item() --- evennia/contrib/turnbattle/tb_items.py | 56 +++++++++++++------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index 8f33e717b1..ea3eb89fc6 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -328,35 +328,35 @@ def spend_item_use(item, user): item.delete() # Delete the spent item def use_item(user, item, target): - """ - Performs the action of using an item. - """ - # Set kwargs to pass to item_func - kwargs = {} - if item.db.item_kwargs: - kwargs = item.db.item_kwargs - - # Match item_func string to function - try: - item_func = ITEMFUNCS[item.db.item_func] - except KeyError: - user.msg("ERROR: %s not defined in ITEMFUNCS" % item.db.item_func) - return + """ + Performs the action of using an item. + """ + # Set kwargs to pass to item_func + kwargs = {} + if item.db.item_kwargs: + kwargs = item.db.item_kwargs - # Call the item function - abort if it returns False, indicating an error. - # This performs the actual action of using the item. - # Regardless of what the function returns (if anything), it's still executed. - if item_func(item, user, target, **kwargs) == False: - return - - # If we haven't returned yet, we assume the item was used successfully. - # Spend one use if item has limited uses - if item.db.item_uses: - spend_item_use(item, user) - - # Spend an action if in combat - if is_in_combat(user): - spend_action(user, 1, action_name="item") + # Match item_func string to function + try: + item_func = ITEMFUNCS[item.db.item_func] + except KeyError: + user.msg("ERROR: %s not defined in ITEMFUNCS" % item.db.item_func) + return + + # Call the item function - abort if it returns False, indicating an error. + # This performs the actual action of using the item. + # Regardless of what the function returns (if anything), it's still executed. + if item_func(item, user, target, **kwargs) == False: + return + + # If we haven't returned yet, we assume the item was used successfully. + # Spend one use if item has limited uses + if item.db.item_uses: + spend_item_use(item, user) + + # Spend an action if in combat + if is_in_combat(user): + spend_action(user, 1, action_name="item") def condition_tickdown(character, turnchar): """ From 44c75d8d18254ea1c0a5c6f3a8173c247aa91cf4 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Sat, 18 Nov 2017 13:22:03 -0800 Subject: [PATCH 13/58] Added functional condition, TickerHandler countdown --- evennia/contrib/turnbattle/tb_items.py | 92 +++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index ea3eb89fc6..8457a13bd6 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -54,6 +54,7 @@ from evennia import DefaultCharacter, Command, default_cmds, DefaultScript from evennia.commands.default.muxcommand import MuxCommand from evennia.commands.default.help import CmdHelp from evennia.utils.spawner import spawn +from evennia import TICKER_HANDLER as tickerhandler """ ---------------------------------------------------------------------------- @@ -360,7 +361,7 @@ def use_item(user, item, target): def condition_tickdown(character, turnchar): """ - Ticks down the duration of conditions on a character at the end of a given character's turn. + Ticks down the duration of conditions on a character at the start of a given character's turn. """ for key in character.db.conditions: @@ -405,6 +406,8 @@ class TBItemsCharacter(DefaultCharacter): self.db.max_hp = 100 # Set maximum HP to 100 self.db.hp = self.db.max_hp # Set current HP to maximum self.db.conditions = {} # Set empty dict for conditions + # Subscribe character to the ticker handler + tickerhandler.add(30, self.at_update) """ Adds attributes for a character's current and maximum HP. We're just going to set this value at '100' by default. @@ -437,6 +440,42 @@ class TBItemsCharacter(DefaultCharacter): self.msg("You can't move, you've been defeated!") return False return True + + def at_turn_start(self): + """ + Hook called at the beginning of this character's turn in combat. + """ + # Prompt the character for their turn and give some information. + self.msg("|wIt's your turn! You have %i HP remaining.|n" % self.db.hp) + + # Apply conditions that fire at the start of each turn. + self.apply_turn_conditions() + + def apply_turn_conditions(self): + """ + Applies the effect of conditions that occur at the start of each + turn in combat, or every 30 seconds out of combat. + """ + if "Regeneration" in self.db.conditions: + to_heal = randint(4, 8) # Restore 4 to 8 HP + if self.db.hp + to_heal > self.db.max_hp: + to_heal = self.db.max_hp - self.db.hp # Cap healing to max HP + self.db.hp += to_heal + self.location.msg_contents("%s regains %i HP from Regeneration." % (self, to_heal)) + + def at_update(self): + """ + Fires every 30 seconds. + """ + # Change all conditions to update on character's turn. + for key in self.db.conditions: + self.db.conditions[key][1] = self + if not is_in_combat(self): # Not in combat + # Apply conditions that fire every turn + self.apply_turn_conditions() + # Tick down condition durations + condition_tickdown(self, self) + """ ---------------------------------------------------------------------------- @@ -546,8 +585,8 @@ class TBItemsTurnHandler(DefaultScript): something similar. """ character.db.combat_actionsleft = ACTIONS_PER_TURN # Replenish actions - # Prompt the character for their turn and give some information. - character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) + # Call character's at_turn_start() hook. + character.at_turn_start() def next_turn(self): """ @@ -582,16 +621,17 @@ class TBItemsTurnHandler(DefaultScript): self.db.turn += 1 # Go to the next in the turn order. if self.db.turn > len(self.db.fighters) - 1: self.db.turn = 0 # Go back to the first in the turn order once you reach the end. - - # Count down condition timers. - for fighter in self.db.fighters: - condition_tickdown(fighter, newchar) newchar = self.db.fighters[self.db.turn] # Note the new character + self.db.timer = TURN_TIMEOUT + self.time_until_next_repeat() # Reset the timer. self.db.timeout_warning_given = False # Reset the timeout warning. self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) self.start_turn(newchar) # Start the new character's turn. + + # Count down condition timers. + for fighter in self.db.fighters: + condition_tickdown(fighter, newchar) def turn_end_check(self, character): """ @@ -939,6 +979,32 @@ def itemfunc_heal(item, user, target, **kwargs): user.location.msg_contents("%s uses %s! %s regains %i HP!" % (user, item, target, to_heal)) +def itemfunc_add_condition(item, user, target, **kwargs): + """ + Item function that gives the target a condition. + + Should mostly be used for beneficial conditions - use itemfunc_attack + for an item that can give an enemy a harmful condition. + """ + condition = "Regeneration" + duration = 5 + + if not target: + target = user # Target user if none specified + + if not target.attributes.has("max_hp"): # Is not a fighter + user.msg("You can't use %s on that." % item) + return False # Returning false aborts the item use + + # Retrieve condition / duration from kwargs, if present + if "condition" in kwargs: + condition = kwargs["condition"] + if "duration" in kwargs: + duration = kwargs["duration"] + + user.location.msg_contents("%s uses %s!" % (user, item)) + add_condition(target, user, condition, duration) # Add condition to the target + def itemfunc_attack(item, user, target, **kwargs): """ Item function that attacks a target. @@ -982,7 +1048,8 @@ def itemfunc_attack(item, user, target, **kwargs): # a callable in this dictionary. ITEMFUNCS = { "heal":itemfunc_heal, - "attack":itemfunc_attack + "attack":itemfunc_attack, + "add_condition":itemfunc_add_condition } """ @@ -1015,6 +1082,15 @@ HEALTH_POTION = { "item_kwargs" : {"healing_range":(35, 50)} } +REGEN_POTION = { + "key" : "a regeneration potion", + "desc" : "A glass bottle full of a mystical potion that regenerates wounds over time.", + "item_func" : "add_condition", + "item_uses" : 1, + "item_consumable" : "GLASS_BOTTLE", + "item_kwargs" : {"condition":"Regeneration", "duration":10} +} + BOMB = { "key" : "a rotund bomb", "desc" : "A large black sphere with a fuse at the end. Can be used on enemies in combat.", From 686b290b5da1709de4b2d1a31bfafed4890510d5 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Sat, 18 Nov 2017 13:25:47 -0800 Subject: [PATCH 14/58] Fix condition ticking --- evennia/contrib/turnbattle/tb_items.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index 8457a13bd6..b7cc6dd808 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -473,8 +473,8 @@ class TBItemsCharacter(DefaultCharacter): if not is_in_combat(self): # Not in combat # Apply conditions that fire every turn self.apply_turn_conditions() - # Tick down condition durations - condition_tickdown(self, self) + # Tick down condition durations + condition_tickdown(self, self) """ From 9d7921fee5e159c99ad2abbabdaa6e5236a90ed8 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Sat, 18 Nov 2017 17:28:52 -0800 Subject: [PATCH 15/58] Add "Poisoned" condition, more condition items Added the ability for attack items to inflict conditions on hit, as well as items that can cure specific conditions. --- evennia/contrib/turnbattle/tb_items.py | 70 ++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index b7cc6dd808..22c619cbd8 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -200,7 +200,8 @@ def at_defeat(defeated): """ defeated.location.msg_contents("%s has been defeated!" % defeated) -def resolve_attack(attacker, defender, attack_value=None, defense_value=None, damage_value=None): +def resolve_attack(attacker, defender, attack_value=None, defense_value=None, + damage_value=None, inflict_condition=[]): """ Resolves an attack and outputs the result. @@ -228,6 +229,9 @@ def resolve_attack(attacker, defender, attack_value=None, defense_value=None, da # Announce damage dealt and apply damage. attacker.location.msg_contents("%s hits %s for %i damage!" % (attacker, defender, damage_value)) apply_damage(defender, damage_value) + # Inflict conditions on hit, if any specified + for condition in inflict_condition: + add_condition(defender, attacker, condition[0], condition[1]) # If defender HP is reduced to 0 or less, call at_defeat. if defender.db.hp <= 0: at_defeat(defender) @@ -456,12 +460,22 @@ class TBItemsCharacter(DefaultCharacter): Applies the effect of conditions that occur at the start of each turn in combat, or every 30 seconds out of combat. """ + # Regeneration: restores 4 to 8 HP at the start of character's turn if "Regeneration" in self.db.conditions: to_heal = randint(4, 8) # Restore 4 to 8 HP if self.db.hp + to_heal > self.db.max_hp: to_heal = self.db.max_hp - self.db.hp # Cap healing to max HP self.db.hp += to_heal self.location.msg_contents("%s regains %i HP from Regeneration." % (self, to_heal)) + + # Poisoned: does 4 to 8 damage at the start of character's turn + if "Poisoned" in self.db.conditions: + to_hurt = randint(4, 8) # Deal 4 to 8 damage + apply_damage(self, to_hurt) + self.location.msg_contents("%s takes %i damage from being Poisoned." % (self, to_hurt)) + if self.db.hp <= 0: + # Call at_defeat if poison defeats the character + at_defeat(self) def at_update(self): """ @@ -1005,6 +1019,33 @@ def itemfunc_add_condition(item, user, target, **kwargs): user.location.msg_contents("%s uses %s!" % (user, item)) add_condition(target, user, condition, duration) # Add condition to the target +def itemfunc_cure_condition(item, user, target, **kwargs): + """ + Item function that'll remove given conditions from a target. + """ + to_cure = ["Poisoned"] + + if not target: + target = user # Target user if none specified + + if not target.attributes.has("max_hp"): # Is not a fighter + user.msg("You can't use %s on that." % item) + return False # Returning false aborts the item use + + # Retrieve condition(s) to cure from kwargs, if present + if "to_cure" in kwargs: + to_cure = kwargs["to_cure"] + + item_msg = "%s uses %s! " % (user, item) + + for key in target.db.conditions: + if key in to_cure: + # If condition specified in to_cure, remove it. + item_msg += "%s no longer has the '%s' condition. " % (str(target), str(key)) + del target.db.conditions[key] + + user.location.msg_contents(item_msg) + def itemfunc_attack(item, user, target, **kwargs): """ Item function that attacks a target. @@ -1028,6 +1069,7 @@ def itemfunc_attack(item, user, target, **kwargs): min_damage = 20 max_damage = 40 accuracy = 0 + inflict_condition = [] # Retrieve values from kwargs, if present if "damage_range" in kwargs: @@ -1035,13 +1077,16 @@ def itemfunc_attack(item, user, target, **kwargs): max_damage = kwargs["damage_range"][1] if "accuracy" in kwargs: accuracy = kwargs["accuracy"] + if "inflict_condition" in kwargs: + inflict_condition = kwargs["inflict_condition"] # Roll attack and damage attack_value = randint(1, 100) + accuracy damage_value = randint(min_damage, max_damage) user.location.msg_contents("%s attacks %s with %s!" % (user, target, item)) - resolve_attack(user, target, attack_value=attack_value, damage_value=damage_value) + resolve_attack(user, target, attack_value=attack_value, + damage_value=damage_value, inflict_condition=inflict_condition) # Match strings to item functions here. We can't store callables on # prototypes, so we store a string instead, matching that string to @@ -1049,7 +1094,8 @@ def itemfunc_attack(item, user, target, **kwargs): ITEMFUNCS = { "heal":itemfunc_heal, "attack":itemfunc_attack, - "add_condition":itemfunc_add_condition + "add_condition":itemfunc_add_condition, + "cure_condition":itemfunc_cure_condition } """ @@ -1098,4 +1144,22 @@ BOMB = { "item_uses" : 1, "item_consumable" : True, "item_kwargs" : {"damage_range":(25, 40), "accuracy":25} +} + +POISON_DART = { + "key" : "a poison dart", + "desc" : "A thin dart coated in deadly poison. Can be used on enemies in combat", + "item_func" : "attack", + "item_uses" : 1, + "item_consumable" : True, + "item_kwargs" : {"damage_range":(5, 10), "accuracy":25, "inflict_condition":[("Poisoned", 10)]} +} + +ANTIDOTE_POTION = { + "key" : "an antidote potion", + "desc" : "A glass bottle full of a mystical potion that cures poison when used.", + "item_func" : "cure_condition", + "item_uses" : 1, + "item_consumable" : "GLASS_BOTTLE", + "item_kwargs" : {"to_cure":["Poisoned"]} } \ No newline at end of file From 7a933425f3e1c6ed215796d228e48f5ec688e729 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Sat, 18 Nov 2017 18:02:54 -0800 Subject: [PATCH 16/58] More documentation, 'True' duration for indefinite conditions --- evennia/contrib/turnbattle/tb_items.py | 71 ++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index 22c619cbd8..1ab0e1d324 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -372,13 +372,15 @@ def condition_tickdown(character, turnchar): # The first value is the remaining turns - the second value is whose turn to count down on. condition_duration = character.db.conditions[key][0] condition_turnchar = character.db.conditions[key][1] - # Count down if the given turn character matches the condition's turn character. - if condition_turnchar == turnchar: - character.db.conditions[key][0] -= 1 - if character.db.conditions[key][0] <= 0: - # If the duration is brought down to 0, remove the condition and inform everyone. - character.location.msg_contents("%s no longer has the '%s' condition." % (str(character), str(key))) - del character.db.conditions[key] + # If the duration is 'True', then condition doesn't tick down - it lasts indefinitely. + if not condition_duration == True: + # Count down if the given turn character matches the condition's turn character. + if condition_turnchar == turnchar: + character.db.conditions[key][0] -= 1 + if character.db.conditions[key][0] <= 0: + # If the duration is brought down to 0, remove the condition and inform everyone. + character.location.msg_contents("%s no longer has the '%s' condition." % (str(character), str(key))) + del character.db.conditions[key] def add_condition(character, turnchar, condition, duration): """ @@ -960,12 +962,35 @@ class BattleCmdSet(default_cmds.CharacterCmdSet): self.add(CmdUse()) """ +---------------------------------------------------------------------------- ITEM FUNCTIONS START HERE +---------------------------------------------------------------------------- + +These functions carry out the action of using an item - every item should +contain a db entry "item_func", with its value being a string that is +matched to one of these functions in the ITEMFUNCS dictionary below. + +Every item function must take the following arguments: + item (obj): The item being used + user (obj): The character using the item + target (obj): The target of the item use + +Item functions must also accept **kwargs - these keyword arguments can be +used to define how different items that use the same function can have +different effects (for example, different attack items doing different +amounts of damage). + +Each function below contains a description of what kwargs the function will +take and the effect they have on the result. """ def itemfunc_heal(item, user, target, **kwargs): """ Item function that heals HP. + + kwargs: + min_healing(int): Minimum amount of HP recovered + max_healing(int): Maximum amount of HP recovered """ if not target: target = user # Target user if none specified @@ -997,8 +1022,13 @@ def itemfunc_add_condition(item, user, target, **kwargs): """ Item function that gives the target a condition. - Should mostly be used for beneficial conditions - use itemfunc_attack - for an item that can give an enemy a harmful condition. + kwargs: + condition(str): Condition added by the item + duration(int): Number of turns the condition lasts, or True for indefinite + + Notes: + Should mostly be used for beneficial conditions - use itemfunc_attack + for an item that can give an enemy a harmful condition. """ condition = "Regeneration" duration = 5 @@ -1022,6 +1052,9 @@ def itemfunc_add_condition(item, user, target, **kwargs): def itemfunc_cure_condition(item, user, target, **kwargs): """ Item function that'll remove given conditions from a target. + + kwargs: + to_cure(list): List of conditions (str) that the item cures when used """ to_cure = ["Poisoned"] @@ -1049,6 +1082,17 @@ def itemfunc_cure_condition(item, user, target, **kwargs): def itemfunc_attack(item, user, target, **kwargs): """ Item function that attacks a target. + + kwargs: + min_damage(int): Minimum damage dealt by the attack + max_damage(int): Maximum damage dealth by the attack + accuracy(int): Bonus / penalty to attack accuracy roll + inflict_condition(list): List of conditions inflicted on hit, + formatted as a (str, int) tuple containing condition name + and duration. + + Notes: + Calls resolve_attack at the end. """ if not is_in_combat(user): user.msg("You can only use that in combat.") @@ -1099,9 +1143,14 @@ ITEMFUNCS = { } """ -ITEM PROTOTYPES START HERE +---------------------------------------------------------------------------- +PROTOTYPES START HERE +---------------------------------------------------------------------------- -Copy these to your game's /world/prototypes.py module! +You can paste these prototypes into your game's prototypes.py module in your +/world/ folder, and use the spawner to create them - they serve as examples +of items you can make and a handy way to demonstrate the system for +conditions as well. """ MEDKIT = { From 1d65a0a0cf202f822a623e34622edaf3a9dd6db2 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Sat, 18 Nov 2017 18:43:14 -0800 Subject: [PATCH 17/58] More documentation, fix error in at_update() at_update() erroneously changed the turnchar on conditions during combat - this has been fixed. --- evennia/contrib/turnbattle/tb_items.py | 73 +++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index 1ab0e1d324..db5be80be8 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -304,10 +304,17 @@ def spend_action(character, actions, action_name=None): def spend_item_use(item, user): """ - Spends one use on an item with limited uses. If item.db.item_consumable - is 'True', the item is destroyed if it runs out of uses - if it's a string - instead of 'True', it will also spawn a new object as residue, using the - value of item.db.item_consumable as the name of the prototype to spawn. + Spends one use on an item with limited uses. + + Args: + item (obj): Item being used + user (obj): Character using the item + + Notes: + If item.db.item_consumable is 'True', the item is destroyed if it + runs out of uses - if it's a string instead of 'True', it will also + spawn a new object as residue, using the value of item.db.item_consumable + as the name of the prototype to spawn. """ item.db.item_uses -= 1 # Spend one use @@ -335,7 +342,17 @@ def spend_item_use(item, user): def use_item(user, item, target): """ Performs the action of using an item. + + Args: + user (obj): Character using the item + item (obj): Item being used + target (obj): Target of the item use """ + # If item is self only, abort use + if item.db.item_selfonly and user == target: + user.msg("%s can only be used on yourself." % item) + return + # Set kwargs to pass to item_func kwargs = {} if item.db.item_kwargs: @@ -344,7 +361,7 @@ def use_item(user, item, target): # Match item_func string to function try: item_func = ITEMFUNCS[item.db.item_func] - except KeyError: + except KeyError: # If item_func string doesn't match to a function in ITEMFUNCS user.msg("ERROR: %s not defined in ITEMFUNCS" % item.db.item_func) return @@ -366,6 +383,15 @@ def use_item(user, item, target): def condition_tickdown(character, turnchar): """ Ticks down the duration of conditions on a character at the start of a given character's turn. + + Args: + character (obj): Character to tick down the conditions of + turnchar (obj): Character whose turn it currently is + + Notes: + In combat, this is called on every fighter at the start of every character's turn. Out of + combat, it's instead called when a character's at_update() hook is called, which is every + 30 seconds. """ for key in character.db.conditions: @@ -385,6 +411,12 @@ def condition_tickdown(character, turnchar): def add_condition(character, turnchar, condition, duration): """ Adds a condition to a fighter. + + Args: + character (obj): Character to give the condition to + turnchar (obj): Character whose turn to tick down the condition on in combat + condition (str): Name of the condition + duration (int or True): Number of turns the condition lasts, or True for indefinite """ # The first value is the remaining turns - the second value is whose turn to count down on. character.db.conditions.update({condition:[duration, turnchar]}) @@ -417,6 +449,11 @@ class TBItemsCharacter(DefaultCharacter): """ Adds attributes for a character's current and maximum HP. We're just going to set this value at '100' by default. + + An empty dictionary is created to store conditions later, + and the character is subscribed to the Ticker Handler, which + will call at_update() on the character every 30 seconds. This + is used to tick down conditions out of combat. You may want to expand this to include various 'stats' that can be changed at creation and factor into combat calculations. @@ -483,10 +520,10 @@ class TBItemsCharacter(DefaultCharacter): """ Fires every 30 seconds. """ - # Change all conditions to update on character's turn. - for key in self.db.conditions: - self.db.conditions[key][1] = self if not is_in_combat(self): # Not in combat + # Change all conditions to update on character's turn. + for key in self.db.conditions: + self.db.conditions[key][1] = self # Apply conditions that fire every turn self.apply_turn_conditions() # Tick down condition durations @@ -1151,6 +1188,26 @@ You can paste these prototypes into your game's prototypes.py module in your /world/ folder, and use the spawner to create them - they serve as examples of items you can make and a handy way to demonstrate the system for conditions as well. + +Items don't have any particular typeclass - any object with a db entry +"item_func" that references one of the functions given above can be used as +an item with the 'use' command. + +Only "item_func" is required, but item behavior can be further modified by +specifying any of the following: + + item_uses (int): If defined, item has a limited number of uses + + item_selfonly (bool): If True, user can only use the item on themself + + item_consumable(True or str): If True, item is destroyed when it runs + out of uses. If a string is given, the item will spawn a new + object as it's destroyed, with the string specifying what prototype + to spawn. + + item_kwargs (dict): Keyword arguments to pass to the function defined in + item_func. Unique to each function, and can be used to make multiple + items using the same function work differently. """ MEDKIT = { From 42db3aa7f50fc14d26e2cde37fa07207ff489470 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 19 Nov 2017 12:16:38 -0800 Subject: [PATCH 18/58] More conditions and documentation --- evennia/contrib/turnbattle/tb_items.py | 116 ++++++++++++++++++------- 1 file changed, 84 insertions(+), 32 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index db5be80be8..c101dca9a8 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -3,17 +3,34 @@ Simple turn-based combat system with items and status effects Contrib - Tim Ashley Jenkins 2017 -This is a version of the 'turnbattle' combat system that includes status -effects and usable items, which can instill these status effects, cure +This is a version of the 'turnbattle' combat system that includes +conditions and usable items, which can instill these conditions, cure them, or do just about anything else. -Status effects are stored on characters as a dictionary, where the key -is the name of the status effect and the value is a list of two items: -an integer representing the number of turns left until the status runs -out, and the character upon whose turn the condition timer is ticked -down. Unlike most combat-related attributes, conditions aren't wiped -once combat ends - if out of combat, they tick down in real time -instead. +Conditions are stored on characters as a dictionary, where the key +is the name of the condition and the value is a list of two items: +an integer representing the number of turns left until the condition +runs out, and the character upon whose turn the condition timer is +ticked down. Unlike most combat-related attributes, conditions aren't +wiped once combat ends - if out of combat, they tick down in real time +instead. + +This module includes a number of example conditions: + + Regeneration: Character recovers HP every turn + Poisoned: Character loses HP every turn + Accuracy Up: +25 to character's attack rolls + Accuracy Down: -25 to character's attack rolls + Damage Up: +5 to character's damage + Damage Down: -5 to character's damage + Defense Up: +15 to character's defense + Defense Down: -15 to character's defense + Haste: +1 action per turn + Paralyzed: No actions per turn + Frightened: Character can't use the 'attack' command + +Since conditions can have a wide variety of effects, their code is +scattered throughout the other functions wherever they may apply. Items aren't given any sort of special typeclass - instead, whether or not an object counts as an item is determined by its attributes. To make @@ -64,6 +81,7 @@ OPTIONS TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds ACTIONS_PER_TURN = 1 # Number of actions allowed per turn +NONCOMBAT_TURN_TIME = 30 # Time per turn count out of combat """ ---------------------------------------------------------------------------- @@ -111,15 +129,18 @@ def get_attack(attacker, defender): to determine whether an attack hits or misses. Notes: - By default, returns a random integer from 1 to 100 without using any - properties from either the attacker or defender. - - This can easily be expanded to return a value based on characters stats, - equipment, and abilities. This is why the attacker and defender are passed - to this function, even though nothing from either one are used in this example. + This is where conditions affecting attack rolls are applied, as well. + Accuracy Up and Accuracy Down are also accounted for in itemfunc_attack(), + so that attack items' accuracy is affected as well. """ # For this example, just return a random integer up to 100. attack_value = randint(1, 100) + # Add 25 to the roll if the attacker has the "Accuracy Up" condition. + if "Accuracy Up" in attacker.db.conditions: + attack_value += 25 + # Subtract 25 from the roll if the attack has the "Accuracy Down" condition. + if "Accuracy Down" in attacker.db.conditions: + attack_value -= 25 return attack_value @@ -137,13 +158,16 @@ def get_defense(attacker, defender): to determine whether an attack hits or misses. Notes: - By default, returns 50, not taking any properties of the defender or - attacker into account. - - As above, this can be expanded upon based on character stats and equipment. + This is where conditions affecting defense are accounted for. """ # For this example, just return 50, for about a 50/50 chance of hit. defense_value = 50 + # Add 15 to defense if the defender has the "Defense Up" condition. + if "Defense Up" in defender.db.conditions: + defense_value += 15 + # Subtract 15 from defense if the defender has the "Defense Down" condition. + if "Defense Down" in defender.db.conditions: + defense_value -= 15 return defense_value @@ -161,13 +185,18 @@ def get_damage(attacker, defender): character's HP. Notes: - By default, returns a random integer from 15 to 25 without using any - properties from either the attacker or defender. - - Again, this can be expanded upon. + This is where conditions affecting damage are accounted for. Since attack items + roll their own damage in itemfunc_attack(), their damage is unaffected by any + conditions. """ # For this example, just generate a number between 15 and 25. damage_value = randint(15, 25) + # Add 5 to damage roll if attacker has the "Damage Up" condition. + if "Damage Up" in attacker.db.conditions: + damage_value += 5 + # Subtract 5 from the roll if the attacker has the "Damage Down" condition. + if "Damage Down" in attacker.db.conditions: + damage_value -= 5 return damage_value @@ -208,11 +237,17 @@ def resolve_attack(attacker, defender, attack_value=None, defense_value=None, Args: attacker (obj): Character doing the attacking defender (obj): Character being attacked + + Options: + attack_value (int): Override for attack roll + defense_value (int): Override for defense value + damage_value (int): Override for damage value + inflict_condition (list): Conditions to inflict upon hit, a + list of tuples formated as (condition(str), duration(int)) Notes: - Even though the attack and defense values are calculated - extremely simply, they are separated out into their own functions - so that they are easier to expand upon. + This function is called by normal attacks as well as attacks + made with items. """ # Get an attack roll from the attacker. if not attack_value: @@ -391,14 +426,14 @@ def condition_tickdown(character, turnchar): Notes: In combat, this is called on every fighter at the start of every character's turn. Out of combat, it's instead called when a character's at_update() hook is called, which is every - 30 seconds. + 30 seconds by default. """ for key in character.db.conditions: # The first value is the remaining turns - the second value is whose turn to count down on. condition_duration = character.db.conditions[key][0] condition_turnchar = character.db.conditions[key][1] - # If the duration is 'True', then condition doesn't tick down - it lasts indefinitely. + # If the duration is 'True', then the condition doesn't tick down - it lasts indefinitely. if not condition_duration == True: # Count down if the given turn character matches the condition's turn character. if condition_turnchar == turnchar: @@ -445,15 +480,16 @@ class TBItemsCharacter(DefaultCharacter): self.db.hp = self.db.max_hp # Set current HP to maximum self.db.conditions = {} # Set empty dict for conditions # Subscribe character to the ticker handler - tickerhandler.add(30, self.at_update) + tickerhandler.add(NONCOMBAT_TURN_TIME, self.at_update) """ Adds attributes for a character's current and maximum HP. We're just going to set this value at '100' by default. An empty dictionary is created to store conditions later, and the character is subscribed to the Ticker Handler, which - will call at_update() on the character every 30 seconds. This - is used to tick down conditions out of combat. + will call at_update() on the character, with the interval + specified by NONCOMBAT_TURN_TIME above. This is used to tick + down conditions out of combat. You may want to expand this to include various 'stats' that can be changed at creation and factor into combat calculations. @@ -515,6 +551,16 @@ class TBItemsCharacter(DefaultCharacter): if self.db.hp <= 0: # Call at_defeat if poison defeats the character at_defeat(self) + + # Haste: Gain an extra action in combat. + if is_in_combat(self) and "Haste" in self.db.conditions: + self.db.combat_actionsleft += 1 + self.msg("You gain an extra action this turn from Haste!") + + # Paralyzed: Have no actions in combat. + if is_in_combat(self) and "Paralyzed" in self.db.conditions: + self.db.combat_actionsleft = 0 + self.msg("You're Paralyzed, and can't act this turn!") def at_update(self): """ @@ -791,6 +837,10 @@ class CmdAttack(Command): if not self.caller.db.hp: # Can't attack if you have no HP. self.caller.msg("You can't attack, you've been defeated.") return + + if "Frightened" in self.caller.db.conditions: # Can't attack if frightened + self.caller.msg("You're too frightened to attack!") + return attacker = self.caller defender = self.caller.search(self.args) @@ -939,7 +989,9 @@ class CmdUse(MuxCommand): Usage: use [= target] - Items: you just GOTTA use them. + An item can have various function - looking at the item may + provide information as to its effects. Some items can be used + to attack others, and as such can only be used in combat. """ key = "use" From ba964797def81a7babe6454ea38138729a39a8dd Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 19 Nov 2017 12:59:07 -0800 Subject: [PATCH 19/58] Fixed all conditions lasting indefinitely Turns out 1 == True, but not 1 is True - learn something new every day! --- evennia/contrib/turnbattle/tb_items.py | 49 +++++++++++++++++++------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index c101dca9a8..6a30b3cda3 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -384,7 +384,7 @@ def use_item(user, item, target): target (obj): Target of the item use """ # If item is self only, abort use - if item.db.item_selfonly and user == target: + if item.db.item_selfonly and user != target: user.msg("%s can only be used on yourself." % item) return @@ -434,7 +434,7 @@ def condition_tickdown(character, turnchar): condition_duration = character.db.conditions[key][0] condition_turnchar = character.db.conditions[key][1] # If the duration is 'True', then the condition doesn't tick down - it lasts indefinitely. - if not condition_duration == True: + if not condition_duration is True: # Count down if the given turn character matches the condition's turn character. if condition_turnchar == turnchar: character.db.conditions[key][0] -= 1 @@ -1109,18 +1109,17 @@ def itemfunc_heal(item, user, target, **kwargs): def itemfunc_add_condition(item, user, target, **kwargs): """ - Item function that gives the target a condition. + Item function that gives the target one or more conditions. kwargs: - condition(str): Condition added by the item - duration(int): Number of turns the condition lasts, or True for indefinite + conditions (list): Conditions added by the item + formatted as a list of tuples: (condition (str), duration (int or True)) Notes: Should mostly be used for beneficial conditions - use itemfunc_attack for an item that can give an enemy a harmful condition. """ - condition = "Regeneration" - duration = 5 + conditions = [("Regeneration", 5)] if not target: target = user # Target user if none specified @@ -1130,13 +1129,14 @@ def itemfunc_add_condition(item, user, target, **kwargs): return False # Returning false aborts the item use # Retrieve condition / duration from kwargs, if present - if "condition" in kwargs: - condition = kwargs["condition"] - if "duration" in kwargs: - duration = kwargs["duration"] + if "conditions" in kwargs: + conditions = kwargs["conditions"] user.location.msg_contents("%s uses %s!" % (user, item)) - add_condition(target, user, condition, duration) # Add condition to the target + + # Add conditions to the target + for condition in conditions: + add_condition(target, user, condition[0], condition[1]) def itemfunc_cure_condition(item, user, target, **kwargs): """ @@ -1217,6 +1217,12 @@ def itemfunc_attack(item, user, target, **kwargs): attack_value = randint(1, 100) + accuracy damage_value = randint(min_damage, max_damage) + # Account for "Accuracy Up" and "Accuracy Down" conditions + if "Accuracy Up" in user.db.conditions: + attack_value += 25 + if "Accuracy Down" in user.db.conditions: + attack_value -= 25 + user.location.msg_contents("%s attacks %s with %s!" % (user, target, item)) resolve_attack(user, target, attack_value=attack_value, damage_value=damage_value, inflict_condition=inflict_condition) @@ -1292,7 +1298,16 @@ REGEN_POTION = { "item_func" : "add_condition", "item_uses" : 1, "item_consumable" : "GLASS_BOTTLE", - "item_kwargs" : {"condition":"Regeneration", "duration":10} + "item_kwargs" : {"conditions":[("Regeneration", 10)]} +} + +HASTE_POTION = { + "key" : "a haste potion", + "desc" : "A glass bottle full of a mystical potion that hastens its user.", + "item_func" : "add_condition", + "item_uses" : 1, + "item_consumable" : "GLASS_BOTTLE", + "item_kwargs" : {"conditions":[("Haste", 10)]} } BOMB = { @@ -1320,4 +1335,12 @@ ANTIDOTE_POTION = { "item_uses" : 1, "item_consumable" : "GLASS_BOTTLE", "item_kwargs" : {"to_cure":["Poisoned"]} +} + +AMULET_OF_MIGHT = { + "key" : "The Amulet of Might", + "desc" : "The one who holds this amulet can call upon its power to gain great strength.", + "item_func" : "add_condition", + "item_selfonly" : True, + "item_kwargs" : {"conditions":[("Damage Up", 3), ("Accuracy Up", 3), ("Defense Up", 3)]} } \ No newline at end of file From d99b0b7819a4b98434a44fc548a2a84399af4374 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 19 Nov 2017 13:18:55 -0800 Subject: [PATCH 20/58] More item prototypes - probably ready to go! --- evennia/contrib/turnbattle/tb_items.py | 32 ++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index 6a30b3cda3..25b7625991 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -383,7 +383,11 @@ def use_item(user, item, target): item (obj): Item being used target (obj): Target of the item use """ - # If item is self only, abort use + # If item is self only and no target given, set target to self. + if item.db.item_selfonly and target == None: + target = user + + # If item is self only, abort use if used on others. if item.db.item_selfonly and user != target: user.msg("%s can only be used on yourself." % item) return @@ -560,7 +564,8 @@ class TBItemsCharacter(DefaultCharacter): # Paralyzed: Have no actions in combat. if is_in_combat(self) and "Paralyzed" in self.db.conditions: self.db.combat_actionsleft = 0 - self.msg("You're Paralyzed, and can't act this turn!") + self.location.msg_contents("%s is Paralyzed, and can't act this turn!" % self) + self.db.combat_turnhandler.turn_end_check(self) def at_update(self): """ @@ -1328,6 +1333,21 @@ POISON_DART = { "item_kwargs" : {"damage_range":(5, 10), "accuracy":25, "inflict_condition":[("Poisoned", 10)]} } +TASER = { + "key" : "a taser", + "desc" : "A device that can be used to paralyze enemies in combat.", + "item_func" : "attack", + "item_kwargs" : {"damage_range":(10, 20), "accuracy":0, "inflict_condition":[("Paralyzed", 1)]} +} + +GHOST_GUN = { + "key" : "a ghost gun", + "desc" : "A gun that fires scary ghosts at people. Anyone hit by a ghost becomes frightened.", + "item_func" : "attack", + "item_uses" : 6, + "item_kwargs" : {"damage_range":(5, 10), "accuracy":15, "inflict_condition":[("Frightened", 1)]} +} + ANTIDOTE_POTION = { "key" : "an antidote potion", "desc" : "A glass bottle full of a mystical potion that cures poison when used.", @@ -1343,4 +1363,12 @@ AMULET_OF_MIGHT = { "item_func" : "add_condition", "item_selfonly" : True, "item_kwargs" : {"conditions":[("Damage Up", 3), ("Accuracy Up", 3), ("Defense Up", 3)]} +} + +AMULET_OF_WEAKNESS = { + "key" : "The Amulet of Weakness", + "desc" : "The one who holds this amulet can call upon its power to gain great weakness. It's not a terribly useful artifact.", + "item_func" : "add_condition", + "item_selfonly" : True, + "item_kwargs" : {"conditions":[("Damage Down", 3), ("Accuracy Down", 3), ("Defense Down", 3)]} } \ No newline at end of file From 68e46f1e4ed842253dd30e2501dd5c668ddf94b3 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 19 Nov 2017 13:24:44 -0800 Subject: [PATCH 21/58] Update readme --- evennia/contrib/turnbattle/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/evennia/contrib/turnbattle/README.md b/evennia/contrib/turnbattle/README.md index 729c42a099..d5c86d90f3 100644 --- a/evennia/contrib/turnbattle/README.md +++ b/evennia/contrib/turnbattle/README.md @@ -21,6 +21,19 @@ implemented and customized: the battle system, including commands for wielding weapons and donning armor, and modifiers to accuracy and damage based on currently used equipment. + + tb_items.py - Adds usable items and conditions/status effects, and gives + a lot of examples for each. Items can perform nearly any sort of + function, including healing, adding or curing conditions, or + being used to attack. Conditions affect a fighter's attributes + and options in combat and persist outside of fights, counting + down per turn in combat and in real time outside combat. + + tb_magic.py - Adds a spellcasting system, allowing characters to cast + spells with a variety of effects by spending MP. Spells are + linked to functions, and as such can perform any sort of action + the developer can imagine - spells for attacking, healing and + conjuring objects are included as examples. tb_range.py - Adds a system for abstract positioning and movement, which tracks the distance between different characters and objects in From 9f86034cf301276375b8ce1516c09f341c7e41e1 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 19 Nov 2017 13:25:37 -0800 Subject: [PATCH 22/58] Fix readme spacing --- evennia/contrib/turnbattle/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/evennia/contrib/turnbattle/README.md b/evennia/contrib/turnbattle/README.md index d5c86d90f3..fd2563bceb 100644 --- a/evennia/contrib/turnbattle/README.md +++ b/evennia/contrib/turnbattle/README.md @@ -23,17 +23,17 @@ implemented and customized: currently used equipment. tb_items.py - Adds usable items and conditions/status effects, and gives - a lot of examples for each. Items can perform nearly any sort of - function, including healing, adding or curing conditions, or - being used to attack. Conditions affect a fighter's attributes - and options in combat and persist outside of fights, counting - down per turn in combat and in real time outside combat. + a lot of examples for each. Items can perform nearly any sort of + function, including healing, adding or curing conditions, or + being used to attack. Conditions affect a fighter's attributes + and options in combat and persist outside of fights, counting + down per turn in combat and in real time outside combat. tb_magic.py - Adds a spellcasting system, allowing characters to cast - spells with a variety of effects by spending MP. Spells are - linked to functions, and as such can perform any sort of action - the developer can imagine - spells for attacking, healing and - conjuring objects are included as examples. + spells with a variety of effects by spending MP. Spells are + linked to functions, and as such can perform any sort of action + the developer can imagine - spells for attacking, healing and + conjuring objects are included as examples. tb_range.py - Adds a system for abstract positioning and movement, which tracks the distance between different characters and objects in From 0c0b5c982a9c517f52981195be878fb1106b2f49 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Wed, 29 Nov 2017 13:54:22 -0800 Subject: [PATCH 23/58] Added options for conditions at top of module --- evennia/contrib/turnbattle/tb_items.py | 38 ++++++++++++++++---------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index 25b7625991..98a39774c4 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -83,6 +83,16 @@ TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds ACTIONS_PER_TURN = 1 # Number of actions allowed per turn NONCOMBAT_TURN_TIME = 30 # Time per turn count out of combat +# Condition options start here +REGEN_RATE = (4, 8) # Min and max HP regen for Regeneration +POISON_RATE = (4, 8) # Min and max damage for Poisoned +ACC_UP_MOD = 25 # Accuracy Up attack roll bonus +ACC_DOWN_MOD = -25 # Accuracy Down attack roll penalty +DMG_UP_MOD = 5 # Damage Up damage roll bonus +DMG_DOWN_MOD = -5 # Damage Down damage roll penalty +DEF_UP_MOD = 15 # Defense Up defense bonus +DEF_DOWN_MOD = -15 # Defense Down defense penalty + """ ---------------------------------------------------------------------------- COMBAT FUNCTIONS START HERE @@ -135,12 +145,12 @@ def get_attack(attacker, defender): """ # For this example, just return a random integer up to 100. attack_value = randint(1, 100) - # Add 25 to the roll if the attacker has the "Accuracy Up" condition. + # Add to the roll if the attacker has the "Accuracy Up" condition. if "Accuracy Up" in attacker.db.conditions: - attack_value += 25 - # Subtract 25 from the roll if the attack has the "Accuracy Down" condition. + attack_value += ACC_UP_MOD + # Subtract from the roll if the attack has the "Accuracy Down" condition. if "Accuracy Down" in attacker.db.conditions: - attack_value -= 25 + attack_value += ACC_DOWN_MOD return attack_value @@ -162,12 +172,12 @@ def get_defense(attacker, defender): """ # For this example, just return 50, for about a 50/50 chance of hit. defense_value = 50 - # Add 15 to defense if the defender has the "Defense Up" condition. + # Add to defense if the defender has the "Defense Up" condition. if "Defense Up" in defender.db.conditions: - defense_value += 15 - # Subtract 15 from defense if the defender has the "Defense Down" condition. + defense_value += DEF_UP_MOD + # Subtract from defense if the defender has the "Defense Down" condition. if "Defense Down" in defender.db.conditions: - defense_value -= 15 + defense_value += DEF_DOWN_MOD return defense_value @@ -191,12 +201,12 @@ def get_damage(attacker, defender): """ # For this example, just generate a number between 15 and 25. damage_value = randint(15, 25) - # Add 5 to damage roll if attacker has the "Damage Up" condition. + # Add to damage roll if attacker has the "Damage Up" condition. if "Damage Up" in attacker.db.conditions: - damage_value += 5 - # Subtract 5 from the roll if the attacker has the "Damage Down" condition. + damage_value += DMG_UP_MOD + # Subtract from the roll if the attacker has the "Damage Down" condition. if "Damage Down" in attacker.db.conditions: - damage_value -= 5 + damage_value += DMG_DOWN_MOD return damage_value @@ -541,7 +551,7 @@ class TBItemsCharacter(DefaultCharacter): """ # Regeneration: restores 4 to 8 HP at the start of character's turn if "Regeneration" in self.db.conditions: - to_heal = randint(4, 8) # Restore 4 to 8 HP + to_heal = randint(REGEN_RATE[0], REGEN_RAGE[1]) # Restore HP if self.db.hp + to_heal > self.db.max_hp: to_heal = self.db.max_hp - self.db.hp # Cap healing to max HP self.db.hp += to_heal @@ -549,7 +559,7 @@ class TBItemsCharacter(DefaultCharacter): # Poisoned: does 4 to 8 damage at the start of character's turn if "Poisoned" in self.db.conditions: - to_hurt = randint(4, 8) # Deal 4 to 8 damage + to_hurt = randint(POISON_RATE[0], POISON_RATE[1]) # Deal damage apply_damage(self, to_hurt) self.location.msg_contents("%s takes %i damage from being Poisoned." % (self, to_hurt)) if self.db.hp <= 0: From eb95416ee84b37d04c77d8f73569be105a6b4e0f Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Wed, 29 Nov 2017 15:01:49 -0800 Subject: [PATCH 24/58] Unit tests for tb_items --- evennia/contrib/tests.py | 125 ++++++++++++++++++++++++- evennia/contrib/turnbattle/tb_items.py | 5 +- 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 4076ade4dd..6c5798cb34 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -916,7 +916,7 @@ class TestTutorialWorldRooms(CommandTest): # test turnbattle -from evennia.contrib.turnbattle import tb_basic, tb_equip, tb_range +from evennia.contrib.turnbattle import tb_basic, tb_equip, tb_range, tb_items from evennia.objects.objects import DefaultRoom @@ -962,6 +962,18 @@ class TestTurnBattleCmd(CommandTest): self.call(tb_range.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_range.CmdRest(), "", "Char rests to recover HP.") + # Test item commands + def test_turnbattlecmd(self): + testitem = create_object(key="test item") + testitem.move_to(self.char1) + self.call(tb_items.CmdUse(), "item", "'Test item' is not a usable item.") + # Also test the commands that are the same in the basic module + self.call(tb_items.CmdFight(), "", "You can't start a fight if you've been defeated!") + self.call(tb_items.CmdAttack(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_items.CmdPass(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_items.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_items.CmdRest(), "", "Char rests to recover HP.") + class TestTurnBattleFunc(EvenniaTest): @@ -1214,6 +1226,117 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(tb_range.get_range(attacker, defender) == 1) # Remove the script at the end turnhandler.stop() + + # Test functions in tb_items. + def test_tbitemsfunc(self): + attacker = create_object(tb_items.TBItemsCharacter, key="Attacker") + defender = create_object(tb_items.TBItemsCharacter, key="Defender") + testroom = create_object(DefaultRoom, key="Test Room") + attacker.location = testroom + defender.loaction = testroom + # Initiative roll + initiative = tb_items.roll_init(attacker) + self.assertTrue(initiative >= 0 and initiative <= 1000) + # Attack roll + attack_roll = tb_items.get_attack(attacker, defender) + self.assertTrue(attack_roll >= 0 and attack_roll <= 100) + # Defense roll + defense_roll = tb_items.get_defense(attacker, defender) + self.assertTrue(defense_roll == 50) + # Damage roll + damage_roll = tb_items.get_damage(attacker, defender) + self.assertTrue(damage_roll >= 15 and damage_roll <= 25) + # Apply damage + defender.db.hp = 10 + tb_items.apply_damage(defender, 3) + self.assertTrue(defender.db.hp == 7) + # Resolve attack + defender.db.hp = 40 + tb_items.resolve_attack(attacker, defender, attack_value=20, defense_value=10) + self.assertTrue(defender.db.hp < 40) + # Combat cleanup + attacker.db.Combat_attribute = True + tb_items.combat_cleanup(attacker) + self.assertFalse(attacker.db.combat_attribute) + # Is in combat + self.assertFalse(tb_items.is_in_combat(attacker)) + # Set up turn handler script for further tests + attacker.location.scripts.add(tb_items.TBItemsTurnHandler) + turnhandler = attacker.db.combat_TurnHandler + self.assertTrue(attacker.db.combat_TurnHandler) + # Set the turn handler's interval very high to keep it from repeating during tests. + turnhandler.interval = 10000 + # Force turn order + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + # Test is turn + self.assertTrue(tb_items.is_turn(attacker)) + # Spend actions + attacker.db.Combat_ActionsLeft = 1 + tb_items.spend_action(attacker, 1, action_name="Test") + self.assertTrue(attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(attacker.db.Combat_LastAction == "Test") + # Initialize for combat + attacker.db.Combat_ActionsLeft = 983 + turnhandler.initialize_for_combat(attacker) + self.assertTrue(attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(attacker.db.Combat_LastAction == "null") + # Start turn + defender.db.Combat_ActionsLeft = 0 + turnhandler.start_turn(defender) + self.assertTrue(defender.db.Combat_ActionsLeft == 1) + # Next turn + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + turnhandler.next_turn() + self.assertTrue(turnhandler.db.turn == 1) + # Turn end check + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + attacker.db.Combat_ActionsLeft = 0 + turnhandler.turn_end_check(attacker) + self.assertTrue(turnhandler.db.turn == 1) + # Join fight + joiner = create_object(tb_items.TBItemsCharacter, key="Joiner") + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + turnhandler.join_fight(joiner) + self.assertTrue(turnhandler.db.turn == 1) + self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) + # Remove the script at the end + turnhandler.stop() + # Now time to test item stuff. + user = create_object(tb_items.TBItemsCharacter, key="User") + testroom = create_object(DefaultRoom, key="Test Room") + user.location = testroom + test_healpotion = create_object(key="healing potion") + test_healpotion.db.item_func = "heal" + test_healpotion.db.item_uses = 3 + # Spend item use + tb_items.spend_item_use(test_healpotion, user) + self.assertTrue(test_healpotion.db.item_uses == 2) + # Use item + user.db.hp = 2 + tb_items.use_item(user, test_healpotion, user) + self.assertTrue(user.db.hp > 2) + # Add contition + tb_items.add_condition(user, user, "Test", 5) + self.assertTrue(user.db.conditions == {"Test":[5, user]}) + # Condition tickdown + tb_items.condition_tickdown(user, user) + self.assertTrue(user.db.conditions == {"Test":[4, user]}) + # Test item functions now! + # Item heal + user.db.hp = 2 + tb_items.itemfunc_heal(test_healpotion, user, user) + # Item add condition + user.db.conditions = {} + tb_items.itemfunc_add_condition(test_healpotion, user, user) + self.assertTrue(user.db.conditions == {"Regeneration":[5, user]}) + # Item cure condition + user.db.conditions = {"Poisoned":[5, user]} + tb_items.itemfunc_cure_condition(test_healpotion, user, user) + self.assertTrue(user.db.conditions == {}) # Test tree select diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index 98a39774c4..f367fec269 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -83,7 +83,10 @@ TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds ACTIONS_PER_TURN = 1 # Number of actions allowed per turn NONCOMBAT_TURN_TIME = 30 # Time per turn count out of combat -# Condition options start here +# Condition options start here. +# If you need to make changes to how your conditions work later, +# it's best to put the easily tweakable values all in one place! + REGEN_RATE = (4, 8) # Min and max HP regen for Regeneration POISON_RATE = (4, 8) # Min and max damage for Poisoned ACC_UP_MOD = 25 # Accuracy Up attack roll bonus From 785522fb3ceeae2f55f6c9b4dc908ab5b28f174f Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Wed, 29 Nov 2017 15:08:55 -0800 Subject: [PATCH 25/58] Attempt to fix TickerHandler error in unit tests --- evennia/contrib/tests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 6c5798cb34..bf03036117 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1337,6 +1337,8 @@ class TestTurnBattleFunc(EvenniaTest): user.db.conditions = {"Poisoned":[5, user]} tb_items.itemfunc_cure_condition(test_healpotion, user, user) self.assertTrue(user.db.conditions == {}) + # Delete the test character to prevent ticker handler problems + user.delete() # Test tree select From 9e8a400049cf9db6f6d3780e9000d6e3791422cd Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Wed, 29 Nov 2017 15:15:47 -0800 Subject: [PATCH 26/58] Manually unsubscribe ticker handler --- evennia/contrib/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index bf03036117..04a1753933 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1307,6 +1307,7 @@ class TestTurnBattleFunc(EvenniaTest): turnhandler.stop() # Now time to test item stuff. user = create_object(tb_items.TBItemsCharacter, key="User") + user.TICKER_HANDLER.remove(interval=30, callback=user.at_update) testroom = create_object(DefaultRoom, key="Test Room") user.location = testroom test_healpotion = create_object(key="healing potion") @@ -1337,7 +1338,7 @@ class TestTurnBattleFunc(EvenniaTest): user.db.conditions = {"Poisoned":[5, user]} tb_items.itemfunc_cure_condition(test_healpotion, user, user) self.assertTrue(user.db.conditions == {}) - # Delete the test character to prevent ticker handler problems + # Delete the test character user.delete() # Test tree select From 0c8db01d5616367f225869d31c7ab188968739a3 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Wed, 29 Nov 2017 15:29:49 -0800 Subject: [PATCH 27/58] TickerHandler stuff, more --- evennia/contrib/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 04a1753933..7a3c660a72 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1307,7 +1307,7 @@ class TestTurnBattleFunc(EvenniaTest): turnhandler.stop() # Now time to test item stuff. user = create_object(tb_items.TBItemsCharacter, key="User") - user.TICKER_HANDLER.remove(interval=30, callback=user.at_update) + tb_items.tickerhandler.remove(interval=30, callback=user.at_update) testroom = create_object(DefaultRoom, key="Test Room") user.location = testroom test_healpotion = create_object(key="healing potion") From dc67f4b871913995ff3dc424be1177172172dbbd Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Wed, 29 Nov 2017 15:37:40 -0800 Subject: [PATCH 28/58] Ugh!!! TickerHandler changes, more --- evennia/contrib/tests.py | 2 +- evennia/contrib/turnbattle/tb_items.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 7a3c660a72..8d40aeaadb 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1307,7 +1307,7 @@ class TestTurnBattleFunc(EvenniaTest): turnhandler.stop() # Now time to test item stuff. user = create_object(tb_items.TBItemsCharacter, key="User") - tb_items.tickerhandler.remove(interval=30, callback=user.at_update) + tb_items.tickerhandler.remove(interval=30, callback=user.at_update, idstring="update") testroom = create_object(DefaultRoom, key="Test Room") user.location = testroom test_healpotion = create_object(key="healing potion") diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index f367fec269..dca5856fe5 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -497,7 +497,7 @@ class TBItemsCharacter(DefaultCharacter): self.db.hp = self.db.max_hp # Set current HP to maximum self.db.conditions = {} # Set empty dict for conditions # Subscribe character to the ticker handler - tickerhandler.add(NONCOMBAT_TURN_TIME, self.at_update) + tickerhandler.add(NONCOMBAT_TURN_TIME, self.at_update, idstring="update") """ Adds attributes for a character's current and maximum HP. We're just going to set this value at '100' by default. From 2549a8d8e1ff08f936978f5f01e61e6132ef82f8 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Wed, 29 Nov 2017 20:08:22 -0800 Subject: [PATCH 29/58] Comment out tb_items tests for now --- evennia/contrib/tests.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 8d40aeaadb..9091fa65ec 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1316,6 +1316,8 @@ class TestTurnBattleFunc(EvenniaTest): # Spend item use tb_items.spend_item_use(test_healpotion, user) self.assertTrue(test_healpotion.db.item_uses == 2) + # Commenting this stuff out just to make sure it's the problem. + """ # Use item user.db.hp = 2 tb_items.use_item(user, test_healpotion, user) @@ -1340,6 +1342,7 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(user.db.conditions == {}) # Delete the test character user.delete() + """ # Test tree select From 0932a839ba53bb85beb6bdb8b3543237e73b9a33 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Wed, 29 Nov 2017 20:18:54 -0800 Subject: [PATCH 30/58] Also remove ticker handler for 'attacker' and 'defender' Whoops! I forgot that ALL my test characters are getting subscribed to the ticker handler here - maybe that's the problem? --- evennia/contrib/tests.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 9091fa65ec..8de5d61fe9 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1230,7 +1230,9 @@ class TestTurnBattleFunc(EvenniaTest): # Test functions in tb_items. def test_tbitemsfunc(self): attacker = create_object(tb_items.TBItemsCharacter, key="Attacker") + tb_items.tickerhandler.remove(interval=30, callback=attacker.at_update, idstring="update") defender = create_object(tb_items.TBItemsCharacter, key="Defender") + tb_items.tickerhandler.remove(interval=30, callback=defender.at_update, idstring="update") testroom = create_object(DefaultRoom, key="Test Room") attacker.location = testroom defender.loaction = testroom @@ -1316,8 +1318,6 @@ class TestTurnBattleFunc(EvenniaTest): # Spend item use tb_items.spend_item_use(test_healpotion, user) self.assertTrue(test_healpotion.db.item_uses == 2) - # Commenting this stuff out just to make sure it's the problem. - """ # Use item user.db.hp = 2 tb_items.use_item(user, test_healpotion, user) @@ -1342,7 +1342,6 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(user.db.conditions == {}) # Delete the test character user.delete() - """ # Test tree select From a3caaf8c5ffc1b1978f36bd2b3d6909ae3805b9b Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Wed, 29 Nov 2017 20:26:26 -0800 Subject: [PATCH 31/58] Just comment it all out. Travis won't even tell me why it failed this time. --- evennia/contrib/tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 8de5d61fe9..088377380a 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1226,7 +1226,8 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(tb_range.get_range(attacker, defender) == 1) # Remove the script at the end turnhandler.stop() - + + """ # Test functions in tb_items. def test_tbitemsfunc(self): attacker = create_object(tb_items.TBItemsCharacter, key="Attacker") @@ -1342,6 +1343,7 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(user.db.conditions == {}) # Delete the test character user.delete() + """" # Test tree select From 1420c8773e8e345ec60de43570253e269b67da02 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Wed, 29 Nov 2017 20:32:00 -0800 Subject: [PATCH 32/58] Comment it out right this time --- evennia/contrib/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 088377380a..5e8a73bb19 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1227,7 +1227,7 @@ class TestTurnBattleFunc(EvenniaTest): # Remove the script at the end turnhandler.stop() - """ +""" # Test functions in tb_items. def test_tbitemsfunc(self): attacker = create_object(tb_items.TBItemsCharacter, key="Attacker") @@ -1343,7 +1343,7 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(user.db.conditions == {}) # Delete the test character user.delete() - """" +""" # Test tree select From b213f4a2433c0cd7509a7386408d99172fc5bf96 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Sun, 10 Dec 2017 18:50:34 -0800 Subject: [PATCH 33/58] Test character class without tickerhandler --- evennia/contrib/tests.py | 9 ++++----- evennia/contrib/turnbattle/tb_items.py | 12 +++++++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 8d40aeaadb..8af7fe59eb 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1229,8 +1229,8 @@ class TestTurnBattleFunc(EvenniaTest): # Test functions in tb_items. def test_tbitemsfunc(self): - attacker = create_object(tb_items.TBItemsCharacter, key="Attacker") - defender = create_object(tb_items.TBItemsCharacter, key="Defender") + attacker = create_object(tb_items.TBItemsCharacterTest, key="Attacker") + defender = create_object(tb_items.TBItemsCharacterTest, key="Defender") testroom = create_object(DefaultRoom, key="Test Room") attacker.location = testroom defender.loaction = testroom @@ -1297,7 +1297,7 @@ class TestTurnBattleFunc(EvenniaTest): turnhandler.turn_end_check(attacker) self.assertTrue(turnhandler.db.turn == 1) # Join fight - joiner = create_object(tb_items.TBItemsCharacter, key="Joiner") + joiner = create_object(tb_items.TBItemsCharacterTest, key="Joiner") turnhandler.db.fighters = [attacker, defender] turnhandler.db.turn = 0 turnhandler.join_fight(joiner) @@ -1306,8 +1306,7 @@ class TestTurnBattleFunc(EvenniaTest): # Remove the script at the end turnhandler.stop() # Now time to test item stuff. - user = create_object(tb_items.TBItemsCharacter, key="User") - tb_items.tickerhandler.remove(interval=30, callback=user.at_update, idstring="update") + user = create_object(tb_items.TBItemsCharacterTest, key="User") testroom = create_object(DefaultRoom, key="Test Room") user.location = testroom test_healpotion = create_object(key="healing potion") diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index dca5856fe5..a51edbbbdc 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -592,7 +592,17 @@ class TBItemsCharacter(DefaultCharacter): self.apply_turn_conditions() # Tick down condition durations condition_tickdown(self, self) - + +class TBItemsCharacterTest(TBItemsCharacter): + """ + Just like the TBItemsCharacter, but doesn't subscribe to the TickerHandler. + This makes it easier to run unit tests on. + """ + def at_object_creation(self): + self.db.max_hp = 100 # Set maximum HP to 100 + self.db.hp = self.db.max_hp # Set current HP to maximum + self.db.conditions = {} # Set empty dict for conditions + """ ---------------------------------------------------------------------------- From f738427ff3d12de74249e36e0c8686ae3d939ffe Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 10 Dec 2017 18:52:34 -0800 Subject: [PATCH 34/58] Add tickerhandler-free character class for tests --- evennia/contrib/turnbattle/tb_items.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_items.py b/evennia/contrib/turnbattle/tb_items.py index dca5856fe5..d0e9fe8e34 100644 --- a/evennia/contrib/turnbattle/tb_items.py +++ b/evennia/contrib/turnbattle/tb_items.py @@ -592,7 +592,17 @@ class TBItemsCharacter(DefaultCharacter): self.apply_turn_conditions() # Tick down condition durations condition_tickdown(self, self) - + +class TBItemsCharacterTest(TBItemsCharacter): + """ + Just like the TBItemsCharacter, but doesn't subscribe to the TickerHandler. + This makes it easier to run unit tests on. + """ + def at_object_creation(self): + self.db.max_hp = 100 # Set maximum HP to 100 + self.db.hp = self.db.max_hp # Set current HP to maximum + self.db.conditions = {} # Set empty dict for conditions + """ ---------------------------------------------------------------------------- @@ -1384,4 +1394,4 @@ AMULET_OF_WEAKNESS = { "item_func" : "add_condition", "item_selfonly" : True, "item_kwargs" : {"conditions":[("Damage Down", 3), ("Accuracy Down", 3), ("Defense Down", 3)]} -} \ No newline at end of file +} From 52d505b011bbbb46cfcb768e0d18c5a92afdd20f Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 10 Dec 2017 18:53:10 -0800 Subject: [PATCH 35/58] Use test character class instead --- evennia/contrib/tests.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 5e8a73bb19..8af7fe59eb 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1226,14 +1226,11 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(tb_range.get_range(attacker, defender) == 1) # Remove the script at the end turnhandler.stop() - -""" + # Test functions in tb_items. def test_tbitemsfunc(self): - attacker = create_object(tb_items.TBItemsCharacter, key="Attacker") - tb_items.tickerhandler.remove(interval=30, callback=attacker.at_update, idstring="update") - defender = create_object(tb_items.TBItemsCharacter, key="Defender") - tb_items.tickerhandler.remove(interval=30, callback=defender.at_update, idstring="update") + attacker = create_object(tb_items.TBItemsCharacterTest, key="Attacker") + defender = create_object(tb_items.TBItemsCharacterTest, key="Defender") testroom = create_object(DefaultRoom, key="Test Room") attacker.location = testroom defender.loaction = testroom @@ -1300,7 +1297,7 @@ class TestTurnBattleFunc(EvenniaTest): turnhandler.turn_end_check(attacker) self.assertTrue(turnhandler.db.turn == 1) # Join fight - joiner = create_object(tb_items.TBItemsCharacter, key="Joiner") + joiner = create_object(tb_items.TBItemsCharacterTest, key="Joiner") turnhandler.db.fighters = [attacker, defender] turnhandler.db.turn = 0 turnhandler.join_fight(joiner) @@ -1309,8 +1306,7 @@ class TestTurnBattleFunc(EvenniaTest): # Remove the script at the end turnhandler.stop() # Now time to test item stuff. - user = create_object(tb_items.TBItemsCharacter, key="User") - tb_items.tickerhandler.remove(interval=30, callback=user.at_update, idstring="update") + user = create_object(tb_items.TBItemsCharacterTest, key="User") testroom = create_object(DefaultRoom, key="Test Room") user.location = testroom test_healpotion = create_object(key="healing potion") @@ -1343,7 +1339,6 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(user.db.conditions == {}) # Delete the test character user.delete() -""" # Test tree select From 717999f56f5f27db00b5f45756e0de2ececa02b0 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Mon, 11 Dec 2017 14:35:18 -0800 Subject: [PATCH 36/58] Unit tests for tb_magic --- evennia/contrib/tests.py | 96 +++++++++++++++++++++++++- evennia/contrib/turnbattle/tb_magic.py | 8 +++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 8af7fe59eb..5203354fff 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -916,7 +916,7 @@ class TestTutorialWorldRooms(CommandTest): # test turnbattle -from evennia.contrib.turnbattle import tb_basic, tb_equip, tb_range, tb_items +from evennia.contrib.turnbattle import tb_basic, tb_equip, tb_range, tb_items, tb_magic from evennia.objects.objects import DefaultRoom @@ -963,7 +963,7 @@ class TestTurnBattleCmd(CommandTest): self.call(tb_range.CmdRest(), "", "Char rests to recover HP.") # Test item commands - def test_turnbattlecmd(self): + def test_turnbattleitemcmd(self): testitem = create_object(key="test item") testitem.move_to(self.char1) self.call(tb_items.CmdUse(), "item", "'Test item' is not a usable item.") @@ -974,6 +974,18 @@ class TestTurnBattleCmd(CommandTest): self.call(tb_items.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_items.CmdRest(), "", "Char rests to recover HP.") + # Test magic commands + def test_turnbattlemagiccmd(self): + self.call(tb_magic.CmdStatus(), "", "You have 100 / 100 HP and 20 / 20 MP.") + self.call(tb_magic.CmdLearnSpell(), "test spell", "There is no spell with that name.") + self.call(tb_magic.CmdCast(), "", "Usage: cast = , ") + # Also test the commands that are the same in the basic module + self.call(tb_magic.CmdFight(), "", "There's nobody here to fight!") + self.call(tb_magic.CmdAttack(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_magic.CmdPass(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_magic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_magic.CmdRest(), "", "Char rests to recover HP and MP.") + class TestTurnBattleFunc(EvenniaTest): @@ -1339,6 +1351,86 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(user.db.conditions == {}) # Delete the test character user.delete() + + # Test combat functions in tb_magic. + def test_tbbasicfunc(self): + attacker = create_object(tb_magic.TBMagicCharacter, key="Attacker") + defender = create_object(tb_magic.TBMagicCharacter, key="Defender") + testroom = create_object(DefaultRoom, key="Test Room") + attacker.location = testroom + defender.loaction = testroom + # Initiative roll + initiative = tb_magic.roll_init(attacker) + self.assertTrue(initiative >= 0 and initiative <= 1000) + # Attack roll + attack_roll = tb_magic.get_attack(attacker, defender) + self.assertTrue(attack_roll >= 0 and attack_roll <= 100) + # Defense roll + defense_roll = tb_magic.get_defense(attacker, defender) + self.assertTrue(defense_roll == 50) + # Damage roll + damage_roll = tb_magic.get_damage(attacker, defender) + self.assertTrue(damage_roll >= 15 and damage_roll <= 25) + # Apply damage + defender.db.hp = 10 + tb_magic.apply_damage(defender, 3) + self.assertTrue(defender.db.hp == 7) + # Resolve attack + defender.db.hp = 40 + tb_magic.resolve_attack(attacker, defender, attack_value=20, defense_value=10) + self.assertTrue(defender.db.hp < 40) + # Combat cleanup + attacker.db.Combat_attribute = True + tb_magic.combat_cleanup(attacker) + self.assertFalse(attacker.db.combat_attribute) + # Is in combat + self.assertFalse(tb_magic.is_in_combat(attacker)) + # Set up turn handler script for further tests + attacker.location.scripts.add(tb_magic.TBMagicTurnHandler) + turnhandler = attacker.db.combat_TurnHandler + self.assertTrue(attacker.db.combat_TurnHandler) + # Set the turn handler's interval very high to keep it from repeating during tests. + turnhandler.interval = 10000 + # Force turn order + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + # Test is turn + self.assertTrue(tb_magic.is_turn(attacker)) + # Spend actions + attacker.db.Combat_ActionsLeft = 1 + tb_magic.spend_action(attacker, 1, action_name="Test") + self.assertTrue(attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(attacker.db.Combat_LastAction == "Test") + # Initialize for combat + attacker.db.Combat_ActionsLeft = 983 + turnhandler.initialize_for_combat(attacker) + self.assertTrue(attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(attacker.db.Combat_LastAction == "null") + # Start turn + defender.db.Combat_ActionsLeft = 0 + turnhandler.start_turn(defender) + self.assertTrue(defender.db.Combat_ActionsLeft == 1) + # Next turn + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + turnhandler.next_turn() + self.assertTrue(turnhandler.db.turn == 1) + # Turn end check + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + attacker.db.Combat_ActionsLeft = 0 + turnhandler.turn_end_check(attacker) + self.assertTrue(turnhandler.db.turn == 1) + # Join fight + joiner = create_object(tb_magic.TBMagicCharacter, key="Joiner") + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + turnhandler.join_fight(joiner) + self.assertTrue(turnhandler.db.turn == 1) + self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) + # Remove the script at the end + turnhandler.stop() + # Test tree select diff --git a/evennia/contrib/turnbattle/tb_magic.py b/evennia/contrib/turnbattle/tb_magic.py index 6e16cd0d46..01101837b3 100644 --- a/evennia/contrib/turnbattle/tb_magic.py +++ b/evennia/contrib/turnbattle/tb_magic.py @@ -1000,6 +1000,14 @@ class CmdStatus(Command): def func(self): "This performs the actual command." char = self.caller + + if not char.db.max_hp: # Character not initialized, IE in unit tests + char.db.hp = 100 + char.db.max_hp = 100 + char.db.spells_known = [] + char.db.max_mp = 20 + char.db.mp = char.db.max_mp + char.msg("You have %i / %i HP and %i / %i MP." % (char.db.hp, char.db.max_hp, char.db.mp, char.db.max_mp)) class CmdCombatHelp(CmdHelp): From 055bf93ac242a4c1be04642cae3481c66c86b983 Mon Sep 17 00:00:00 2001 From: Tehom Date: Sun, 22 Apr 2018 06:56:27 -0400 Subject: [PATCH 37/58] Fix regex in CommandTest breaking negative numbers --- evennia/commands/default/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index e3715a30c8..6d122b11d6 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -31,7 +31,7 @@ from evennia import DefaultObject, DefaultCharacter # set up signal here since we are not starting the server -_RE = re.compile(r"^\+|-+\+|\+-+|--*|\|(?:\s|$)", re.MULTILINE) +_RE = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE) # ------------------------------------------------------------ From 459a26fd74b8f64ff51e7bbd77fe83b520fe7edd Mon Sep 17 00:00:00 2001 From: Tehom Date: Sun, 22 Apr 2018 07:25:01 -0400 Subject: [PATCH 38/58] Add hyphens to some tests. Probably more to come --- evennia/commands/default/tests.py | 14 +++++++------- evennia/contrib/tests.py | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 6d122b11d6..be889317d5 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -128,7 +128,7 @@ class TestGeneral(CommandTest): self.call(general.CmdPose(), "looks around", "Char looks around") def test_nick(self): - self.call(general.CmdNick(), "testalias = testaliasedstring1", "Inputlinenick 'testalias' mapped to 'testaliasedstring1'.") + self.call(general.CmdNick(), "testalias = testaliasedstring1", "Inputline-nick 'testalias' mapped to 'testaliasedstring1'.") self.call(general.CmdNick(), "/account testalias = testaliasedstring2", "Accountnick 'testalias' mapped to 'testaliasedstring2'.") self.call(general.CmdNick(), "/object testalias = testaliasedstring3", "Objectnick 'testalias' mapped to 'testaliasedstring3'.") self.assertEqual(u"testaliasedstring1", self.char1.nicks.get("testalias")) @@ -190,14 +190,14 @@ class TestAdmin(CommandTest): self.call(admin.CmdWall(), "Test", "Announcing to all connected sessions ...") def test_ban(self): - self.call(admin.CmdBan(), "Char", "NameBan char was added.") + self.call(admin.CmdBan(), "Char", "Name-Ban char was added.") class TestAccount(CommandTest): def test_ooc_look(self): if settings.MULTISESSION_MODE < 2: - self.call(account.CmdOOCLook(), "", "You are outofcharacter (OOC).", caller=self.account) + self.call(account.CmdOOCLook(), "", "You are out-of-character (OOC).", caller=self.account) if settings.MULTISESSION_MODE == 2: self.call(account.CmdOOCLook(), "", "Account TestAccount (you are OutofCharacter)", caller=self.account) @@ -250,8 +250,8 @@ class TestBuilding(CommandTest): def test_attribute_commands(self): self.call(building.CmdSetAttribute(), "Obj/test1=\"value1\"", "Created attribute Obj/test1 = 'value1'") self.call(building.CmdSetAttribute(), "Obj2/test2=\"value2\"", "Created attribute Obj2/test2 = 'value2'") - self.call(building.CmdMvAttr(), "Obj2/test2 = Obj/test3", "Moved Obj2.test2 > Obj.test3") - self.call(building.CmdCpAttr(), "Obj/test1 = Obj2/test3", "Copied Obj.test1 > Obj2.test3") + self.call(building.CmdMvAttr(), "Obj2/test2 = Obj/test3", "Moved Obj2.test2 -> Obj.test3") + self.call(building.CmdCpAttr(), "Obj/test1 = Obj2/test3", "Copied Obj.test1 -> Obj2.test3") self.call(building.CmdWipe(), "Obj2/test2/test3", "Wiped attributes test2,test3 on Obj2.") def test_name(self): @@ -277,7 +277,7 @@ class TestBuilding(CommandTest): def test_exit_commands(self): self.call(building.CmdOpen(), "TestExit1=Room2", "Created new Exit 'TestExit1' from Room to Room2") - self.call(building.CmdLink(), "TestExit1=Room", "Link created TestExit1 > Room (one way).") + self.call(building.CmdLink(), "TestExit1=Room", "Link created TestExit1 -> Room (one way).") self.call(building.CmdUnLink(), "TestExit1", "Former exit TestExit1 no longer links anywhere.") def test_set_home(self): @@ -412,7 +412,7 @@ class TestComms(CommandTest): class TestBatchProcess(CommandTest): def test_batch_commands(self): # cannot test batchcode here, it must run inside the server process - self.call(batchprocess.CmdBatchCommands(), "example_batch_cmds", "Running Batchcommand processor Automatic mode for example_batch_cmds") + self.call(batchprocess.CmdBatchCommands(), "example_batch_cmds", "Running Batch-command processor - Automatic mode for example_batch_cmds") # we make sure to delete the button again here to stop the running reactor confirm = building.CmdDestroy.confirm building.CmdDestroy.confirm = False diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index ff6b01d5cf..3ba56d3410 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -688,7 +688,7 @@ class TestMail(CommandTest): "You have received a new @mail from TestAccount2(account 2)|You sent your message.", caller=self.account2) self.call(mail.CmdMail(), "TestAccount=Message 1", "You sent your message.", caller=self.account2) self.call(mail.CmdMail(), "TestAccount=Message 2", "You sent your message.", caller=self.account2) - self.call(mail.CmdMail(), "", "| ID: From: Subject:", caller=self.account) + self.call(mail.CmdMail(), "", "------------------------------------------------------------------------------| ID: From: Subject:", caller=self.account) self.call(mail.CmdMail(), "2", "From: TestAccount2", caller=self.account) self.call(mail.CmdMail(), "/forward TestAccount2 = 1/Forward message", "You sent your message.|Message forwarded.", caller=self.account) self.call(mail.CmdMail(), "/reply 2=Reply Message2", "You sent your message.", caller=self.account) @@ -714,9 +714,9 @@ class TestMapBuilder(CommandTest): "evennia.contrib.mapbuilder.EXAMPLE2_MAP evennia.contrib.mapbuilder.EXAMPLE2_LEGEND", """Creating Map...|≈ ≈ ≈ ≈ ≈ -≈ ♣♣♣ ≈ +≈ ♣-♣-♣ ≈ ≈ ♣ ♣ ♣ ≈ - ≈ ♣♣♣ ≈ + ≈ ♣-♣-♣ ≈ ≈ ≈ ≈ ≈ ≈ |Creating Landmass...|""") @@ -759,8 +759,8 @@ from evennia.contrib import simpledoor class TestSimpleDoor(CommandTest): def test_cmdopen(self): self.call(simpledoor.CmdOpen(), "newdoor;door:contrib.simpledoor.SimpleDoor,backdoor;door = Room2", - "Created new Exit 'newdoor' from Room to Room2 (aliases: door).|Note: A doortype exit was " - "created ignored eventual custom returnexit type.|Created new Exit 'newdoor' from Room2 to Room (aliases: door).") + "Created new Exit 'newdoor' from Room to Room2 (aliases: door).|Note: A door-type exit was " + "created - ignored eventual custom return-exit type.|Created new Exit 'newdoor' from Room2 to Room (aliases: door).") self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "You close newdoor.", cmdstring="close") self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "newdoor is already closed.", cmdstring="close") self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "You open newdoor.", cmdstring="open") From cb0c2d11f2cf5652ee859d01a959436c22ac72cc Mon Sep 17 00:00:00 2001 From: Tehom Date: Sun, 22 Apr 2018 07:37:14 -0400 Subject: [PATCH 39/58] Add yet more hyphens. --- evennia/commands/default/tests.py | 4 ++-- evennia/contrib/tests.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index be889317d5..a0492d6aab 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -129,8 +129,8 @@ class TestGeneral(CommandTest): def test_nick(self): self.call(general.CmdNick(), "testalias = testaliasedstring1", "Inputline-nick 'testalias' mapped to 'testaliasedstring1'.") - self.call(general.CmdNick(), "/account testalias = testaliasedstring2", "Accountnick 'testalias' mapped to 'testaliasedstring2'.") - self.call(general.CmdNick(), "/object testalias = testaliasedstring3", "Objectnick 'testalias' mapped to 'testaliasedstring3'.") + self.call(general.CmdNick(), "/account testalias = testaliasedstring2", "Account-nick 'testalias' mapped to 'testaliasedstring2'.") + self.call(general.CmdNick(), "/object testalias = testaliasedstring3", "Object-nick 'testalias' mapped to 'testaliasedstring3'.") self.assertEqual(u"testaliasedstring1", self.char1.nicks.get("testalias")) self.assertEqual(None, self.char1.nicks.get("testalias", category="account")) self.assertEqual(u"testaliasedstring2", self.char1.account.nicks.get("testalias", category="account")) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 3ba56d3410..78ecc0f4fb 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -689,7 +689,7 @@ class TestMail(CommandTest): self.call(mail.CmdMail(), "TestAccount=Message 1", "You sent your message.", caller=self.account2) self.call(mail.CmdMail(), "TestAccount=Message 2", "You sent your message.", caller=self.account2) self.call(mail.CmdMail(), "", "------------------------------------------------------------------------------| ID: From: Subject:", caller=self.account) - self.call(mail.CmdMail(), "2", "From: TestAccount2", caller=self.account) + self.call(mail.CmdMail(), "2", "------------------------------------------------------------------------------\nFrom: TestAccount2", caller=self.account) self.call(mail.CmdMail(), "/forward TestAccount2 = 1/Forward message", "You sent your message.|Message forwarded.", caller=self.account) self.call(mail.CmdMail(), "/reply 2=Reply Message2", "You sent your message.", caller=self.account) self.call(mail.CmdMail(), "/delete 2", "Message 2 deleted", caller=self.account) From 80c6d401e5239c7d4452ec5350300a55703337cf Mon Sep 17 00:00:00 2001 From: Tehom Date: Tue, 24 Apr 2018 06:03:01 -0400 Subject: [PATCH 40/58] Call parse_ansi on each returned message before passing to regex to handle individually colored characters. --- evennia/commands/default/tests.py | 9 ++++++--- evennia/contrib/tests.py | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index a0492d6aab..a3be98984a 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -94,8 +94,11 @@ class CommandTest(EvenniaTest): # Get the first element of a tuple if msg received a tuple instead of a string stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg] if msg is not None: - returned_msg = "||".join(_RE.sub("", mess) for mess in stored_msg) - returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip() + # set our separator for returned messages based on parsing ansi or not + msg_sep = "|" if noansi else "||" + # Have to strip ansi for each returned message for the regex to handle it correctly + returned_msg = msg_sep.join(_RE.sub("", ansi.parse_ansi(mess, strip_ansi=noansi)) + for mess in stored_msg).strip() if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()): sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n" sep2 = "\n" + "=" * 30 + "Returned message" + "=" * 32 + "\n" @@ -166,7 +169,7 @@ class TestSystem(CommandTest): self.call(system.CmdPy(), "1+2", ">>> 1+2|3") def test_scripts(self): - self.call(system.CmdScripts(), "", "| dbref |") + self.call(system.CmdScripts(), "", "dbref ") def test_objects(self): self.call(system.CmdObjects(), "", "Object subtype totals") diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 78ecc0f4fb..233327a264 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -688,8 +688,8 @@ class TestMail(CommandTest): "You have received a new @mail from TestAccount2(account 2)|You sent your message.", caller=self.account2) self.call(mail.CmdMail(), "TestAccount=Message 1", "You sent your message.", caller=self.account2) self.call(mail.CmdMail(), "TestAccount=Message 2", "You sent your message.", caller=self.account2) - self.call(mail.CmdMail(), "", "------------------------------------------------------------------------------| ID: From: Subject:", caller=self.account) - self.call(mail.CmdMail(), "2", "------------------------------------------------------------------------------\nFrom: TestAccount2", caller=self.account) + self.call(mail.CmdMail(), "", "| ID: From: Subject:", caller=self.account) + self.call(mail.CmdMail(), "2", "From: TestAccount2", caller=self.account) self.call(mail.CmdMail(), "/forward TestAccount2 = 1/Forward message", "You sent your message.|Message forwarded.", caller=self.account) self.call(mail.CmdMail(), "/reply 2=Reply Message2", "You sent your message.", caller=self.account) self.call(mail.CmdMail(), "/delete 2", "Message 2 deleted", caller=self.account) From e31b9f0d27a1ca56cbea652fe7752bbeb2ea6146 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 25 Apr 2018 21:53:28 +0200 Subject: [PATCH 41/58] Update CODING_STYLE.md --- CODING_STYLE.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CODING_STYLE.md b/CODING_STYLE.md index 79488e94ec..460dfc15d5 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -97,15 +97,15 @@ def funcname(a, b, c, d=False, **kwargs): Args: a (str): This is a string argument that we can talk about over multiple lines. - b (int or str): Another argument - c (list): A list argument - d (bool, optional): An optional keyword argument + b (int or str): Another argument. + c (list): A list argument. + d (bool, optional): An optional keyword argument. Kwargs: - test (list): A test keyword + test (list): A test keyword. Returns: - e (str): The result of the function + e (str): The result of the function. Raises: RuntimeException: If there is a critical error, From b4283a642ba090451e80a4a1d637b60a2cb7ac17 Mon Sep 17 00:00:00 2001 From: Tehom Date: Thu, 10 May 2018 19:23:59 -0400 Subject: [PATCH 42/58] Add special handling for scripts when flushed from cache to avoid duplicate ExtendedLoopingCalls. --- evennia/scripts/scripts.py | 46 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/evennia/scripts/scripts.py b/evennia/scripts/scripts.py index db6e9652cb..eafb87ee72 100644 --- a/evennia/scripts/scripts.py +++ b/evennia/scripts/scripts.py @@ -18,6 +18,17 @@ from future.utils import with_metaclass __all__ = ["DefaultScript", "DoNothing", "Store"] +FLUSHING_INSTANCES = False # whether we're in the process of flushing scripts from the cache +SCRIPT_FLUSH_TIMERS = {} # stores timers for scripts that are currently being flushed + + +def restart_scripts_after_flush(): + """After instances are flushed, validate scripts so they're not dead for a long period of time""" + global FLUSHING_INSTANCES + ScriptDB.objects.validate() + FLUSHING_INSTANCES = False + + class ExtendedLoopingCall(LoopingCall): """ LoopingCall that can start at a delay different @@ -278,6 +289,27 @@ class DefaultScript(ScriptBase): return max(0, self.db_repeats - task.callcount) return None + def at_idmapper_flush(self): + """If we're flushing this object, make sure the LoopingCall is gone too""" + ret = super(DefaultScript, self).at_idmapper_flush() + if ret: + try: + from twisted.internet import reactor + global FLUSHING_INSTANCES + # store the current timers for the _task and stop it to avoid duplicates after cache flush + paused_time = self.ndb._task.next_call_time() + callcount = self.ndb._task.callcount + self._stop_task() + SCRIPT_FLUSH_TIMERS[self.id] = (paused_time, callcount) + # here we ensure that the restart call only happens once, not once per script + if not FLUSHING_INSTANCES: + FLUSHING_INSTANCES = True + reactor.callLater(2, restart_scripts_after_flush) + except Exception: + import traceback + traceback.print_exc() + return ret + def start(self, force_restart=False): """ Called every time the script is started (for persistent @@ -294,9 +326,19 @@ class DefaultScript(ScriptBase): started or not. Used in counting. """ - if self.is_active and not force_restart: - # script already runs and should not be restarted. + # The script is already running, but make sure we have a _task if this is after a cache flush + if not self.ndb._task: + self.ndb._task = ExtendedLoopingCall(self._step_task) + try: + start_delay, callcount = SCRIPT_FLUSH_TIMERS[self.id] + del SCRIPT_FLUSH_TIMERS[self.id] + now = False + except (KeyError, ValueError, TypeError): + now = not self.db_start_delay + start_delay = None + callcount = 0 + self.ndb._task.start(self.db_interval, now=now, start_delay=start_delay, count_start=callcount) return 0 obj = self.obj From f0bae25ca04bdc9ecb7d929204fc5e3c8cc8b363 Mon Sep 17 00:00:00 2001 From: Tehom Date: Thu, 10 May 2018 19:30:58 -0400 Subject: [PATCH 43/58] Fix paused tasks. --- evennia/scripts/scripts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/scripts/scripts.py b/evennia/scripts/scripts.py index eafb87ee72..2cade33281 100644 --- a/evennia/scripts/scripts.py +++ b/evennia/scripts/scripts.py @@ -292,7 +292,7 @@ class DefaultScript(ScriptBase): def at_idmapper_flush(self): """If we're flushing this object, make sure the LoopingCall is gone too""" ret = super(DefaultScript, self).at_idmapper_flush() - if ret: + if ret and self.ndb._task: try: from twisted.internet import reactor global FLUSHING_INSTANCES From af558ede7c535d0104170b4808c3bbe74fffdfcd Mon Sep 17 00:00:00 2001 From: Tehom Date: Thu, 10 May 2018 19:40:58 -0400 Subject: [PATCH 44/58] Handle scripts with negative intervals. --- evennia/scripts/scripts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/scripts/scripts.py b/evennia/scripts/scripts.py index 2cade33281..2b4bea3569 100644 --- a/evennia/scripts/scripts.py +++ b/evennia/scripts/scripts.py @@ -328,7 +328,7 @@ class DefaultScript(ScriptBase): """ if self.is_active and not force_restart: # The script is already running, but make sure we have a _task if this is after a cache flush - if not self.ndb._task: + if not self.ndb._task and self.db_interval >= 0: self.ndb._task = ExtendedLoopingCall(self._step_task) try: start_delay, callcount = SCRIPT_FLUSH_TIMERS[self.id] From bf66bb744038d21fb7e2379d7f9fea4f4f4b1e86 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Mon, 21 May 2018 18:06:29 -0700 Subject: [PATCH 45/58] Proper formatting for comments and notes --- evennia/contrib/turnbattle/tb_magic.py | 33 ++++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_magic.py b/evennia/contrib/turnbattle/tb_magic.py index 01101837b3..7bf87d70be 100644 --- a/evennia/contrib/turnbattle/tb_magic.py +++ b/evennia/contrib/turnbattle/tb_magic.py @@ -326,19 +326,19 @@ class TBMagicCharacter(DefaultCharacter): """ Called once, when this object is first created. This is the normal hook to overload for most object types. - """ - self.db.max_hp = 100 # Set maximum HP to 100 - self.db.hp = self.db.max_hp # Set current HP to maximum - self.db.spells_known = [] # Set empty spells known list - self.db.max_mp = 20 # Set maximum MP to 20 - self.db.mp = self.db.max_mp # Set current MP to maximum - """ + Adds attributes for a character's current and maximum HP. We're just going to set this value at '100' by default. You may want to expand this to include various 'stats' that can be changed at creation and factor into combat calculations. """ + self.db.max_hp = 100 # Set maximum HP to 100 + self.db.hp = self.db.max_hp # Set current HP to maximum + self.db.spells_known = [] # Set empty spells known list + self.db.max_mp = 20 # Set maximum MP to 20 + self.db.mp = self.db.max_mp # Set current MP to maximum + def at_before_move(self, destination): """ @@ -795,6 +795,13 @@ class CmdCast(MuxCommand): def func(self): """ This performs the actual command. + + Note: This is a quite long command, since it has to cope with all + the different circumstances in which you may or may not be able + to cast a spell. None of the spell's effects are handled by the + command - all the command does is verify that the player's input + is valid for the spell being cast and then call the spell's + function. """ caller = self.caller @@ -945,14 +952,6 @@ class CmdCast(MuxCommand): spelldata["spellfunc"](caller, spell_to_cast, spell_targets, spelldata["cost"], **kwargs) except Exception: log_trace("Error in callback for spell: %s." % spell_to_cast) - """ - Note: This is a quite long command, since it has to cope with all - the different circumstances in which you may or may not be able - to cast a spell. None of the spell's effects are handled by the - command - all the command does is verify that the player's input - is valid for the spell being cast and then call the spell's - function. - """ class CmdRest(Command): @@ -979,9 +978,7 @@ class CmdRest(Command): self.caller.db.hp = self.caller.db.max_hp # Set current HP to maximum self.caller.db.mp = self.caller.db.max_mp # Set current MP to maximum self.caller.location.msg_contents("%s rests to recover HP and MP." % self.caller) - """ - You'll probably want to replace this with your own system for recovering HP and MP. - """ + # You'll probably want to replace this with your own system for recovering HP and MP. class CmdStatus(Command): """ From 1125dc92fc4bce36d4489f51606f810f32aca51d Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Mon, 21 May 2018 18:27:04 -0700 Subject: [PATCH 46/58] Start splitting unit tests, add setUp/tearDown --- evennia/contrib/tests.py | 131 +++++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 55 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 5203354fff..2de8f2156e 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -987,87 +987,101 @@ class TestTurnBattleCmd(CommandTest): self.call(tb_magic.CmdRest(), "", "Char rests to recover HP and MP.") -class TestTurnBattleFunc(EvenniaTest): +class TestTurnBattleBasicFunc(EvenniaTest): + def setUp(self): + super(TestTurnBattleBasicFunc, self).setUp() + self.attacker = create_object(tb_basic.TBBasicCharacter, key="Attacker") + self.defender = create_object(tb_basic.TBBasicCharacter, key="Defender") + self.joiner = create_object(tb_basic.TBBasicCharacter, key="Joiner") + self.testroom = create_object(DefaultRoom, key="Test Room") + self.attacker.location = self.testroom + self.defender.loaction = self.testroom + self.joiner.loaction = None + + def tearDown(self): + super(TestTurnBattleBasicFunc, self).tearDown() + self.attacker.delete() + self.defender.delete() + self.joiner.delete() + self.testroom.delete() + self.turnhandler.stop() + # Test combat functions def test_tbbasicfunc(self): - attacker = create_object(tb_basic.TBBasicCharacter, key="Attacker") - defender = create_object(tb_basic.TBBasicCharacter, key="Defender") - testroom = create_object(DefaultRoom, key="Test Room") - attacker.location = testroom - defender.loaction = testroom # Initiative roll - initiative = tb_basic.roll_init(attacker) + initiative = tb_basic.roll_init(self.attacker) self.assertTrue(initiative >= 0 and initiative <= 1000) # Attack roll - attack_roll = tb_basic.get_attack(attacker, defender) + attack_roll = tb_basic.get_attack(self.attacker, self.defender) self.assertTrue(attack_roll >= 0 and attack_roll <= 100) # Defense roll - defense_roll = tb_basic.get_defense(attacker, defender) + defense_roll = tb_basic.get_defense(self.attacker, self.defender) self.assertTrue(defense_roll == 50) # Damage roll - damage_roll = tb_basic.get_damage(attacker, defender) + damage_roll = tb_basic.get_damage(self.attacker, self.defender) self.assertTrue(damage_roll >= 15 and damage_roll <= 25) # Apply damage - defender.db.hp = 10 - tb_basic.apply_damage(defender, 3) - self.assertTrue(defender.db.hp == 7) + self.defender.db.hp = 10 + tb_basic.apply_damage(self.defender, 3) + self.assertTrue(self.defender.db.hp == 7) # Resolve attack - defender.db.hp = 40 - tb_basic.resolve_attack(attacker, defender, attack_value=20, defense_value=10) - self.assertTrue(defender.db.hp < 40) + self.defender.db.hp = 40 + tb_basic.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10) + self.assertTrue(self.defender.db.hp < 40) # Combat cleanup - attacker.db.Combat_attribute = True - tb_basic.combat_cleanup(attacker) - self.assertFalse(attacker.db.combat_attribute) + self.attacker.db.Combat_attribute = True + tb_basic.combat_cleanup(self.attacker) + self.assertFalse(self.attacker.db.combat_attribute) # Is in combat - self.assertFalse(tb_basic.is_in_combat(attacker)) + self.assertFalse(tb_basic.is_in_combat(self.attacker)) # Set up turn handler script for further tests - attacker.location.scripts.add(tb_basic.TBBasicTurnHandler) - turnhandler = attacker.db.combat_TurnHandler - self.assertTrue(attacker.db.combat_TurnHandler) + self.attacker.location.scripts.add(tb_basic.TBBasicTurnHandler) + self.turnhandler = self.attacker.db.combat_TurnHandler + self.assertTrue(self.attacker.db.combat_TurnHandler) # Set the turn handler's interval very high to keep it from repeating during tests. - turnhandler.interval = 10000 + self.turnhandler.interval = 10000 # Force turn order - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 # Test is turn - self.assertTrue(tb_basic.is_turn(attacker)) + self.assertTrue(tb_basic.is_turn(self.attacker)) # Spend actions - attacker.db.Combat_ActionsLeft = 1 - tb_basic.spend_action(attacker, 1, action_name="Test") - self.assertTrue(attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(attacker.db.Combat_LastAction == "Test") + self.attacker.db.Combat_ActionsLeft = 1 + tb_basic.spend_action(self.attacker, 1, action_name="Test") + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "Test") # Initialize for combat - attacker.db.Combat_ActionsLeft = 983 - turnhandler.initialize_for_combat(attacker) - self.assertTrue(attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(attacker.db.Combat_LastAction == "null") + self.attacker.db.Combat_ActionsLeft = 983 + self.turnhandler.initialize_for_combat(self.attacker) + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "null") # Start turn - defender.db.Combat_ActionsLeft = 0 - turnhandler.start_turn(defender) - self.assertTrue(defender.db.Combat_ActionsLeft == 1) + self.defender.db.Combat_ActionsLeft = 0 + self.turnhandler.start_turn(self.defender) + self.assertTrue(self.defender.db.Combat_ActionsLeft == 1) # Next turn - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - turnhandler.next_turn() - self.assertTrue(turnhandler.db.turn == 1) + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.next_turn() + self.assertTrue(self.turnhandler.db.turn == 1) # Turn end check - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - attacker.db.Combat_ActionsLeft = 0 - turnhandler.turn_end_check(attacker) - self.assertTrue(turnhandler.db.turn == 1) + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.attacker.db.Combat_ActionsLeft = 0 + self.turnhandler.turn_end_check(self.attacker) + self.assertTrue(self.turnhandler.db.turn == 1) # Join fight - joiner = create_object(tb_basic.TBBasicCharacter, key="Joiner") - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - turnhandler.join_fight(joiner) - self.assertTrue(turnhandler.db.turn == 1) - self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) - # Remove the script at the end - turnhandler.stop() + self.joiner.location = self.testroom + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.join_fight(self.joiner) + self.assertTrue(self.turnhandler.db.turn == 1) + self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) + +class TestTurnBattleEquipFunc(EvenniaTest): + # Test the combat functions in tb_equip too. They work mostly the same. def test_tbequipfunc(self): attacker = create_object(tb_equip.TBEquipCharacter, key="Attacker") @@ -1147,6 +1161,8 @@ class TestTurnBattleFunc(EvenniaTest): # Remove the script at the end turnhandler.stop() +class TestTurnBattleRangeFunc(EvenniaTest): + # Test combat functions in tb_range too. def test_tbrangefunc(self): testroom = create_object(DefaultRoom, key="Test Room") @@ -1239,7 +1255,10 @@ class TestTurnBattleFunc(EvenniaTest): # Remove the script at the end turnhandler.stop() +class TestTurnBattleItemsFunc(EvenniaTest): + # Test functions in tb_items. + def test_tbitemsfunc(self): attacker = create_object(tb_items.TBItemsCharacterTest, key="Attacker") defender = create_object(tb_items.TBItemsCharacterTest, key="Defender") @@ -1352,6 +1371,8 @@ class TestTurnBattleFunc(EvenniaTest): # Delete the test character user.delete() +class TestTurnBattleMagicFunc(EvenniaTest): + # Test combat functions in tb_magic. def test_tbbasicfunc(self): attacker = create_object(tb_magic.TBMagicCharacter, key="Attacker") From 17cedbfde7ad80b355ec07d102a1ef4fe8e840e8 Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Mon, 21 May 2018 18:45:07 -0700 Subject: [PATCH 47/58] More setUp/tearDown --- evennia/contrib/tests.py | 118 +++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 54 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 2de8f2156e..721fd43801 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1081,85 +1081,95 @@ class TestTurnBattleBasicFunc(EvenniaTest): class TestTurnBattleEquipFunc(EvenniaTest): + + def setUp(self): + super(TestTurnBattleEquipFunc, self).setUp() + self.attacker = create_object(tb_equip.TBEquipCharacter, key="Attacker") + self.defender = create_object(tb_equip.TBEquipCharacter, key="Defender") + self.testroom = create_object(DefaultRoom, key="Test Room") + self.joiner = create_object(tb_equip.TBEquipCharacter, key="Joiner") + self.attacker.location = self.testroom + self.defender.loaction = self.testroom + self.joiner.loaction = None + + def tearDown(self): + super(TestTurnBattleEquipFunc, self).tearDown() + self.attacker.delete() + self.defender.delete() + self.joiner.delete() + self.testroom.delete() + self.turnhandler.stop() # Test the combat functions in tb_equip too. They work mostly the same. def test_tbequipfunc(self): - attacker = create_object(tb_equip.TBEquipCharacter, key="Attacker") - defender = create_object(tb_equip.TBEquipCharacter, key="Defender") - testroom = create_object(DefaultRoom, key="Test Room") - attacker.location = testroom - defender.loaction = testroom # Initiative roll - initiative = tb_equip.roll_init(attacker) + initiative = tb_equip.roll_init(self.attacker) self.assertTrue(initiative >= 0 and initiative <= 1000) # Attack roll - attack_roll = tb_equip.get_attack(attacker, defender) + attack_roll = tb_equip.get_attack(self.attacker, self.defender) self.assertTrue(attack_roll >= -50 and attack_roll <= 150) # Defense roll - defense_roll = tb_equip.get_defense(attacker, defender) + defense_roll = tb_equip.get_defense(self.attacker, self.defender) self.assertTrue(defense_roll == 50) # Damage roll - damage_roll = tb_equip.get_damage(attacker, defender) + damage_roll = tb_equip.get_damage(self.attacker, self.defender) self.assertTrue(damage_roll >= 0 and damage_roll <= 50) # Apply damage - defender.db.hp = 10 - tb_equip.apply_damage(defender, 3) - self.assertTrue(defender.db.hp == 7) + self.defender.db.hp = 10 + tb_equip.apply_damage(self.defender, 3) + self.assertTrue(self.defender.db.hp == 7) # Resolve attack - defender.db.hp = 40 - tb_equip.resolve_attack(attacker, defender, attack_value=20, defense_value=10) - self.assertTrue(defender.db.hp < 40) + self.defender.db.hp = 40 + tb_equip.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10) + self.assertTrue(self.defender.db.hp < 40) # Combat cleanup - attacker.db.Combat_attribute = True - tb_equip.combat_cleanup(attacker) - self.assertFalse(attacker.db.combat_attribute) + self.attacker.db.Combat_attribute = True + tb_equip.combat_cleanup(self.attacker) + self.assertFalse(self.attacker.db.combat_attribute) # Is in combat - self.assertFalse(tb_equip.is_in_combat(attacker)) + self.assertFalse(tb_equip.is_in_combat(self.attacker)) # Set up turn handler script for further tests - attacker.location.scripts.add(tb_equip.TBEquipTurnHandler) - turnhandler = attacker.db.combat_TurnHandler - self.assertTrue(attacker.db.combat_TurnHandler) + self.attacker.location.scripts.add(tb_equip.TBEquipTurnHandler) + self.turnhandler = self.attacker.db.combat_TurnHandler + self.assertTrue(self.attacker.db.combat_TurnHandler) # Set the turn handler's interval very high to keep it from repeating during tests. - turnhandler.interval = 10000 + self.turnhandler.interval = 10000 # Force turn order - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 # Test is turn - self.assertTrue(tb_equip.is_turn(attacker)) + self.assertTrue(tb_equip.is_turn(self.attacker)) # Spend actions - attacker.db.Combat_ActionsLeft = 1 - tb_equip.spend_action(attacker, 1, action_name="Test") - self.assertTrue(attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(attacker.db.Combat_LastAction == "Test") + self.attacker.db.Combat_ActionsLeft = 1 + tb_equip.spend_action(self.attacker, 1, action_name="Test") + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "Test") # Initialize for combat - attacker.db.Combat_ActionsLeft = 983 - turnhandler.initialize_for_combat(attacker) - self.assertTrue(attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(attacker.db.Combat_LastAction == "null") + self.attacker.db.Combat_ActionsLeft = 983 + self.turnhandler.initialize_for_combat(self.attacker) + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "null") # Start turn - defender.db.Combat_ActionsLeft = 0 - turnhandler.start_turn(defender) - self.assertTrue(defender.db.Combat_ActionsLeft == 1) + self.defender.db.Combat_ActionsLeft = 0 + self.turnhandler.start_turn(self.defender) + self.assertTrue(self.defender.db.Combat_ActionsLeft == 1) # Next turn - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - turnhandler.next_turn() - self.assertTrue(turnhandler.db.turn == 1) + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.next_turn() + self.assertTrue(self.turnhandler.db.turn == 1) # Turn end check - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - attacker.db.Combat_ActionsLeft = 0 - turnhandler.turn_end_check(attacker) - self.assertTrue(turnhandler.db.turn == 1) + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.attacker.db.Combat_ActionsLeft = 0 + self.turnhandler.turn_end_check(self.attacker) + self.assertTrue(self.turnhandler.db.turn == 1) # Join fight - joiner = create_object(tb_equip.TBEquipCharacter, key="Joiner") - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - turnhandler.join_fight(joiner) - self.assertTrue(turnhandler.db.turn == 1) - self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) - # Remove the script at the end - turnhandler.stop() + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.join_fight(self.joiner) + self.assertTrue(self.turnhandler.db.turn == 1) + self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) class TestTurnBattleRangeFunc(EvenniaTest): From a97383bc38ed7d94be72dc8ed1ea204dc93fdc7f Mon Sep 17 00:00:00 2001 From: BattleJenkins Date: Mon, 21 May 2018 19:27:43 -0700 Subject: [PATCH 48/58] Finish splitting TB test classes + adding setUp/tearDown --- evennia/contrib/tests.py | 465 +++++++++++++++++++++------------------ 1 file changed, 246 insertions(+), 219 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 721fd43801..c0721b3ed6 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -920,23 +920,28 @@ from evennia.contrib.turnbattle import tb_basic, tb_equip, tb_range, tb_items, t from evennia.objects.objects import DefaultRoom -class TestTurnBattleCmd(CommandTest): +class TestTurnBattleBasicCmd(CommandTest): - # Test combat commands + # Test basic combat commands def test_turnbattlecmd(self): self.call(tb_basic.CmdFight(), "", "You can't start a fight if you've been defeated!") self.call(tb_basic.CmdAttack(), "", "You can only do that in combat. (see: help fight)") self.call(tb_basic.CmdPass(), "", "You can only do that in combat. (see: help fight)") self.call(tb_basic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_basic.CmdRest(), "", "Char rests to recover HP.") - + +class TestTurnBattleEquipCmd(CommandTest): + + def setUp(self): + super(TestTurnBattleEquipCmd, self).setUp() + self.testweapon = create_object(tb_equip.TBEWeapon, key="test weapon") + self.testarmor = create_object(tb_equip.TBEArmor, key="test armor") + self.testweapon.move_to(self.char1) + self.testarmor.move_to(self.char1) + # Test equipment commands def test_turnbattleequipcmd(self): # Start with equip module specific commands. - testweapon = create_object(tb_equip.TBEWeapon, key="test weapon") - testarmor = create_object(tb_equip.TBEArmor, key="test armor") - testweapon.move_to(self.char1) - testarmor.move_to(self.char1) self.call(tb_equip.CmdWield(), "weapon", "Char wields test weapon.") self.call(tb_equip.CmdUnwield(), "", "Char lowers test weapon.") self.call(tb_equip.CmdDon(), "armor", "Char dons test armor.") @@ -947,6 +952,8 @@ class TestTurnBattleCmd(CommandTest): self.call(tb_equip.CmdPass(), "", "You can only do that in combat. (see: help fight)") self.call(tb_equip.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.") + +class TestTurnBattleRangeCmd(CommandTest): # Test range commands def test_turnbattlerangecmd(self): @@ -961,11 +968,16 @@ class TestTurnBattleCmd(CommandTest): self.call(tb_range.CmdPass(), "", "You can only do that in combat. (see: help fight)") self.call(tb_range.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_range.CmdRest(), "", "Char rests to recover HP.") - + +class TestTurnBattleItemsCmd(CommandTest): + + def setUp(self): + super(TestTurnBattleItemsCmd, self).setUp() + self.testitem = create_object(key="test item") + self.testitem.move_to(self.char1) + # Test item commands def test_turnbattleitemcmd(self): - testitem = create_object(key="test item") - testitem.move_to(self.char1) self.call(tb_items.CmdUse(), "item", "'Test item' is not a usable item.") # Also test the commands that are the same in the basic module self.call(tb_items.CmdFight(), "", "You can't start a fight if you've been defeated!") @@ -974,6 +986,8 @@ class TestTurnBattleCmd(CommandTest): self.call(tb_items.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_items.CmdRest(), "", "Char rests to recover HP.") +class TestTurnBattleMagicCmd(CommandTest): + # Test magic commands def test_turnbattlemagiccmd(self): self.call(tb_magic.CmdStatus(), "", "You have 100 / 100 HP and 20 / 20 MP.") @@ -991,13 +1005,10 @@ class TestTurnBattleBasicFunc(EvenniaTest): def setUp(self): super(TestTurnBattleBasicFunc, self).setUp() - self.attacker = create_object(tb_basic.TBBasicCharacter, key="Attacker") - self.defender = create_object(tb_basic.TBBasicCharacter, key="Defender") - self.joiner = create_object(tb_basic.TBBasicCharacter, key="Joiner") self.testroom = create_object(DefaultRoom, key="Test Room") - self.attacker.location = self.testroom - self.defender.loaction = self.testroom - self.joiner.loaction = None + self.attacker = create_object(tb_basic.TBBasicCharacter, key="Attacker", location=self.testroom) + self.defender = create_object(tb_basic.TBBasicCharacter, key="Defender", location=self.testroom) + self.joiner = create_object(tb_basic.TBBasicCharacter, key="Joiner", location=None) def tearDown(self): super(TestTurnBattleBasicFunc, self).tearDown() @@ -1084,13 +1095,10 @@ class TestTurnBattleEquipFunc(EvenniaTest): def setUp(self): super(TestTurnBattleEquipFunc, self).setUp() - self.attacker = create_object(tb_equip.TBEquipCharacter, key="Attacker") - self.defender = create_object(tb_equip.TBEquipCharacter, key="Defender") self.testroom = create_object(DefaultRoom, key="Test Room") - self.joiner = create_object(tb_equip.TBEquipCharacter, key="Joiner") - self.attacker.location = self.testroom - self.defender.loaction = self.testroom - self.joiner.loaction = None + self.attacker = create_object(tb_equip.TBEquipCharacter, key="Attacker", location=self.testroom) + self.defender = create_object(tb_equip.TBEquipCharacter, key="Defender", location=self.testroom) + self.joiner = create_object(tb_equip.TBEquipCharacter, key="Joiner", location=None) def tearDown(self): super(TestTurnBattleEquipFunc, self).tearDown() @@ -1173,294 +1181,313 @@ class TestTurnBattleEquipFunc(EvenniaTest): class TestTurnBattleRangeFunc(EvenniaTest): + def setUp(self): + super(TestTurnBattleRangeFunc, self).setUp() + self.testroom = create_object(DefaultRoom, key="Test Room") + self.attacker = create_object(tb_range.TBRangeCharacter, key="Attacker", location=self.testroom) + self.defender = create_object(tb_range.TBRangeCharacter, key="Defender", location=self.testroom) + self.joiner = create_object(tb_range.TBRangeCharacter, key="Joiner", location=self.testroom) + + def tearDown(self): + super(TestTurnBattleRangeFunc, self).tearDown() + self.attacker.delete() + self.defender.delete() + self.joiner.delete() + self.testroom.delete() + self.turnhandler.stop() + # Test combat functions in tb_range too. def test_tbrangefunc(self): - testroom = create_object(DefaultRoom, key="Test Room") - attacker = create_object(tb_range.TBRangeCharacter, key="Attacker", location=testroom) - defender = create_object(tb_range.TBRangeCharacter, key="Defender", location=testroom) # Initiative roll - initiative = tb_range.roll_init(attacker) + initiative = tb_range.roll_init(self.attacker) self.assertTrue(initiative >= 0 and initiative <= 1000) # Attack roll - attack_roll = tb_range.get_attack(attacker, defender, "test") + attack_roll = tb_range.get_attack(self.attacker, self.defender, "test") self.assertTrue(attack_roll >= 0 and attack_roll <= 100) # Defense roll - defense_roll = tb_range.get_defense(attacker, defender, "test") + defense_roll = tb_range.get_defense(self.attacker, self.defender, "test") self.assertTrue(defense_roll == 50) # Damage roll - damage_roll = tb_range.get_damage(attacker, defender) + damage_roll = tb_range.get_damage(self.attacker, self.defender) self.assertTrue(damage_roll >= 15 and damage_roll <= 25) # Apply damage - defender.db.hp = 10 - tb_range.apply_damage(defender, 3) - self.assertTrue(defender.db.hp == 7) + self.defender.db.hp = 10 + tb_range.apply_damage(self.defender, 3) + self.assertTrue(self.defender.db.hp == 7) # Resolve attack - defender.db.hp = 40 - tb_range.resolve_attack(attacker, defender, "test", attack_value=20, defense_value=10) - self.assertTrue(defender.db.hp < 40) + self.defender.db.hp = 40 + tb_range.resolve_attack(self.attacker, self.defender, "test", attack_value=20, defense_value=10) + self.assertTrue(self.defender.db.hp < 40) # Combat cleanup - attacker.db.Combat_attribute = True - tb_range.combat_cleanup(attacker) - self.assertFalse(attacker.db.combat_attribute) + self.attacker.db.Combat_attribute = True + tb_range.combat_cleanup(self.attacker) + self.assertFalse(self.attacker.db.combat_attribute) # Is in combat - self.assertFalse(tb_range.is_in_combat(attacker)) + self.assertFalse(tb_range.is_in_combat(self.attacker)) # Set up turn handler script for further tests - attacker.location.scripts.add(tb_range.TBRangeTurnHandler) - turnhandler = attacker.db.combat_TurnHandler - self.assertTrue(attacker.db.combat_TurnHandler) + self.attacker.location.scripts.add(tb_range.TBRangeTurnHandler) + self.turnhandler = self.attacker.db.combat_TurnHandler + self.assertTrue(self.attacker.db.combat_TurnHandler) # Set the turn handler's interval very high to keep it from repeating during tests. - turnhandler.interval = 10000 + self.turnhandler.interval = 10000 # Force turn order - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 # Test is turn - self.assertTrue(tb_range.is_turn(attacker)) + self.assertTrue(tb_range.is_turn(self.attacker)) # Spend actions - attacker.db.Combat_ActionsLeft = 1 - tb_range.spend_action(attacker, 1, action_name="Test") - self.assertTrue(attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(attacker.db.Combat_LastAction == "Test") + self.attacker.db.Combat_ActionsLeft = 1 + tb_range.spend_action(self.attacker, 1, action_name="Test") + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "Test") # Initialize for combat - attacker.db.Combat_ActionsLeft = 983 - turnhandler.initialize_for_combat(attacker) - self.assertTrue(attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(attacker.db.Combat_LastAction == "null") + self.attacker.db.Combat_ActionsLeft = 983 + self.turnhandler.initialize_for_combat(self.attacker) + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "null") # Set up ranges again, since initialize_for_combat clears them - attacker.db.combat_range = {} - attacker.db.combat_range[attacker] = 0 - attacker.db.combat_range[defender] = 1 - defender.db.combat_range = {} - defender.db.combat_range[defender] = 0 - defender.db.combat_range[attacker] = 1 + self.attacker.db.combat_range = {} + self.attacker.db.combat_range[self.attacker] = 0 + self.attacker.db.combat_range[self.defender] = 1 + self.defender.db.combat_range = {} + self.defender.db.combat_range[self.defender] = 0 + self.defender.db.combat_range[self.attacker] = 1 # Start turn - defender.db.Combat_ActionsLeft = 0 - turnhandler.start_turn(defender) - self.assertTrue(defender.db.Combat_ActionsLeft == 2) + self.defender.db.Combat_ActionsLeft = 0 + self.turnhandler.start_turn(self.defender) + self.assertTrue(self.defender.db.Combat_ActionsLeft == 2) # Next turn - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - turnhandler.next_turn() - self.assertTrue(turnhandler.db.turn == 1) + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.next_turn() + self.assertTrue(self.turnhandler.db.turn == 1) # Turn end check - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - attacker.db.Combat_ActionsLeft = 0 - turnhandler.turn_end_check(attacker) - self.assertTrue(turnhandler.db.turn == 1) + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.attacker.db.Combat_ActionsLeft = 0 + self.turnhandler.turn_end_check(self.attacker) + self.assertTrue(self.turnhandler.db.turn == 1) # Join fight - joiner = create_object(tb_range.TBRangeCharacter, key="Joiner", location=testroom) - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - turnhandler.join_fight(joiner) - self.assertTrue(turnhandler.db.turn == 1) - self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.join_fight(self.joiner) + self.assertTrue(self.turnhandler.db.turn == 1) + self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) # Now, test for approach/withdraw functions - self.assertTrue(tb_range.get_range(attacker, defender) == 1) + self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 1) # Approach - tb_range.approach(attacker, defender) - self.assertTrue(tb_range.get_range(attacker, defender) == 0) + tb_range.approach(self.attacker, self.defender) + self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 0) # Withdraw - tb_range.withdraw(attacker, defender) - self.assertTrue(tb_range.get_range(attacker, defender) == 1) - # Remove the script at the end - turnhandler.stop() + tb_range.withdraw(self.attacker, self.defender) + self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 1) class TestTurnBattleItemsFunc(EvenniaTest): + def setUp(self): + super(TestTurnBattleItemsFunc, self).setUp() + self.testroom = create_object(DefaultRoom, key="Test Room") + self.attacker = create_object(tb_items.TBItemsCharacter, key="Attacker", location=self.testroom) + self.defender = create_object(tb_items.TBItemsCharacter, key="Defender", location=self.testroom) + self.joiner = create_object(tb_items.TBItemsCharacter, key="Joiner", location=self.testroom) + self.user = create_object(tb_items.TBItemsCharacter, key="User", location=self.testroom) + self.test_healpotion = create_object(key="healing potion") + self.test_healpotion.db.item_func = "heal" + self.test_healpotion.db.item_uses = 3 + + def tearDown(self): + super(TestTurnBattleItemsFunc, self).tearDown() + self.attacker.delete() + self.defender.delete() + self.joiner.delete() + self.user.delete() + self.testroom.delete() + self.turnhandler.stop() + # Test functions in tb_items. - def test_tbitemsfunc(self): - attacker = create_object(tb_items.TBItemsCharacterTest, key="Attacker") - defender = create_object(tb_items.TBItemsCharacterTest, key="Defender") - testroom = create_object(DefaultRoom, key="Test Room") - attacker.location = testroom - defender.loaction = testroom # Initiative roll - initiative = tb_items.roll_init(attacker) + initiative = tb_items.roll_init(self.attacker) self.assertTrue(initiative >= 0 and initiative <= 1000) # Attack roll - attack_roll = tb_items.get_attack(attacker, defender) + attack_roll = tb_items.get_attack(self.attacker, self.defender) self.assertTrue(attack_roll >= 0 and attack_roll <= 100) # Defense roll - defense_roll = tb_items.get_defense(attacker, defender) + defense_roll = tb_items.get_defense(self.attacker, self.defender) self.assertTrue(defense_roll == 50) # Damage roll - damage_roll = tb_items.get_damage(attacker, defender) + damage_roll = tb_items.get_damage(self.attacker, self.defender) self.assertTrue(damage_roll >= 15 and damage_roll <= 25) # Apply damage - defender.db.hp = 10 - tb_items.apply_damage(defender, 3) - self.assertTrue(defender.db.hp == 7) + self.defender.db.hp = 10 + tb_items.apply_damage(self.defender, 3) + self.assertTrue(self.defender.db.hp == 7) # Resolve attack - defender.db.hp = 40 - tb_items.resolve_attack(attacker, defender, attack_value=20, defense_value=10) - self.assertTrue(defender.db.hp < 40) + self.defender.db.hp = 40 + tb_items.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10) + self.assertTrue(self.defender.db.hp < 40) # Combat cleanup - attacker.db.Combat_attribute = True - tb_items.combat_cleanup(attacker) - self.assertFalse(attacker.db.combat_attribute) + self.attacker.db.Combat_attribute = True + tb_items.combat_cleanup(self.attacker) + self.assertFalse(self.attacker.db.combat_attribute) # Is in combat - self.assertFalse(tb_items.is_in_combat(attacker)) + self.assertFalse(tb_items.is_in_combat(self.attacker)) # Set up turn handler script for further tests - attacker.location.scripts.add(tb_items.TBItemsTurnHandler) - turnhandler = attacker.db.combat_TurnHandler - self.assertTrue(attacker.db.combat_TurnHandler) + self.attacker.location.scripts.add(tb_items.TBItemsTurnHandler) + self.turnhandler = self.attacker.db.combat_TurnHandler + self.assertTrue(self.attacker.db.combat_TurnHandler) # Set the turn handler's interval very high to keep it from repeating during tests. - turnhandler.interval = 10000 + self.turnhandler.interval = 10000 # Force turn order - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 # Test is turn - self.assertTrue(tb_items.is_turn(attacker)) + self.assertTrue(tb_items.is_turn(self.attacker)) # Spend actions - attacker.db.Combat_ActionsLeft = 1 - tb_items.spend_action(attacker, 1, action_name="Test") - self.assertTrue(attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(attacker.db.Combat_LastAction == "Test") + self.attacker.db.Combat_ActionsLeft = 1 + tb_items.spend_action(self.attacker, 1, action_name="Test") + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "Test") # Initialize for combat - attacker.db.Combat_ActionsLeft = 983 - turnhandler.initialize_for_combat(attacker) - self.assertTrue(attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(attacker.db.Combat_LastAction == "null") + self.attacker.db.Combat_ActionsLeft = 983 + self.turnhandler.initialize_for_combat(self.attacker) + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "null") # Start turn - defender.db.Combat_ActionsLeft = 0 - turnhandler.start_turn(defender) - self.assertTrue(defender.db.Combat_ActionsLeft == 1) + self.defender.db.Combat_ActionsLeft = 0 + self.turnhandler.start_turn(self.defender) + self.assertTrue(self.defender.db.Combat_ActionsLeft == 1) # Next turn - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - turnhandler.next_turn() - self.assertTrue(turnhandler.db.turn == 1) + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.next_turn() + self.assertTrue(self.turnhandler.db.turn == 1) # Turn end check - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - attacker.db.Combat_ActionsLeft = 0 - turnhandler.turn_end_check(attacker) - self.assertTrue(turnhandler.db.turn == 1) + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.attacker.db.Combat_ActionsLeft = 0 + self.turnhandler.turn_end_check(self.attacker) + self.assertTrue(self.turnhandler.db.turn == 1) # Join fight - joiner = create_object(tb_items.TBItemsCharacterTest, key="Joiner") - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - turnhandler.join_fight(joiner) - self.assertTrue(turnhandler.db.turn == 1) - self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) - # Remove the script at the end - turnhandler.stop() + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.join_fight(self.joiner) + self.assertTrue(self.turnhandler.db.turn == 1) + self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) # Now time to test item stuff. - user = create_object(tb_items.TBItemsCharacterTest, key="User") - testroom = create_object(DefaultRoom, key="Test Room") - user.location = testroom - test_healpotion = create_object(key="healing potion") - test_healpotion.db.item_func = "heal" - test_healpotion.db.item_uses = 3 # Spend item use - tb_items.spend_item_use(test_healpotion, user) - self.assertTrue(test_healpotion.db.item_uses == 2) + tb_items.spend_item_use(self.test_healpotion, self.user) + self.assertTrue(self.test_healpotion.db.item_uses == 2) # Use item - user.db.hp = 2 - tb_items.use_item(user, test_healpotion, user) - self.assertTrue(user.db.hp > 2) + self.user.db.hp = 2 + tb_items.use_item(self.user, self.test_healpotion, self.user) + self.assertTrue(self.user.db.hp > 2) # Add contition - tb_items.add_condition(user, user, "Test", 5) - self.assertTrue(user.db.conditions == {"Test":[5, user]}) + tb_items.add_condition(self.user, self.user, "Test", 5) + self.assertTrue(self.user.db.conditions == {"Test":[5, self.user]}) # Condition tickdown - tb_items.condition_tickdown(user, user) - self.assertTrue(user.db.conditions == {"Test":[4, user]}) + tb_items.condition_tickdown(self.user, self.user) + self.assertTrue(self.user.db.conditions == {"Test":[4, self.user]}) # Test item functions now! # Item heal - user.db.hp = 2 - tb_items.itemfunc_heal(test_healpotion, user, user) + self.user.db.hp = 2 + tb_items.itemfunc_heal(self.test_healpotion, self.user, self.user) # Item add condition - user.db.conditions = {} - tb_items.itemfunc_add_condition(test_healpotion, user, user) - self.assertTrue(user.db.conditions == {"Regeneration":[5, user]}) + self.user.db.conditions = {} + tb_items.itemfunc_add_condition(self.test_healpotion, self.user, self.user) + self.assertTrue(self.user.db.conditions == {"Regeneration":[5, self.user]}) # Item cure condition - user.db.conditions = {"Poisoned":[5, user]} - tb_items.itemfunc_cure_condition(test_healpotion, user, user) - self.assertTrue(user.db.conditions == {}) - # Delete the test character - user.delete() + self.user.db.conditions = {"Poisoned":[5, self.user]} + tb_items.itemfunc_cure_condition(self.test_healpotion, self.user, self.user) + self.assertTrue(self.user.db.conditions == {}) class TestTurnBattleMagicFunc(EvenniaTest): + + def setUp(self): + super(TestTurnBattleMagicFunc, self).setUp() + self.testroom = create_object(DefaultRoom, key="Test Room") + self.attacker = create_object(tb_magic.TBMagicCharacter, key="Attacker", location=self.testroom) + self.defender = create_object(tb_magic.TBMagicCharacter, key="Defender", location=self.testroom) + self.joiner = create_object(tb_magic.TBMagicCharacter, key="Joiner", location=self.testroom) + def tearDown(self): + super(TestTurnBattleMagicFunc, self).tearDown() + self.attacker.delete() + self.defender.delete() + self.joiner.delete() + self.testroom.delete() + self.turnhandler.stop() + # Test combat functions in tb_magic. def test_tbbasicfunc(self): - attacker = create_object(tb_magic.TBMagicCharacter, key="Attacker") - defender = create_object(tb_magic.TBMagicCharacter, key="Defender") - testroom = create_object(DefaultRoom, key="Test Room") - attacker.location = testroom - defender.loaction = testroom # Initiative roll - initiative = tb_magic.roll_init(attacker) + initiative = tb_magic.roll_init(self.attacker) self.assertTrue(initiative >= 0 and initiative <= 1000) # Attack roll - attack_roll = tb_magic.get_attack(attacker, defender) + attack_roll = tb_magic.get_attack(self.attacker, self.defender) self.assertTrue(attack_roll >= 0 and attack_roll <= 100) # Defense roll - defense_roll = tb_magic.get_defense(attacker, defender) + defense_roll = tb_magic.get_defense(self.attacker, self.defender) self.assertTrue(defense_roll == 50) # Damage roll - damage_roll = tb_magic.get_damage(attacker, defender) + damage_roll = tb_magic.get_damage(self.attacker, self.defender) self.assertTrue(damage_roll >= 15 and damage_roll <= 25) # Apply damage - defender.db.hp = 10 - tb_magic.apply_damage(defender, 3) - self.assertTrue(defender.db.hp == 7) + self.defender.db.hp = 10 + tb_magic.apply_damage(self.defender, 3) + self.assertTrue(self.defender.db.hp == 7) # Resolve attack - defender.db.hp = 40 - tb_magic.resolve_attack(attacker, defender, attack_value=20, defense_value=10) - self.assertTrue(defender.db.hp < 40) + self.defender.db.hp = 40 + tb_magic.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10) + self.assertTrue(self.defender.db.hp < 40) # Combat cleanup - attacker.db.Combat_attribute = True - tb_magic.combat_cleanup(attacker) - self.assertFalse(attacker.db.combat_attribute) + self.attacker.db.Combat_attribute = True + tb_magic.combat_cleanup(self.attacker) + self.assertFalse(self.attacker.db.combat_attribute) # Is in combat - self.assertFalse(tb_magic.is_in_combat(attacker)) + self.assertFalse(tb_magic.is_in_combat(self.attacker)) # Set up turn handler script for further tests - attacker.location.scripts.add(tb_magic.TBMagicTurnHandler) - turnhandler = attacker.db.combat_TurnHandler - self.assertTrue(attacker.db.combat_TurnHandler) + self.attacker.location.scripts.add(tb_magic.TBMagicTurnHandler) + self.turnhandler = self.attacker.db.combat_TurnHandler + self.assertTrue(self.attacker.db.combat_TurnHandler) # Set the turn handler's interval very high to keep it from repeating during tests. - turnhandler.interval = 10000 + self.turnhandler.interval = 10000 # Force turn order - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 # Test is turn - self.assertTrue(tb_magic.is_turn(attacker)) + self.assertTrue(tb_magic.is_turn(self.attacker)) # Spend actions - attacker.db.Combat_ActionsLeft = 1 - tb_magic.spend_action(attacker, 1, action_name="Test") - self.assertTrue(attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(attacker.db.Combat_LastAction == "Test") + self.attacker.db.Combat_ActionsLeft = 1 + tb_magic.spend_action(self.attacker, 1, action_name="Test") + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "Test") # Initialize for combat - attacker.db.Combat_ActionsLeft = 983 - turnhandler.initialize_for_combat(attacker) - self.assertTrue(attacker.db.Combat_ActionsLeft == 0) - self.assertTrue(attacker.db.Combat_LastAction == "null") + self.attacker.db.Combat_ActionsLeft = 983 + self.turnhandler.initialize_for_combat(self.attacker) + self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(self.attacker.db.Combat_LastAction == "null") # Start turn - defender.db.Combat_ActionsLeft = 0 - turnhandler.start_turn(defender) - self.assertTrue(defender.db.Combat_ActionsLeft == 1) + self.defender.db.Combat_ActionsLeft = 0 + self.turnhandler.start_turn(self.defender) + self.assertTrue(self.defender.db.Combat_ActionsLeft == 1) # Next turn - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - turnhandler.next_turn() - self.assertTrue(turnhandler.db.turn == 1) + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.next_turn() + self.assertTrue(self.turnhandler.db.turn == 1) # Turn end check - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - attacker.db.Combat_ActionsLeft = 0 - turnhandler.turn_end_check(attacker) - self.assertTrue(turnhandler.db.turn == 1) + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.attacker.db.Combat_ActionsLeft = 0 + self.turnhandler.turn_end_check(self.attacker) + self.assertTrue(self.turnhandler.db.turn == 1) # Join fight - joiner = create_object(tb_magic.TBMagicCharacter, key="Joiner") - turnhandler.db.fighters = [attacker, defender] - turnhandler.db.turn = 0 - turnhandler.join_fight(joiner) - self.assertTrue(turnhandler.db.turn == 1) - self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) - # Remove the script at the end - turnhandler.stop() + self.turnhandler.db.fighters = [self.attacker, self.defender] + self.turnhandler.db.turn = 0 + self.turnhandler.join_fight(self.joiner) + self.assertTrue(self.turnhandler.db.turn == 1) + self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender]) # Test tree select From 315977c2858289527f1165f517a5b32c16ca4922 Mon Sep 17 00:00:00 2001 From: Tehom Date: Tue, 29 May 2018 23:05:34 -0400 Subject: [PATCH 49/58] Fix __add__ in SaverList --- evennia/utils/dbserialize.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/evennia/utils/dbserialize.py b/evennia/utils/dbserialize.py index 80203923e8..c08704be0e 100644 --- a/evennia/utils/dbserialize.py +++ b/evennia/utils/dbserialize.py @@ -237,10 +237,13 @@ class _SaverList(_SaverMutable, MutableSequence): self._data = list() @_save - def __add__(self, otherlist): + def __iadd__(self, otherlist): self._data = self._data.__add__(otherlist) return self._data + def __add__(self, otherlist): + return list(self._data) + otherlist + @_save def insert(self, index, value): self._data.insert(index, self._convert_mutables(value)) From 41a1652fdc91ae570ad16cda3c127086c8f0bf01 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 12 Jun 2018 20:20:20 +0200 Subject: [PATCH 50/58] Fix unittests after merge --- evennia/commands/default/tests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 941e247f0a..625af4b5ac 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -333,8 +333,8 @@ class TestBuilding(CommandTest): def test_find(self): self.call(building.CmdFind(), "oom2", "One Match") - expect = "One Match(#1#7, loc):\n " +\ - "Char2(#7) evennia.objects.objects.DefaultCharacter (location: Room(#1))" + expect = "One Match(#1-#7, loc):\n " +\ + "Char2(#7) - evennia.objects.objects.DefaultCharacter (location: Room(#1))" self.call(building.CmdFind(), "Char2", expect, cmdstring="locate") self.call(building.CmdFind(), "/ex Char2", # /ex is an ambiguous switch "locate: Ambiguous switch supplied: Did you mean /exit or /exact?|" + expect, @@ -350,7 +350,7 @@ class TestBuilding(CommandTest): def test_teleport(self): self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#2)\n|Teleported to Room2.") self.call(building.CmdTeleport(), "/t", # /t switch is abbreviated form of /tonone - "Cannot teleport a puppeted object (Char, puppeted by TestAccount(account 1)) to a Nonelocation.") + "Cannot teleport a puppeted object (Char, puppeted by TestAccount(account 1)) to a None-location.") self.call(building.CmdTeleport(), "/l Room2", # /l switch is abbreviated form of /loc "Destination has no location.") self.call(building.CmdTeleport(), "/q me to Room2", # /q switch is abbreviated form of /quiet @@ -507,5 +507,5 @@ class TestUnconnectedCommand(CommandTest): expected = "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO" % ( settings.SERVERNAME, datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(), - SESSIONS.account_count(), utils.get_evennia_version().replace("-", "")) + SESSIONS.account_count(), utils.get_evennia_version()) self.call(unloggedin.CmdUnconnectedInfo(), "", expected) From a31ac6b68f6a6e7208db28aa591be129461952ac Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 12 Jun 2018 23:50:50 +0200 Subject: [PATCH 51/58] Patch out tickerhandler to avoid reactor testing issues --- evennia/contrib/tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index ba319e3974..e3577997cf 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1325,6 +1325,7 @@ class TestTurnBattleRangeFunc(EvenniaTest): class TestTurnBattleItemsFunc(EvenniaTest): + @patch("evennia.contrib.turnbattle.tb_items.tickerhandler", new=MagicMock()) def setUp(self): super(TestTurnBattleItemsFunc, self).setUp() self.testroom = create_object(DefaultRoom, key="Test Room") From 0b19902dcd8ce237b8b019752eff5f8a09ae61ca Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 13 Jun 2018 00:55:44 +0200 Subject: [PATCH 52/58] Clarify channel log rotate setting is in bytes --- evennia/settings_default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 7ae45c4be2..4a0d336db8 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -137,7 +137,7 @@ LOCKWARNING_LOG_FILE = os.path.join(LOG_DIR, 'lockwarnings.log') CYCLE_LOGFILES = True # Number of lines to append to rotating channel logs when they rotate CHANNEL_LOG_NUM_TAIL_LINES = 20 -# Max size of channel log files before they rotate +# Max size (in bytes) of channel log files before they rotate CHANNEL_LOG_ROTATE_SIZE = 1000000 # Local time zone for this installation. All choices can be found here: # http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE From 925529837eab5be22ae9ab316d2638127fe165b4 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 17 Jun 2018 01:26:18 +0200 Subject: [PATCH 53/58] Backport inlinefunc regex update from develop olc branch. Resolves #1498. --- evennia/utils/inlinefuncs.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/evennia/utils/inlinefuncs.py b/evennia/utils/inlinefuncs.py index e103e217d7..b09cc432e7 100644 --- a/evennia/utils/inlinefuncs.py +++ b/evennia/utils/inlinefuncs.py @@ -191,15 +191,18 @@ except AttributeError: _RE_STARTTOKEN = re.compile(r"(?.*?)(?.*?)(?(?(?(?\\'|\\"|\\\)|\\$\w+\()| # escaped tokens should re-appear in text - (?P[\w\s.-\/#!%\^&\*;:=\-_`~\|\(}{\[\]]+|\"{1}|\'{1}) # everything else should also be included""", - re.UNICODE + re.IGNORECASE + re.VERBOSE + re.DOTALL) + (?.*?)(?.*?)(?(?(?(? # escaped tokens to re-insert sans backslash + \\\'|\\\"|\\\)|\\\$\w+\()| + (?P # everything else to re-insert verbatim + \$(?!\w+\()|\'{1}|\"{1}|\\{1}|[^),$\'\"\\]+)""", + re.UNICODE | re.IGNORECASE | re.VERBOSE | re.DOTALL) # Cache for function lookups. From fb481c98fbe0e152cd85d85e2d8196f8ca80c5c2 Mon Sep 17 00:00:00 2001 From: Tehom Date: Tue, 19 Jun 2018 04:12:46 -0400 Subject: [PATCH 54/58] Move query/unpickling out of loop for mutelist. --- evennia/comms/comms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/evennia/comms/comms.py b/evennia/comms/comms.py index e40de664d1..32ea2731a1 100644 --- a/evennia/comms/comms.py +++ b/evennia/comms/comms.py @@ -91,7 +91,8 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)): @property def wholist(self): subs = self.subscriptions.all() - listening = [ob for ob in subs if ob.is_connected and ob not in self.mutelist] + muted = list(self.mutelist) + listening = [ob for ob in subs if ob.is_connected and ob not in muted] if subs: # display listening subscribers in bold string = ", ".join([account.key if account not in listening else "|w%s|n" % account.key for account in subs]) From 24bdf124f542c2d61968c4d89421f83f1716c6a5 Mon Sep 17 00:00:00 2001 From: Tehom Date: Thu, 28 Jun 2018 08:23:52 -0400 Subject: [PATCH 55/58] Add annotate method to TypedManager to filter by typeclass appropriately. --- evennia/typeclasses/managers.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/evennia/typeclasses/managers.py b/evennia/typeclasses/managers.py index e6ef778eb8..d6a08de16d 100644 --- a/evennia/typeclasses/managers.py +++ b/evennia/typeclasses/managers.py @@ -619,6 +619,18 @@ class TypeclassManager(TypedObjectManager): """ return super(TypeclassManager, self).filter(db_typeclass_path=self.model.path).count() + def annotate(self, *args, **kwargs): + """ + Overload annotate method to first call .all() to filter on typeclass before annotating. + Args: + *args (any): Positional arguments passed along to queryset annotate method. + **kwargs (any): Keyword arguments passed along to queryset annotate method. + + Returns: + Annotated queryset. + """ + return super(TypeclassManager, self).filter(db_typeclass_path=self.model.path).annotate(*args, **kwargs) + def _get_subclasses(self, cls): """ Recursively get all subclasses to a class. From c729b6b91629dcbc50d26875e1e3dc558e5200df Mon Sep 17 00:00:00 2001 From: Tehom Date: Sat, 30 Jun 2018 02:31:47 -0400 Subject: [PATCH 56/58] Add on missing values and values_list methods while we're at it, for the same reasons. --- evennia/typeclasses/managers.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/evennia/typeclasses/managers.py b/evennia/typeclasses/managers.py index d6a08de16d..be9a6dd2c4 100644 --- a/evennia/typeclasses/managers.py +++ b/evennia/typeclasses/managers.py @@ -621,7 +621,7 @@ class TypeclassManager(TypedObjectManager): def annotate(self, *args, **kwargs): """ - Overload annotate method to first call .all() to filter on typeclass before annotating. + Overload annotate method to filter on typeclass before annotating. Args: *args (any): Positional arguments passed along to queryset annotate method. **kwargs (any): Keyword arguments passed along to queryset annotate method. @@ -631,6 +631,30 @@ class TypeclassManager(TypedObjectManager): """ return super(TypeclassManager, self).filter(db_typeclass_path=self.model.path).annotate(*args, **kwargs) + def values(self, *args, **kwargs): + """ + Overload values method to filter on typeclass first. + Args: + *args (any): Positional arguments passed along to values method. + **kwargs (any): Keyword arguments passed along to values method. + + Returns: + Queryset of values dictionaries, just filtered by typeclass first. + """ + return super(TypeclassManager, self).filter(db_typeclass_path=self.model.path).values(*args, **kwargs) + + def values_list(self, *args, **kwargs): + """ + Overload values method to filter on typeclass first. + Args: + *args (any): Positional arguments passed along to values_list method. + **kwargs (any): Keyword arguments passed along to values_list method. + + Returns: + Queryset of value_list tuples, just filtered by typeclass first. + """ + return super(TypeclassManager, self).filter(db_typeclass_path=self.model.path).values_list(*args, **kwargs) + def _get_subclasses(self, cls): """ Recursively get all subclasses to a class. From fe1aa30463991443a9e6d410e34e506a4fe7cbae Mon Sep 17 00:00:00 2001 From: Jerry Aldrich Date: Mon, 9 Jul 2018 08:33:47 -0700 Subject: [PATCH 57/58] Add space to the tutorial's CrumblingWall message Signed-off-by: Jerry Aldrich --- evennia/contrib/tutorial_world/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/tutorial_world/objects.py b/evennia/contrib/tutorial_world/objects.py index b260770577..0561f98d03 100644 --- a/evennia/contrib/tutorial_world/objects.py +++ b/evennia/contrib/tutorial_world/objects.py @@ -674,7 +674,7 @@ class CrumblingWall(TutorialObject, DefaultExit): # we found the button by moving the roots result = ["Having moved all the roots aside, you find that the center of the wall, " "previously hidden by the vegetation, hid a curious square depression. It was maybe once " - "concealed and made to look a part of the wall, but with the crumbling of stone around it," + "concealed and made to look a part of the wall, but with the crumbling of stone around it, " "it's now easily identifiable as some sort of button."] elif self.db.exit_open: # we pressed the button; the exit is open From 27875ba04d8ac1745e838a4aaaa48127842a830d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 21 Jul 2018 14:13:58 +0200 Subject: [PATCH 58/58] Fix to redirect default at_first_login msg to right session --- evennia/accounts/accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index fe7693cce0..b32c3a31cb 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -777,7 +777,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): # any was deleted in the interim. self.db._playable_characters = [char for char in self.db._playable_characters if char] self.msg(self.at_look(target=self.db._playable_characters, - session=session)) + session=session), session=session) def at_failed_login(self, session, **kwargs): """