From 6cbd9984d71eb08a33340208857a6c2411144fc6 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sat, 14 Oct 2017 22:31:10 -0700 Subject: [PATCH 01/17] Delete turnbattle.py Now that this system has been moved to /turnbattle/, this file is obsolete. --- evennia/contrib/turnbattle.py | 735 ---------------------------------- 1 file changed, 735 deletions(-) delete mode 100644 evennia/contrib/turnbattle.py diff --git a/evennia/contrib/turnbattle.py b/evennia/contrib/turnbattle.py deleted file mode 100644 index 692656bc3a..0000000000 --- a/evennia/contrib/turnbattle.py +++ /dev/null @@ -1,735 +0,0 @@ -""" -Simple turn-based combat system - -Contrib - Tim Ashley Jenkins 2017 - -This is a framework for a simple turn-based combat system, similar -to those used in D&D-style tabletop role playing games. It allows -any character to start a fight in a room, at which point initiative -is rolled and a turn order is established. Each participant in combat -has a limited time to decide their action for that turn (30 seconds by -default), and combat progresses through the turn order, looping through -the participants until the fight ends. - -Only simple rolls for attacking are implemented here, but this system -is easily extensible and can be used as the foundation for implementing -the rules from your turn-based tabletop game of choice or making your -own battle system. - -To install and test, import this module's BattleCharacter object into -your game's character.py module: - - from evennia.contrib.turnbattle import BattleCharacter - -And change your game's character typeclass to inherit from BattleCharacter -instead of the default: - - class Character(BattleCharacter): - -Next, import this module into your default_cmdsets.py module: - - from evennia.contrib import turnbattle - -And add the battle command set to your default command set: - - # - # any commands you add below will overload the default ones. - # - self.add(turnbattle.BattleCmdSet()) - -This module is meant to be heavily expanded on, so you may want to copy it -to your game's 'world' folder and modify it there rather than importing it -in your game and using it as-is. -""" - -from random import randint -from evennia import DefaultCharacter, Command, default_cmds, DefaultScript -from evennia.commands.default.help import CmdHelp - -""" ----------------------------------------------------------------------------- -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 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, announce defeat. - if defender.db.hp <= 0: - attacker.location.msg_contents("%s has been defeated!" % 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 - """ - if character.db.Combat_TurnHandler: - return True - return False - - -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] - if character == currentchar: - return True - return False - - -def spend_action(character, actions, action_name=None): - """ - Spends a character's available combat actions and checks for end of turn. - - Args: - character (obj): Character spending the action - actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions - - Kwargs: - action_name (str or None): If a string is given, sets character's last action in - combat to provided string - """ - if action_name: - character.db.Combat_LastAction = action_name - if actions == 'all': # If spending all actions - character.db.Combat_ActionsLeft = 0 # Set actions to 0 - else: - character.db.Combat_ActionsLeft -= actions # Use up actions. - if character.db.Combat_ActionsLeft < 0: - character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions - character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn. - - -""" ----------------------------------------------------------------------------- -CHARACTER TYPECLASS ----------------------------------------------------------------------------- -""" - - -class BattleCharacter(DefaultCharacter): - """ - A character able to participate in turn-based combat. Has attributes for current - and maximum HP, and access to combat commands. - """ - - def at_object_creation(self): - """ - Called once, when this object is first created. This is the - normal hook to overload for most object types. - """ - self.db.max_hp = 100 # Set maximum HP to 100 - self.db.hp = self.db.max_hp # Set current HP to maximum - """ - Adds attributes for a character's current and maximum HP. - We're just going to set this value at '100' by default. - - You may want to expand this to include various 'stats' that - can be changed at creation and factor into combat calculations. - """ - - def at_before_move(self, destination): - """ - Called just before starting to move this object to - destination. - - Args: - destination (Object): The object we are moving to - - Returns: - shouldmove (bool): If we should move or not. - - Notes: - If this method returns False/None, the move is cancelled - before it is even started. - - """ - # Keep the character from moving if at 0 HP or in combat. - if is_in_combat(self): - self.msg("You can't exit a room while in combat!") - return False # Returning false keeps the character from moving. - if self.db.HP <= 0: - self.msg("You can't move, you've been defeated!") - return False - return True - - -""" ----------------------------------------------------------------------------- -COMMANDS START HERE ----------------------------------------------------------------------------- -""" - - -class CmdFight(Command): - """ - Starts a fight with everyone in the same room as you. - - Usage: - fight - - When you start a fight, everyone in the room who is able to - fight is added to combat, and a turn order is randomly rolled. - When it's your turn, you can attack other characters. - """ - key = "fight" - help_category = "combat" - - def func(self): - """ - This performs the actual command. - """ - here = self.caller.location - fighters = [] - - if not self.caller.db.hp: # If you don't have any hp - self.caller.msg("You can't start a fight if you've been defeated!") - return - if is_in_combat(self.caller): # Already in a fight - self.caller.msg("You're already in a fight!") - return - for thing in here.contents: # Test everything in the room to add it to the fight. - if thing.db.HP: # If the object has HP... - fighters.append(thing) # ...then add it to the fight. - if len(fighters) <= 1: # If you're the only able fighter in the room - self.caller.msg("There's nobody here to fight!") - return - if here.db.Combat_TurnHandler: # If there's already a fight going on... - here.msg_contents("%s joins the fight!" % self.caller) - here.db.Combat_TurnHandler.join_fight(self.caller) # Join the fight! - return - here.msg_contents("%s starts a fight!" % self.caller) - # Add a turn handler script to the room, which starts combat. - here.scripts.add("contrib.turnbattle.TurnHandler") - # Remember you'll have to change the path to the script if you copy this code to your own modules! - - -class CmdAttack(Command): - """ - Attacks another character. - - Usage: - attack - - When in a fight, you may attack another character. The attack has - a chance to hit, and if successful, will deal damage. - """ - - key = "attack" - help_category = "combat" - - def func(self): - "This performs the actual command." - "Set the attacker to the caller and the defender to the target." - - if not is_in_combat(self.caller): # If not in combat, can't attack. - self.caller.msg("You can only do that in combat. (see: help fight)") - return - - if not is_turn(self.caller): # If it's not your turn, can't attack. - self.caller.msg("You can only do that on your turn.") - return - - if not self.caller.db.hp: # Can't attack if you have no HP. - self.caller.msg("You can't attack, you've been defeated.") - return - - attacker = self.caller - defender = self.caller.search(self.args) - - if not defender: # No valid target given. - return - - if not defender.db.hp: # Target object has no HP left or to begin with - self.caller.msg("You can't fight that!") - return - - if attacker == defender: # Target and attacker are the same - self.caller.msg("You can't attack yourself!") - return - - "If everything checks out, call the attack resolving function." - resolve_attack(attacker, defender) - spend_action(self.caller, 1, action_name="attack") # Use up one action. - - -class CmdPass(Command): - """ - Passes on your turn. - - Usage: - pass - - When in a fight, you can use this command to end your turn early, even - if there are still any actions you can take. - """ - - key = "pass" - aliases = ["wait", "hold"] - help_category = "combat" - - def func(self): - """ - This performs the actual command. - """ - if not is_in_combat(self.caller): # Can only pass a turn in combat. - self.caller.msg("You can only do that in combat. (see: help fight)") - return - - if not is_turn(self.caller): # Can only pass if it's your turn. - self.caller.msg("You can only do that on your turn.") - return - - self.caller.location.msg_contents("%s takes no further action, passing the turn." % self.caller) - spend_action(self.caller, 'all', action_name="pass") # Spend all remaining actions. - - -class CmdDisengage(Command): - """ - Passes your turn and attempts to end combat. - - Usage: - disengage - - Ends your turn early and signals that you're trying to end - the fight. If all participants in a fight disengage, the - fight ends. - """ - - key = "disengage" - aliases = ["spare"] - help_category = "combat" - - def func(self): - """ - This performs the actual command. - """ - if not is_in_combat(self.caller): # If you're not in combat - self.caller.msg("You can only do that in combat. (see: help fight)") - return - - if not is_turn(self.caller): # If it's not your turn - self.caller.msg("You can only do that on your turn.") - return - - self.caller.location.msg_contents("%s disengages, ready to stop fighting." % self.caller) - spend_action(self.caller, 'all', action_name="disengage") # Spend all remaining actions. - """ - The action_name kwarg sets the character's last action to "disengage", which is checked by - the turn handler script to see if all fighters have disengaged. - """ - - -class CmdRest(Command): - """ - Recovers damage. - - Usage: - rest - - Resting recovers your HP to its maximum, but you can only - rest if you're not in a fight. - """ - - key = "rest" - help_category = "combat" - - def func(self): - "This performs the actual command." - - if is_in_combat(self.caller): # If you're in combat - self.caller.msg("You can't rest while you're in combat.") - return - - self.caller.db.hp = self.caller.db.max_hp # Set current HP to maximum - self.caller.location.msg_contents("%s rests to recover HP." % self.caller) - """ - You'll probably want to replace this with your own system for recovering HP. - """ - - -class CmdCombatHelp(CmdHelp): - """ - View help or a list of topics - - Usage: - help - help list - help all - - This will search for help on commands and other - topics related to the game. - """ - # Just like the default help command, but will give quick - # tips on combat when used in a fight with no arguments. - - def func(self): - if is_in_combat(self.caller) and not self.args: # In combat and entered 'help' alone - self.caller.msg("Available combat commands:|/" + - "|wAttack:|n Attack a target, attempting to deal damage.|/" + - "|wPass:|n Pass your turn without further action.|/" + - "|wDisengage:|n End your turn and attempt to end combat.|/") - else: - super(CmdCombatHelp, self).func() # Call the default help command - - -class BattleCmdSet(default_cmds.CharacterCmdSet): - """ - This command set includes all the commmands used in the battle system. - """ - key = "DefaultCharacter" - - def at_cmdset_creation(self): - """ - Populates the cmdset - """ - self.add(CmdFight()) - self.add(CmdAttack()) - self.add(CmdRest()) - self.add(CmdPass()) - self.add(CmdDisengage()) - self.add(CmdCombatHelp()) - - -""" ----------------------------------------------------------------------------- -SCRIPTS START HERE ----------------------------------------------------------------------------- -""" - - -class TurnHandler(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 object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) - - # 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)) - - # Set up the current turn and turn timeout delay. - self.db.turn = 0 - self.db.timer = 30 # 30 seconds - - 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 = 1 # 1 action per turn. - # 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 = 30 + 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) From d150d191e9b2a490e9d3124e21d4d7ccf3c55790 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sat, 14 Oct 2017 22:34:18 -0700 Subject: [PATCH 02/17] Add tb_range - movement & positioning for 'turnbattle' Adds a system for range and movement to the 'turnbattle' contrib. This is based on the abstract movement and positioning system I made for 'The World of Cool Battles', my learning project - fighters' absolute positions are not tracked, only their relative distance to each other and other objects. Commands for movement as well as distinction between melee and ranged attacks are included. --- evennia/contrib/turnbattle/tb_range.py | 1356 ++++++++++++++++++++++++ 1 file changed, 1356 insertions(+) create mode 100644 evennia/contrib/turnbattle/tb_range.py diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py new file mode 100644 index 0000000000..3061d8c873 --- /dev/null +++ b/evennia/contrib/turnbattle/tb_range.py @@ -0,0 +1,1356 @@ +""" +Simple turn-based combat system with range and movement + +Contrib - Tim Ashley Jenkins 2017 + +This is a version of the 'turnbattle' contrib that includes a system +for abstract movement and positioning in combat, including distinction +between melee and ranged attacks. In this system, a fighter or object's +exact position is not recorded - only their relative distance to other +actors in combat. + +In this example, the distance between two objects in combat is expressed +as an integer value: 0 for "engaged" objects that are right next to each +other, 1 for "reach" which is for objects that are near each other but +not directly adjacent, and 2 for "range" for objects that are far apart. + +When combat starts, all fighters are at reach with each other and other +objects, and at range from any exits. On a fighter's turn, they can use +the "approach" command to move closer to an object, or the "withdraw" +command to move further away from an object, either of which takes an +action in combat. In this example, fighters are given two actions per +turn, allowing them to move and attack in the same round, or to attack +twice or move twice. + +When you move toward an object, you will also move toward anything else +that's close to your target - the same goes for moving away from a target, +which will also move you away from anything close to your target. Moving +toward one target may also move you away from anything you're already +close to, but withdrawing from a target will never inadvertently bring +you closer to anything else. + +In this example, there are two attack commands. 'Attack' can only hit +targets that are 'engaged' (range 0) with you. 'Shoot' can hit any target +on the field, but cannot be used if you are engaged with any other fighters. +In addition, strikes made with the 'attack' command are more accurate than +'shoot' attacks. This is only to provide an example of how melee and ranged +attacks can be made to work differently - you can, of course, modify this +to fit your rules system. + +When in combat, the ranges of objects are also accounted for - you can't +pick up an object unless you're engaged with it, and can't give an object +to another fighter without being engaged with them either. Dropped objects +are automatically assigned a range of 'engaged' with the fighter who dropped +them. Additionally, giving or getting an object will take an action in combat. +Dropping an object does not take an action, but can only be done on your turn. + +When combat ends, all range values are erased and all restrictions on getting +or getting objects are lifted - distances are no longer tracked and objects in +the same room can be considered to be in the same space, as is the default +behavior of Evennia and most MUDs. + +This system allows for strategies in combat involving movement and +positioning to be implemented in your battle system without the use of +a 'grid' of coordinates, which can be difficult and clunky to navigate +in text and disadvantageous to players who use screen readers. This loose, +narrative method of tracking position is based around how the matter is +handled in tabletop RPGs played without a grid - typically, a character's +exact position in a room isn't important, only their relative distance to +other actors. + +You may wish to expand this system with a method of distinguishing allies +from enemies (to prevent allied characters from blocking your ranged attacks) +as well as some method by which melee-focused characters can prevent enemies +from withdrawing or punish them from doing so, such as by granting "attacks of +opportunity" or something similar. If you wish, you can also expand the breadth +of values allowed for range - rather than just 0, 1, and 2, you can allow ranges +to go up to much higher values, and give attacks and movements more varying +values for distance for a more granular system. You may also want to implement +a system for fleeing or changing rooms in combat by approaching exits, which +are objects placed in the range field like any other. + +To install and test, import this module's TBRangeCharacter object into +your game's character.py module: + + from evennia.contrib.turnbattle.tb_range import TBRangeCharacter + +And change your game's character typeclass to inherit from TBRangeCharacter +instead of the default: + + class Character(TBRangeCharacter): + +Next, import this module into your default_cmdsets.py module: + + from evennia.contrib.turnbattle import tb_range + +And add the battle command set to your default command set: + + # + # any commands you add below will overload the default ones. + # + self.add(tb_range.BattleCmdSet()) + +This module is meant to be heavily expanded on, so you may want to copy it +to your game's 'world' folder and modify it there rather than importing it +in your game and using it as-is. +""" + +from random import randint +from evennia import DefaultCharacter, DefaultObject, Command, default_cmds, DefaultScript +from evennia.commands.default.help import CmdHelp + +""" +---------------------------------------------------------------------------- +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, attack_type): + """ + Returns a value for an attack roll. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + attack_type (str): Type of attack ('melee' or 'ranged') + + Returns: + attack_value (int): Attack roll value, compared against a defense value + to determine whether an attack hits or misses. + + 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 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, announce defeat. + if defender.db.hp <= 0: + attacker.location.msg_contents("%s has been defeated!" % defender) + +def distance_dec(mover, target): + """ + Decreases distance in range field between mover and target. + + Args: + mover (obj): The object moving + target (obj): The object to be moved toward + """ + 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 mover.db.Combat_Range[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 + for object in mover.location.contents: + if object != mover and object != target: + object.db.Combat_Range[mover] = object.db.Combat_Range[target] + +def distance_inc(mover, target): + """ + 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 mover.db.Combat_Range[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. + """ + objects = mover.location.contents + + for thing in objects: + if thing != mover and thing != target: + # Move closer to each object closer to the target than you. + if mover.db.Combat_Range[thing] > target.db.Combat_Range[thing]: + distance_dec(mover, thing) + # Move further from each object that's further from you than from the target. + if mover.db.Combat_Range[thing] < target.db.Combat_Range[thing]: + distance_inc(mover, thing) + # Lastly, move closer to your target. + distance_dec(mover, target) + return + +def withdraw(mover, target): + """ + Manages a character's whole withdrawal, including changes in ranges to other characters. + + Args: + mover (obj): The object moving + target (obj): The object to be moved away from + + 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. + """ + objects = mover.location.contents + for thing in objects: + 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 mover.db.Combat_Range[thing] >= target.db.Combat_Range[thing] and mover.db.Combat_Range[thing] < mover.db.Combat_Range[thing]: + distance_inc(mover, thing) + # Move away from anything your target is engaged with + if target.db.Combat_Range[thing] == 0: + distance_inc(mover, thing) + # Move away from anything you're engaged with. + if mover.db.Combat_Range[thing] == 0: + distance_inc(mover, thing) + # Then, move away from your target. + distance_inc(mover, target) + return + +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 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 + """ + if character.db.Combat_TurnHandler: + return True + return False + + +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] + if character == currentchar: + return True + return False + + +def spend_action(character, actions, action_name=None): + """ + Spends a character's available combat actions and checks for end of turn. + + Args: + character (obj): Character spending the action + actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions + + Kwargs: + action_name (str or None): If a string is given, sets character's last action in + combat to provided string + """ + if action_name: + character.db.Combat_LastAction = action_name + if actions == 'all': # If spending all actions + character.db.Combat_ActionsLeft = 0 # Set actions to 0 + else: + character.db.Combat_ActionsLeft -= actions # Use up actions. + if character.db.Combat_ActionsLeft < 0: + character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions + character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn. + +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. + """ + + 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 object in fighter.db.Combat_Range: + if object != fighter: + if fighter.db.Combat_Range[object] == 0: + engaged_obj.append(object) + if fighter.db.Combat_Range[object] == 1: + reach_obj.append(object) + if fighter.db.Combat_Range[object] > 1: + range_obj.append(object) + + 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) + return + + + +""" +---------------------------------------------------------------------------- +TYPECLASSES START HERE +---------------------------------------------------------------------------- +""" + + +class TBRangeCharacter(DefaultCharacter): + """ + A character able to participate in turn-based combat. Has attributes for current + and maximum HP, and access to combat commands. + """ + + def at_object_creation(self): + """ + Called once, when this object is first created. This is the + normal hook to overload for most object types. + """ + self.db.max_hp = 100 # Set maximum HP to 100 + self.db.hp = self.db.max_hp # Set current HP to maximum + """ + Adds attributes for a character's current and maximum HP. + We're just going to set this value at '100' by default. + + You may want to expand this to include various 'stats' that + can be changed at creation and factor into combat calculations. + """ + + def at_before_move(self, destination): + """ + Called just before starting to move this object to + destination. + + Args: + destination (Object): The object we are moving to + + Returns: + shouldmove (bool): If we should move or not. + + Notes: + If this method returns False/None, the move is cancelled + before it is even started. + + """ + # Keep the character from moving if at 0 HP or in combat. + if is_in_combat(self): + self.msg("You can't exit a room while in combat!") + return False # Returning false keeps the character from moving. + if self.db.HP <= 0: + self.msg("You can't move, you've been defeated!") + return False + return True + +class TBRangeObject(DefaultObject): + """ + An object that is assigned range values in combat. Getting, giving, and dropping + the object has restrictions in combat - you must be next to an object to get it, + must be next to your target to give them something, and can only interact with + objects on your own turn. + """ + def at_before_drop(self, dropper): + """ + Called by the default `drop` command before this object has been + dropped. + + Args: + dropper (Object): The object which will drop this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Returns: + shoulddrop (bool): If the object should be dropped or not. + + Notes: + If this method returns False/None, the dropping is cancelled + before it is even started. + + """ + # Can't drop something if in combat and it's not your turn + if is_in_combat(dropper) and not is_turn(dropper): + dropper.msg("You can only drop things on your turn!") + return False + return True + def at_drop(self, dropper): + """ + Called by the default `drop` command when this object has been + dropped. + + Args: + dropper (Object): The object which just dropped this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Notes: + This hook cannot stop the drop from happening. Use + permissions or the at_before_drop() hook for that. + + """ + # If dropper is currently in combat + if dropper.location.db.Combat_TurnHandler: + # Object joins the range field + self.db.Combat_Range = {} + dropper.location.db.Combat_TurnHandler.join_rangefield(self, anchor_obj=dropper) + def at_before_get(self, getter): + """ + Called by the default `get` command before this object has been + picked up. + + Args: + getter (Object): The object about to get this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Returns: + shouldget (bool): If the object should be gotten or not. + + Notes: + If this method returns False/None, the getting is cancelled + before it is even started. + """ + # Restrictions for getting in combat + if is_in_combat(getter): + if not 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 + getter.msg("You aren't close enough to get that! (see: help approach)") + return False + return True + def at_get(self, getter): + """ + Called by the default `get` command when this object has been + picked up. + + Args: + getter (Object): The object getting this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Notes: + This hook cannot stop the pickup from happening. Use + permissions or the at_before_get() hook for that. + + """ + # If gotten, erase range values + if self.db.Combat_Range: + del self.db.Combat_Range + # Remove this object from everyone's range fields + for object in getter.location.contents: + if object.db.Combat_Range: + if self in object.db.Combat_Range: + object.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. + def at_before_give(self, giver, getter): + """ + Called by the default `give` command before this object has been + given. + + Args: + giver (Object): The object about to give this object. + getter (Object): The object about to get this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Returns: + shouldgive (bool): If the object should be given or not. + + Notes: + If this method returns False/None, the giving is cancelled + before it is even started. + + """ + # Restrictions for giving in combat + if is_in_combat(giver): + if not 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 + giver.msg("You aren't close enough to give things to %s! (see: help approach)" % getter) + return False + return True + def at_give(self, giver, getter): + """ + Called by the default `give` command when this object has been + given. + + Args: + giver (Object): The object giving this object. + getter (Object): The object getting this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Notes: + This hook cannot stop the give from happening. Use + permissions or the at_before_give() hook for that. + + """ + # Spend an action if in combat + if is_in_combat(giver): + spend_action(giver, 1, action_name="give") # Use up one action. + +""" +---------------------------------------------------------------------------- +COMMANDS START HERE +---------------------------------------------------------------------------- +""" + + +class CmdFight(Command): + """ + Starts a fight with everyone in the same room as you. + + Usage: + fight + + When you start a fight, everyone in the room who is able to + fight is added to combat, and a turn order is randomly rolled. + When it's your turn, you can attack other characters. + """ + key = "fight" + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + here = self.caller.location + fighters = [] + + if not self.caller.db.hp: # If you don't have any hp + self.caller.msg("You can't start a fight if you've been defeated!") + return + if is_in_combat(self.caller): # Already in a fight + self.caller.msg("You're already in a fight!") + return + for thing in here.contents: # Test everything in the room to add it to the fight. + if thing.db.HP: # If the object has HP... + fighters.append(thing) # ...then add it to the fight. + if len(fighters) <= 1: # If you're the only able fighter in the room + self.caller.msg("There's nobody here to fight!") + return + if here.db.Combat_TurnHandler: # If there's already a fight going on... + here.msg_contents("%s joins the fight!" % self.caller) + here.db.Combat_TurnHandler.join_fight(self.caller) # Join the fight! + return + here.msg_contents("%s starts a fight!" % self.caller) + # Add a turn handler script to the room, which starts combat. + here.scripts.add("contrib.turnbattle.tb_range.TBRangeTurnHandler") + # 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 in melee. + + Usage: + attack + + When in a fight, you may attack another character. The attack has + a chance to hit, and if successful, will deal damage. You can only + attack engaged targets - that is, targets that are right next to + you. Use the 'approach' command to get closer to a target. + """ + + 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 not 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) + 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. + +class CmdShoot(Command): + """ + Attacks another character from range. + + Usage: + shoot + + When in a fight, you may shoot another character. The attack has + a chance to hit, and if successful, will deal damage. You can attack + any target in combat by shooting, but can't shoot if there are any + targets engaged with you. Use the 'withdraw' command to retreat from + nearby enemies. + """ + + key = "shoot" + 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 + + # Test to see if there are any nearby enemy targets. + 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: + 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)" % ", ".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. + +class CmdApproach(Command): + """ + Approaches an object. + + Usage: + approach + + Move one space toward a character or object. You can only attack + characters you are 0 spaces away from. + """ + + key = "approach" + help_category = "combat" + + def func(self): + "This performs the actual command." + + if not 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. + self.caller.msg("You can only do that on your turn.") + return + + if not self.caller.db.hp: # Can't approach if you have no HP. + self.caller.msg("You can't move, you've been defeated.") + return + + mover = self.caller + target = self.caller.search(self.args) + + if not target: # No valid target given. + return + + if not target.db.Combat_Range: # Target object is not on the range field + self.caller.msg("You can't move toward that!") + return + + if mover == target: # Target and mover are the same + self.caller.msg("You can't move toward yourself!") + return + + if 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) + mover.location.msg_contents("%s moves toward %s." % (mover, target)) + spend_action(self.caller, 1, action_name="move") # Use up one action. + +class CmdWithdraw(Command): + """ + Moves away from an object. + + Usage: + withdraw + + Move one space away from a character or object. + """ + + key = "withdraw" + help_category = "combat" + + def func(self): + "This performs the actual command." + + if not 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. + self.caller.msg("You can only do that on your turn.") + return + + if not self.caller.db.hp: # Can't withdraw if you have no HP. + self.caller.msg("You can't move, you've been defeated.") + return + + mover = self.caller + target = self.caller.search(self.args) + + if not target: # No valid target given. + return + + if not target.db.Combat_Range: # Target object is not on the range field + self.caller.msg("You can't move away from that!") + return + + if mover == target: # Target and mover are the same + self.caller.msg("You can't move away from yourself!") + return + + if mover.db.Combat_Range[target] >= 3: # Already at maximum distance + self.caller.msg("You're as far as you can get from that target!") + return + + # If everything checks out, call the approach resolving function. + 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. + + +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 CmdStatus(Command): + """ + Gives combat information. + + Usage: + status + + Shows your current and maximum HP and your distance from + other targets in combat. + """ + + key = "status" + help_category = "combat" + + def func(self): + "This performs the actual command." + + combat_status_message(self.caller) + + +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 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(CmdCombatHelp, self).func() # Call the default help command + + +class BattleCmdSet(default_cmds.CharacterCmdSet): + """ + This command set includes all the commmands used in the battle system. + """ + key = "DefaultCharacter" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + self.add(CmdFight()) + self.add(CmdAttack()) + self.add(CmdShoot()) + self.add(CmdRest()) + self.add(CmdPass()) + self.add(CmdDisengage()) + self.add(CmdApproach()) + self.add(CmdWithdraw()) + self.add(CmdStatus()) + self.add(CmdCombatHelp()) + + +""" +---------------------------------------------------------------------------- +SCRIPTS START HERE +---------------------------------------------------------------------------- +""" + + +class TBRangeTurnHandler(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 object in self.obj.contents: + if object.db.hp: + self.db.fighters.append(object) + + # 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 object in self.obj.contents: + self.init_range(object) + + # 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 = 30 # 30 seconds + + def at_stop(self): + """ + Called at script termination. + """ + for object in self.obj.contents: + combat_cleanup(object) # 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 + + def init_range(self, to_init): + """ + Initializes range values for an object at the start of a fight. + + Args: + to_init (object): Object to initialize range field for. + """ + rangedict = {} + # Get a list of objects in the room. + objectlist = self.obj.contents + for object in objectlist: + # Object always at distance 0 from itself + if object == to_init: + rangedict.update({object:0}) + else: + if object.destination or to_init.destination: + # Start exits at range 2 to put them at the 'edges' + rangedict.update({object:2}) + else: + # Start objects at range 1 from other objects + rangedict.update({object:1}) + to_init.db.Combat_Range = rangedict + + def join_rangefield(self, to_init, anchor_obj=None, add_distance=0): + """ + Adds a new object to the range field of a fight in progress. + + Args: + to_init (object): Object to initialize range field for. + + Kwargs: + anchor_obj (object): Object to copy range values from, or None for a random object. + add_distance (int): Distance to put between to_init object and anchor object. + + """ + # Get a list of room's contents without to_init object. + contents = self.obj.contents + contents.remove(to_init) + # If no anchor object given, pick one in the room at random. + if not anchor_obj: + anchor_obj = contents[randint(0, (len(contents)-1))] + # Copy the range values from the anchor object. + to_init.db.Combat_Range = anchor_obj.db.Combat_Range + # Add the new object to everyone else's ranges. + for object in contents: + new_objects_range = object.db.Combat_Range[anchor_obj] + object.db.Combat_Range.update({to_init:new_objects_range}) + # Set the new object's range to itself to 0. + 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 + + 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: + In this example, characters are given two actions per turn. This allows + characters to both move and attack in the same turn (or, alternately, + move twice or attack twice). + """ + character.db.Combat_ActionsLeft = 2 # 2 actions per turn. + # 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 = 30 + 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) + # Add the character to the rangefield, at range from everyone, if they're not on it already. + if not character.db.Combat_Range: + self.join_rangefield(character, add_distance=2) From c762d3d7d173b200084cdd29692a47c43ef506d7 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sat, 14 Oct 2017 22:36:18 -0700 Subject: [PATCH 03/17] Update README.md Adds information about the tb_range module to the readme. --- evennia/contrib/turnbattle/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/evennia/contrib/turnbattle/README.md b/evennia/contrib/turnbattle/README.md index 8d709d1ba1..af04f80060 100644 --- a/evennia/contrib/turnbattle/README.md +++ b/evennia/contrib/turnbattle/README.md @@ -21,6 +21,11 @@ implemented and customized: the battle system, including commands for wielding weapons and donning armor, and modifiers to accuracy and damage based on currently used equipment. + + tb_range.py - Adds a system for abstract positioning and movement, which + tracks the distance between different characters and objects in + combat, as well as differentiates between melee and ranged + attacks. This system is meant as a basic framework to start from, and is modeled after the combat systems of popular tabletop role playing games rather than From 6e619d1949153e00175eff313451c63efeadb2c1 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:02:42 -0700 Subject: [PATCH 04/17] Add at_defeat hook, PEP8 capitalization --- evennia/contrib/turnbattle/tb_basic.py | 57 ++++++---- evennia/contrib/turnbattle/tb_equip.py | 56 ++++++---- evennia/contrib/turnbattle/tb_range.py | 140 ++++++++++++++----------- 3 files changed, 147 insertions(+), 106 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_basic.py b/evennia/contrib/turnbattle/tb_basic.py index 70c81debae..22a96016c8 100644 --- a/evennia/contrib/turnbattle/tb_basic.py +++ b/evennia/contrib/turnbattle/tb_basic.py @@ -167,6 +167,20 @@ def apply_damage(defender, damage): 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): """ @@ -195,10 +209,9 @@ def resolve_attack(attacker, defender, attack_value=None, defense_value=None): # 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, announce defeat. + # If defender HP is reduced to 0 or less, call at_defeat. if defender.db.hp <= 0: - attacker.location.msg_contents("%s has been defeated!" % defender) - + at_defeat(defender) def combat_cleanup(character): """ @@ -226,7 +239,7 @@ def is_in_combat(character): Returns: (bool): True if in combat or False if not in combat """ - if character.db.Combat_TurnHandler: + if character.db.combat_turnhandler: return True return False @@ -241,7 +254,7 @@ def is_turn(character): Returns: (bool): True if it is their turn or False otherwise """ - turnhandler = character.db.Combat_TurnHandler + turnhandler = character.db.combat_turnhandler currentchar = turnhandler.db.fighters[turnhandler.db.turn] if character == currentchar: return True @@ -261,14 +274,14 @@ def spend_action(character, actions, action_name=None): combat to provided string """ if action_name: - character.db.Combat_LastAction = action_name + character.db.combat_lastaction = action_name if actions == 'all': # If spending all actions - character.db.Combat_ActionsLeft = 0 # Set actions to 0 + character.db.combat_actionsleft = 0 # Set actions to 0 else: - character.db.Combat_ActionsLeft -= actions # Use up actions. - if character.db.Combat_ActionsLeft < 0: - character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions - character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn. + character.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. """ @@ -365,9 +378,9 @@ class CmdFight(Command): 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... + 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! + 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. @@ -600,7 +613,7 @@ class TBBasicTurnHandler(DefaultScript): self.initialize_for_combat(fighter) # Add a reference to this script to the room - self.obj.db.Combat_TurnHandler = self + 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. @@ -623,7 +636,7 @@ class TBBasicTurnHandler(DefaultScript): """ 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 + self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location def at_repeat(self): """ @@ -650,9 +663,9 @@ class TBBasicTurnHandler(DefaultScript): 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 + 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): """ @@ -666,10 +679,10 @@ class TBBasicTurnHandler(DefaultScript): 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 + separated for movement, by adding "character.db.combat_movesleft = 3" or something similar. """ - character.db.Combat_ActionsLeft = 1 # 1 action per turn. + character.db.combat_actionsleft = 1 # 1 action per turn. # 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) @@ -681,7 +694,7 @@ class TBBasicTurnHandler(DefaultScript): # 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 + 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!") @@ -719,7 +732,7 @@ class TBBasicTurnHandler(DefaultScript): Args: character (obj): Character to test for end of turn """ - if not character.db.Combat_ActionsLeft: # Character has no actions remaining + if not character.db.combat_actionsleft: # Character has no actions remaining self.next_turn() return diff --git a/evennia/contrib/turnbattle/tb_equip.py b/evennia/contrib/turnbattle/tb_equip.py index 7d9ea58442..b415f82428 100644 --- a/evennia/contrib/turnbattle/tb_equip.py +++ b/evennia/contrib/turnbattle/tb_equip.py @@ -203,6 +203,20 @@ def apply_damage(defender, damage): 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): """ @@ -239,9 +253,9 @@ def resolve_attack(attacker, defender, attack_value=None, defense_value=None): 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, announce defeat. + # If defender HP is reduced to 0 or less, call at_defeat. if defender.db.hp <= 0: - attacker.location.msg_contents("%s has been defeated!" % defender) + at_defeat(defender) def combat_cleanup(character): @@ -270,7 +284,7 @@ def is_in_combat(character): Returns: (bool): True if in combat or False if not in combat """ - if character.db.Combat_TurnHandler: + if character.db.combat_turnhandler: return True return False @@ -285,7 +299,7 @@ def is_turn(character): Returns: (bool): True if it is their turn or False otherwise """ - turnhandler = character.db.Combat_TurnHandler + turnhandler = character.db.combat_turnhandler currentchar = turnhandler.db.fighters[turnhandler.db.turn] if character == currentchar: return True @@ -305,14 +319,14 @@ def spend_action(character, actions, action_name=None): combat to provided string """ if action_name: - character.db.Combat_LastAction = action_name + character.db.combat_lastaction = action_name if actions == 'all': # If spending all actions - character.db.Combat_ActionsLeft = 0 # Set actions to 0 + character.db.combat_actionsleft = 0 # Set actions to 0 else: - character.db.Combat_ActionsLeft -= actions # Use up actions. - if character.db.Combat_ActionsLeft < 0: - character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions - character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn. + character.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. """ @@ -482,9 +496,9 @@ class CmdFight(Command): 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... + 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! + 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. @@ -873,7 +887,7 @@ class TBEquipTurnHandler(DefaultScript): self.initialize_for_combat(fighter) # Add a reference to this script to the room - self.obj.db.Combat_TurnHandler = self + 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. @@ -896,7 +910,7 @@ class TBEquipTurnHandler(DefaultScript): """ 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 + self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location def at_repeat(self): """ @@ -923,9 +937,9 @@ class TBEquipTurnHandler(DefaultScript): 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 + 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): """ @@ -939,10 +953,10 @@ class TBEquipTurnHandler(DefaultScript): 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 + separated for movement, by adding "character.db.combat_movesleft = 3" or something similar. """ - character.db.Combat_ActionsLeft = 1 # 1 action per turn. + character.db.combat_actionsleft = 1 # 1 action per turn. # 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) @@ -954,7 +968,7 @@ class TBEquipTurnHandler(DefaultScript): # 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 + 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!") @@ -992,7 +1006,7 @@ class TBEquipTurnHandler(DefaultScript): Args: character (obj): Character to test for end of turn """ - if not character.db.Combat_ActionsLeft: # Character has no actions remaining + if not character.db.combat_actionsleft: # Character has no actions remaining self.next_turn() return diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index 3061d8c873..27566d4d27 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -228,6 +228,20 @@ def apply_damage(defender, damage): 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): """ @@ -257,9 +271,9 @@ def resolve_attack(attacker, defender, attack_type, attack_value=None, defense_v # 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, announce defeat. + # If defender HP is reduced to 0 or less, call at_defeat. if defender.db.hp <= 0: - attacker.location.msg_contents("%s has been defeated!" % defender) + at_defeat(defender) def distance_dec(mover, target): """ @@ -269,17 +283,17 @@ def distance_dec(mover, target): mover (obj): The object moving target (obj): The object to be moved toward """ - mover.db.Combat_Range[target] -= 1 - target.db.Combat_Range[mover] = mover.db.Combat_Range[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 mover.db.Combat_Range[target] <= 0: + if mover.db.combat_range[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 + 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 for object in mover.location.contents: if object != mover and object != target: - object.db.Combat_Range[mover] = object.db.Combat_Range[target] + object.db.combat_range[mover] = object.db.combat_range[target] def distance_inc(mover, target): """ @@ -289,12 +303,12 @@ def distance_inc(mover, target): 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] + mover.db.combat_range[target] += 1 + target.db.combat_range[mover] = mover.db.combat_range[target] # Set a cap of 2: - if mover.db.Combat_Range[target] > 2: - target.db.Combat_Range[mover] = 2 - mover.db.Combat_Range[target] = 2 + if mover.db.combat_range[target] > 2: + target.db.combat_range[mover] = 2 + mover.db.combat_range[target] = 2 def approach(mover, target): """ @@ -314,10 +328,10 @@ def approach(mover, target): for thing in objects: if thing != mover and thing != target: # Move closer to each object closer to the target than you. - if mover.db.Combat_Range[thing] > target.db.Combat_Range[thing]: + if mover.db.combat_range[thing] > target.db.combat_range[thing]: distance_dec(mover, thing) # Move further from each object that's further from you than from the target. - if mover.db.Combat_Range[thing] < target.db.Combat_Range[thing]: + if mover.db.combat_range[thing] < target.db.combat_range[thing]: distance_inc(mover, thing) # Lastly, move closer to your target. distance_dec(mover, target) @@ -340,13 +354,13 @@ def withdraw(mover, target): for thing in objects: 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 mover.db.Combat_Range[thing] >= target.db.Combat_Range[thing] and mover.db.Combat_Range[thing] < mover.db.Combat_Range[thing]: + if mover.db.combat_range[thing] >= target.db.combat_range[thing] and mover.db.combat_range[thing] < mover.db.combat_range[thing]: distance_inc(mover, thing) # Move away from anything your target is engaged with - if target.db.Combat_Range[thing] == 0: + if target.db.combat_range[thing] == 0: distance_inc(mover, thing) # Move away from anything you're engaged with. - if mover.db.Combat_Range[thing] == 0: + if mover.db.combat_range[thing] == 0: distance_inc(mover, thing) # Then, move away from your target. distance_inc(mover, target) @@ -364,16 +378,16 @@ def get_range(obj1, obj2): range (int or None): Distance between two objects or None if not applicable """ # Return None if not applicable. - if not obj1.db.Combat_Range: + if not obj1.db.combat_range: return None - if not obj2.db.Combat_Range: + if not obj2.db.combat_range: return None - if obj1 not in obj2.db.Combat_Range: + if obj1 not in obj2.db.combat_range: return None - if obj2 not in obj1.db.Combat_Range: + if obj2 not in obj1.db.combat_range: return None # Return the range between the two objects. - return obj1.db.Combat_Range[obj2] + return obj1.db.combat_range[obj2] def combat_cleanup(character): """ @@ -401,7 +415,7 @@ def is_in_combat(character): Returns: (bool): True if in combat or False if not in combat """ - if character.db.Combat_TurnHandler: + if character.db.combat_turnhandler: return True return False @@ -416,7 +430,7 @@ def is_turn(character): Returns: (bool): True if it is their turn or False otherwise """ - turnhandler = character.db.Combat_TurnHandler + turnhandler = character.db.combat_turnhandler currentchar = turnhandler.db.fighters[turnhandler.db.turn] if character == currentchar: return True @@ -436,14 +450,14 @@ def spend_action(character, actions, action_name=None): combat to provided string """ if action_name: - character.db.Combat_LastAction = action_name + character.db.combat_lastaction = action_name if actions == 'all': # If spending all actions - character.db.Combat_ActionsLeft = 0 # Set actions to 0 + character.db.combat_actionsleft = 0 # Set actions to 0 else: - character.db.Combat_ActionsLeft -= actions # Use up actions. - if character.db.Combat_ActionsLeft < 0: - character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions - character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn. + character.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): """ @@ -462,13 +476,13 @@ def combat_status_message(fighter): reach_obj = [] range_obj = [] - for object in fighter.db.Combat_Range: + for object in fighter.db.combat_range: if object != fighter: - if fighter.db.Combat_Range[object] == 0: + if fighter.db.combat_range[object] == 0: engaged_obj.append(object) - if fighter.db.Combat_Range[object] == 1: + if fighter.db.combat_range[object] == 1: reach_obj.append(object) - if fighter.db.Combat_Range[object] > 1: + if fighter.db.combat_range[object] > 1: range_obj.append(object) if engaged_obj: @@ -582,10 +596,10 @@ class TBRangeObject(DefaultObject): """ # If dropper is currently in combat - if dropper.location.db.Combat_TurnHandler: + if dropper.location.db.combat_turnhandler: # Object joins the range field - self.db.Combat_Range = {} - dropper.location.db.Combat_TurnHandler.join_rangefield(self, anchor_obj=dropper) + self.db.combat_range = {} + dropper.location.db.combat_turnhandler.join_rangefield(self, anchor_obj=dropper) def at_before_get(self, getter): """ Called by the default `get` command before this object has been @@ -628,13 +642,13 @@ class TBRangeObject(DefaultObject): """ # If gotten, erase range values - if self.db.Combat_Range: - del self.db.Combat_Range + if self.db.combat_range: + del self.db.combat_range # Remove this object from everyone's range fields for object in getter.location.contents: - if object.db.Combat_Range: - if self in object.db.Combat_Range: - object.db.Combat_Range.pop(self, None) + if object.db.combat_range: + if self in object.db.combat_range: + object.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. @@ -726,9 +740,9 @@ class CmdFight(Command): 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... + 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! + 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. @@ -839,7 +853,7 @@ class CmdShoot(Command): # Test to see if there are any nearby enemy targets. in_melee = [] - for target in attacker.db.Combat_Range: + 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: in_melee.append(target) # Add to list of targets in melee @@ -887,7 +901,7 @@ class CmdApproach(Command): if not target: # No valid target given. return - if not target.db.Combat_Range: # Target object is not on the range field + if not target.db.combat_range: # Target object is not on the range field self.caller.msg("You can't move toward that!") return @@ -938,7 +952,7 @@ class CmdWithdraw(Command): if not target: # No valid target given. return - if not target.db.Combat_Range: # Target object is not on the range field + if not target.db.combat_range: # Target object is not on the range field self.caller.msg("You can't move away from that!") return @@ -946,7 +960,7 @@ class CmdWithdraw(Command): self.caller.msg("You can't move away from yourself!") return - if mover.db.Combat_Range[target] >= 3: # Already at maximum distance + if mover.db.combat_range[target] >= 3: # Already at maximum distance self.caller.msg("You're as far as you can get from that target!") return @@ -1159,7 +1173,7 @@ class TBRangeTurnHandler(DefaultScript): self.initialize_for_combat(fighter) # Add a reference to this script to the room - self.obj.db.Combat_TurnHandler = self + self.obj.db.combat_turnhandler = self # Initialize range field for all objects in the room for object in self.obj.contents: @@ -1186,7 +1200,7 @@ class TBRangeTurnHandler(DefaultScript): """ for object in self.obj.contents: combat_cleanup(object) # Clean up the combat attributes for every object in the room. - self.obj.db.Combat_TurnHandler = None # Remove reference to turn handler in location + self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location def at_repeat(self): """ @@ -1226,7 +1240,7 @@ class TBRangeTurnHandler(DefaultScript): else: # Start objects at range 1 from other objects rangedict.update({object:1}) - to_init.db.Combat_Range = rangedict + to_init.db.combat_range = rangedict def join_rangefield(self, to_init, anchor_obj=None, add_distance=0): """ @@ -1247,13 +1261,13 @@ class TBRangeTurnHandler(DefaultScript): if not anchor_obj: anchor_obj = contents[randint(0, (len(contents)-1))] # Copy the range values from the anchor object. - to_init.db.Combat_Range = anchor_obj.db.Combat_Range + to_init.db.combat_range = anchor_obj.db.combat_range # Add the new object to everyone else's ranges. for object in contents: - new_objects_range = object.db.Combat_Range[anchor_obj] - object.db.Combat_Range.update({to_init:new_objects_range}) + new_objects_range = object.db.combat_range[anchor_obj] + object.db.combat_range.update({to_init:new_objects_range}) # Set the new object's range to itself to 0. - to_init.db.Combat_Range.update({to_init:0}) + 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) @@ -1266,9 +1280,9 @@ class TBRangeTurnHandler(DefaultScript): 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 + 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): """ @@ -1283,7 +1297,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 = 2 # 2 actions per turn. + character.db.combat_actionsleft = 2 # 2 actions per turn. # Prompt the character for their turn and give some information. character.msg("|wIt's your turn!|n") combat_status_message(character) @@ -1296,7 +1310,7 @@ class TBRangeTurnHandler(DefaultScript): # 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 + 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!") @@ -1334,7 +1348,7 @@ class TBRangeTurnHandler(DefaultScript): Args: character (obj): Character to test for end of turn """ - if not character.db.Combat_ActionsLeft: # Character has no actions remaining + if not character.db.combat_actionsleft: # Character has no actions remaining self.next_turn() return @@ -1352,5 +1366,5 @@ class TBRangeTurnHandler(DefaultScript): # Initialize the character like you do at the start. self.initialize_for_combat(character) # Add the character to the rangefield, at range from everyone, if they're not on it already. - if not character.db.Combat_Range: + if not character.db.combat_range: self.join_rangefield(character, add_distance=2) From 347f161d94c420e5f1bbecd2512ee03289a34adf Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:16:19 -0700 Subject: [PATCH 05/17] Simplified bool returns in is_in_combat, is_turn --- evennia/contrib/turnbattle/tb_basic.py | 8 ++------ evennia/contrib/turnbattle/tb_equip.py | 8 ++------ evennia/contrib/turnbattle/tb_range.py | 8 ++------ 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_basic.py b/evennia/contrib/turnbattle/tb_basic.py index 22a96016c8..e392715238 100644 --- a/evennia/contrib/turnbattle/tb_basic.py +++ b/evennia/contrib/turnbattle/tb_basic.py @@ -239,9 +239,7 @@ def is_in_combat(character): Returns: (bool): True if in combat or False if not in combat """ - if character.db.combat_turnhandler: - return True - return False + return bool(character.db.combat_turnhandler) def is_turn(character): @@ -256,9 +254,7 @@ def is_turn(character): """ turnhandler = character.db.combat_turnhandler currentchar = turnhandler.db.fighters[turnhandler.db.turn] - if character == currentchar: - return True - return False + return bool(character == currentchar) def spend_action(character, actions, action_name=None): diff --git a/evennia/contrib/turnbattle/tb_equip.py b/evennia/contrib/turnbattle/tb_equip.py index b415f82428..e3820393c4 100644 --- a/evennia/contrib/turnbattle/tb_equip.py +++ b/evennia/contrib/turnbattle/tb_equip.py @@ -284,9 +284,7 @@ def is_in_combat(character): Returns: (bool): True if in combat or False if not in combat """ - if character.db.combat_turnhandler: - return True - return False + return bool(character.db.combat_turnhandler) def is_turn(character): @@ -301,9 +299,7 @@ def is_turn(character): """ turnhandler = character.db.combat_turnhandler currentchar = turnhandler.db.fighters[turnhandler.db.turn] - if character == currentchar: - return True - return False + return bool(character == currentchar) def spend_action(character, actions, action_name=None): diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index 27566d4d27..91ccc3836e 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -415,9 +415,7 @@ def is_in_combat(character): Returns: (bool): True if in combat or False if not in combat """ - if character.db.combat_turnhandler: - return True - return False + return bool(character.db.combat_turnhandler) def is_turn(character): @@ -432,9 +430,7 @@ def is_turn(character): """ turnhandler = character.db.combat_turnhandler currentchar = turnhandler.db.fighters[turnhandler.db.turn] - if character == currentchar: - return True - return False + return bool(character == currentchar) def spend_action(character, actions, action_name=None): From f5760057722199c6823675cd4634e547f4088f23 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:24:20 -0700 Subject: [PATCH 06/17] Moved turn handler script up higher in module --- evennia/contrib/turnbattle/tb_basic.py | 353 +++++++++---------- evennia/contrib/turnbattle/tb_equip.py | 350 +++++++++--------- evennia/contrib/turnbattle/tb_range.py | 470 ++++++++++++------------- 3 files changed, 584 insertions(+), 589 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_basic.py b/evennia/contrib/turnbattle/tb_basic.py index e392715238..d01ad7ed4a 100644 --- a/evennia/contrib/turnbattle/tb_basic.py +++ b/evennia/contrib/turnbattle/tb_basic.py @@ -333,7 +333,182 @@ class TBBasicCharacter(DefaultCharacter): return False return True +""" +---------------------------------------------------------------------------- +SCRIPTS START HERE +---------------------------------------------------------------------------- +""" + +class TBBasicTurnHandler(DefaultScript): + """ + This is the script that handles the progression of combat through turns. + On creation (when a fight is started) it adds all combat-ready characters + to its roster and then sorts them into a turn order. There can only be one + fight going on in a single room at a time, so the script is assigned to a + room as its object. + + Fights persist until only one participant is left with any HP or all + remaining participants choose to end the combat with the 'disengage' command. + """ + + def at_script_creation(self): + """ + Called once, when the script is created. + """ + self.key = "Combat Turn Handler" + self.interval = 5 # Once every 5 seconds + self.persistent = True + self.db.fighters = [] + + # Add all fighters in the room with at least 1 HP to the combat." + for object in self.obj.contents: + if object.db.hp: + self.db.fighters.append(object) + + # 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 = 30 # 30 seconds + + 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 = 1 # 1 action per turn. + # 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 = 30 + self.time_until_next_repeat() # Reset the timer. + self.db.timeout_warning_given = False # Reset the timeout warning. + self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) + self.start_turn(newchar) # Start the new character's turn. + + def turn_end_check(self, character): + """ + Tests to see if a character's turn is over, and cycles to the next turn if it is. + + Args: + character (obj): Character to test for end of turn + """ + if not character.db.combat_actionsleft: # Character has no actions remaining + self.next_turn() + return + + def join_fight(self, character): + """ + Adds a new character to a fight already in progress. + + Args: + character (obj): Character to be added to the fight. + """ + # Inserts the fighter to the turn order, right behind whoever's turn it currently is. + self.db.fighters.insert(self.db.turn, character) + # Tick the turn counter forward one to compensate. + self.db.turn += 1 + # Initialize the character like you do at the start. + self.initialize_for_combat(character) + + """ ---------------------------------------------------------------------------- COMMANDS START HERE @@ -568,180 +743,4 @@ class BattleCmdSet(default_cmds.CharacterCmdSet): self.add(CmdRest()) self.add(CmdPass()) self.add(CmdDisengage()) - self.add(CmdCombatHelp()) - - -""" ----------------------------------------------------------------------------- -SCRIPTS START HERE ----------------------------------------------------------------------------- -""" - - -class TBBasicTurnHandler(DefaultScript): - """ - This is the script that handles the progression of combat through turns. - On creation (when a fight is started) it adds all combat-ready characters - to its roster and then sorts them into a turn order. There can only be one - fight going on in a single room at a time, so the script is assigned to a - room as its object. - - Fights persist until only one participant is left with any HP or all - remaining participants choose to end the combat with the 'disengage' command. - """ - - def at_script_creation(self): - """ - Called once, when the script is created. - """ - self.key = "Combat Turn Handler" - self.interval = 5 # Once every 5 seconds - self.persistent = True - self.db.fighters = [] - - # Add all fighters in the room with at least 1 HP to the combat." - for object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) - - # 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 = 30 # 30 seconds - - 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 = 1 # 1 action per turn. - # 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 = 30 + 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) + self.add(CmdCombatHelp()) \ No newline at end of file diff --git a/evennia/contrib/turnbattle/tb_equip.py b/evennia/contrib/turnbattle/tb_equip.py index e3820393c4..41fa93d4dd 100644 --- a/evennia/contrib/turnbattle/tb_equip.py +++ b/evennia/contrib/turnbattle/tb_equip.py @@ -324,6 +324,180 @@ def spend_action(character, actions, action_name=None): character.db.combat_actionsleft = 0 # Can't have fewer than 0 actions character.db.combat_turnhandler.turn_end_check(character) # Signal potential end of turn. +""" +---------------------------------------------------------------------------- +SCRIPTS START HERE +---------------------------------------------------------------------------- +""" + + +class TBEquipTurnHandler(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 object in self.obj.contents: + if object.db.hp: + self.db.fighters.append(object) + + # 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 = 30 # 30 seconds + + 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 = 1 # 1 action per turn. + # 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 = 30 + 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) """ ---------------------------------------------------------------------------- @@ -844,182 +1018,6 @@ class BattleCmdSet(default_cmds.CharacterCmdSet): self.add(CmdDon()) self.add(CmdDoff()) - -""" ----------------------------------------------------------------------------- -SCRIPTS START HERE ----------------------------------------------------------------------------- -""" - - -class TBEquipTurnHandler(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 object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) - - # 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 = 30 # 30 seconds - - 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 = 1 # 1 action per turn. - # 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 = 30 + 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) - """ ---------------------------------------------------------------------------- PROTOTYPES START HERE diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index 91ccc3836e..daeb4bad8f 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -491,7 +491,240 @@ def combat_status_message(fighter): fighter.msg(status_msg) return +""" +---------------------------------------------------------------------------- +SCRIPTS START HERE +---------------------------------------------------------------------------- +""" + + +class TBRangeTurnHandler(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 object in self.obj.contents: + if object.db.hp: + self.db.fighters.append(object) + + # 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 object in self.obj.contents: + self.init_range(object) + + # 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 = 30 # 30 seconds + + def at_stop(self): + """ + Called at script termination. + """ + for object in self.obj.contents: + combat_cleanup(object) # 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 + + def init_range(self, to_init): + """ + Initializes range values for an object at the start of a fight. + + Args: + to_init (object): Object to initialize range field for. + """ + rangedict = {} + # Get a list of objects in the room. + objectlist = self.obj.contents + for object in objectlist: + # Object always at distance 0 from itself + if object == to_init: + rangedict.update({object:0}) + else: + if object.destination or to_init.destination: + # Start exits at range 2 to put them at the 'edges' + rangedict.update({object:2}) + else: + # Start objects at range 1 from other objects + rangedict.update({object:1}) + to_init.db.combat_range = rangedict + + def join_rangefield(self, to_init, anchor_obj=None, add_distance=0): + """ + Adds a new object to the range field of a fight in progress. + + Args: + to_init (object): Object to initialize range field for. + + Kwargs: + anchor_obj (object): Object to copy range values from, or None for a random object. + add_distance (int): Distance to put between to_init object and anchor object. + + """ + # Get a list of room's contents without to_init object. + contents = self.obj.contents + contents.remove(to_init) + # If no anchor object given, pick one in the room at random. + if not anchor_obj: + anchor_obj = contents[randint(0, (len(contents)-1))] + # Copy the range values from the anchor object. + to_init.db.combat_range = anchor_obj.db.combat_range + # Add the new object to everyone else's ranges. + for object in contents: + new_objects_range = object.db.combat_range[anchor_obj] + object.db.combat_range.update({to_init:new_objects_range}) + # Set the new object's range to itself to 0. + 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 + + 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: + In this example, characters are given two actions per turn. This allows + characters to both move and attack in the same turn (or, alternately, + move twice or attack twice). + """ + character.db.combat_actionsleft = 2 # 2 actions per turn. + # 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 = 30 + 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) + # Add the character to the rangefield, at range from everyone, if they're not on it already. + if not character.db.combat_range: + self.join_rangefield(character, add_distance=2) + """ ---------------------------------------------------------------------------- @@ -1128,239 +1361,4 @@ class BattleCmdSet(default_cmds.CharacterCmdSet): self.add(CmdApproach()) self.add(CmdWithdraw()) self.add(CmdStatus()) - self.add(CmdCombatHelp()) - - -""" ----------------------------------------------------------------------------- -SCRIPTS START HERE ----------------------------------------------------------------------------- -""" - - -class TBRangeTurnHandler(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 object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) - - # 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 object in self.obj.contents: - self.init_range(object) - - # 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 = 30 # 30 seconds - - def at_stop(self): - """ - Called at script termination. - """ - for object in self.obj.contents: - combat_cleanup(object) # 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 - - def init_range(self, to_init): - """ - Initializes range values for an object at the start of a fight. - - Args: - to_init (object): Object to initialize range field for. - """ - rangedict = {} - # Get a list of objects in the room. - objectlist = self.obj.contents - for object in objectlist: - # Object always at distance 0 from itself - if object == to_init: - rangedict.update({object:0}) - else: - if object.destination or to_init.destination: - # Start exits at range 2 to put them at the 'edges' - rangedict.update({object:2}) - else: - # Start objects at range 1 from other objects - rangedict.update({object:1}) - to_init.db.combat_range = rangedict - - def join_rangefield(self, to_init, anchor_obj=None, add_distance=0): - """ - Adds a new object to the range field of a fight in progress. - - Args: - to_init (object): Object to initialize range field for. - - Kwargs: - anchor_obj (object): Object to copy range values from, or None for a random object. - add_distance (int): Distance to put between to_init object and anchor object. - - """ - # Get a list of room's contents without to_init object. - contents = self.obj.contents - contents.remove(to_init) - # If no anchor object given, pick one in the room at random. - if not anchor_obj: - anchor_obj = contents[randint(0, (len(contents)-1))] - # Copy the range values from the anchor object. - to_init.db.combat_range = anchor_obj.db.combat_range - # Add the new object to everyone else's ranges. - for object in contents: - new_objects_range = object.db.combat_range[anchor_obj] - object.db.combat_range.update({to_init:new_objects_range}) - # Set the new object's range to itself to 0. - 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 - - 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: - In this example, characters are given two actions per turn. This allows - characters to both move and attack in the same turn (or, alternately, - move twice or attack twice). - """ - character.db.combat_actionsleft = 2 # 2 actions per turn. - # 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 = 30 + 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) - # Add the character to the rangefield, at range from everyone, if they're not on it already. - if not character.db.combat_range: - self.join_rangefield(character, add_distance=2) + self.add(CmdCombatHelp()) \ No newline at end of file From 30eea75ad72eff7ee857cbd307abe035cca16166 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:36:42 -0700 Subject: [PATCH 07/17] Options for turn timeout and actions per turn --- evennia/contrib/turnbattle/tb_basic.py | 16 ++++++++++++---- evennia/contrib/turnbattle/tb_equip.py | 16 ++++++++++++---- evennia/contrib/turnbattle/tb_range.py | 16 ++++++++++++---- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_basic.py b/evennia/contrib/turnbattle/tb_basic.py index d01ad7ed4a..017094d0ec 100644 --- a/evennia/contrib/turnbattle/tb_basic.py +++ b/evennia/contrib/turnbattle/tb_basic.py @@ -48,10 +48,18 @@ from evennia.commands.default.help import CmdHelp """ ---------------------------------------------------------------------------- -COMBAT FUNCTIONS START HERE +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): """ @@ -386,7 +394,7 @@ class TBBasicTurnHandler(DefaultScript): # Set up the current turn and turn timeout delay. self.db.turn = 0 - self.db.timer = 30 # 30 seconds + self.db.timer = TURN_TIMEOUT # Set timer to turn timeout specified in options def at_stop(self): """ @@ -440,7 +448,7 @@ class TBBasicTurnHandler(DefaultScript): separated for movement, by adding "character.db.combat_movesleft = 3" or something similar. """ - character.db.combat_actionsleft = 1 # 1 action per turn. + 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) @@ -478,7 +486,7 @@ class TBBasicTurnHandler(DefaultScript): 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 = 30 + self.time_until_next_repeat() # Reset the timer. + 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. diff --git a/evennia/contrib/turnbattle/tb_equip.py b/evennia/contrib/turnbattle/tb_equip.py index 41fa93d4dd..087cc5b83f 100644 --- a/evennia/contrib/turnbattle/tb_equip.py +++ b/evennia/contrib/turnbattle/tb_equip.py @@ -60,10 +60,18 @@ from evennia.commands.default.help import CmdHelp """ ---------------------------------------------------------------------------- -COMBAT FUNCTIONS START HERE +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): """ @@ -377,7 +385,7 @@ class TBEquipTurnHandler(DefaultScript): # Set up the current turn and turn timeout delay. self.db.turn = 0 - self.db.timer = 30 # 30 seconds + self.db.timer = TURN_TIMEOUT # Set timer to turn timeout specified in options def at_stop(self): """ @@ -431,7 +439,7 @@ class TBEquipTurnHandler(DefaultScript): separated for movement, by adding "character.db.combat_movesleft = 3" or something similar. """ - character.db.combat_actionsleft = 1 # 1 action per turn. + 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) @@ -469,7 +477,7 @@ class TBEquipTurnHandler(DefaultScript): 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 = 30 + self.time_until_next_repeat() # Reset the timer. + 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. diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index daeb4bad8f..43d9492f24 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -101,10 +101,18 @@ from evennia.commands.default.help import CmdHelp """ ---------------------------------------------------------------------------- -COMBAT FUNCTIONS START HERE +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): """ @@ -548,7 +556,7 @@ class TBRangeTurnHandler(DefaultScript): # Set up the current turn and turn timeout delay. self.db.turn = 0 - self.db.timer = 30 # 30 seconds + self.db.timer = TURN_TIMEOUT # Set timer to turn timeout specified in options def at_stop(self): """ @@ -653,7 +661,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 = 2 # 2 actions per turn. + 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) @@ -692,7 +700,7 @@ class TBRangeTurnHandler(DefaultScript): 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 = 30 + self.time_until_next_repeat() # Reset the timer. + 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. From b2ec29db81864994a24f92e2aae22c9fe930d4dc Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:51:03 -0700 Subject: [PATCH 08/17] distance_inc & distance_dec changed to helper funcs --- evennia/contrib/turnbattle/tb_range.py | 78 ++++++++++++++------------ 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index 43d9492f24..ef2859c677 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -78,6 +78,11 @@ And change your game's character typeclass to inherit from TBRangeCharacter instead of the default: class Character(TBRangeCharacter): + +Do the same thing in your game's objects.py module for TBRangeObject: + + from evennia.contrib.turnbattle.tb_range import TBRangeObject + class Object(TBRangeObject): Next, import this module into your default_cmdsets.py module: @@ -106,7 +111,7 @@ OPTIONS """ TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds -ACTIONS_PER_TURN = 1 # Number of actions allowed per turn +ACTIONS_PER_TURN = 2 # Number of actions allowed per turn """ ---------------------------------------------------------------------------- @@ -282,41 +287,6 @@ def resolve_attack(attacker, defender, attack_type, attack_value=None, defense_v # If defender HP is reduced to 0 or less, call at_defeat. if defender.db.hp <= 0: at_defeat(defender) - -def distance_dec(mover, target): - """ - Decreases distance in range field between mover and target. - - Args: - mover (obj): The object moving - target (obj): The object to be moved toward - """ - 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 mover.db.combat_range[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 - for object in mover.location.contents: - if object != mover and object != target: - object.db.combat_range[mover] = object.db.combat_range[target] - -def distance_inc(mover, target): - """ - 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 mover.db.combat_range[target] > 2: - target.db.combat_range[mover] = 2 - mover.db.combat_range[target] = 2 def approach(mover, target): """ @@ -331,6 +301,26 @@ def approach(mover, target): target than the mover is. The mover will also move away from anything they started out close to. """ + def distance_dec(mover, target): + """ + Helper function that decreases distance in range field between mover and target. + + Args: + mover (obj): The object moving + target (obj): The object to be moved toward + """ + 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 mover.db.combat_range[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 + for object in mover.location.contents: + if object != mover and object != target: + object.db.combat_range[mover] = object.db.combat_range[target] + objects = mover.location.contents for thing in objects: @@ -358,7 +348,23 @@ def withdraw(mover, 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 distance_inc(mover, target): + """ + Helper 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 mover.db.combat_range[target] > 2: + target.db.combat_range[mover] = 2 + mover.db.combat_range[target] = 2 + objects = mover.location.contents + for thing in objects: 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. From e742179310142ce8850273fb0b2ad5c0d2fdf9eb Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:57:00 -0700 Subject: [PATCH 09/17] References to 'object' changed to 'thing' instead --- evennia/contrib/turnbattle/tb_basic.py | 6 +-- evennia/contrib/turnbattle/tb_equip.py | 6 +-- evennia/contrib/turnbattle/tb_range.py | 70 +++++++++++++------------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_basic.py b/evennia/contrib/turnbattle/tb_basic.py index 017094d0ec..88e5c176c8 100644 --- a/evennia/contrib/turnbattle/tb_basic.py +++ b/evennia/contrib/turnbattle/tb_basic.py @@ -370,9 +370,9 @@ class TBBasicTurnHandler(DefaultScript): self.db.fighters = [] # Add all fighters in the room with at least 1 HP to the combat." - for object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) + 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: diff --git a/evennia/contrib/turnbattle/tb_equip.py b/evennia/contrib/turnbattle/tb_equip.py index 087cc5b83f..b6b7ae6035 100644 --- a/evennia/contrib/turnbattle/tb_equip.py +++ b/evennia/contrib/turnbattle/tb_equip.py @@ -361,9 +361,9 @@ class TBEquipTurnHandler(DefaultScript): self.db.fighters = [] # Add all fighters in the room with at least 1 HP to the combat." - for object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) + 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: diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index ef2859c677..bfec0893f0 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -317,13 +317,13 @@ def approach(mover, target): 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 - for object in mover.location.contents: - if object != mover and object != target: - object.db.combat_range[mover] = object.db.combat_range[target] + for thing in mover.location.contents: + if thing != mover and thing != target: + thing.db.combat_range[mover] = thing.db.combat_range[target] - objects = mover.location.contents + contents = mover.location.contents - for thing in objects: + for thing in contents: if thing != mover and thing != target: # Move closer to each object closer to the target than you. if mover.db.combat_range[thing] > target.db.combat_range[thing]: @@ -363,9 +363,9 @@ def withdraw(mover, target): target.db.combat_range[mover] = 2 mover.db.combat_range[target] = 2 - objects = mover.location.contents + contents = mover.location.contents - for thing in objects: + 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 mover.db.combat_range[thing] >= target.db.combat_range[thing] and mover.db.combat_range[thing] < mover.db.combat_range[thing]: @@ -486,14 +486,14 @@ def combat_status_message(fighter): reach_obj = [] range_obj = [] - for object in fighter.db.combat_range: - if object != fighter: - if fighter.db.combat_range[object] == 0: - engaged_obj.append(object) - if fighter.db.combat_range[object] == 1: - reach_obj.append(object) - if fighter.db.combat_range[object] > 1: - range_obj.append(object) + 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) @@ -534,9 +534,9 @@ class TBRangeTurnHandler(DefaultScript): self.db.fighters = [] # Add all fighters in the room with at least 1 HP to the combat." - for object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) + 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: @@ -546,8 +546,8 @@ class TBRangeTurnHandler(DefaultScript): self.obj.db.combat_turnhandler = self # Initialize range field for all objects in the room - for object in self.obj.contents: - self.init_range(object) + 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. @@ -568,8 +568,8 @@ class TBRangeTurnHandler(DefaultScript): """ Called at script termination. """ - for object in self.obj.contents: - combat_cleanup(object) # Clean up the combat attributes for every object in the room. + 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): @@ -599,17 +599,17 @@ class TBRangeTurnHandler(DefaultScript): rangedict = {} # Get a list of objects in the room. objectlist = self.obj.contents - for object in objectlist: + for thing in objectlist: # Object always at distance 0 from itself - if object == to_init: - rangedict.update({object:0}) + if thing == to_init: + rangedict.update({thing:0}) else: - if object.destination or to_init.destination: + if thing.destination or to_init.destination: # Start exits at range 2 to put them at the 'edges' - rangedict.update({object:2}) + rangedict.update({thing:2}) else: # Start objects at range 1 from other objects - rangedict.update({object:1}) + rangedict.update({thing:1}) to_init.db.combat_range = rangedict def join_rangefield(self, to_init, anchor_obj=None, add_distance=0): @@ -633,9 +633,9 @@ class TBRangeTurnHandler(DefaultScript): # Copy the range values from the anchor object. to_init.db.combat_range = anchor_obj.db.combat_range # Add the new object to everyone else's ranges. - for object in contents: - new_objects_range = object.db.combat_range[anchor_obj] - object.db.combat_range.update({to_init:new_objects_range}) + for thing in contents: + new_objects_range = thing.db.combat_range[anchor_obj] + thing.db.combat_range.update({to_init:new_objects_range}) # Set the new object's range to itself to 0. to_init.db.combat_range.update({to_init:0}) # Add additional distance from anchor object, if any. @@ -888,10 +888,10 @@ class TBRangeObject(DefaultObject): if self.db.combat_range: del self.db.combat_range # Remove this object from everyone's range fields - for object in getter.location.contents: - if object.db.combat_range: - if self in object.db.combat_range: - object.db.combat_range.pop(self, None) + for thing in getter.location.contents: + if thing.db.combat_range: + 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. From 66bb313c34e528aeb5a7991e89b3d3c63bd570a8 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:59:01 -0700 Subject: [PATCH 10/17] Empty returns deleted, methods properly spaced --- evennia/contrib/turnbattle/tb_range.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index bfec0893f0..143134beec 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -333,7 +333,6 @@ def approach(mover, target): distance_inc(mover, thing) # Lastly, move closer to your target. distance_dec(mover, target) - return def withdraw(mover, target): """ @@ -378,7 +377,6 @@ def withdraw(mover, target): distance_inc(mover, thing) # Then, move away from your target. distance_inc(mover, target) - return def get_range(obj1, obj2): """ @@ -503,7 +501,6 @@ def combat_status_message(fighter): status_msg += "|/Ranged targets: %s" % ", ".join(obj.key for obj in range_obj) fighter.msg(status_msg) - return """ ---------------------------------------------------------------------------- @@ -823,6 +820,7 @@ class TBRangeObject(DefaultObject): dropper.msg("You can only drop things on your turn!") return False return True + def at_drop(self, dropper): """ Called by the default `drop` command when this object has been @@ -843,6 +841,7 @@ class TBRangeObject(DefaultObject): # Object joins the range field self.db.combat_range = {} dropper.location.db.combat_turnhandler.join_rangefield(self, anchor_obj=dropper) + def at_before_get(self, getter): """ Called by the default `get` command before this object has been @@ -869,6 +868,7 @@ class TBRangeObject(DefaultObject): getter.msg("You aren't close enough to get that! (see: help approach)") return False return True + def at_get(self, getter): """ Called by the default `get` command when this object has been @@ -895,6 +895,7 @@ class TBRangeObject(DefaultObject): # If in combat, getter spends an action if is_in_combat(getter): spend_action(getter, 1, action_name="get") # Use up one action. + def at_before_give(self, giver, getter): """ Called by the default `give` command before this object has been @@ -923,6 +924,7 @@ class TBRangeObject(DefaultObject): giver.msg("You aren't close enough to give things to %s! (see: help approach)" % getter) return False return True + def at_give(self, giver, getter): """ Called by the default `give` command when this object has been From 95f840ac7abc6bbfad8eb37bbbc72f654c5db783 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 22:05:56 -0700 Subject: [PATCH 11/17] get_range integrated into movement functions This makes the code a bit more readable and fixes a bug in withdrawing that didn't take other objects into account properly. --- evennia/contrib/turnbattle/tb_range.py | 60 +++++++++++++------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index 143134beec..d848d445b7 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -287,6 +287,29 @@ def resolve_attack(attacker, defender, attack_type, attack_value=None, defense_v # 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 approach(mover, target): """ @@ -312,7 +335,7 @@ 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 mover.db.combat_range[target] <= 0: + if 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 @@ -326,10 +349,10 @@ def approach(mover, target): for thing in contents: if thing != mover and thing != target: # Move closer to each object closer to the target than you. - if mover.db.combat_range[thing] > target.db.combat_range[thing]: + 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 mover.db.combat_range[thing] < target.db.combat_range[thing]: + if get_range(mover, thing) < get_range(target, thing): distance_inc(mover, thing) # Lastly, move closer to your target. distance_dec(mover, target) @@ -358,7 +381,7 @@ def withdraw(mover, target): mover.db.combat_range[target] += 1 target.db.combat_range[mover] = mover.db.combat_range[target] # Set a cap of 2: - if mover.db.combat_range[target] > 2: + if get_range(mover, target) > 2: target.db.combat_range[mover] = 2 mover.db.combat_range[target] = 2 @@ -367,39 +390,16 @@ def withdraw(mover, target): 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 mover.db.combat_range[thing] >= target.db.combat_range[thing] and mover.db.combat_range[thing] < mover.db.combat_range[thing]: + 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 target.db.combat_range[thing] == 0: + if get_range(target, thing) == 0: distance_inc(mover, thing) # Move away from anything you're engaged with. - if mover.db.combat_range[thing] == 0: + if get_range(mover, thing) == 0: distance_inc(mover, thing) # Then, move away from your target. distance_inc(mover, target) - -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 combat_cleanup(character): """ From 1fe9bf3dceb09658bd25c3a9a6283003a8506f22 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 22:29:45 -0700 Subject: [PATCH 12/17] Moved distance_inc back to being its own function I almost forgot - distance_inc is actually used by both 'approach' and 'withdraw', since approaching an object might put you farther away from others. So, I moved it back to its own function. --- evennia/contrib/turnbattle/tb_range.py | 29 +++++++++++++------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index d848d445b7..0eb617c48b 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -310,6 +310,21 @@ def get_range(obj1, obj2): 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): """ @@ -370,20 +385,6 @@ def withdraw(mover, 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 distance_inc(mover, target): - """ - Helper 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 contents = mover.location.contents From 9bc3fcf4860a3a1477940ce345e02c6b870758ff Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 29 Oct 2017 21:39:11 -0700 Subject: [PATCH 13/17] Unit tests for tb_range added --- evennia/contrib/tests.py | 110 ++++++++++++++++++++++++- evennia/contrib/turnbattle/tb_range.py | 6 +- 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 1fdd7dde75..4b2c1a696f 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -907,7 +907,7 @@ class TestTutorialWorldRooms(CommandTest): # test turnbattle -from evennia.contrib.turnbattle import tb_basic, tb_equip +from evennia.contrib.turnbattle import tb_basic, tb_equip, tb_range from evennia.objects.objects import DefaultRoom @@ -939,11 +939,25 @@ class TestTurnBattleCmd(CommandTest): self.call(tb_equip.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.") + # Test range commands + def test_turnbattlerangecmd(self): + # Start with range module specific commands. + self.call(tb_range.CmdShoot(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdApproach(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdWithdraw(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdStatus(), "", "HP Remaining: 100 / 100") + # Also test the commands that are the same in the basic module + self.call(tb_range.CmdFight(), "", "There's nobody here to fight!") + self.call(tb_range.CmdAttack(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdPass(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdRest(), "", "Char rests to recover HP.") + class TestTurnBattleFunc(EvenniaTest): # Test combat functions - def test_turnbattlefunc(self): + def test_tbbasicfunc(self): attacker = create_object(tb_basic.TBBasicCharacter, key="Attacker") defender = create_object(tb_basic.TBBasicCharacter, key="Defender") testroom = create_object(DefaultRoom, key="Test Room") @@ -1020,7 +1034,7 @@ class TestTurnBattleFunc(EvenniaTest): turnhandler.stop() # Test the combat functions in tb_equip too. They work mostly the same. - def test_turnbattlefunc(self): + def test_tbequipfunc(self): attacker = create_object(tb_equip.TBEquipCharacter, key="Attacker") defender = create_object(tb_equip.TBEquipCharacter, key="Defender") testroom = create_object(DefaultRoom, key="Test Room") @@ -1095,6 +1109,96 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) # Remove the script at the end turnhandler.stop() + + # Test combat functions in tb_range too. + def test_tbrangefunc(self): + testroom = create_object(DefaultRoom, key="Test Room") + attacker = create_object(tb_range.TBRangeCharacter, key="Attacker", location=testroom) + defender = create_object(tb_range.TBRangeCharacter, key="Defender", location=testroom) + # Initiative roll + initiative = tb_range.roll_init(attacker) + self.assertTrue(initiative >= 0 and initiative <= 1000) + # Attack roll + attack_roll = tb_range.get_attack(attacker, defender, "test") + self.assertTrue(attack_roll >= 0 and attack_roll <= 100) + # Defense roll + defense_roll = tb_range.get_defense(attacker, defender, "test") + self.assertTrue(defense_roll == 50) + # Damage roll + damage_roll = tb_range.get_damage(attacker, defender) + self.assertTrue(damage_roll >= 15 and damage_roll <= 25) + # Apply damage + defender.db.hp = 10 + tb_range.apply_damage(defender, 3) + self.assertTrue(defender.db.hp == 7) + # Resolve attack + defender.db.hp = 40 + tb_range.resolve_attack(attacker, defender, "test", attack_value=20, defense_value=10) + self.assertTrue(defender.db.hp < 40) + # Combat cleanup + attacker.db.Combat_attribute = True + tb_range.combat_cleanup(attacker) + self.assertFalse(attacker.db.combat_attribute) + # Is in combat + self.assertFalse(tb_range.is_in_combat(attacker)) + # Set up turn handler script for further tests + attacker.location.scripts.add(tb_range.TBRangeTurnHandler) + turnhandler = attacker.db.combat_TurnHandler + self.assertTrue(attacker.db.combat_TurnHandler) + # Force turn order + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + # Test is turn + self.assertTrue(tb_range.is_turn(attacker)) + # Spend actions + attacker.db.Combat_ActionsLeft = 1 + tb_range.spend_action(attacker, 1, action_name="Test") + self.assertTrue(attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(attacker.db.Combat_LastAction == "Test") + # Initialize for combat + attacker.db.Combat_ActionsLeft = 983 + turnhandler.initialize_for_combat(attacker) + self.assertTrue(attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(attacker.db.Combat_LastAction == "null") + # Set up ranges again, since initialize_for_combat clears them + attacker.db.combat_range = {} + attacker.db.combat_range[attacker] = 0 + attacker.db.combat_range[defender] = 1 + defender.db.combat_range = {} + defender.db.combat_range[defender] = 0 + defender.db.combat_range[attacker] = 1 + # Start turn + defender.db.Combat_ActionsLeft = 0 + turnhandler.start_turn(defender) + self.assertTrue(defender.db.Combat_ActionsLeft == 2) + # Next turn + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + turnhandler.next_turn() + self.assertTrue(turnhandler.db.turn == 1) + # Turn end check + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + attacker.db.Combat_ActionsLeft = 0 + turnhandler.turn_end_check(attacker) + self.assertTrue(turnhandler.db.turn == 1) + # Join fight + joiner = create_object(tb_range.TBRangeCharacter, key="Joiner", location=testroom) + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + turnhandler.join_fight(joiner) + self.assertTrue(turnhandler.db.turn == 1) + self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) + # Now, test for approach/withdraw functions + self.assertTrue(tb_range.get_range(attacker, defender) == 1) + # Approach + tb_range.approach(attacker, defender) + self.assertTrue(tb_range.get_range(attacker, defender) == 0) + # Withdraw + tb_range.withdraw(attacker, defender) + self.assertTrue(tb_range.get_range(attacker, defender) == 1) + # Remove the script at the end + turnhandler.stop() # Test of the unixcommand module diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index 0eb617c48b..5596573a2d 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -474,7 +474,10 @@ def combat_status_message(fighter): 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): @@ -1326,7 +1329,6 @@ class CmdStatus(Command): def func(self): "This performs the actual command." - combat_status_message(self.caller) From df9072253f9a99986689e397b66dcee90c0b2b6e Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 29 Oct 2017 23:00:22 -0700 Subject: [PATCH 14/17] Set turn handler's intervals higher during tests This was an attempt to try to fix some strange 'unhandled error in deffered' results while unit testing the contrib folder. It didn't work, but it's probably good to do anyway. --- evennia/contrib/tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 4b2c1a696f..79a61ce65d 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -993,6 +993,8 @@ class TestTurnBattleFunc(EvenniaTest): attacker.location.scripts.add(tb_basic.TBBasicTurnHandler) turnhandler = attacker.db.combat_TurnHandler self.assertTrue(attacker.db.combat_TurnHandler) + # Set the turn handler's interval very high to keep it from repeating during tests. + turnhandler.interval = 10000 # Force turn order turnhandler.db.fighters = [attacker, defender] turnhandler.db.turn = 0 @@ -1070,6 +1072,8 @@ class TestTurnBattleFunc(EvenniaTest): attacker.location.scripts.add(tb_equip.TBEquipTurnHandler) turnhandler = attacker.db.combat_TurnHandler self.assertTrue(attacker.db.combat_TurnHandler) + # Set the turn handler's interval very high to keep it from repeating during tests. + turnhandler.interval = 10000 # Force turn order turnhandler.db.fighters = [attacker, defender] turnhandler.db.turn = 0 @@ -1145,6 +1149,8 @@ class TestTurnBattleFunc(EvenniaTest): attacker.location.scripts.add(tb_range.TBRangeTurnHandler) turnhandler = attacker.db.combat_TurnHandler self.assertTrue(attacker.db.combat_TurnHandler) + # Set the turn handler's interval very high to keep it from repeating during tests. + turnhandler.interval = 10000 # Force turn order turnhandler.db.fighters = [attacker, defender] turnhandler.db.turn = 0 From c6f422d44b1bfe81e0cb7c7cdb1eafb9737d4c8a Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 05:03:26 -0700 Subject: [PATCH 15/17] Updated username in contrib readme --- evennia/contrib/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/README.md b/evennia/contrib/README.md index ed3c048c32..e37b1ee294 100644 --- a/evennia/contrib/README.md +++ b/evennia/contrib/README.md @@ -19,7 +19,7 @@ things you want from here into your game folder and change them there. for any game. Allows safe trading of any godds (including coin) * CharGen (Griatch 2011) - A simple Character creator for OOC mode. Meant as a starting point for a more fleshed-out system. -* Clothing (BattleJenkins 2017) - A layered clothing system with +* Clothing (FlutterSprite 2017) - A layered clothing system with slots for different types of garments auto-showing in description. * Color-markups (Griatch, 2017) - Alternative in-game color markups. * Custom gametime (Griatch, vlgeoff 2017) - Implements Evennia's @@ -50,7 +50,7 @@ things you want from here into your game folder and change them there. time to pass depending on if you are walking/running etc. * Talking NPC (Griatch 2011) - A talking NPC object that offers a menu-driven conversation tree. -* Turnbattle (BattleJenkins 2017) - A turn-based combat engine meant +* Turnbattle (FlutterSprite 2017) - A turn-based combat engine meant as a start to build from. Has attack/disengage and turn timeouts. * Wilderness (titeuf87 2017) - Make infinitely large wilderness areas with dynamically created locations. From 89773e58608660f49b56d0c0a73169fdc52f76c6 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 05:11:53 -0700 Subject: [PATCH 16/17] Moved 'turnbattle' to packages section in readme --- evennia/contrib/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/README.md b/evennia/contrib/README.md index e37b1ee294..63abb2f713 100644 --- a/evennia/contrib/README.md +++ b/evennia/contrib/README.md @@ -50,8 +50,6 @@ things you want from here into your game folder and change them there. time to pass depending on if you are walking/running etc. * Talking NPC (Griatch 2011) - A talking NPC object that offers a menu-driven conversation tree. -* Turnbattle (FlutterSprite 2017) - A turn-based combat engine meant - as a start to build from. Has attack/disengage and turn timeouts. * Wilderness (titeuf87 2017) - Make infinitely large wilderness areas with dynamically created locations. * UnixCommand (Vincent Le Geoff 2017) - Add commands with UNIX-style syntax. @@ -62,6 +60,9 @@ things you want from here into your game folder and change them there. to the Evennia game index (games.evennia.com) * In-game Python (Vincent Le Goff 2017) - Allow trusted builders to script objects and events using Python from in-game. +* Turnbattle (FlutterSprite 2017) - A turn-based combat engine meant + as a start to build from. Has attack/disengage and turn timeouts, + and includes optional expansions for equipment and combat movement. * Tutorial examples (Griatch 2011, 2015) - A folder of basic example objects, commands and scripts. * Tutorial world (Griatch 2011, 2015) - A folder containing the From e48763a02e4c732968037ca579986e9a85727822 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 13:23:06 -0700 Subject: [PATCH 17/17] Clarify the nature of the different modules --- evennia/contrib/turnbattle/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/turnbattle/README.md b/evennia/contrib/turnbattle/README.md index af04f80060..729c42a099 100644 --- a/evennia/contrib/turnbattle/README.md +++ b/evennia/contrib/turnbattle/README.md @@ -26,9 +26,17 @@ implemented and customized: tracks the distance between different characters and objects in combat, as well as differentiates between melee and ranged attacks. - + This system is meant as a basic framework to start from, and is modeled after the combat systems of popular tabletop role playing games rather than the real-time battle systems that many MMOs and some MUDs use. As such, it may be better suited to role-playing or more story-oriented games, or games meant to closely emulate the experience of playing a tabletop RPG. + +Each of these modules contains the full functionality of the battle system +with different customizations added in - the instructions to install each +one is contained in the module itself. It's recommended that you install +and test tb_basic first, so you can better understand how the other +modules expand on it and get a better idea of how you can customize the +system to your liking and integrate the subsystems presented here into +your own combat system.