More conditions and documentation

This commit is contained in:
FlutterSprite 2017-11-19 12:16:38 -08:00
parent 1d65a0a0cf
commit 42db3aa7f5

View file

@ -3,18 +3,35 @@ Simple turn-based combat system with items and status effects
Contrib - Tim Ashley Jenkins 2017 Contrib - Tim Ashley Jenkins 2017
This is a version of the 'turnbattle' combat system that includes status This is a version of the 'turnbattle' combat system that includes
effects and usable items, which can instill these status effects, cure conditions and usable items, which can instill these conditions, cure
them, or do just about anything else. them, or do just about anything else.
Status effects are stored on characters as a dictionary, where the key Conditions are stored on characters as a dictionary, where the key
is the name of the status effect and the value is a list of two items: is the name of the condition and the value is a list of two items:
an integer representing the number of turns left until the status runs an integer representing the number of turns left until the condition
out, and the character upon whose turn the condition timer is ticked runs out, and the character upon whose turn the condition timer is
down. Unlike most combat-related attributes, conditions aren't wiped ticked down. Unlike most combat-related attributes, conditions aren't
once combat ends - if out of combat, they tick down in real time wiped once combat ends - if out of combat, they tick down in real time
instead. instead.
This module includes a number of example conditions:
Regeneration: Character recovers HP every turn
Poisoned: Character loses HP every turn
Accuracy Up: +25 to character's attack rolls
Accuracy Down: -25 to character's attack rolls
Damage Up: +5 to character's damage
Damage Down: -5 to character's damage
Defense Up: +15 to character's defense
Defense Down: -15 to character's defense
Haste: +1 action per turn
Paralyzed: No actions per turn
Frightened: Character can't use the 'attack' command
Since conditions can have a wide variety of effects, their code is
scattered throughout the other functions wherever they may apply.
Items aren't given any sort of special typeclass - instead, whether or Items aren't given any sort of special typeclass - instead, whether or
not an object counts as an item is determined by its attributes. To make not an object counts as an item is determined by its attributes. To make
an object into an item, it must have the attribute 'item_func', with an object into an item, it must have the attribute 'item_func', with
@ -64,6 +81,7 @@ OPTIONS
TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds
ACTIONS_PER_TURN = 1 # Number of actions allowed per turn ACTIONS_PER_TURN = 1 # Number of actions allowed per turn
NONCOMBAT_TURN_TIME = 30 # Time per turn count out of combat
""" """
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
@ -111,15 +129,18 @@ def get_attack(attacker, defender):
to determine whether an attack hits or misses. to determine whether an attack hits or misses.
Notes: Notes:
By default, returns a random integer from 1 to 100 without using any This is where conditions affecting attack rolls are applied, as well.
properties from either the attacker or defender. Accuracy Up and Accuracy Down are also accounted for in itemfunc_attack(),
so that attack items' accuracy is affected as well.
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. # For this example, just return a random integer up to 100.
attack_value = randint(1, 100) attack_value = randint(1, 100)
# Add 25 to the roll if the attacker has the "Accuracy Up" condition.
if "Accuracy Up" in attacker.db.conditions:
attack_value += 25
# Subtract 25 from the roll if the attack has the "Accuracy Down" condition.
if "Accuracy Down" in attacker.db.conditions:
attack_value -= 25
return attack_value return attack_value
@ -137,13 +158,16 @@ def get_defense(attacker, defender):
to determine whether an attack hits or misses. to determine whether an attack hits or misses.
Notes: Notes:
By default, returns 50, not taking any properties of the defender or This is where conditions affecting defense are accounted for.
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. # For this example, just return 50, for about a 50/50 chance of hit.
defense_value = 50 defense_value = 50
# Add 15 to defense if the defender has the "Defense Up" condition.
if "Defense Up" in defender.db.conditions:
defense_value += 15
# Subtract 15 from defense if the defender has the "Defense Down" condition.
if "Defense Down" in defender.db.conditions:
defense_value -= 15
return defense_value return defense_value
@ -161,13 +185,18 @@ def get_damage(attacker, defender):
character's HP. character's HP.
Notes: Notes:
By default, returns a random integer from 15 to 25 without using any This is where conditions affecting damage are accounted for. Since attack items
properties from either the attacker or defender. roll their own damage in itemfunc_attack(), their damage is unaffected by any
conditions.
Again, this can be expanded upon.
""" """
# For this example, just generate a number between 15 and 25. # For this example, just generate a number between 15 and 25.
damage_value = randint(15, 25) damage_value = randint(15, 25)
# Add 5 to damage roll if attacker has the "Damage Up" condition.
if "Damage Up" in attacker.db.conditions:
damage_value += 5
# Subtract 5 from the roll if the attacker has the "Damage Down" condition.
if "Damage Down" in attacker.db.conditions:
damage_value -= 5
return damage_value return damage_value
@ -209,10 +238,16 @@ def resolve_attack(attacker, defender, attack_value=None, defense_value=None,
attacker (obj): Character doing the attacking attacker (obj): Character doing the attacking
defender (obj): Character being attacked defender (obj): Character being attacked
Options:
attack_value (int): Override for attack roll
defense_value (int): Override for defense value
damage_value (int): Override for damage value
inflict_condition (list): Conditions to inflict upon hit, a
list of tuples formated as (condition(str), duration(int))
Notes: Notes:
Even though the attack and defense values are calculated This function is called by normal attacks as well as attacks
extremely simply, they are separated out into their own functions made with items.
so that they are easier to expand upon.
""" """
# Get an attack roll from the attacker. # Get an attack roll from the attacker.
if not attack_value: if not attack_value:
@ -391,14 +426,14 @@ def condition_tickdown(character, turnchar):
Notes: Notes:
In combat, this is called on every fighter at the start of every character's turn. Out of In combat, this is called on every fighter at the start of every character's turn. Out of
combat, it's instead called when a character's at_update() hook is called, which is every combat, it's instead called when a character's at_update() hook is called, which is every
30 seconds. 30 seconds by default.
""" """
for key in character.db.conditions: for key in character.db.conditions:
# The first value is the remaining turns - the second value is whose turn to count down on. # The first value is the remaining turns - the second value is whose turn to count down on.
condition_duration = character.db.conditions[key][0] condition_duration = character.db.conditions[key][0]
condition_turnchar = character.db.conditions[key][1] condition_turnchar = character.db.conditions[key][1]
# If the duration is 'True', then condition doesn't tick down - it lasts indefinitely. # If the duration is 'True', then the condition doesn't tick down - it lasts indefinitely.
if not condition_duration == True: if not condition_duration == True:
# Count down if the given turn character matches the condition's turn character. # Count down if the given turn character matches the condition's turn character.
if condition_turnchar == turnchar: if condition_turnchar == turnchar:
@ -445,15 +480,16 @@ class TBItemsCharacter(DefaultCharacter):
self.db.hp = self.db.max_hp # Set current HP to maximum self.db.hp = self.db.max_hp # Set current HP to maximum
self.db.conditions = {} # Set empty dict for conditions self.db.conditions = {} # Set empty dict for conditions
# Subscribe character to the ticker handler # Subscribe character to the ticker handler
tickerhandler.add(30, self.at_update) tickerhandler.add(NONCOMBAT_TURN_TIME, self.at_update)
""" """
Adds attributes for a character's current and maximum HP. Adds attributes for a character's current and maximum HP.
We're just going to set this value at '100' by default. We're just going to set this value at '100' by default.
An empty dictionary is created to store conditions later, An empty dictionary is created to store conditions later,
and the character is subscribed to the Ticker Handler, which and the character is subscribed to the Ticker Handler, which
will call at_update() on the character every 30 seconds. This will call at_update() on the character, with the interval
is used to tick down conditions out of combat. specified by NONCOMBAT_TURN_TIME above. This is used to tick
down conditions out of combat.
You may want to expand this to include various 'stats' that You may want to expand this to include various 'stats' that
can be changed at creation and factor into combat calculations. can be changed at creation and factor into combat calculations.
@ -516,6 +552,16 @@ class TBItemsCharacter(DefaultCharacter):
# Call at_defeat if poison defeats the character # Call at_defeat if poison defeats the character
at_defeat(self) at_defeat(self)
# Haste: Gain an extra action in combat.
if is_in_combat(self) and "Haste" in self.db.conditions:
self.db.combat_actionsleft += 1
self.msg("You gain an extra action this turn from Haste!")
# Paralyzed: Have no actions in combat.
if is_in_combat(self) and "Paralyzed" in self.db.conditions:
self.db.combat_actionsleft = 0
self.msg("You're Paralyzed, and can't act this turn!")
def at_update(self): def at_update(self):
""" """
Fires every 30 seconds. Fires every 30 seconds.
@ -792,6 +838,10 @@ class CmdAttack(Command):
self.caller.msg("You can't attack, you've been defeated.") self.caller.msg("You can't attack, you've been defeated.")
return return
if "Frightened" in self.caller.db.conditions: # Can't attack if frightened
self.caller.msg("You're too frightened to attack!")
return
attacker = self.caller attacker = self.caller
defender = self.caller.search(self.args) defender = self.caller.search(self.args)
@ -939,7 +989,9 @@ class CmdUse(MuxCommand):
Usage: Usage:
use <item> [= target] use <item> [= target]
Items: you just GOTTA use them. An item can have various function - looking at the item may
provide information as to its effects. Some items can be used
to attack others, and as such can only be used in combat.
""" """
key = "use" key = "use"