Stepwise/source/useable.py
Kayne Ruse 6a42bb8c59 Added confusion, fireball, and various utils
While the scrolls themselves won't be around forever, the tools used to
make them work will be.
2025-04-06 17:20:08 +10:00

183 lines
5.2 KiB
Python

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