diff --git a/evennia/contrib/rpg/traits/traits.py b/evennia/contrib/rpg/traits/traits.py index 303fe89bf7..b0d7fa57f7 100644 --- a/evennia/contrib/rpg/traits/traits.py +++ b/evennia/contrib/rpg/traits/traits.py @@ -175,7 +175,7 @@ if trait1 > trait2: The static trait has a `base` value and an optional `mod`-ifier and 'mult'-iplier. The modifier defaults to 0, and the multiplier to 1.0, for no change in value. -A typical use of a static trait would be a Strength stat or Skill value. That is, +A typical use of a static trait would be a Strength stat or Skill value. That is, somethingthat varies slowly or not at all, and which may be modified in-place. ```python @@ -207,9 +207,9 @@ somethingthat varies slowly or not at all, and which may be modified in-place. A counter describes a value that can move from a base. The `.current` property is the thing usually modified. It starts at the `.base`. One can also add a -modifier, which is added to both the base and to current. '.value' is then formed -by multiplying by the multiplier, which defaults to 1.0 for no change. The min/max -of the range are optional, a boundary set to None will remove it. A suggested use +modifier, which is added to both the base and to current. '.value' is then formed +by multiplying by the multiplier, which defaults to 1.0 for no change. The min/max +of the range are optional, a boundary set to None will remove it. A suggested use for a Counter Trait would be to track skill values. ```python @@ -1148,7 +1148,7 @@ class Trait: class StaticTrait(Trait): """ - Static Trait. This is a single value with a modifier, + Static Trait. This is a single value with a modifier, multiplier, and no concept of a 'current' value or min/max etc. value = (base + mod) * mult @@ -1189,7 +1189,7 @@ class StaticTrait(Trait): def mult(self): """The trait's multiplier.""" return self._data["mult"] - + @mult.setter def mult(self, amount): if type(amount) in (int, float): @@ -1378,7 +1378,7 @@ class CounterTrait(Trait): @property def mult(self): return self._data["mult"] - + @mult.setter def mult(self, amount): if type(amount) in (int, float): @@ -1596,11 +1596,11 @@ class GaugeTrait(CounterTrait): if value + self.base < self.min: value = self.min - self.base self._data["mod"] = value - + @property def mult(self): return self._data["mult"] - + @mult.setter def mult(self, amount): if type(amount) in (int, float): diff --git a/evennia/contrib/tutorials/evadventure/characters.py b/evennia/contrib/tutorials/evadventure/characters.py index 7d9565223b..c139abf30f 100644 --- a/evennia/contrib/tutorials/evadventure/characters.py +++ b/evennia/contrib/tutorials/evadventure/characters.py @@ -328,6 +328,29 @@ class EvAdventureCharacter(DefaultCharacter): """ # TODO + @property + def hurt_level(self): + """ + String describing how hurt this character is. + """ + percent = max(0, min(100, 100 * (self.hp / self.hp_max))) + if 95 < percent <= 100: + return "|gPerfect|n" + elif 80 < percent <= 95: + return "|gScraped|n" + elif 60 < percent <= 80: + return "|GBruised|n" + elif 45 < percent <= 60: + return "|yHurt|n" + elif 30 < percent <= 45: + return "|yWounded|n" + elif 15 < percent <= 30: + return "|rBadly wounded|n" + elif 1 < percent <= 15: + return "|rBarely hanging on|n" + elif percent == 0: + return "|RCollapsed!|n" + def heal(self, hp, healer=None): """ Heal the character by a certain amount of HP. diff --git a/evennia/contrib/tutorials/evadventure/combat_turnbased.py b/evennia/contrib/tutorials/evadventure/combat_turnbased.py index b1ae9cf816..6da926fc91 100644 --- a/evennia/contrib/tutorials/evadventure/combat_turnbased.py +++ b/evennia/contrib/tutorials/evadventure/combat_turnbased.py @@ -20,7 +20,7 @@ from collections import defaultdict from evennia.scripts.scripts import DefaultScript from evennia.typeclasses.attributes import AttributeProperty from evennia.utils.utils import make_iter -from evennia.utils import evmenu +from evennia.utils import evmenu, evtable from . import rules MIN_RANGE = 0 @@ -48,26 +48,37 @@ class CombatAction: """ key = 'action' - status_text = "{combatant} performs an action." + post_action_text = "{combatant} performed an action." + # move actions can be combined with other actions is_move_action = False - def __init__(self, combathandler): + def __init__(self, combathandler, combatant): self.combathandler = combathandler + self.combatant = combatant def can_use(self, combatant, *args, **kwargs): """ Determine if combatant can use this action. - """ - return True + Args: + combatant (Object): The one performing the action. + *args: Any optional arguments. + **kwargs: Any optional keyword arguments. - def use(self, combatant, *args, **kwargs): + Returns: + tuple: (bool, motivation) - if not available, will describe why, + if available, should describe what the action does. + + """ + return True + + def use(self, *args, **kwargs): """ Use action """ - self.combathandler.msg(self.status_text.format(combatant=combatant)) + self.combathandler.msg(self.post_action_text.format(combatant=combatant)) class CombatActionDoNothing(CombatAction): @@ -75,7 +86,7 @@ class CombatActionDoNothing(CombatAction): Do nothing this turn. """ - status_text = "{combatant} does nothing this turn." + post_action_text = "{combatant} does nothing this turn." class CombatActionStunt(CombatAction): @@ -83,6 +94,28 @@ class CombatActionStunt(CombatAction): Perform a stunt. """ + optimal_distance = 0 + suboptimal_distance = 1 + advantage = True + attack_type = "dexterity" + defense_type = "dexterity" + + def can_use(self, combatant, defender, *args, **kwargs): + distance = self.combathandler.distance_matrix[attacker][defender] + + disadvantage = False + if self.suboptimal_distance == distance: + # stunts need to be within range + disadvantage = True + elif self.optimal_distance != distance: + # if we are neither at optimal nor suboptimal distance, we can't do the stunt + # from here. + return False, (f"you can't perform this stunt " + f"from {range_names[distance]} distance (must be " + f"{range_names[suboptimal_distance]} or, even better, " + f"{range_names[optimal_distance]}).") + + @@ -111,6 +144,12 @@ class EvAdventureCombatHandler(DefaultScript): # actions that will be performed before a normal action move_actions = ("approach", "withdraw") + def at_init(self): + self.ndb.actions = { + "do_nothing": CombatActionDoNothing, + } + + def _refresh_distance_matrix(self): """ Refresh the distance matrix, either after movement or when a @@ -248,6 +287,37 @@ class EvAdventureCombatHandler(DefaultScript): self.combatants.remove(combatant) self._refresh_distance_matrix() + def get_combat_summary(self, combatant): + """ + Get a summary of the current combat state. + + You (5/10 health) + Foo (Hurt) distance: You__0__1___X____3_____4 (medium) + Bar (Perfect health): You__X__1___2____3_____4 (close) + + """ + table = evtable.EvTable(border_width=0) + + table.add_row(f"You ({combatant.hp} / {combatant.hp_max} health)") + + dist_template = "|x(You)__{0}|x__{1}|x___{2}|x____{3}|x_____{4} |x({distname})" + + for comb in self.combatants: + + if comb is combatant: + continue + + name = combatant.key + distance = self.distance_matrix[combatant][comb] + dist_map = {i: '|wX' if i == distance else i for i in range(MAX_RANGE)} + dist_map["distname"] = RANGE_NAMES[distance] + health = f"{comb.hurt_level}" + distance_string = dist_template.format(**dist_map) + + table.add_row(f"{name} ({health})", distance_string) + + return str(table) + def msg(self, message, targets=None): """ Central place for sending messages to combatants. This allows @@ -399,10 +469,10 @@ class EvAdventureCombatHandler(DefaultScript): elif self._get_optimal_distance(attacker) != distance: # if we are neither at optimal nor suboptimal distance, we can't do the stunt # from here. - raise CombatFailure(f"You can't perform this stunt " - f"from {RANGE_NAMES[distance]} distance (must be " - f"{RANGE_NAMES[suboptimal_distance]} or, even better, " - f"{RANGE_NAMES[optimal_distance]}).") + raise combatfailure(f"you can't perform this stunt " + f"from {range_names[distance]} distance (must be " + f"{range_names[suboptimal_distance]} or, even better, " + f"{range_names[optimal_distance]}).") # quality doesn't matter for stunts, they are either successful or not is_success, _ = rules.EvAdventureRollEngine.opposed_saving_throw( attacker, defender, @@ -582,7 +652,6 @@ def node_select_action(caller, raw_string, **kwargs): text = combat.get_previous_turn_status(caller) options = combat.get_available_options(caller) - # TODO - reshuffle options options = { "desc": action,