From ae4a6833f474634f85736d62e995775cbab5f82a Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 27 Jan 2022 00:55:28 +0100 Subject: [PATCH] Start refactoring turnbattle contrib --- .../game_systems/turnbattle/tb_basic.py | 479 +++-- .../game_systems/turnbattle/tb_equip.py | 750 ++----- .../game_systems/turnbattle/tb_items.py | 1912 +++++++---------- .../game_systems/turnbattle/tb_magic.py | 1134 +++------- .../game_systems/turnbattle/tb_range.py | 884 ++------ 5 files changed, 1687 insertions(+), 3472 deletions(-) diff --git a/evennia/contrib/game_systems/turnbattle/tb_basic.py b/evennia/contrib/game_systems/turnbattle/tb_basic.py index 8b385b2943..27c15372d7 100644 --- a/evennia/contrib/game_systems/turnbattle/tb_basic.py +++ b/evennia/contrib/game_systems/turnbattle/tb_basic.py @@ -1,7 +1,7 @@ """ Simple turn-based combat system -Contrib - Tim Ashley Jenkins 2017 +Contrib - Tim Ashley Jenkins 2017, Refactor by Griatch 2022 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 @@ -62,237 +62,235 @@ COMBAT FUNCTIONS START HERE """ -def roll_init(character): +class BasicCombatRules: """ - Rolls a number between 1-1000 to determine initiative. + Stores all combat rules and helper methods. - 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 roll_init(self, character): + """ + Rolls a number between 1-1000 to determine initiative. -def get_attack(attacker, defender): - """ - Returns a value for an attack roll. + Args: + character (obj): The character to determine initiative for - Args: - attacker (obj): Character doing the attacking - defender (obj): Character being attacked + Returns: + initiative (int): The character's place in initiative - higher + numbers go first. - Returns: - attack_value (int): Attack roll value, compared against a defense value - to determine whether an attack hits or misses. + Notes: + By default, does not reference the character and simply returns + a random integer from 1 to 1000. - Notes: - By default, returns a random integer from 1 to 100 without using any - properties from either the attacker or defender. + 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: - 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 + 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_defense(attacker, defender): - """ - Returns a value for defense, which an attack roll must equal or exceed in order - for an attack to hit. + def get_attack(self, attacker, defender): + """ + Returns a value for an attack roll. - Args: - attacker (obj): Character doing the attacking - defender (obj): Character being attacked + 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. + Returns: + attack_value (int): Attack roll value, compared against a defense value + to determine whether an attack hits or misses. - Notes: - By default, returns 50, not taking any properties of the defender or - attacker into account. + Notes: + By default, returns a random integer from 1 to 100 without using any + properties from either the attacker or defender. - 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 + 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(self, attacker, defender): + """ + Returns a value for defense, which an attack roll must equal or exceed in order + for an attack to hit. -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 attacked - Args: - attacker (obj): Character doing the attacking - defender (obj): Character being damaged + Returns: + defense_value (int): Defense value, compared against an attack roll + to determine whether an attack hits or misses. - Returns: - damage_value (int): Damage value, which is to be deducted from the defending - character's HP. + Notes: + By default, returns 50, not taking any properties of the defender or + attacker into account. - Notes: - By default, returns a random integer from 15 to 25 without using any - properties from either the attacker or defender. + 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 - 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 get_damage(self, 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 -def apply_damage(defender, damage): - """ - Applies damage to a target, reducing their HP by the damage amount to a - minimum of 0. + Returns: + damage_value (int): Damage value, which is to be deducted from the defending + character's HP. - 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 + 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 at_defeat(defeated): - """ - Announces the defeat of a fighter in combat. + def apply_damage(self, defender, damage): + """ + Applies damage to a target, reducing their HP by the damage amount to a + minimum of 0. - 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. + 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: - at_defeat(defender) + defender.db.hp = 0 + + def at_defeat(self, 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(self, 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 = self.get_attack(attacker, defender) + # Get a defense value from the defender. + if not defense_value: + defense_value = self.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 = self.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) + ) + self.apply_damage(defender, damage_value) + # If defender HP is reduced to 0 or less, call at_defeat. + if defender.db.hp <= 0: + self.at_defeat(defender) + + def combat_cleanup(self, 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(self, 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(self, 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(self, 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 + + Keyword Args: + 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. -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 - - Keyword Args: - 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. - +COMBAT_RULES = BasicCombatRules() """ ---------------------------------------------------------------------------- @@ -306,6 +304,7 @@ class TBBasicCharacter(DefaultCharacter): A character able to participate in turn-based combat. Has attributes for current and maximum HP, and access to combat commands. """ + rules = COMBAT_RULES def at_object_creation(self): """ @@ -339,7 +338,7 @@ class TBBasicCharacter(DefaultCharacter): """ # Keep the character from moving if at 0 HP or in combat. - if is_in_combat(self): + if self.rules.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: @@ -367,6 +366,8 @@ class TBBasicTurnHandler(DefaultScript): remaining participants choose to end the combat with the 'disengage' command. """ + rules = COMBAT_RULES + def at_script_creation(self): """ Called once, when the script is created. @@ -388,9 +389,10 @@ class TBBasicTurnHandler(DefaultScript): # 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) + # 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 method and can be + # customized easily. + ordered_by_roll = sorted(self.db.fighters, key=self.rules.roll_init, reverse=True) self.db.fighters = ordered_by_roll # Announce the turn order. @@ -408,7 +410,7 @@ class TBBasicTurnHandler(DefaultScript): Called at script termination. """ for fighter in self.db.fighters: - combat_cleanup(fighter) # Clean up the combat attributes for every fighter. + self.rules.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): @@ -423,7 +425,7 @@ class TBBasicTurnHandler(DefaultScript): 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( + self.rules.spend_action( currentchar, "all", action_name="disengage" ) # Spend all remaining actions. return @@ -439,7 +441,8 @@ class TBBasicTurnHandler(DefaultScript): Args: character (obj): Character to initialize for combat. """ - combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. + # Clean up leftover combat attributes beforehand, just in case. + self.rules.combat_cleanup(character) character.db.combat_actionsleft = ( 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 ) @@ -560,6 +563,9 @@ class CmdFight(Command): key = "fight" help_category = "combat" + rules = COMBAT_RULES + combat_handler_class = TBBasicTurnHandler + def func(self): """ This performs the actual command. @@ -570,7 +576,7 @@ class CmdFight(Command): 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 + if self.rules.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. @@ -585,8 +591,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.game_systems.turnbattle.tb_basic.TBBasicTurnHandler") - # Remember you'll have to change the path to the script if you copy this code to your own modules! + here.scripts.add(self.command_handler_class) class CmdAttack(Command): @@ -603,15 +608,17 @@ class CmdAttack(Command): key = "attack" help_category = "combat" + rules = COMBAT_RULES + 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. + if not self.rules.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. + if not self.rules.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 @@ -634,8 +641,8 @@ class CmdAttack(Command): 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. + self.rules.resolve_attack(attacker, defender) + self.rules.spend_action(self.caller, 1, action_name="attack") # Use up one action. class CmdPass(Command): @@ -653,22 +660,25 @@ class CmdPass(Command): aliases = ["wait", "hold"] help_category = "combat" + rules = COMBAT_RULES + def func(self): """ This performs the actual command. """ - if not is_in_combat(self.caller): # Can only pass a turn in combat. + if not self.rules.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. + if not self.rules.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. + # Spend all remaining actions. + self.rules.spend_action(self.caller, "all", action_name="pass") class CmdDisengage(Command): @@ -687,20 +697,23 @@ class CmdDisengage(Command): aliases = ["spare"] help_category = "combat" + rules = COMBAT_RULES + def func(self): """ This performs the actual command. """ - if not is_in_combat(self.caller): # If you're not in combat + if not self.rules.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 + if not self.rules.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. + # Spend all remaining actions. + self.rules.spend_action(self.caller, "all", action_name="disengage") """ 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. @@ -721,10 +734,12 @@ class CmdRest(Command): key = "rest" help_category = "combat" + rules = COMBAT_RULES + def func(self): "This performs the actual command." - if is_in_combat(self.caller): # If you're in combat + if self.rules.is_in_combat(self.caller): # If you're in combat self.caller.msg("You can't rest while you're in combat.") return @@ -748,17 +763,21 @@ class CmdCombatHelp(CmdHelp): topics related to the game. """ + rules = COMBAT_RULES + combat_help_text = ( + "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.|/" + ) + # 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.|/" - ) + # In combat and entered 'help' alone + if self.rules.is_in_combat(self.caller) and not self.args: + self.caller.msg(self.combat_help_text) else: super().func() # Call the default help command diff --git a/evennia/contrib/game_systems/turnbattle/tb_equip.py b/evennia/contrib/game_systems/turnbattle/tb_equip.py index 20c1c066bf..62d6bc790f 100644 --- a/evennia/contrib/game_systems/turnbattle/tb_equip.py +++ b/evennia/contrib/game_systems/turnbattle/tb_equip.py @@ -1,7 +1,7 @@ """ Simple turn-based combat system with equipment -Contrib - Tim Ashley Jenkins 2017 +Contrib - Tim Ashley Jenkins 2017, Refactor by Griatch 2022 This is a version of the 'turnbattle' contrib with a basic system for weapons and armor implemented. Weapons can have unique damage ranges @@ -55,8 +55,8 @@ in your game and using it as-is. """ from random import randint -from evennia import DefaultCharacter, Command, default_cmds, DefaultScript, DefaultObject -from evennia.commands.default.help import CmdHelp +from evennia import Command, default_cmds, DefaultObject +from . import tb_basic """ ---------------------------------------------------------------------------- @@ -74,276 +74,156 @@ COMBAT FUNCTIONS START HERE """ -def roll_init(character): +class EquipmentCombatRules(tb_basic.BasicCombatRules): """ - Rolls a number between 1-1000 to determine initiative. + Has all the methods of the basic combat, with the addition of equipment. - 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(self, attacker, defender): + """ + Returns a value for an attack roll. -def get_attack(attacker, defender): - """ - Returns a value for an attack roll. + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked - 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. - Returns: - attack_value (int): Attack roll value, compared against a defense value - to determine whether an attack hits or misses. + Notes: + In this example, a weapon's accuracy bonus is factored into the attack + roll. Lighter weapons are more accurate but less damaging, and heavier + weapons are less accurate but deal more damage. Of course, you can + change this paradigm completely in your own game. + """ + # Start with a roll from 1 to 100. + attack_value = randint(1, 100) + accuracy_bonus = 0 + # If armed, add weapon's accuracy bonus. + if attacker.db.wielded_weapon: + weapon = attacker.db.wielded_weapon + accuracy_bonus += weapon.db.accuracy_bonus + # If unarmed, use character's unarmed accuracy bonus. + else: + accuracy_bonus += attacker.db.unarmed_accuracy + # Add the accuracy bonus to the attack roll. + attack_value += accuracy_bonus + return attack_value - Notes: - In this example, a weapon's accuracy bonus is factored into the attack - roll. Lighter weapons are more accurate but less damaging, and heavier - weapons are less accurate but deal more damage. Of course, you can - change this paradigm completely in your own game. - """ - # Start with a roll from 1 to 100. - attack_value = randint(1, 100) - accuracy_bonus = 0 - # If armed, add weapon's accuracy bonus. - if attacker.db.wielded_weapon: - weapon = attacker.db.wielded_weapon - accuracy_bonus += weapon.db.accuracy_bonus - # If unarmed, use character's unarmed accuracy bonus. - else: - accuracy_bonus += attacker.db.unarmed_accuracy - # Add the accuracy bonus to the attack roll. - attack_value += accuracy_bonus - return attack_value + def get_defense(self, 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 -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. + Returns: + defense_value (int): Defense value, compared against an attack roll + to determine whether an attack hits or misses. - Args: - attacker (obj): Character doing the attacking - defender (obj): Character being attacked + Notes: + Characters are given a default defense value of 50 which can be + modified up or down by armor. In this example, wearing armor actually + makes you a little easier to hit, but reduces incoming damage. + """ + # Start with a defense value of 50 for a 50/50 chance to hit. + defense_value = 50 + # Modify this value based on defender's armor. + if defender.db.worn_armor: + armor = defender.db.worn_armor + defense_value += armor.db.defense_modifier + return defense_value - Returns: - defense_value (int): Defense value, compared against an attack roll - to determine whether an attack hits or misses. + def get_damage(self, attacker, defender): + """ + Returns a value for damage to be deducted from the defender's HP after abilities + successful hit. - Notes: - Characters are given a default defense value of 50 which can be - modified up or down by armor. In this example, wearing armor actually - makes you a little easier to hit, but reduces incoming damage. - """ - # Start with a defense value of 50 for a 50/50 chance to hit. - defense_value = 50 - # Modify this value based on defender's armor. - if defender.db.worn_armor: - armor = defender.db.worn_armor - defense_value += armor.db.defense_modifier - return defense_value + 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. -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: - Damage is determined by the attacker's wielded weapon, or the attacker's - unarmed damage range if no weapon is wielded. Incoming damage is reduced - by the defender's armor. - """ - damage_value = 0 - # Generate a damage value from wielded weapon if armed - if attacker.db.wielded_weapon: - weapon = attacker.db.wielded_weapon - # Roll between minimum and maximum damage - damage_value = randint(weapon.db.damage_range[0], weapon.db.damage_range[1]) - # Use attacker's unarmed damage otherwise - else: - damage_value = randint( - attacker.db.unarmed_damage_range[0], attacker.db.unarmed_damage_range[1] - ) - # If defender is armored, reduce incoming damage - if defender.db.worn_armor: - armor = defender.db.worn_armor - damage_value -= armor.db.damage_reduction - # Make sure minimum damage is 0 - if damage_value < 0: + Notes: + Damage is determined by the attacker's wielded weapon, or the attacker's + unarmed damage range if no weapon is wielded. Incoming damage is reduced + by the defender's armor. + """ damage_value = 0 - return damage_value + # Generate a damage value from wielded weapon if armed + if attacker.db.wielded_weapon: + weapon = attacker.db.wielded_weapon + # Roll between minimum and maximum damage + damage_value = randint(weapon.db.damage_range[0], weapon.db.damage_range[1]) + # Use attacker's unarmed damage otherwise + else: + damage_value = randint( + attacker.db.unarmed_damage_range[0], attacker.db.unarmed_damage_range[1] + ) + # If defender is armored, reduce incoming damage + if defender.db.worn_armor: + armor = defender.db.worn_armor + damage_value -= armor.db.damage_reduction + # Make sure minimum damage is 0 + if damage_value < 0: + damage_value = 0 + return damage_value + def resolve_attack(self, attacker, defender, attack_value=None, defense_value=None): + """ + Resolves an attack and outputs the result. -def apply_damage(defender, damage): - """ - Applies damage to a target, reducing their HP by the damage amount to a - minimum of 0. + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked - 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 the attacker's weapon type to reference in combat messages. - attackers_weapon = "attack" - if attacker.db.wielded_weapon: - weapon = attacker.db.wielded_weapon - attackers_weapon = weapon.db.weapon_type_name - # 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 %s misses %s!" % (attacker, attackers_weapon, defender) - ) - else: - damage_value = get_damage(attacker, defender) # Calculate damage value. - # Announce damage dealt and apply damage. - if damage_value > 0: + 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 the attacker's weapon type to reference in combat messages. + attackers_weapon = "attack" + if attacker.db.wielded_weapon: + weapon = attacker.db.wielded_weapon + attackers_weapon = weapon.db.weapon_type_name + # Get an attack roll from the attacker. + if not attack_value: + attack_value = self.get_attack(attacker, defender) + # Get a defense value from the defender. + if not defense_value: + defense_value = self.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 %s strikes %s for %i damage!" - % (attacker, attackers_weapon, defender, damage_value) + "%s's %s misses %s!" % (attacker, attackers_weapon, defender) ) else: - attacker.location.msg_contents( - "%s's %s bounces harmlessly off %s!" % (attacker, attackers_weapon, defender) - ) - 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) + damage_value = self.get_damage(attacker, defender) # Calculate damage value. + # Announce damage dealt and apply damage. + if damage_value > 0: + attacker.location.msg_contents( + "%s's %s strikes %s for %i damage!" + % (attacker, attackers_weapon, defender, damage_value) + ) + else: + attacker.location.msg_contents( + "%s's %s bounces harmlessly off %s!" % (attacker, attackers_weapon, defender) + ) + self.apply_damage(defender, damage_value) + # If defender HP is reduced to 0 or less, call at_defeat. + if defender.db.hp <= 0: + self.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 - - Keyword Args: - 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. - +COMBAT_RULES = EquipmentCombatRules() """ ---------------------------------------------------------------------------- @@ -352,7 +232,7 @@ SCRIPTS START HERE """ -class TBEquipTurnHandler(DefaultScript): +class TBEquipTurnHandler(tb_basic.TBBasicTurnHandler): """ 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 @@ -363,174 +243,7 @@ class TBEquipTurnHandler(DefaultScript): 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) + rules = COMBAT_RULES """ @@ -543,7 +256,9 @@ TYPECLASSES START HERE class TBEWeapon(DefaultObject): """ A weapon which can be wielded in combat with the 'wield' command. + """ + rules = COMBAT_RULES def at_object_creation(self): """ @@ -592,7 +307,7 @@ class TBEArmor(DefaultObject): """ Can't drop in combat. """ - if is_in_combat(dropper): + if self.rules.is_in_combat(dropper): dropper.msg("You can't doff armor in a fight!") return False return True @@ -609,7 +324,7 @@ class TBEArmor(DefaultObject): """ Can't give away in combat. """ - if is_in_combat(giver): + if self.rules.is_in_combat(giver): dropper.msg("You can't doff armor in a fight!") return False return True @@ -623,7 +338,7 @@ class TBEArmor(DefaultObject): giver.location.msg_contents("%s removes %s." % (giver, self)) -class TBEquipCharacter(DefaultCharacter): +class TBEquipCharacter(tb_basic.TBBasicCharacter): """ A character able to participate in turn-based combat. Has attributes for current and maximum HP, and access to combat commands. @@ -649,31 +364,6 @@ class TBEquipCharacter(DefaultCharacter): can be changed at creation and factor into combat calculations. """ - def at_pre_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 - """ ---------------------------------------------------------------------------- @@ -682,7 +372,7 @@ COMMANDS START HERE """ -class CmdFight(Command): +class CmdFight(tb_basic.CmdFight): """ Starts a fight with everyone in the same room as you. @@ -697,36 +387,11 @@ class CmdFight(Command): 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.game_systems.turnbattle.tb_equip.TBEquipTurnHandler") - # Remember you'll have to change the path to the script if you copy this code to your own modules! + rules = COMBAT_RULES + command_handler_class = TBEquipTurnHandler -class CmdAttack(Command): +class CmdAttack(tb_basic.CmdAttack): """ Attacks another character. @@ -740,42 +405,10 @@ class CmdAttack(Command): 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. + rules = COMBAT_RULES -class CmdPass(Command): +class CmdPass(tb_basic.CmdPass): """ Passes on your turn. @@ -790,25 +423,10 @@ class CmdPass(Command): 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. + rules = COMBAT_RULES -class CmdDisengage(Command): +class CmdDisengage(tb_basic.CmdDisengage): """ Passes your turn and attempts to end combat. @@ -824,27 +442,10 @@ class CmdDisengage(Command): 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. - """ + rules = COMBAT_RULES -class CmdRest(Command): +class CmdRest(tb_basic.CmdRest): """ Recovers damage. @@ -858,21 +459,10 @@ class CmdRest(Command): 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. - """ + rules = COMBAT_RULES -class CmdCombatHelp(CmdHelp): +class CmdCombatHelp(tb_basic.CmdCombatHelp): """ View help or a list of topics @@ -885,19 +475,7 @@ class CmdCombatHelp(CmdHelp): 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().func() # Call the default help command + rules = COMBAT_RULES class CmdWield(Command): @@ -918,13 +496,15 @@ class CmdWield(Command): key = "wield" help_category = "combat" + rules = COMBAT_RULES + def func(self): """ This performs the actual command. """ # If in combat, check to see if it's your turn. - if is_in_combat(self.caller): - if not is_turn(self.caller): + if self.rules.is_in_combat(self.caller): + if not self.rules.is_turn(self.caller): self.caller.msg("You can only do that on your turn.") return if not self.args: @@ -933,7 +513,8 @@ class CmdWield(Command): weapon = self.caller.search(self.args, candidates=self.caller.contents) if not weapon: return - if not weapon.is_typeclass("evennia.contrib.game_systems.turnbattle.tb_equip.TBEWeapon", exact=True): + if not weapon.is_typeclass("evennia.contrib.game_systems.turnbattle.tb_equip.TBEWeapon", + exact=True): self.caller.msg("That's not a weapon!") # Remember to update the path to the weapon typeclass if you move this module! return @@ -948,8 +529,8 @@ class CmdWield(Command): "%s lowers %s and wields %s." % (self.caller, old_weapon, weapon) ) # Spend an action if in combat. - if is_in_combat(self.caller): - spend_action(self.caller, 1, action_name="wield") # Use up one action. + if self.rules.is_in_combat(self.caller): + self.rules.spend_action(self.caller, 1, action_name="wield") # Use up one action. class CmdUnwield(Command): @@ -966,13 +547,15 @@ class CmdUnwield(Command): key = "unwield" help_category = "combat" + rules = COMBAT_RULES + def func(self): """ This performs the actual command. """ # If in combat, check to see if it's your turn. - if is_in_combat(self.caller): - if not is_turn(self.caller): + if self.rules.is_in_combat(self.caller): + if not self.rules.is_turn(self.caller): self.caller.msg("You can only do that on your turn.") return if not self.caller.db.wielded_weapon: @@ -998,12 +581,14 @@ class CmdDon(Command): key = "don" help_category = "combat" + rules = COMBAT_RULES + def func(self): """ This performs the actual command. """ # Can't do this in combat - if is_in_combat(self.caller): + if self.rules.is_in_combat(self.caller): self.caller.msg("You can't don armor in a fight!") return if not self.args: @@ -1012,7 +597,8 @@ class CmdDon(Command): armor = self.caller.search(self.args, candidates=self.caller.contents) if not armor: return - if not armor.is_typeclass("evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor", exact=True): + if not armor.is_typeclass("evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor", + exact=True): self.caller.msg("That's not armor!") # Remember to update the path to the armor typeclass if you move this module! return @@ -1043,12 +629,14 @@ class CmdDoff(Command): key = "doff" help_category = "combat" + rules = COMBAT_RULES + def func(self): """ This performs the actual command. """ # Can't do this in combat - if is_in_combat(self.caller): + if self.rules.is_in_combat(self.caller): self.caller.msg("You can't doff armor in a fight!") return if not self.caller.db.worn_armor: diff --git a/evennia/contrib/game_systems/turnbattle/tb_items.py b/evennia/contrib/game_systems/turnbattle/tb_items.py index d828c9bf0a..dd06a70f76 100644 --- a/evennia/contrib/game_systems/turnbattle/tb_items.py +++ b/evennia/contrib/game_systems/turnbattle/tb_items.py @@ -67,11 +67,11 @@ in your game and using it as-is. """ from random import randint -from evennia import DefaultCharacter, Command, default_cmds, DefaultScript +from evennia import Command, default_cmds from evennia.commands.default.muxcommand import MuxCommand -from evennia.commands.default.help import CmdHelp from evennia.prototypes.spawner import spawn from evennia import TICKER_HANDLER as tickerhandler +from . import tb_basic """ ---------------------------------------------------------------------------- @@ -103,1217 +103,465 @@ COMBAT FUNCTIONS START HERE """ -def roll_init(character): - """ - Rolls a number between 1-1000 to determine initiative. +class ItemCombatRules(tb_basic.BasicCombatRules): - 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: - 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 to the roll if the attacker has the "Accuracy Up" condition. - if "Accuracy Up" in attacker.db.conditions: - 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 += ACC_DOWN_MOD - 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: - 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 to defense if the defender has the "Defense Up" condition. - if "Defense Up" in defender.db.conditions: - 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 += DEF_DOWN_MOD - 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: - 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 to damage roll if attacker has the "Damage Up" condition. - if "Damage Up" in attacker.db.conditions: - 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 += DMG_DOWN_MOD - 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, - damage_value=None, - inflict_condition=[], -): - """ - Resolves an attack and outputs the result. - - 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: - 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: - 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: - 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) - # 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) - - -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 - - Keyword Args: - 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. - - -def spend_item_use(item, user): - """ - 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 - - 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): - """ - 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 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 - - # 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: # 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 - - # 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): - """ - 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 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 the condition doesn't tick down - it lasts indefinitely. - 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 - 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. - - 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]}) - # Tell everyone! - character.location.msg_contents("%s gains the '%s' condition." % (character, condition)) - - -""" ----------------------------------------------------------------------------- -CHARACTER TYPECLASS ----------------------------------------------------------------------------- -""" - - -class TBItemsCharacter(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): + def get_attack(self, attacker, defender): """ - 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.conditions = {} # Set empty dict for conditions - # Subscribe character to the ticker handler - 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. - - 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, 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. - """ - - def at_pre_move(self, destination): - """ - Called just before starting to move this object to - destination. + Returns a value for an attack roll. Args: - destination (Object): The object we are moving to + attacker (obj): Character doing the attacking + defender (obj): Character being attacked Returns: - shouldmove (bool): If we should move or not. + attack_value (int): Attack roll value, compared against a defense value + to determine whether an attack hits or misses. Notes: - If this method returns False/None, the move is cancelled - before it is even started. - + 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. """ - # 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 + # For this example, just return a random integer up to 100. + attack_value = randint(1, 100) + # Add to the roll if the attacker has the "Accuracy Up" condition. + if "Accuracy Up" in attacker.db.conditions: + 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 += ACC_DOWN_MOD + return attack_value - def at_turn_start(self): + def get_defense(self, attacker, defender): """ - 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. - """ - # Regeneration: restores 4 to 8 HP at the start of character's turn - if "Regeneration" in self.db.conditions: - 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 - 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(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: - # 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.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): - """ - Fires every 30 seconds. - """ - 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 - 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 - - -""" ----------------------------------------------------------------------------- -SCRIPTS START HERE ----------------------------------------------------------------------------- -""" - - -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 - 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. + Returns a value for defense, which an attack roll must equal or exceed in order + for an attack to hit. 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 + attacker (obj): Character doing the attacking + defender (obj): Character being attacked - 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. + Returns: + defense_value (int): Defense value, compared against an attack roll + to determine whether an attack hits or misses. 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. + This is where conditions affecting defense are accounted for. """ - character.db.combat_actionsleft = ACTIONS_PER_TURN # Replenish actions - # Call character's at_turn_start() hook. - character.at_turn_start() + # For this example, just return 50, for about a 50/50 chance of hit. + defense_value = 50 + # Add to defense if the defender has the "Defense Up" condition. + if "Defense Up" in defender.db.conditions: + 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 += DEF_DOWN_MOD + return defense_value - def next_turn(self): + def get_damage(self, attacker, defender): """ - 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. - - # Count down condition timers. - for fighter in self.db.fighters: - condition_tickdown(fighter, newchar) - - 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. + Returns a value for damage to be deducted from the defender's HP after abilities + successful hit. 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 + attacker (obj): Character doing the attacking + defender (obj): Character being damaged - def join_fight(self, character): + Returns: + damage_value (int): Damage value, which is to be deducted from the defending + character's HP. + + Notes: + 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. """ - Adds a new character to a fight already in progress. + # For this example, just generate a number between 15 and 25. + damage_value = randint(15, 25) + # Add to damage roll if attacker has the "Damage Up" condition. + if "Damage Up" in attacker.db.conditions: + 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 += DMG_DOWN_MOD + return damage_value + + def resolve_attack( + self, + attacker, + defender, + attack_value=None, + defense_value=None, + damage_value=None, + inflict_condition=[], + ): + """ + Resolves an attack and outputs the result. Args: - character (obj): Character to be added to the fight. + 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: + This function is called by normal attacks as well as attacks + made with items. """ - # 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.game_systems.turnbattle.tb_items.TBItemsTurnHandler") - # 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 - - 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) - - 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.|/" - + "|wUse:|n Use an item you're carrying." - ) + # Get an attack roll from the attacker. + if not attack_value: + attack_value = self.get_attack(attacker, defender) + # Get a defense value from the defender. + if not defense_value: + defense_value = self.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: - super().func() # Call the default help command + if not damage_value: + damage_value = self.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) + ) + self.apply_damage(defender, damage_value) + # Inflict conditions on hit, if any specified + for condition in inflict_condition: + self.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: + self.at_defeat(defender) - -class CmdUse(MuxCommand): - """ - Use an item. - - Usage: - use [= target] - - 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" - help_category = "combat" - - def func(self): + def spend_item_use(self, item, user): """ - This performs the actual command. + 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. """ - # Search for item - item = self.caller.search(self.lhs, candidates=self.caller.contents) - if not 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 the value is 'True', just destroy the item + if item.db.item_consumable: + 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 + # Move the residue to the same place as the item + residue.location = item.location + user.msg("After using %s, you are left with %s." % (item, residue)) + item.delete() # Delete the spent item + + def use_item(self, 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 and no target given, set target to self. + if item.db.item_selfonly and target is 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 - # Search for target, if any is given - target = None - if self.rhs: - target = self.caller.search(self.rhs) - if not target: - return + # Set kwargs to pass to item_func + kwargs = {} + if item.db.item_kwargs: + kwargs = item.db.item_kwargs - # 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.") - 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()) + # Match item_func string to function + try: + item_func = ITEMFUNCS[item.db.item_func] + 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 - 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 + # 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 not item_func(item, user, target, **kwargs): + return - # If everything checks out, call the use_item function - use_item(self.caller, item, target) + # 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: + self.spend_item_use(item, user) + # Spend an action if in combat + if self.is_in_combat(user): + self.spend_action(user, 1, action_name="item") -class BattleCmdSet(default_cmds.CharacterCmdSet): - """ - This command set includes all the commmands used in the battle system. - """ - - key = "DefaultCharacter" - - def at_cmdset_creation(self): + def condition_tickdown(self, character, turnchar): """ - Populates the cmdset + 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 by default. """ - self.add(CmdFight()) - self.add(CmdAttack()) - self.add(CmdRest()) - self.add(CmdPass()) - self.add(CmdDisengage()) - self.add(CmdCombatHelp()) - self.add(CmdUse()) + + 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 the condition doesn't tick down - it lasts + # indefinitely. + if not condition_duration: + # 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(self, 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]}) + # Tell everyone! + character.location.msg_contents("%s gains the '%s' condition." % (character, condition)) + + # ---------------------------------------------------------------------------- + # 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(self, 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 + + 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 # Returning false aborts the item use + + 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_add_condition(self, item, user, target, **kwargs): + """ + Item function that gives the target one or more conditions. + + kwargs: + 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. + """ + conditions = [("Regeneration", 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 "conditions" in kwargs: + conditions = kwargs["conditions"] + + user.location.msg_contents("%s uses %s!" % (user, item)) + + # Add conditions to the target + for condition in conditions: + self.add_condition(target, user, condition[0], condition[1]) + + 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"] + + 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(self, 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 self.is_in_combat(user): + user.msg("You can only use that in combat.") + return False # Returning false aborts the item use + + 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 + inflict_condition = [] + + # 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"] + 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) + + # 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)) + self.resolve_attack( + user, + target, + attack_value=attack_value, + damage_value=damage_value, + inflict_condition=inflict_condition, + ) -""" ----------------------------------------------------------------------------- -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 - - 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 # Returning false aborts the item use - - 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_add_condition(item, user, target, **kwargs): - """ - Item function that gives the target one or more conditions. - - kwargs: - 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. - """ - conditions = [("Regeneration", 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 "conditions" in kwargs: - conditions = kwargs["conditions"] - - user.location.msg_contents("%s uses %s!" % (user, item)) - - # 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): - """ - 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"] - - 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. - - 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.") - return False # Returning false aborts the item use - - 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 - inflict_condition = [] - - # 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"] - 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) - - # 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, - ) +COMBAT_RULES = ItemCombatRules() # 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, - "add_condition": itemfunc_add_condition, - "cure_condition": itemfunc_cure_condition, + "heal": COMBAT_RULES.itemfunc_heal, + "attack": COMBAT_RULES.itemfunc_attack, + "add_condition": COMBAT_RULES.itemfunc_add_condition, + "cure_condition": COMBAT_RULES.itemfunc_cure_condition, } """ @@ -1450,8 +698,340 @@ AMULET_OF_MIGHT = { 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.", + "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)]}, } + + +""" +---------------------------------------------------------------------------- +CHARACTER TYPECLASS +---------------------------------------------------------------------------- +""" + + +class TBItemsCharacter(tb_basic.TBBasicCharacter): + """ + A character able to participate in turn-based combat. Has attributes for current + and maximum HP, and access to combat commands. + """ + + rules = ItemCombatRules() + + 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.conditions = {} # Set empty dict for conditions + # Subscribe character to the ticker handler + 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. + + 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, 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. + """ + + 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. + """ + # Regeneration: restores 4 to 8 HP at the start of character's turn + if "Regeneration" in self.db.conditions: + to_heal = randint(REGEN_RATE[0], REGEN_RATE[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 + 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(POISON_RATE[0], POISON_RATE[1]) # Deal damage + self.rules.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 + self.rules.at_defeat(self) + + # Haste: Gain an extra action in combat. + if self.rules.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 self.rules.is_in_combat(self) and "Paralyzed" in self.db.conditions: + self.db.combat_actionsleft = 0 + 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): + """ + Fires every 30 seconds. + """ + if not self.rules.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 + self.rules.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 + + +""" +---------------------------------------------------------------------------- +SCRIPTS START HERE +---------------------------------------------------------------------------- +""" + + +class TBItemsTurnHandler(tb_basic.TBBasicTurnHandler): + """ + 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. + """ + + rules = COMBAT_RULES + + def next_turn(self): + """ + Advances to the next character in the turn order. + """ + super().next_turn() + + # Count down condition timers. + next_fighter = self.db.fighters[self.db.turn] + for fighter in self.db.fighters: + self.rules.condition_tickdown(fighter, next_fighter) + + +""" +---------------------------------------------------------------------------- +COMMANDS START HERE +---------------------------------------------------------------------------- +""" + + +class CmdFight(tb_basic.CmdFight): + """ + 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" + + rules = COMBAT_RULES + combat_handler_class = TBItemsTurnHandler + + +class CmdAttack(tb_basic.CmdFight): + """ + 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" + rules = COMBAT_RULES + + +class CmdPass(tb_basic.CmdPass): + """ + 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" + rules = COMBAT_RULES + + +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" + + rules = COMBAT_RULES + + +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" + + rules = COMBAT_RULES + + +class CmdCombatHelp(tb_basic.CmdCombatHelp): + """ + 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. + """ + + rules = COMBAT_RULES + combat_help_text = ( + "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.|/" + "|wUse:|n Use an item you're carrying." + ) + + +class CmdUse(MuxCommand): + """ + Use an item. + + Usage: + use [= target] + + 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" + help_category = "combat" + + rules = COMBAT_RULES + + def func(self): + """ + 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 self.rules.is_in_combat(self.caller): + if not self.rules.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 + + # If everything checks out, call the use_item function + self.rules.use_item(self.caller, item, target) + + +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(CmdUse()) diff --git a/evennia/contrib/game_systems/turnbattle/tb_magic.py b/evennia/contrib/game_systems/turnbattle/tb_magic.py index e7b2766e59..f9750422a9 100644 --- a/evennia/contrib/game_systems/turnbattle/tb_magic.py +++ b/evennia/contrib/game_systems/turnbattle/tb_magic.py @@ -1,7 +1,7 @@ """ Simple turn-based combat system with spell casting -Contrib - Tim Ashley Jenkins 2017 +Contrib - Tim Ashley Jenkins 2017, Refactor by Griatch, 2022 This is a version of the 'turnbattle' contrib that includes a basic, expandable framework for a 'magic system', whereby players can spend @@ -67,9 +67,291 @@ in your game and using it as-is. """ from random import randint -from evennia import DefaultCharacter, Command, default_cmds, DefaultScript, create_object +from evennia.utils.logger import log_trace +from evennia import Command, default_cmds, DefaultScript, create_object from evennia.commands.default.muxcommand import MuxCommand -from evennia.commands.default.help import CmdHelp + +from . import tb_basic + +""" +---------------------------------------------------------------------------- +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. +""" + +class MagicCombatRules(tb_basic.BasicCombatRules): + + def spell_healing(self, caster, spell_name, targets, cost, **kwargs): + """ + 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) + + 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(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 + 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 self.is_in_combat(caster): # Spend action if in combat + self.spend_action(caster, 1, action_name="cast") + + def spell_attack(self, 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) + + 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 = [] + # 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 = to_attack + 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 = self.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 + + 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 + self.apply_damage(fighter, total_damage[fighter]) + # If fighter HP is reduced to 0 or less, call at_defeat. + if fighter.db.hp <= 0: + self.at_defeat(fighter) + + if self.is_in_combat(caster): # Spend action if in combat + self.spend_action(caster, 1, action_name="cast") + + def spell_conjure(self, 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 + + # 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) + ) + + +COMBAT_RULES = MagicCombatRules() + + +""" +---------------------------------------------------------------------------- +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 + 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), spell_name (str), + targets (list), and cost (int), as well as **kwargs. + +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 'spellfunc'. +You can use kwargs to effectively re-use the same function for different but similar +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 = { + "magic missile": { + "spellfunc": COMBAT_RULES.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": COMBAT_RULES.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": COMBAT_RULES.spell_healing, + "target": "anychar", + "cost": 5 + }, + "mass cure wounds": { + "spellfunc": COMBAT_RULES.spell_healing, + "target": "anychar", + "cost": 10, + "max_targets": 5, + }, + "full heal": { + "spellfunc": COMBAT_RULES.spell_healing, + "target": "anychar", + "cost": 12, + "healing_range": (100, 100), + }, + "cactus conjuration": { + "spellfunc": COMBAT_RULES.spell_conjure, + "target": "none", + "cost": 2, + "combat_spell": False, + "obj_key": "a cactus", + "obj_desc": "An ordinary green cactus with little spines.", + }, +} + """ ---------------------------------------------------------------------------- @@ -80,246 +362,6 @@ 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 - - Keyword Args: - 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. - """ ---------------------------------------------------------------------------- @@ -328,11 +370,13 @@ CHARACTER TYPECLASS """ -class TBMagicCharacter(DefaultCharacter): +class TBMagicCharacter(tb_basic.TBBasicCharacter): """ A character able to participate in turn-based combat. Has attributes for current - and maximum HP, and access to combat commands. + and maximum HP, access to combat commands and magic. + """ + rules = COMBAT_RULES def at_object_creation(self): """ @@ -351,31 +395,6 @@ class TBMagicCharacter(DefaultCharacter): self.db.max_mp = 20 # Set maximum MP to 20 self.db.mp = self.db.max_mp # Set current MP to maximum - def at_pre_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 - """ ---------------------------------------------------------------------------- @@ -396,173 +415,7 @@ class TBMagicTurnHandler(DefaultScript): 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) + rules = COMBAT_RULES """ @@ -572,7 +425,7 @@ COMMANDS START HERE """ -class CmdFight(Command): +class CmdFight(tb_basic.CmdFight): """ Starts a fight with everyone in the same room as you. @@ -587,36 +440,11 @@ class CmdFight(Command): 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.game_systems.turnbattle.tb_magic.TBMagicTurnHandler") - # Remember you'll have to change the path to the script if you copy this code to your own modules! + rules = COMBAT_RULES + combat_handler_class = TBMagicTurnHandler -class CmdAttack(Command): +class CmdAttack(tb_basic.CmdAttack): """ Attacks another character. @@ -630,42 +458,10 @@ class CmdAttack(Command): 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. + rules = COMBAT_RULES -class CmdPass(Command): +class CmdPass(tb_basic.CmdPass): """ Passes on your turn. @@ -680,25 +476,10 @@ class CmdPass(Command): 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. + rules = COMBAT_RULES -class CmdDisengage(Command): +class CmdDisengage(tb_basic.CmdDisengage): """ Passes your turn and attempts to end combat. @@ -714,24 +495,7 @@ class CmdDisengage(Command): 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. - """ + rules = COMBAT_RULES class CmdLearnSpell(Command): @@ -821,6 +585,8 @@ class CmdCast(MuxCommand): key = "cast" help_category = "magic" + rules = COMBAT_RULES + def func(self): """ This performs the actual command. @@ -847,7 +613,7 @@ class CmdCast(MuxCommand): caller.msg(spells_known_msg) # List the spells the player knows return - spellname = self.lhs.lower() + spellname = self.lhs.lower() # noqa - not used but potentially useful spell_to_cast = [] spell_targets = [] @@ -908,12 +674,12 @@ class CmdCast(MuxCommand): 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): + if spelldata["combat_spell"] is False and self.rules.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: + if spelldata["noncombat_spell"] is False and self.rules.is_in_combat(caller) is False: caller.msg("You can't use the spell '%s' outside of combat." % spell_to_cast) return @@ -1014,7 +780,7 @@ class CmdRest(Command): def func(self): "This performs the actual command." - if is_in_combat(self.caller): # If you're in combat + if self.rules.is_in_combat(self.caller): # If you're in combat self.caller.msg("You can't rest while you're in combat.") return @@ -1055,7 +821,7 @@ class CmdStatus(Command): ) -class CmdCombatHelp(CmdHelp): +class CmdCombatHelp(tb_basic.CmdCombatHelp): """ View help or a list of topics @@ -1068,20 +834,6 @@ class CmdCombatHelp(CmdHelp): 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().func() # Call the default help command - class BattleCmdSet(default_cmds.CharacterCmdSet): """ @@ -1103,277 +855,3 @@ class BattleCmdSet(default_cmds.CharacterCmdSet): self.add(CmdLearnSpell()) self.add(CmdCast()) self.add(CmdStatus()) - - -""" ----------------------------------------------------------------------------- -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_healing(caster, spell_name, targets, cost, **kwargs): - """ - 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) - - 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(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 - 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") - - -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) - - 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 = [] - # 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 = to_attack + 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 - - 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") - - -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 - - # 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) - ) - - -""" ----------------------------------------------------------------------------- -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 - 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), spell_name (str), - targets (list), and cost (int), as well as **kwargs. - -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 'spellfunc'. -You can use kwargs to effectively re-use the same function for different but similar -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 = { - "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_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.", - }, -} diff --git a/evennia/contrib/game_systems/turnbattle/tb_range.py b/evennia/contrib/game_systems/turnbattle/tb_range.py index f1c701ac10..db27122fa0 100644 --- a/evennia/contrib/game_systems/turnbattle/tb_range.py +++ b/evennia/contrib/game_systems/turnbattle/tb_range.py @@ -101,8 +101,9 @@ in your game and using it as-is. """ from random import randint -from evennia import DefaultCharacter, DefaultObject, Command, default_cmds, DefaultScript +from evennia import DefaultObject, Command, default_cmds, DefaultScript from evennia.commands.default.help import CmdHelp +from . import tb_basic """ ---------------------------------------------------------------------------- @@ -120,238 +121,78 @@ COMBAT FUNCTIONS START HERE """ -def roll_init(character): - """ - Rolls a number between 1-1000 to determine initiative. +class RangedCombatRules(tb_basic.BasicCombatRules): - Args: - character (obj): The character to determine initiative for + def get_attack(self, attacker, defender, attack_type): + """ + Returns a value for an attack roll. - Returns: - initiative (int): The character's place in initiative - higher - numbers go first. + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + attack_type (str): Type of attack ('melee' or 'ranged') - Notes: - By default, does not reference the character and simply returns - a random integer from 1 to 1000. + Returns: + attack_value (int): Attack roll value, compared against a defense value + to determine whether an attack hits or misses. - 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: + Notes: + By default, generates a random integer from 1 to 100 without using any + properties from either the attacker or defender, and modifies the result + based on whether it's for a melee or ranged attack. - return (randint(1,20)) + character.db.dexterity + 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) + # Make melee attacks more accurate, ranged attacks less accurate + if attack_type == "melee": + attack_value += 15 + if attack_type == "ranged": + attack_value -= 15 + return attack_value - This way, characters with a higher dexterity will go first more often. - """ - return randint(1, 1000) + def get_range(self, obj1, obj2): + """ + Gets the combat range between two objects. + Args: + obj1 (obj): First object + obj2 (obj): Second object -def get_attack(attacker, defender, attack_type): - """ - Returns a value for an attack roll. + Returns: + range (int or None): Distance between two objects or None if not applicable + """ + # Return None if not applicable. + if not obj1.db.combat_range: + return None + if not obj2.db.combat_range: + return None + if obj1 not in obj2.db.combat_range: + return None + if obj2 not in obj1.db.combat_range: + return None + # Return the range between the two objects. + return obj1.db.combat_range[obj2] - Args: - attacker (obj): Character doing the attacking - defender (obj): Character being attacked - attack_type (str): Type of attack ('melee' or 'ranged') + def distance_inc(self, mover, target): + """ + Function that increases distance in range field between mover and target. - Returns: - attack_value (int): Attack roll value, compared against a defense value - to determine whether an attack hits or misses. + Args: + mover (obj): The object moving + target (obj): The object to be moved away from + """ + mover.db.combat_range[target] += 1 + target.db.combat_range[mover] = mover.db.combat_range[target] + # Set a cap of 2: + if self.get_range(mover, target) > 2: + target.db.combat_range[mover] = 2 + mover.db.combat_range[target] = 2 - Notes: - By default, generates a random integer from 1 to 100 without using any - properties from either the attacker or defender, and modifies the result - based on whether it's for a melee or ranged attack. - - 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) - # Make melee attacks more accurate, ranged attacks less accurate - if attack_type == "melee": - attack_value += 15 - if attack_type == "ranged": - attack_value -= 15 - return attack_value - - -def get_defense(attacker, defender, attack_type): - """ - 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 - attack_type (str): Type of attack ('melee' or 'ranged') - - 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_type, 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 - attack_type (str): Type of attack (melee or ranged) - - 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, attack_type) - # Get a defense value from the defender. - if not defense_value: - defense_value = get_defense(attacker, defender, attack_type) - # If the attack value is lower than the defense value, miss. Otherwise, hit. - if attack_value < defense_value: - attacker.location.msg_contents( - "%s's %s attack misses %s!" % (attacker, attack_type, defender) - ) - else: - damage_value = get_damage(attacker, defender) # Calculate damage value. - # Announce damage dealt and apply damage. - attacker.location.msg_contents( - "%s hits %s with a %s attack for %i damage!" - % (attacker, defender, attack_type, 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 get_range(obj1, obj2): - """ - Gets the combat range between two objects. - - Args: - obj1 (obj): First object - obj2 (obj): Second object - - Returns: - range (int or None): Distance between two objects or None if not applicable - """ - # Return None if not applicable. - if not obj1.db.combat_range: - return None - if not obj2.db.combat_range: - return None - if obj1 not in obj2.db.combat_range: - return None - if obj2 not in obj1.db.combat_range: - return None - # Return the range between the two objects. - return obj1.db.combat_range[obj2] - - -def distance_inc(mover, target): - """ - Function that increases distance in range field between mover and target. - - Args: - mover (obj): The object moving - target (obj): The object to be moved away from - """ - mover.db.combat_range[target] += 1 - target.db.combat_range[mover] = mover.db.combat_range[target] - # Set a cap of 2: - if get_range(mover, target) > 2: - target.db.combat_range[mover] = 2 - mover.db.combat_range[target] = 2 - - -def approach(mover, target): - """ - Manages a character's whole approach, including changes in ranges to other characters. - - Args: - mover (obj): The object moving - target (obj): The object to be moved toward - - Notes: - The mover will also automatically move toward any objects that are closer to the - target than the mover is. The mover will also move away from anything they started - out close to. - """ - - def distance_dec(mover, target): + def _distance_dec(self, mover, target): """ Helper function that decreases distance in range field between mover and target. @@ -362,167 +203,115 @@ def approach(mover, target): mover.db.combat_range[target] -= 1 target.db.combat_range[mover] = mover.db.combat_range[target] # If this brings mover to range 0 (Engaged): - if get_range(mover, target) <= 0: + if self.get_range(mover, target) <= 0: # Reset range to each other to 0 and copy target's ranges to mover. target.db.combat_range[mover] = 0 mover.db.combat_range = target.db.combat_range - # Assure everything else has the same distance from the mover and target, now that they're together + # Assure everything else has the same distance from the mover and target, now that + # they're together for thing in mover.location.contents: if thing != mover and thing != target: thing.db.combat_range[mover] = thing.db.combat_range[target] - contents = mover.location.contents + def approach(self, mover, target): + """ + Manages a character's whole approach, including changes in ranges to other characters. - for thing in contents: - if thing != mover and thing != target: - # Move closer to each object closer to the target than you. - if get_range(mover, thing) > get_range(target, thing): - distance_dec(mover, thing) - # Move further from each object that's further from you than from the target. - if get_range(mover, thing) < get_range(target, thing): - distance_inc(mover, thing) - # Lastly, move closer to your target. - distance_dec(mover, target) + Args: + mover (obj): The object moving + target (obj): The object to be moved toward + Notes: + The mover will also automatically move toward any objects that are closer to the + target than the mover is. The mover will also move away from anything they started + out close to. + """ -def withdraw(mover, target): - """ - Manages a character's whole withdrawal, including changes in ranges to other characters. + contents = mover.location.contents - Args: - mover (obj): The object moving - target (obj): The object to be moved away from + for thing in contents: + if thing != mover and thing != target: + # Move closer to each object closer to the target than you. + if self.get_range(mover, thing) > self.get_range(target, thing): + self._distance_dec(mover, thing) + # Move further from each object that's further from you than from the target. + if self.get_range(mover, thing) < self.get_range(target, thing): + self._distance_inc(mover, thing) + # Lastly, move closer to your target. + self._distance_dec(mover, target) - Notes: - The mover will also automatically move away from objects that are close to the target - of their withdrawl. The mover will never inadvertently move toward anything else while - withdrawing - they can be considered to be moving to open space. - """ + def withdraw(self, mover, target): + """ + Manages a character's whole withdrawal, including changes in ranges to other characters. - contents = mover.location.contents + Args: + mover (obj): The object moving + target (obj): The object to be moved away from - for thing in contents: - if thing != mover and thing != target: - # Move away from each object closer to the target than you, if it's also closer to you than you are to the target. - if get_range(mover, thing) >= get_range(target, thing) and get_range( - mover, thing - ) < get_range(mover, target): - distance_inc(mover, thing) - # Move away from anything your target is engaged with - if get_range(target, thing) == 0: - distance_inc(mover, thing) - # Move away from anything you're engaged with. - if get_range(mover, thing) == 0: - distance_inc(mover, thing) - # Then, move away from your target. - distance_inc(mover, target) + Notes: + The mover will also automatically move away from objects that are close to the target + of their withdrawl. The mover will never inadvertently move toward anything else while + withdrawing - they can be considered to be moving to open space. + """ + contents = mover.location.contents -def combat_cleanup(character): - """ - Cleans up all the temporary combat-related attributes on a character. + for thing in contents: + if thing != mover and thing != target: + # Move away from each object closer to the target than you, if it's also closer to + # you than you are to the target. + if (self.get_range(mover, thing) >= self.get_range(target, thing) + and self.get_range(mover, thing) < self.get_range(mover, target)): + self._distance_inc(mover, thing) + # Move away from anything your target is engaged with + if self.get_range(target, thing) == 0: + self._distance_inc(mover, thing) + # Move away from anything you're engaged with. + if self.get_range(mover, thing) == 0: + self._distance_inc(mover, thing) + # Then, move away from your target. + self._distance_inc(mover, target) - Args: - character (obj): Character to have their combat attributes removed + def combat_status_message(self, fighter): + """ + Sends a message to a player with their current HP and + distances to other fighters and objects. Called at turn + start and by the 'status' command. + """ + if not fighter.db.max_hp: + fighter.db.hp = 100 + fighter.db.max_hp = 100 - 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! + status_msg = "HP Remaining: %i / %i" % (fighter.db.hp, fighter.db.max_hp) + if not self.is_in_combat(fighter): + fighter.msg(status_msg) + return -def is_in_combat(character): - """ - Returns true if the given character is in combat. + engaged_obj = [] + reach_obj = [] + range_obj = [] - Args: - character (obj): Character to determine if is in combat or not + for thing in fighter.db.combat_range: + if thing != fighter: + if fighter.db.combat_range[thing] == 0: + engaged_obj.append(thing) + if fighter.db.combat_range[thing] == 1: + reach_obj.append(thing) + if fighter.db.combat_range[thing] > 1: + range_obj.append(thing) - Returns: - (bool): True if in combat or False if not in combat - """ - return bool(character.db.combat_turnhandler) + if engaged_obj: + status_msg += "|/Engaged targets: %s" % ", ".join(obj.key for obj in engaged_obj) + if reach_obj: + status_msg += "|/Reach targets: %s" % ", ".join(obj.key for obj in reach_obj) + if range_obj: + status_msg += "|/Ranged targets: %s" % ", ".join(obj.key for obj in range_obj) - -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 - - Keyword Args: - 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. - - -def combat_status_message(fighter): - """ - Sends a message to a player with their current HP and - distances to other fighters and objects. Called at turn - start and by the 'status' command. - """ - if not fighter.db.max_hp: - fighter.db.hp = 100 - fighter.db.max_hp = 100 - - status_msg = "HP Remaining: %i / %i" % (fighter.db.hp, fighter.db.max_hp) - - if not is_in_combat(fighter): fighter.msg(status_msg) - return - engaged_obj = [] - reach_obj = [] - range_obj = [] - - for thing in fighter.db.combat_range: - if thing != fighter: - if fighter.db.combat_range[thing] == 0: - engaged_obj.append(thing) - if fighter.db.combat_range[thing] == 1: - reach_obj.append(thing) - if fighter.db.combat_range[thing] > 1: - range_obj.append(thing) - - if engaged_obj: - status_msg += "|/Engaged targets: %s" % ", ".join(obj.key for obj in engaged_obj) - if reach_obj: - status_msg += "|/Reach targets: %s" % ", ".join(obj.key for obj in reach_obj) - if range_obj: - status_msg += "|/Ranged targets: %s" % ", ".join(obj.key for obj in range_obj) - - fighter.msg(status_msg) +COMBAT_RULES = RangedCombatRules() """ ---------------------------------------------------------------------------- @@ -544,74 +333,7 @@ class TBRangeTurnHandler(DefaultScript): 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 - - # Initialize range field for all objects in the room - for thing in self.obj.contents: - self.init_range(thing) - - # 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 thing in self.obj.contents: - combat_cleanup(thing) # Clean up the combat attributes for every object in the room. - 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 + rules = COMBAT_RULES def init_range(self, to_init): """ @@ -663,23 +385,7 @@ class TBRangeTurnHandler(DefaultScript): to_init.db.combat_range.update({to_init: 0}) # Add additional distance from anchor object, if any. for n in range(add_distance): - withdraw(to_init, anchor_obj) - - 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 + self.rules.withdraw(to_init, anchor_obj) def start_turn(self, character): """ @@ -694,64 +400,7 @@ class TBRangeTurnHandler(DefaultScript): characters to both move and attack in the same turn (or, alternately, move twice or attack twice). """ - 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!|n") - combat_status_message(character) - - 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 + super().start_turn() def join_fight(self, character): """ @@ -778,51 +427,12 @@ TYPECLASSES START HERE """ -class TBRangeCharacter(DefaultCharacter): +class TBRangeCharacter(tb_basic.TBBasicCharacter): """ 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_pre_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 + rules = COMBAT_RULES class TBRangeObject(DefaultObject): @@ -852,7 +462,7 @@ class TBRangeObject(DefaultObject): """ # Can't drop something if in combat and it's not your turn - if is_in_combat(dropper) and not is_turn(dropper): + if self.rules.is_in_combat(dropper) and not self.rules.is_turn(dropper): dropper.msg("You can only drop things on your turn!") return False return True @@ -896,11 +506,11 @@ class TBRangeObject(DefaultObject): before it is even started. """ # Restrictions for getting in combat - if is_in_combat(getter): - if not is_turn(getter): # Not your turn + if self.rules.is_in_combat(getter): + if not self.rules.is_turn(getter): # Not your turn getter.msg("You can only get things on your turn!") return False - if get_range(self, getter) > 0: # Too far away + if self.rules.get_range(self, getter) > 0: # Too far away getter.msg("You aren't close enough to get that! (see: help approach)") return False return True @@ -929,8 +539,8 @@ class TBRangeObject(DefaultObject): if self in thing.db.combat_range: thing.db.combat_range.pop(self, None) # If in combat, getter spends an action - if is_in_combat(getter): - spend_action(getter, 1, action_name="get") # Use up one action. + if self.rules.is_in_combat(getter): + self.rules.spend_action(getter, 1, action_name="get") # Use up one action. def at_pre_give(self, giver, getter): """ @@ -952,11 +562,11 @@ class TBRangeObject(DefaultObject): """ # Restrictions for giving in combat - if is_in_combat(giver): - if not is_turn(giver): # Not your turn + if self.rules.is_in_combat(giver): + if not self.rules.is_turn(giver): # Not your turn giver.msg("You can only give things on your turn!") return False - if get_range(giver, getter) > 0: # Too far away from target + if self.rules.get_range(giver, getter) > 0: # Too far away from target giver.msg( "You aren't close enough to give things to %s! (see: help approach)" % getter ) @@ -980,8 +590,8 @@ class TBRangeObject(DefaultObject): """ # Spend an action if in combat - if is_in_combat(giver): - spend_action(giver, 1, action_name="give") # Use up one action. + if self.rules.is_in_combat(giver): + self.rules.spend_action(giver, 1, action_name="give") # Use up one action. """ @@ -991,7 +601,7 @@ COMMANDS START HERE """ -class CmdFight(Command): +class CmdFight(tb_basic.CmdFight): """ Starts a fight with everyone in the same room as you. @@ -1006,36 +616,11 @@ class CmdFight(Command): 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.game_systems.turnbattle.tb_range.TBRangeTurnHandler") - # Remember you'll have to change the path to the script if you copy this code to your own modules! + rules = COMBAT_RULES + combat_handler_class = TBRangeTurnHandler -class CmdAttack(Command): +class CmdAttack(tb_basic.CmdAttack): """ Attacks another character in melee. @@ -1051,15 +636,17 @@ class CmdAttack(Command): key = "attack" help_category = "combat" + rules = COMBAT_RULES + 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. + if not self.rules.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. + if not self.rules.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 @@ -1081,7 +668,7 @@ class CmdAttack(Command): self.caller.msg("You can't attack yourself!") return - if not get_range(attacker, defender) == 0: # Target isn't in melee + if not self.rules.get_range(attacker, defender) == 0: # Target isn't in melee self.caller.msg( "%s is too far away to attack - you need to get closer! (see: help approach)" % defender @@ -1089,8 +676,8 @@ class CmdAttack(Command): return "If everything checks out, call the attack resolving function." - resolve_attack(attacker, defender, "melee") - spend_action(self.caller, 1, action_name="attack") # Use up one action. + self.rules.resolve_attack(attacker, defender, "melee") + self.rules.spend_action(self.caller, 1, action_name="attack") # Use up one action. class CmdShoot(Command): @@ -1110,15 +697,17 @@ class CmdShoot(Command): key = "shoot" help_category = "combat" + rules = COMBAT_RULES + 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. + if not self.rules.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. + if not self.rules.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 @@ -1144,19 +733,21 @@ class CmdShoot(Command): in_melee = [] for target in attacker.db.combat_range: # Object is engaged and has HP - if get_range(attacker, defender) == 0 and target.db.hp and target != self.caller: + if (self.rules.get_range(attacker, defender) == 0 + and target.db.hp and target != self.caller): in_melee.append(target) # Add to list of targets in melee if len(in_melee) > 0: self.caller.msg( - "You can't shoot because there are fighters engaged with you (%s) - you need to retreat! (see: help withdraw)" + "You can't shoot because there are fighters engaged with you (%s) - you need " + "to retreat! (see: help withdraw)" % ", ".join(obj.key for obj in in_melee) ) return "If everything checks out, call the attack resolving function." - resolve_attack(attacker, defender, "ranged") - spend_action(self.caller, 1, action_name="attack") # Use up one action. + self.rules.resolve_attack(attacker, defender, "ranged") + self.rules.spend_action(self.caller, 1, action_name="attack") # Use up one action. class CmdApproach(Command): @@ -1173,14 +764,16 @@ class CmdApproach(Command): key = "approach" help_category = "combat" + rules = COMBAT_RULES + def func(self): "This performs the actual command." - if not is_in_combat(self.caller): # If not in combat, can't approach. + if not self.rules.is_in_combat(self.caller): # If not in combat, can't approach. 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 approach. + if not self.rules.is_turn(self.caller): # If it's not your turn, can't approach. self.caller.msg("You can only do that on your turn.") return @@ -1202,14 +795,14 @@ class CmdApproach(Command): self.caller.msg("You can't move toward yourself!") return - if get_range(mover, target) <= 0: # Already engaged with target + if self.rules.get_range(mover, target) <= 0: # Already engaged with target self.caller.msg("You're already next to that target!") return # If everything checks out, call the approach resolving function. - approach(mover, target) + self.rules.approach(mover, target) mover.location.msg_contents("%s moves toward %s." % (mover, target)) - spend_action(self.caller, 1, action_name="move") # Use up one action. + self.rules.spend_action(self.caller, 1, action_name="move") # Use up one action. class CmdWithdraw(Command): @@ -1225,14 +818,16 @@ class CmdWithdraw(Command): key = "withdraw" help_category = "combat" + rules = COMBAT_RULES + def func(self): "This performs the actual command." - if not is_in_combat(self.caller): # If not in combat, can't withdraw. + if not self.rules.is_in_combat(self.caller): # If not in combat, can't withdraw. 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 withdraw. + if not self.rules.is_turn(self.caller): # If it's not your turn, can't withdraw. self.caller.msg("You can only do that on your turn.") return @@ -1259,12 +854,12 @@ class CmdWithdraw(Command): return # If everything checks out, call the approach resolving function. - withdraw(mover, target) + self.rules.withdraw(mover, target) mover.location.msg_contents("%s moves away from %s." % (mover, target)) - spend_action(self.caller, 1, action_name="move") # Use up one action. + self.rules.spend_action(self.caller, 1, action_name="move") # Use up one action. -class CmdPass(Command): +class CmdPass(tb_basic.CmdPass): """ Passes on your turn. @@ -1279,25 +874,10 @@ class CmdPass(Command): 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. + rules = COMBAT_RULES -class CmdDisengage(Command): +class CmdDisengage(tb_basic.CmdDisengage): """ Passes your turn and attempts to end combat. @@ -1313,27 +893,10 @@ class CmdDisengage(Command): 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. - """ + rules = COMBAT_RULES -class CmdRest(Command): +class CmdRest(tb_basic.CmdRest): """ Recovers damage. @@ -1347,18 +910,7 @@ class CmdRest(Command): 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. - """ + rules = COMBAT_RULES class CmdStatus(Command): @@ -1375,12 +927,14 @@ class CmdStatus(Command): key = "status" help_category = "combat" + rules = COMBAT_RULES + def func(self): "This performs the actual command." - combat_status_message(self.caller) + self.rules.combat_status_message(self.caller) -class CmdCombatHelp(CmdHelp): +class CmdCombatHelp(tb_basic.CmdCombatHelp): """ View help or a list of topics @@ -1395,21 +949,17 @@ class CmdCombatHelp(CmdHelp): # 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 an engaged target, attempting to deal damage.|/" - + "|wShoot:|n Attack from a distance, if not engaged with other fighters.|/" - + "|wApproach:|n Move one step cloer to a target.|/" - + "|wWithdraw:|n Move one step away from a target.|/" - + "|wPass:|n Pass your turn without further action.|/" - + "|wStatus:|n View current HP and ranges to other targets.|/" - + "|wDisengage:|n End your turn and attempt to end combat.|/" - ) - else: - super().func() # Call the default help command + rules = COMBAT_RULES + combat_help_text = ( + "Available combat commands:|/" + "|wAttack:|n Attack an engaged target, attempting to deal damage.|/" + "|wShoot:|n Attack from a distance, if not engaged with other fighters.|/" + "|wApproach:|n Move one step cloer to a target.|/" + "|wWithdraw:|n Move one step away from a target.|/" + "|wPass:|n Pass your turn without further action.|/" + "|wStatus:|n View current HP and ranges to other targets.|/" + "|wDisengage:|n End your turn and attempt to end combat.|/" + ) class BattleCmdSet(default_cmds.CharacterCmdSet):