Advertisement
Guest User

tb_items.py

a guest
Nov 16th, 2017
122
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 35.08 KB | None | 0 0
  1. """
  2. Simple turn-based combat system with items and status effects
  3.  
  4. Contrib - Tim Ashley Jenkins 2017
  5.  
  6. This is a version of the 'turnbattle' combat system that includes status
  7. effects and usable items, which can instill these status effects, cure
  8. them, or do just about anything else.
  9.  
  10. Status effects are stored on characters as a dictionary, where the key
  11. is the name of the status effect and the value is a list of two items:
  12. an integer representing the number of turns left until the status runs
  13. out, and the character upon whose turn the condition timer is ticked
  14. down. Unlike most combat-related attributes, conditions aren't wiped
  15. once combat ends - if out of combat, they tick down in real time
  16. instead.
  17.  
  18. Items aren't given any sort of special typeclass - instead, whether or
  19. not an object counts as an item is determined by its attributes. To make
  20. an object into an item, it must have the attribute 'item_func', with
  21. the value given as a callable - this is the function that will be called
  22. when an item is used. Other properties of the item, such as how many
  23. uses it has, whether it's destroyed when its uses are depleted, and such
  24. can be specified on the item as well, but they are optional.
  25.  
  26. To install and test, import this module's TBItemsCharacter object into
  27. your game's character.py module:
  28.  
  29.    from evennia.contrib.turnbattle.tb_items import TBItemsCharacter
  30.  
  31. And change your game's character typeclass to inherit from TBItemsCharacter
  32. instead of the default:
  33.  
  34.    class Character(TBItemsCharacter):
  35.  
  36. Next, import this module into your default_cmdsets.py module:
  37.  
  38.    from evennia.contrib.turnbattle import tb_items
  39.  
  40. And add the battle command set to your default command set:
  41.  
  42.    #
  43.    # any commands you add below will overload the default ones.
  44.    #
  45.    self.add(tb_items.BattleCmdSet())
  46.  
  47. This module is meant to be heavily expanded on, so you may want to copy it
  48. to your game's 'world' folder and modify it there rather than importing it
  49. in your game and using it as-is.
  50. """
  51.  
  52. from random import randint
  53. from evennia import DefaultCharacter, Command, default_cmds, DefaultScript
  54. from evennia.commands.default.muxcommand import MuxCommand
  55. from evennia.commands.default.help import CmdHelp
  56. from evennia.utils.spawner import spawn
  57.  
  58. """
  59. ----------------------------------------------------------------------------
  60. OPTIONS
  61. ----------------------------------------------------------------------------
  62. """
  63.  
  64. TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds
  65. ACTIONS_PER_TURN = 1 # Number of actions allowed per turn
  66.  
  67. """
  68. ----------------------------------------------------------------------------
  69. COMBAT FUNCTIONS START HERE
  70. ----------------------------------------------------------------------------
  71. """
  72.  
  73. def roll_init(character):
  74.     """
  75.    Rolls a number between 1-1000 to determine initiative.
  76.  
  77.    Args:
  78.        character (obj): The character to determine initiative for
  79.  
  80.    Returns:
  81.        initiative (int): The character's place in initiative - higher
  82.        numbers go first.
  83.  
  84.    Notes:
  85.        By default, does not reference the character and simply returns
  86.        a random integer from 1 to 1000.
  87.  
  88.        Since the character is passed to this function, you can easily reference
  89.        a character's stats to determine an initiative roll - for example, if your
  90.        character has a 'dexterity' attribute, you can use it to give that character
  91.        an advantage in turn order, like so:
  92.  
  93.        return (randint(1,20)) + character.db.dexterity
  94.  
  95.        This way, characters with a higher dexterity will go first more often.
  96.    """
  97.     return randint(1, 1000)
  98.  
  99.  
  100. def get_attack(attacker, defender):
  101.     """
  102.    Returns a value for an attack roll.
  103.  
  104.    Args:
  105.        attacker (obj): Character doing the attacking
  106.        defender (obj): Character being attacked
  107.  
  108.    Returns:
  109.        attack_value (int): Attack roll value, compared against a defense value
  110.            to determine whether an attack hits or misses.
  111.  
  112.    Notes:
  113.        By default, returns a random integer from 1 to 100 without using any
  114.        properties from either the attacker or defender.
  115.  
  116.        This can easily be expanded to return a value based on characters stats,
  117.        equipment, and abilities. This is why the attacker and defender are passed
  118.        to this function, even though nothing from either one are used in this example.
  119.    """
  120.     # For this example, just return a random integer up to 100.
  121.     attack_value = randint(1, 100)
  122.     return attack_value
  123.  
  124.  
  125. def get_defense(attacker, defender):
  126.     """
  127.    Returns a value for defense, which an attack roll must equal or exceed in order
  128.    for an attack to hit.
  129.  
  130.    Args:
  131.        attacker (obj): Character doing the attacking
  132.        defender (obj): Character being attacked
  133.  
  134.    Returns:
  135.        defense_value (int): Defense value, compared against an attack roll
  136.            to determine whether an attack hits or misses.
  137.  
  138.    Notes:
  139.        By default, returns 50, not taking any properties of the defender or
  140.        attacker into account.
  141.  
  142.        As above, this can be expanded upon based on character stats and equipment.
  143.    """
  144.     # For this example, just return 50, for about a 50/50 chance of hit.
  145.     defense_value = 50
  146.     return defense_value
  147.  
  148.  
  149. def get_damage(attacker, defender):
  150.     """
  151.    Returns a value for damage to be deducted from the defender's HP after abilities
  152.    successful hit.
  153.  
  154.    Args:
  155.        attacker (obj): Character doing the attacking
  156.        defender (obj): Character being damaged
  157.  
  158.    Returns:
  159.        damage_value (int): Damage value, which is to be deducted from the defending
  160.            character's HP.
  161.  
  162.    Notes:
  163.        By default, returns a random integer from 15 to 25 without using any
  164.        properties from either the attacker or defender.
  165.  
  166.        Again, this can be expanded upon.
  167.    """
  168.     # For this example, just generate a number between 15 and 25.
  169.     damage_value = randint(15, 25)
  170.     return damage_value
  171.  
  172.  
  173. def apply_damage(defender, damage):
  174.     """
  175.    Applies damage to a target, reducing their HP by the damage amount to a
  176.    minimum of 0.
  177.  
  178.    Args:
  179.        defender (obj): Character taking damage
  180.        damage (int): Amount of damage being taken
  181.    """
  182.     defender.db.hp -= damage  # Reduce defender's HP by the damage dealt.
  183.     # If this reduces it to 0 or less, set HP to 0.
  184.     if defender.db.hp <= 0:
  185.         defender.db.hp = 0
  186.  
  187. def at_defeat(defeated):
  188.     """
  189.    Announces the defeat of a fighter in combat.
  190.    
  191.    Args:
  192.        defeated (obj): Fighter that's been defeated.
  193.    
  194.    Notes:
  195.        All this does is announce a defeat message by default, but if you
  196.        want anything else to happen to defeated fighters (like putting them
  197.        into a dying state or something similar) then this is the place to
  198.        do it.
  199.    """
  200.     defeated.location.msg_contents("%s has been defeated!" % defeated)
  201.  
  202. def resolve_attack(attacker, defender, attack_value=None, defense_value=None, damage_value=None):
  203.     """
  204.    Resolves an attack and outputs the result.
  205.  
  206.    Args:
  207.        attacker (obj): Character doing the attacking
  208.        defender (obj): Character being attacked
  209.  
  210.    Notes:
  211.        Even though the attack and defense values are calculated
  212.        extremely simply, they are separated out into their own functions
  213.        so that they are easier to expand upon.
  214.    """
  215.     # Get an attack roll from the attacker.
  216.     if not attack_value:
  217.         attack_value = get_attack(attacker, defender)
  218.     # Get a defense value from the defender.
  219.     if not defense_value:
  220.         defense_value = get_defense(attacker, defender)
  221.     # If the attack value is lower than the defense value, miss. Otherwise, hit.
  222.     if attack_value < defense_value:
  223.         attacker.location.msg_contents("%s's attack misses %s!" % (attacker, defender))
  224.     else:
  225.         if not damage_value:
  226.             damage_value = get_damage(attacker, defender)  # Calculate damage value.
  227.         # Announce damage dealt and apply damage.
  228.         attacker.location.msg_contents("%s hits %s for %i damage!" % (attacker, defender, damage_value))
  229.         apply_damage(defender, damage_value)
  230.         # If defender HP is reduced to 0 or less, call at_defeat.
  231.         if defender.db.hp <= 0:
  232.             at_defeat(defender)
  233.  
  234. def combat_cleanup(character):
  235.     """
  236.    Cleans up all the temporary combat-related attributes on a character.
  237.  
  238.    Args:
  239.        character (obj): Character to have their combat attributes removed
  240.  
  241.    Notes:
  242.        Any attribute whose key begins with 'combat_' is temporary and no
  243.        longer needed once a fight ends.
  244.    """
  245.     for attr in character.attributes.all():
  246.         if attr.key[:7] == "combat_":  # If the attribute name starts with 'combat_'...
  247.             character.attributes.remove(key=attr.key)  # ...then delete it!
  248.  
  249.  
  250. def is_in_combat(character):
  251.     """
  252.    Returns true if the given character is in combat.
  253.  
  254.    Args:
  255.        character (obj): Character to determine if is in combat or not
  256.  
  257.    Returns:
  258.        (bool): True if in combat or False if not in combat
  259.    """
  260.     return bool(character.db.combat_turnhandler)
  261.  
  262.  
  263. def is_turn(character):
  264.     """
  265.    Returns true if it's currently the given character's turn in combat.
  266.  
  267.    Args:
  268.        character (obj): Character to determine if it is their turn or not
  269.  
  270.    Returns:
  271.        (bool): True if it is their turn or False otherwise
  272.    """
  273.     turnhandler = character.db.combat_turnhandler
  274.     currentchar = turnhandler.db.fighters[turnhandler.db.turn]
  275.     return bool(character == currentchar)
  276.  
  277.  
  278. def spend_action(character, actions, action_name=None):
  279.     """
  280.    Spends a character's available combat actions and checks for end of turn.
  281.  
  282.    Args:
  283.        character (obj): Character spending the action
  284.        actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions
  285.  
  286.    Kwargs:
  287.        action_name (str or None): If a string is given, sets character's last action in
  288.        combat to provided string
  289.    """
  290.     if action_name:
  291.         character.db.combat_lastaction = action_name
  292.     if actions == 'all':  # If spending all actions
  293.         character.db.combat_actionsleft = 0  # Set actions to 0
  294.     else:
  295.         character.db.combat_actionsleft -= actions  # Use up actions.
  296.         if character.db.combat_actionsleft < 0:
  297.             character.db.combat_actionsleft = 0  # Can't have fewer than 0 actions
  298.     character.db.combat_turnhandler.turn_end_check(character)  # Signal potential end of turn.
  299.  
  300. def spend_item_use(item):
  301.     """
  302.    Spends one use on an item with limited uses. If item.db.item_consumable
  303.    is 'True', the item is destroyed if it runs out of uses - if it's a string
  304.    instead of 'True', it will also spawn a new object as residue, using the
  305.    value of item.db.item_consumable as the name of the prototype to spawn.
  306.    
  307.    
  308.    """
  309.     if item.db.item_uses:
  310.         item.db.item_uses -= 1 # Spend one use
  311.         if item.db.item_uses > 0: # Has uses remaining
  312.             # Inform th eplayer
  313.             self.caller.msg("%s has %i uses remaining." % (item.key.capitalize(), item.db.item_uses))
  314.         else: # All uses spent
  315.             if not item.db.item_consumable:
  316.                 # If not consumable, just inform the player that the uses are gone
  317.                 self.caller.msg("%s has no uses remaining." % item.key.capitalize())
  318.             else: # If consumable
  319.                 if item.db.item_consumable == True: # If the value is 'True', just destroy the item
  320.                     self.caller.msg("%s has been consumed." % item.key.capitalize())
  321.                     item.delete() # Delete the spent item
  322.                 else: # If a string, use value of item_consumable to spawn an object in its place
  323.                     residue = spawn({"prototype":item.db.item_consumable})[0] # Spawn the residue
  324.                     residue.location = item.location # Move the residue to the same place as the item
  325.                     self.caller.msg("After using %s, you are left with %s." % (item, residue))
  326.                     item.delete() # Delete the spent item
  327.  
  328. """
  329. ----------------------------------------------------------------------------
  330. CHARACTER TYPECLASS
  331. ----------------------------------------------------------------------------
  332. """
  333.  
  334.  
  335. class TBItemsCharacter(DefaultCharacter):
  336.     """
  337.    A character able to participate in turn-based combat. Has attributes for current
  338.    and maximum HP, and access to combat commands.
  339.    """
  340.  
  341.     def at_object_creation(self):
  342.         """
  343.        Called once, when this object is first created. This is the
  344.        normal hook to overload for most object types.
  345.        """
  346.         self.db.max_hp = 100  # Set maximum HP to 100
  347.         self.db.hp = self.db.max_hp  # Set current HP to maximum
  348.         """
  349.        Adds attributes for a character's current and maximum HP.
  350.        We're just going to set this value at '100' by default.
  351.  
  352.        You may want to expand this to include various 'stats' that
  353.        can be changed at creation and factor into combat calculations.
  354.        """
  355.  
  356.     def at_before_move(self, destination):
  357.         """
  358.        Called just before starting to move this object to
  359.        destination.
  360.  
  361.        Args:
  362.            destination (Object): The object we are moving to
  363.  
  364.        Returns:
  365.            shouldmove (bool): If we should move or not.
  366.  
  367.        Notes:
  368.            If this method returns False/None, the move is cancelled
  369.            before it is even started.
  370.  
  371.        """
  372.         # Keep the character from moving if at 0 HP or in combat.
  373.         if is_in_combat(self):
  374.             self.msg("You can't exit a room while in combat!")
  375.             return False  # Returning false keeps the character from moving.
  376.         if self.db.HP <= 0:
  377.             self.msg("You can't move, you've been defeated!")
  378.             return False
  379.         return True
  380.  
  381. """
  382. ----------------------------------------------------------------------------
  383. SCRIPTS START HERE
  384. ----------------------------------------------------------------------------
  385. """
  386.  
  387.  
  388. class TBItemsTurnHandler(DefaultScript):
  389.     """
  390.    This is the script that handles the progression of combat through turns.
  391.    On creation (when a fight is started) it adds all combat-ready characters
  392.    to its roster and then sorts them into a turn order. There can only be one
  393.    fight going on in a single room at a time, so the script is assigned to a
  394.    room as its object.
  395.  
  396.    Fights persist until only one participant is left with any HP or all
  397.    remaining participants choose to end the combat with the 'disengage' command.
  398.    """
  399.  
  400.     def at_script_creation(self):
  401.         """
  402.        Called once, when the script is created.
  403.        """
  404.         self.key = "Combat Turn Handler"
  405.         self.interval = 5  # Once every 5 seconds
  406.         self.persistent = True
  407.         self.db.fighters = []
  408.  
  409.         # Add all fighters in the room with at least 1 HP to the combat."
  410.         for thing in self.obj.contents:
  411.             if thing.db.hp:
  412.                 self.db.fighters.append(thing)
  413.  
  414.         # Initialize each fighter for combat
  415.         for fighter in self.db.fighters:
  416.             self.initialize_for_combat(fighter)
  417.  
  418.         # Add a reference to this script to the room
  419.         self.obj.db.combat_turnhandler = self
  420.  
  421.         # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order.
  422.         # The initiative roll is determined by the roll_init function and can be customized easily.
  423.         ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True)
  424.         self.db.fighters = ordered_by_roll
  425.  
  426.         # Announce the turn order.
  427.         self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters))
  428.        
  429.         # Start first fighter's turn.
  430.         self.start_turn(self.db.fighters[0])
  431.  
  432.         # Set up the current turn and turn timeout delay.
  433.         self.db.turn = 0
  434.         self.db.timer = TURN_TIMEOUT  # Set timer to turn timeout specified in options
  435.  
  436.     def at_stop(self):
  437.         """
  438.        Called at script termination.
  439.        """
  440.         for fighter in self.db.fighters:
  441.             combat_cleanup(fighter)  # Clean up the combat attributes for every fighter.
  442.         self.obj.db.combat_turnhandler = None  # Remove reference to turn handler in location
  443.  
  444.     def at_repeat(self):
  445.         """
  446.        Called once every self.interval seconds.
  447.        """
  448.         currentchar = self.db.fighters[self.db.turn]  # Note the current character in the turn order.
  449.         self.db.timer -= self.interval  # Count down the timer.
  450.  
  451.         if self.db.timer <= 0:
  452.             # Force current character to disengage if timer runs out.
  453.             self.obj.msg_contents("%s's turn timed out!" % currentchar)
  454.             spend_action(currentchar, 'all', action_name="disengage")  # Spend all remaining actions.
  455.             return
  456.         elif self.db.timer <= 10 and not self.db.timeout_warning_given:  # 10 seconds left
  457.             # Warn the current character if they're about to time out.
  458.             currentchar.msg("WARNING: About to time out!")
  459.             self.db.timeout_warning_given = True
  460.  
  461.     def initialize_for_combat(self, character):
  462.         """
  463.        Prepares a character for combat when starting or entering a fight.
  464.  
  465.        Args:
  466.            character (obj): Character to initialize for combat.
  467.        """
  468.         combat_cleanup(character)  # Clean up leftover combat attributes beforehand, just in case.
  469.         character.db.combat_actionsleft = 0  # Actions remaining - start of turn adds to this, turn ends when it reaches 0
  470.         character.db.combat_turnhandler = self  # Add a reference to this turn handler script to the character
  471.         character.db.combat_lastaction = "null"  # Track last action taken in combat
  472.  
  473.     def start_turn(self, character):
  474.         """
  475.        Readies a character for the start of their turn by replenishing their
  476.        available actions and notifying them that their turn has come up.
  477.  
  478.        Args:
  479.            character (obj): Character to be readied.
  480.  
  481.        Notes:
  482.            Here, you only get one action per turn, but you might want to allow more than
  483.            one per turn, or even grant a number of actions based on a character's
  484.            attributes. You can even add multiple different kinds of actions, I.E. actions
  485.            separated for movement, by adding "character.db.combat_movesleft = 3" or
  486.            something similar.
  487.        """
  488.         character.db.combat_actionsleft = ACTIONS_PER_TURN  # Replenish actions
  489.         # Prompt the character for their turn and give some information.
  490.         character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp)
  491.  
  492.     def next_turn(self):
  493.         """
  494.        Advances to the next character in the turn order.
  495.        """
  496.  
  497.         # Check to see if every character disengaged as their last action. If so, end combat.
  498.         disengage_check = True
  499.         for fighter in self.db.fighters:
  500.             if fighter.db.combat_lastaction != "disengage":  # If a character has done anything but disengage
  501.                 disengage_check = False
  502.         if disengage_check:  # All characters have disengaged
  503.             self.obj.msg_contents("All fighters have disengaged! Combat is over!")
  504.             self.stop()  # Stop this script and end combat.
  505.             return
  506.  
  507.         # Check to see if only one character is left standing. If so, end combat.
  508.         defeated_characters = 0
  509.         for fighter in self.db.fighters:
  510.             if fighter.db.HP == 0:
  511.                 defeated_characters += 1  # Add 1 for every fighter with 0 HP left (defeated)
  512.         if defeated_characters == (len(self.db.fighters) - 1):  # If only one character isn't defeated
  513.             for fighter in self.db.fighters:
  514.                 if fighter.db.HP != 0:
  515.                     LastStanding = fighter  # Pick the one fighter left with HP remaining
  516.             self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding)
  517.             self.stop()  # Stop this script and end combat.
  518.             return
  519.  
  520.         # Cycle to the next turn.
  521.         currentchar = self.db.fighters[self.db.turn]
  522.         self.db.turn += 1  # Go to the next in the turn order.
  523.         if self.db.turn > len(self.db.fighters) - 1:
  524.             self.db.turn = 0  # Go back to the first in the turn order once you reach the end.
  525.         newchar = self.db.fighters[self.db.turn]  # Note the new character
  526.         self.db.timer = TURN_TIMEOUT + self.time_until_next_repeat()  # Reset the timer.
  527.         self.db.timeout_warning_given = False  # Reset the timeout warning.
  528.         self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar))
  529.         self.start_turn(newchar)  # Start the new character's turn.
  530.  
  531.     def turn_end_check(self, character):
  532.         """
  533.        Tests to see if a character's turn is over, and cycles to the next turn if it is.
  534.  
  535.        Args:
  536.            character (obj): Character to test for end of turn
  537.        """
  538.         if not character.db.combat_actionsleft:  # Character has no actions remaining
  539.             self.next_turn()
  540.             return
  541.  
  542.     def join_fight(self, character):
  543.         """
  544.        Adds a new character to a fight already in progress.
  545.  
  546.        Args:
  547.            character (obj): Character to be added to the fight.
  548.        """
  549.         # Inserts the fighter to the turn order, right behind whoever's turn it currently is.
  550.         self.db.fighters.insert(self.db.turn, character)
  551.         # Tick the turn counter forward one to compensate.
  552.         self.db.turn += 1
  553.         # Initialize the character like you do at the start.
  554.         self.initialize_for_combat(character)
  555.  
  556.        
  557. """
  558. ----------------------------------------------------------------------------
  559. COMMANDS START HERE
  560. ----------------------------------------------------------------------------
  561. """
  562.  
  563.  
  564. class CmdFight(Command):
  565.     """
  566.    Starts a fight with everyone in the same room as you.
  567.  
  568.    Usage:
  569.      fight
  570.  
  571.    When you start a fight, everyone in the room who is able to
  572.    fight is added to combat, and a turn order is randomly rolled.
  573.    When it's your turn, you can attack other characters.
  574.    """
  575.     key = "fight"
  576.     help_category = "combat"
  577.  
  578.     def func(self):
  579.         """
  580.        This performs the actual command.
  581.        """
  582.         here = self.caller.location
  583.         fighters = []
  584.  
  585.         if not self.caller.db.hp:  # If you don't have any hp
  586.             self.caller.msg("You can't start a fight if you've been defeated!")
  587.             return
  588.         if is_in_combat(self.caller):  # Already in a fight
  589.             self.caller.msg("You're already in a fight!")
  590.             return
  591.         for thing in here.contents:  # Test everything in the room to add it to the fight.
  592.             if thing.db.HP:  # If the object has HP...
  593.                 fighters.append(thing)  # ...then add it to the fight.
  594.         if len(fighters) <= 1:  # If you're the only able fighter in the room
  595.             self.caller.msg("There's nobody here to fight!")
  596.             return
  597.         if here.db.combat_turnhandler:  # If there's already a fight going on...
  598.             here.msg_contents("%s joins the fight!" % self.caller)
  599.             here.db.combat_turnhandler.join_fight(self.caller)  # Join the fight!
  600.             return
  601.         here.msg_contents("%s starts a fight!" % self.caller)
  602.         # Add a turn handler script to the room, which starts combat.
  603.         here.scripts.add("contrib.turnbattle.tb_items.TBItemsTurnHandler")
  604.         # Remember you'll have to change the path to the script if you copy this code to your own modules!
  605.  
  606.  
  607. class CmdAttack(Command):
  608.     """
  609.    Attacks another character.
  610.  
  611.    Usage:
  612.      attack <target>
  613.  
  614.    When in a fight, you may attack another character. The attack has
  615.    a chance to hit, and if successful, will deal damage.
  616.    """
  617.  
  618.     key = "attack"
  619.     help_category = "combat"
  620.  
  621.     def func(self):
  622.         "This performs the actual command."
  623.         "Set the attacker to the caller and the defender to the target."
  624.  
  625.         if not is_in_combat(self.caller):  # If not in combat, can't attack.
  626.             self.caller.msg("You can only do that in combat. (see: help fight)")
  627.             return
  628.  
  629.         if not is_turn(self.caller):  # If it's not your turn, can't attack.
  630.             self.caller.msg("You can only do that on your turn.")
  631.             return
  632.  
  633.         if not self.caller.db.hp:  # Can't attack if you have no HP.
  634.             self.caller.msg("You can't attack, you've been defeated.")
  635.             return
  636.  
  637.         attacker = self.caller
  638.         defender = self.caller.search(self.args)
  639.  
  640.         if not defender:  # No valid target given.
  641.             return
  642.  
  643.         if not defender.db.hp:  # Target object has no HP left or to begin with
  644.             self.caller.msg("You can't fight that!")
  645.             return
  646.  
  647.         if attacker == defender:  # Target and attacker are the same
  648.             self.caller.msg("You can't attack yourself!")
  649.             return
  650.  
  651.         "If everything checks out, call the attack resolving function."
  652.         resolve_attack(attacker, defender)
  653.         spend_action(self.caller, 1, action_name="attack")  # Use up one action.
  654.  
  655.  
  656. class CmdPass(Command):
  657.     """
  658.    Passes on your turn.
  659.  
  660.    Usage:
  661.      pass
  662.  
  663.    When in a fight, you can use this command to end your turn early, even
  664.    if there are still any actions you can take.
  665.    """
  666.  
  667.     key = "pass"
  668.     aliases = ["wait", "hold"]
  669.     help_category = "combat"
  670.  
  671.     def func(self):
  672.         """
  673.        This performs the actual command.
  674.        """
  675.         if not is_in_combat(self.caller):  # Can only pass a turn in combat.
  676.             self.caller.msg("You can only do that in combat. (see: help fight)")
  677.             return
  678.  
  679.         if not is_turn(self.caller):  # Can only pass if it's your turn.
  680.             self.caller.msg("You can only do that on your turn.")
  681.             return
  682.  
  683.         self.caller.location.msg_contents("%s takes no further action, passing the turn." % self.caller)
  684.         spend_action(self.caller, 'all', action_name="pass")  # Spend all remaining actions.
  685.  
  686.  
  687. class CmdDisengage(Command):
  688.     """
  689.    Passes your turn and attempts to end combat.
  690.  
  691.    Usage:
  692.      disengage
  693.  
  694.    Ends your turn early and signals that you're trying to end
  695.    the fight. If all participants in a fight disengage, the
  696.    fight ends.
  697.    """
  698.  
  699.     key = "disengage"
  700.     aliases = ["spare"]
  701.     help_category = "combat"
  702.  
  703.     def func(self):
  704.         """
  705.        This performs the actual command.
  706.        """
  707.         if not is_in_combat(self.caller):  # If you're not in combat
  708.             self.caller.msg("You can only do that in combat. (see: help fight)")
  709.             return
  710.  
  711.         if not is_turn(self.caller):  # If it's not your turn
  712.             self.caller.msg("You can only do that on your turn.")
  713.             return
  714.  
  715.         self.caller.location.msg_contents("%s disengages, ready to stop fighting." % self.caller)
  716.         spend_action(self.caller, 'all', action_name="disengage")  # Spend all remaining actions.
  717.         """
  718.        The action_name kwarg sets the character's last action to "disengage", which is checked by
  719.        the turn handler script to see if all fighters have disengaged.
  720.        """
  721.  
  722.  
  723. class CmdRest(Command):
  724.     """
  725.    Recovers damage.
  726.  
  727.    Usage:
  728.      rest
  729.  
  730.    Resting recovers your HP to its maximum, but you can only
  731.    rest if you're not in a fight.
  732.    """
  733.  
  734.     key = "rest"
  735.     help_category = "combat"
  736.  
  737.     def func(self):
  738.         "This performs the actual command."
  739.  
  740.         if is_in_combat(self.caller):  # If you're in combat
  741.             self.caller.msg("You can't rest while you're in combat.")
  742.             return
  743.  
  744.         self.caller.db.hp = self.caller.db.max_hp  # Set current HP to maximum
  745.         self.caller.location.msg_contents("%s rests to recover HP." % self.caller)
  746.         """
  747.        You'll probably want to replace this with your own system for recovering HP.
  748.        """
  749.  
  750.  
  751. class CmdCombatHelp(CmdHelp):
  752.     """
  753.    View help or a list of topics
  754.  
  755.    Usage:
  756.      help <topic or command>
  757.      help list
  758.      help all
  759.  
  760.    This will search for help on commands and other
  761.    topics related to the game.
  762.    """
  763.     # Just like the default help command, but will give quick
  764.     # tips on combat when used in a fight with no arguments.
  765.  
  766.     def func(self):
  767.         if is_in_combat(self.caller) and not self.args:  # In combat and entered 'help' alone
  768.             self.caller.msg("Available combat commands:|/" +
  769.                             "|wAttack:|n Attack a target, attempting to deal damage.|/" +
  770.                             "|wPass:|n Pass your turn without further action.|/" +
  771.                             "|wDisengage:|n End your turn and attempt to end combat.|/")
  772.         else:
  773.             super(CmdCombatHelp, self).func()  # Call the default help command
  774.  
  775.  
  776. class CmdUse(MuxCommand):
  777.     """
  778.    Use an item.
  779.  
  780.    Usage:
  781.      use <item> [= target]
  782.  
  783.    Items: you just GOTTA use them.
  784.    """
  785.  
  786.     key = "use"
  787.     help_category = "combat"
  788.  
  789.     def func(self):
  790.         """
  791.        This performs the actual command.
  792.        """
  793.         item = self.caller.search(self.lhs, candidates=self.caller.contents)
  794.         if not item:
  795.             return
  796.        
  797.         target = None
  798.         if self.rhs:
  799.             target = self.caller.search(self.rhs)
  800.             if not target:
  801.                 return
  802.                
  803.         if is_in_combat(self.caller):
  804.             if not is_turn(self.caller):
  805.                 self.caller.msg("You can only use items on your turn.")
  806.                 return
  807.            
  808.         if not item.db.item_func: # Object has no item_func, not usable
  809.             self.caller.msg("'%s' is not a usable item." % item.key.capitalize())
  810.             return
  811.            
  812.         if item.attributes.has("item_uses"): # Item has limited uses
  813.             if item.db.item_uses <= 0: # Limited uses are spent
  814.                 self.caller.msg("'%s' has no uses remaining." % item.key.capitalize())
  815.                 return
  816.        
  817.         kwargs = {}
  818.         if item.db.item_kwargs:
  819.             kwargs = item.db.item_kwargs # Set kwargs to pass to item_func
  820.            
  821.         # Match item_func string to function
  822.         try:
  823.             item_func = ITEMFUNCS[item.db.item_func]
  824.         except KeyError:
  825.             self.caller.msg("ERROR: %s not defined in ITEMFUNCS" % item.db.item_func)
  826.             return
  827.        
  828.         # Call the item function - abort if it returns False, indicating an error.
  829.         # Regardless of what the function returns (if anything), it's still executed.
  830.         if item_func(item, self.caller, target, **kwargs) == False:
  831.             return
  832.            
  833.         # If we haven't returned yet, we assume the item was used successfully.
  834.            
  835.         # Spend one use if item has limited uses
  836.         spend_item_use(item)
  837.            
  838.         # Spend an action if in combat
  839.         if is_in_combat(self.caller):
  840.             spend_action(self.caller, 1, action_name="item")
  841.  
  842.  
  843. class BattleCmdSet(default_cmds.CharacterCmdSet):
  844.     """
  845.    This command set includes all the commmands used in the battle system.
  846.    """
  847.     key = "DefaultCharacter"
  848.  
  849.     def at_cmdset_creation(self):
  850.         """
  851.        Populates the cmdset
  852.        """
  853.         self.add(CmdFight())
  854.         self.add(CmdAttack())
  855.         self.add(CmdRest())
  856.         self.add(CmdPass())
  857.         self.add(CmdDisengage())
  858.         self.add(CmdCombatHelp())
  859.         self.add(CmdUse())
  860.        
  861. """
  862. ITEM FUNCTIONS START HERE
  863. """
  864.  
  865. def itemfunc_heal(item, user, target, **kwargs):
  866.     """
  867.    Item function that heals HP.
  868.    """
  869.     if not target:
  870.         target = user # Target user if none specified
  871.    
  872.     if not target.attributes.has("max_hp"): # Has no HP to speak of
  873.         user.msg("You can't use %s on that." % item)
  874.         return False
  875.        
  876.     if target.db.hp >= target.db.max_hp:
  877.         user.msg("%s is already at full health." % target)
  878.         return False
  879.    
  880.     min_healing = 20
  881.     max_healing = 40
  882.    
  883.     # Retrieve healing range from kwargs, if present
  884.     if "healing_range" in kwargs:
  885.         min_healing = kwargs["healing_range"][0]
  886.         max_healing = kwargs["healing_range"][1]
  887.  
  888.     to_heal = randint(min_healing, max_healing) # Restore 20 to 40 hp
  889.     if target.db.hp + to_heal > target.db.max_hp:
  890.         to_heal = target.db.max_hp - target.db.hp # Cap healing to max HP
  891.     target.db.hp += to_heal
  892.    
  893.     user.location.msg_contents("%s uses %s! %s regains %i HP!" % (user, item, target, to_heal))
  894.    
  895. def itemfunc_attack(item, user, target, **kwargs):
  896.     """
  897.    Item function that attacks a target.
  898.    """
  899.     if not is_in_combat(user):
  900.         user.msg("You can only use that in combat.")
  901.         return False
  902.    
  903.     if not target:
  904.         user.msg("You have to specify a target to use %s! (use <item> = <target>)" % item)
  905.         return False
  906.        
  907.     if target == user:
  908.         user.msg("You can't attack yourself!")
  909.         return False
  910.    
  911.     if not target.db.hp: # Has no HP
  912.         user.msg("You can't use %s on that." % item)
  913.         return False
  914.    
  915.     min_damage = 20
  916.     max_damage = 40
  917.     accuracy = 0
  918.    
  919.     # Retrieve values from kwargs, if present
  920.     if "damage_range" in kwargs:
  921.         min_damage = kwargs["damage_range"][0]
  922.         max_damage = kwargs["damage_range"][1]
  923.     if "accuracy" in kwargs:
  924.         accuracy = kwargs["accuracy"]
  925.        
  926.     # Roll attack and damage
  927.     attack_value = randint(1, 100) + accuracy
  928.     damage_value = randint(min_damage, max_damage)
  929.    
  930.     user.location.msg_contents("%s attacks %s with %s!" % (user, target, item))
  931.     resolve_attack(user, target, attack_value=attack_value, damage_value=damage_value)
  932.  
  933. # Match strings to item functions here. We can't store callables on
  934. # prototypes, so we store a string instead, matching that string to
  935. # a callable in this dictionary.
  936. ITEMFUNCS = {
  937.     "heal":itemfunc_heal,
  938.     "attack":itemfunc_attack
  939. }
  940.  
  941. """
  942. ITEM PROTOTYPES START HERE
  943.  
  944. Copy these to your game's /world/prototypes.py module!
  945. """
  946.  
  947. MEDKIT = {
  948.  "key" : "a medical kit",
  949.  "aliases" : ["medkit"],
  950.  "desc" : "A standard medical kit. It can be used a few times to heal wounds.",
  951.  "item_func" : "heal",
  952.  "item_uses" : 3,
  953.  "item_consumable" : True,
  954.  "item_kwargs" : {"healing_range":(15, 25)}
  955. }
  956.  
  957. GLASS_BOTTLE = {
  958.  "key" : "a glass bottle",
  959.  "desc" : "An empty glass bottle."
  960. }
  961.  
  962. HEALTH_POTION = {
  963.  "key" : "a health potion",
  964.  "desc" : "A glass bottle full of a mystical potion that heals wounds when used.",
  965.  "item_func" : "heal",
  966.  "item_uses" : 1,
  967.  "item_consumable" : "GLASS_BOTTLE",
  968.  "item_kwargs" : {"healing_range":(35, 50)}
  969. }
  970.  
  971. BOMB = {
  972.  "key" : "a rotund bomb",
  973.  "desc" : "A large black sphere with a fuse at the end. Can be used on enemies in combat.",
  974.  "item_func" : "attack",
  975.  "item_uses" : 1,
  976.  "item_consumable" : True,
  977.  "item_kwargs" : {"damage_range":(25, 40), "accuracy":25}
  978. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement