from __future__ import annotations from typing import Optional, TYPE_CHECKING from utils import roll_dice if TYPE_CHECKING: from entity import Entity class BaseUseable: """ Base type for useable items, with various utilities. Please note that distances are calculated with the Pythagorean theorem, so a maximum range of `1` won't work in diagonally adjacent tiles. """ entity: Entity = None consumable: bool current_stack: int maximum_stack: int minimum_range: int maximum_range: int def __init__(self, *, consumable: bool = False, current_stack: int = 1, maximum_stack: int = -1, minimum_range: int = 0, maximum_range: int = -1, ): self.consumable = consumable self.current_stack = current_stack self.maximum_stack = maximum_stack self.minimum_range = minimum_range self.maximum_range = maximum_range def apply(self, target: Entity) -> bool: """ Use this item's effects to the `target`. Returns `True` if this item's state changed. """ raise NotImplementedError() def get_used_msg(self) -> Optional[str]: """ May return a string to display to the player, otherwise returns `None`. """ return None #default #utils def reduce_stack(self, amount: int = 1) -> bool: """ Reduce the size of this item stack by a given `amount`. Returns `True` if this entity should be deleted. """ if self.maximum_stack > 0: self.current_stack -= amount return self.current_stack <= 0 return self.consumable def is_stack_empty(self) -> bool: return self.consumable and self.maximum_stack > 0 and self.current_stack <= 0 def is_stack_mergable(self, other: BaseUseable) -> bool: """ If this returns `True`, this instance can be merged with the other instance. """ if self.__class__ is not other.__class__: return False max_stack = max(self.maximum_stack, other.maximum_stack) return self.current_stack + other.current_stack <= max_stack class Unuseable(BaseUseable): """A placeholder Useable for dead entities.""" def __init__(self): super().__init__() #enforce defaults def apply(self, target: Entity) -> bool: return None def get_used_msg(self) -> Optional[str]: return f"This {self.entity.name} is utterly useless." class PotionOfHealing(BaseUseable): #NetHack's healing potions, (B | U | C): #Healing: 8d4 | 6d4 | 4d4. If the result is above MaxHP, MaxHP is incrased by 1 | 1 | 0. #Extra Healing: 8d8 | 6d8 | 4d8. If the result is above MaxHP, MaxHP is incrased by 5 | 2 | 0. #Full Healing: 400 | 400 | 400. If the result is above MaxHP, MaxHP is incrased by 8 | 4 | 0. """Restores 4d4 health when applied.""" __last_roll: int = -1 def apply(self, target: Entity) -> bool: self.__last_roll = roll_dice(4, 4) target.stats.current_hp += self.__last_roll return self.reduce_stack() def get_used_msg(self) -> Optional[str]: if self.__last_roll >= 0: return f"The {self.entity.name} restored {self.__last_roll} health." else: return None class ScrollOfLightning(BaseUseable): """Deals 2d4 damage when applied.""" __last_roll: int = -1 __last_target_name: str = "" def apply(self, target: Entity) -> bool: self.__last_roll = roll_dice(2, 4) self.__last_target_name = target.name target.stats.current_hp -= self.__last_roll return self.reduce_stack() def get_used_msg(self) -> Optional[str]: if self.__last_roll >= 0: return f"The {self.entity.name} dealt {self.__last_roll} damage to the {self.__last_target_name}!" else: return None class ScrollOfConfusion(BaseUseable): """Causes 3d6 turns of confusion when applied.""" __last_roll: int = -1 __last_target_name: str = "" def apply(self, target: Entity) -> bool: from ai import ConfusedAI self.__last_roll = roll_dice(3, 6) self.__last_target_name = target.name target.ai = ConfusedAI(target, self.__last_roll) return self.reduce_stack() def get_used_msg(self) -> Optional[str]: if self.__last_roll >= 0: return f"The {self.__last_target_name} looks confused?" else: return None class ScrollOfFireball(BaseUseable): """Deals 3d6 damage to all creatures within 1d6 radius of the target when applied.""" #TODO: Take a list of targets __last_damage_roll: int = -1 __last_radius_roll: int = -1 __last_target_name: str = "" __last_entity_hurt: bool = False @property def radius(self) -> int: #TODO: awkwardly hacked in, will fix later if self.__last_radius_roll < 0: self.__last_radius_roll = roll_dice(1, 6) return self.__last_radius_roll def reroll_radius(self) -> int: self.__last_radius_roll = -1 return self.radius def apply(self, target: Entity) -> bool: self.__last_damage_roll = roll_dice(3, 6) self.__last_target_name = target.name #find and apply to all living entities within range for entity in target.floor_map.entities: if entity.is_alive(): if target.x - self.radius <= entity.x <= target.x + self.radius: if target.y - self.radius <= entity.y <= target.y + self.radius: entity.stats.current_hp -= self.__last_damage_roll return self.reduce_stack() def get_used_msg(self) -> Optional[str]: if self.__last_damage_roll >= 0: return f"The {self.entity.name} dealt {self.__last_damage_roll} damage to the {self.__last_target_name} and the surrounding creatures!" else: return None #TODO: "The gobbo died" and "You dealt X damage" are in the wrong order in the log