Added confusion, fireball, and various utils
While the scrolls themselves won't be around forever, the tools used to make them work will be.
This commit is contained in:
parent
1722681823
commit
6a42bb8c59
@ -13,7 +13,7 @@ https://pyinstaller.org/en/stable/
|
||||
python3 -m venv venv
|
||||
|
||||
#useful pre-commit hook
|
||||
lint_results=$(echo "$(grep -Pro '[\t]+$' */*.py)")
|
||||
lint_results="$(echo "$(grep -Pro '[\t]+$' */*.py)") $(echo "$(grep -Pro 'usable' */*.py)")"
|
||||
lint_errors=$(echo "$lint_results" | wc -w)
|
||||
if [ $lint_errors -gt 0 ]; then
|
||||
echo "pre-commit lint errors found: $lint_errors"
|
||||
@ -72,6 +72,7 @@ To-wound:
|
||||
If you use IRL time and date as a mechanic, go big or go home. Maybe the horror dimension is only accessible during full/new moons?
|
||||
Should the selected dimension be known at the start of a run?
|
||||
More of a certain dungeon feature in some runs i.e. too many fountains run?
|
||||
Inventory should show item weights
|
||||
|
||||
## Healing Items
|
||||
|
||||
|
@ -8,7 +8,6 @@ from useable import BaseUseable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from engine import Engine
|
||||
from event_handlers import OptionSelector, TileSelector
|
||||
from entity import Entity
|
||||
|
||||
class BaseAction:
|
||||
@ -142,6 +141,8 @@ class PickupAction(BaseAction):
|
||||
self.entity.inventory.insert(item)
|
||||
engine.message_log.add_message(msg, color=colors.terminal_light)
|
||||
else:
|
||||
from event_handlers import OptionSelector
|
||||
|
||||
#build an options list
|
||||
options: List[str] = []
|
||||
for item in item_pile:
|
||||
@ -263,29 +264,29 @@ class UsageAction(BaseAction):
|
||||
engine: Engine = self.entity.floor_map.engine
|
||||
|
||||
item: Entity = inventory.access(self.index)
|
||||
usable: BaseUseable = item.useable
|
||||
useable: BaseUseable = item.useable
|
||||
|
||||
#TODO: also check visibility?
|
||||
|
||||
#check range
|
||||
distance = self.entity.get_distance_to(self.target.x, self.target.y)
|
||||
|
||||
if usable.minimum_range > distance:
|
||||
if useable.minimum_range >= 0 and useable.minimum_range > distance:
|
||||
engine.message_log.add_message("You're too close to use this!", color=colors.terminal_light)
|
||||
return False
|
||||
|
||||
elif usable.maximum_range < distance:
|
||||
elif useable.maximum_range >= 0 and useable.maximum_range < distance:
|
||||
engine.message_log.add_message("You're too far to use this!", color=colors.terminal_light)
|
||||
return False
|
||||
|
||||
elif usable.apply(self.target.stats) and usable.is_stack_empty():
|
||||
elif useable.apply(self.target) and useable.is_stack_empty():
|
||||
#remove the item from the inventory
|
||||
inventory.discard(self.index)
|
||||
|
||||
if self.display_callback:
|
||||
self.display_callback(-1)
|
||||
|
||||
msg: str = usable.get_used_msg(item.name)
|
||||
msg: str = useable.get_used_msg()
|
||||
if not msg:
|
||||
msg = f"What is a(n) {item.name}?"
|
||||
|
||||
|
69
source/ai.py
69
source/ai.py
@ -1,26 +1,37 @@
|
||||
from __future__ import annotations
|
||||
from typing import List, Tuple, TYPE_CHECKING
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
import tcod
|
||||
|
||||
from actions import BaseAction, MeleeAction, MovementAction, WaitAction
|
||||
from actions import (
|
||||
BaseAction,
|
||||
MeleeAction,
|
||||
MovementAction,
|
||||
BumpAction,
|
||||
WaitAction,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from entity import Entity
|
||||
|
||||
class BaseAI:
|
||||
"""Base type for monster AI, with various utilities"""
|
||||
"""Base type for creature AI, with various utilities."""
|
||||
entity: Entity
|
||||
|
||||
def __init__(self, entity):
|
||||
def __init__(self, entity: Entity):
|
||||
#grab the entity
|
||||
self.entity = entity
|
||||
|
||||
#utilities
|
||||
self.path: List[Tuple[int, int]] = []
|
||||
|
||||
def process(self) -> BaseAction:
|
||||
"""Decides what action to take"""
|
||||
"""Allow the AI to think."""
|
||||
raise NotImplementedError()
|
||||
|
||||
#utils
|
||||
def generate_path_to(self, dest_x: int, dest_y: int) -> List[Tuple[int, int]]:
|
||||
#copy the walkables
|
||||
cost = np.array(self.entity.floor_map.tiles["walkable"], dtype=np.int8)
|
||||
@ -44,16 +55,18 @@ class BaseAI:
|
||||
|
||||
class AggressiveWhenSeen(BaseAI):
|
||||
"""
|
||||
If the player can seem me, try to approach and attack.
|
||||
Otherwise, idle.
|
||||
If I'm close enough to the player, attack.
|
||||
If I see where the player is, approach that point.
|
||||
If I know where the player has been, approach that point.
|
||||
If all else fails, idle.
|
||||
"""
|
||||
def process(self) -> BaseAction:
|
||||
target = self.entity.floor_map.player
|
||||
target = self.entity.floor_map.player #TODO: friendly fire?
|
||||
xdir = target.x - self.entity.x
|
||||
ydir = target.y - self.entity.y
|
||||
distance = max(abs(xdir), abs(ydir))
|
||||
|
||||
#if the player can see me
|
||||
#if the player can see me, create or update my path
|
||||
if self.entity.floor_map.visible[self.entity.x, self.entity.y]:
|
||||
#if I'm close enough to attack
|
||||
if distance <= 1:
|
||||
@ -61,7 +74,7 @@ class AggressiveWhenSeen(BaseAI):
|
||||
|
||||
self.path = self.generate_path_to(target.x, target.y)
|
||||
|
||||
#if I have a path to follow
|
||||
#if I have a path to follow, regardless of current visability
|
||||
if self.path:
|
||||
dest_x, dest_y = self.path.pop(0)
|
||||
return MovementAction(
|
||||
@ -72,3 +85,41 @@ class AggressiveWhenSeen(BaseAI):
|
||||
|
||||
#idle
|
||||
return WaitAction(self.entity)
|
||||
|
||||
|
||||
class ConfusedAI(BaseAI):
|
||||
"""
|
||||
"""
|
||||
parent_ai: BaseAI
|
||||
duration: int
|
||||
|
||||
def __init__(self, entity: Entity, duration: int):
|
||||
super().__init__(entity)
|
||||
self.parent_ai = entity.ai
|
||||
self.duration = duration
|
||||
|
||||
def process(self) -> BaseAction:
|
||||
if self.duration <= 0:
|
||||
self.entity.ai = self.parent_ai
|
||||
return self.entity.ai.process()
|
||||
|
||||
self.duration -= 1
|
||||
|
||||
xdir, ydir = random.choice(
|
||||
[
|
||||
[ -1, -1],
|
||||
[ -1, 0],
|
||||
[ -1, 1],
|
||||
|
||||
[ 0, -1],
|
||||
#[ 0, 0],
|
||||
[ 0, 1],
|
||||
|
||||
[ 1, -1],
|
||||
[ 1, 0],
|
||||
[ 1, 1],
|
||||
]
|
||||
)
|
||||
|
||||
#Oh, this allows for friendly fire!
|
||||
return BumpAction(self.entity, xdir, ydir)
|
@ -21,6 +21,7 @@ purple = (0x80, 0x00, 0x80) #identical to magenta
|
||||
#CSS extended colors (incomplete selection, may be expanded later)
|
||||
pink = (0xFF, 0xC0, 0xCB)
|
||||
orange = (0xFF, 0xA5, 0x00)
|
||||
orange_red = (0xFF, 0x45, 0x00)
|
||||
deep_sky_blue = (0x00, 0xBF, 0xFF)
|
||||
navajo_white = (0xFF, 0xDE, 0xAD) #misnomer
|
||||
goldenrod = (0xDA, 0xA5, 0x20)
|
||||
|
@ -59,6 +59,7 @@ class Entity:
|
||||
|
||||
if useable:
|
||||
self.useable = useable
|
||||
self.useable.entity = self
|
||||
|
||||
#generic entity stuff
|
||||
def spawn(self, x: int, y: int, floor_map: FloorMap):
|
||||
|
@ -38,7 +38,7 @@ gobbo_red = Entity(
|
||||
#items - conumables
|
||||
potion_of_healing = Entity(
|
||||
char = "!",
|
||||
color = colors.deep_sky_blue,
|
||||
color = colors.green,
|
||||
name = "Potion of Healing",
|
||||
walkable = True,
|
||||
useable=useable.PotionOfHealing(consumable=True, current_stack=1, maximum_stack=255),
|
||||
@ -52,5 +52,18 @@ scroll_of_lightning = Entity(
|
||||
useable=useable.ScrollOfLightning(consumable=True, current_stack=1, maximum_stack=255, minimum_range=0, maximum_range=6),
|
||||
)
|
||||
|
||||
#TODO: scroll of confusion, using "confused AI"
|
||||
#TODO: scroll of fireball, dealing AOE damage
|
||||
scroll_of_confusion = Entity(
|
||||
char = "!",
|
||||
color = colors.navajo_white,
|
||||
name = "Scroll of Confusion",
|
||||
walkable = True,
|
||||
useable=useable.ScrollOfConfusion(consumable=True, current_stack=1, maximum_stack=255, minimum_range=1, maximum_range=6),
|
||||
)
|
||||
|
||||
scroll_of_fireball = Entity(
|
||||
char = "!",
|
||||
color = colors.orange_red,
|
||||
name = "Scroll of Fireball",
|
||||
walkable = True,
|
||||
useable=useable.ScrollOfFireball(consumable=True, current_stack=1, maximum_stack=255, minimum_range=1, maximum_range=6),
|
||||
)
|
@ -128,16 +128,25 @@ class EventHandler(tcod.event.EventDispatch[BaseAction]):
|
||||
|
||||
class GameoverViewer(EventHandler):
|
||||
"""Game over, man, GAME OVER!"""
|
||||
def __init__(self,engine: Engine, parent_handler: EventHandler):
|
||||
super().__init__(engine, parent_handler)
|
||||
#Hacky fix, re-parent until you find the gameplay viewer
|
||||
while self.parent_handler and self.parent_handler is not GameplayViewer:
|
||||
self.parent_handler = self.parent_handler.parent_handler
|
||||
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
|
||||
key = event.sym #SDL stuff, neat.
|
||||
|
||||
#player input
|
||||
#special keys
|
||||
if key == tcod.event.KeySym.ESCAPE:
|
||||
return QuitAction()
|
||||
|
||||
#menu keys
|
||||
if key == tcod.event.KeySym.BACKQUOTE: #lowercase tilde
|
||||
self.engine.event_handler = LogHistoryViewer(self.engine, self)
|
||||
|
||||
#TODO: read-only inventory viewer
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@ -175,7 +184,7 @@ class GameplayViewer(EventHandler):
|
||||
self.engine,
|
||||
self,
|
||||
title = "Debug Selector",
|
||||
options = ["Tile Selector"],
|
||||
options = ["Zero", "One", "Two", "Three", "Four", "Five", "Six"],
|
||||
callback = lambda x: self.dbg_callback(x),
|
||||
margin_x = 20,
|
||||
margin_y = 12,
|
||||
@ -186,16 +195,16 @@ class GameplayViewer(EventHandler):
|
||||
self.engine.mouse_location = (event.tile.x, event.tile.y)
|
||||
|
||||
def dbg_callback(self, selected: int) -> Optional[BaseAction]:
|
||||
if selected == 0:
|
||||
player: Entity = self.engine.player
|
||||
player: Entity = self.engine.player
|
||||
|
||||
self.engine.event_handler = TileSelector(
|
||||
self.engine,
|
||||
self,
|
||||
floor_map = self.engine.floor_map,
|
||||
initial_pointer = (player.x, player.y),
|
||||
callback=lambda: None
|
||||
)
|
||||
self.engine.event_handler = TileSelector(
|
||||
self.engine,
|
||||
self,
|
||||
floor_map = self.engine.floor_map,
|
||||
initial_cursor = (player.x, player.y),
|
||||
callback=lambda x, y: print(f"Fireball at ({x},{y})"),
|
||||
cursor_radius=selected
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@ -371,16 +380,17 @@ class InventoryViewer(EventHandler):
|
||||
"""Use the item at the cursor's position."""
|
||||
if self.length > 0:
|
||||
item: Entity = self.entity.inventory.access(self.cursor)
|
||||
usable: BaseUseable = item.useable
|
||||
useable: BaseUseable = item.useable
|
||||
|
||||
#for ranged items, delegate to the tile selector
|
||||
if usable.maximum_range > 0:
|
||||
if useable.maximum_range > 0:
|
||||
self.engine.event_handler = TileSelector(
|
||||
self.engine,
|
||||
parent_handler=self.engine.event_handler,
|
||||
floor_map = self.engine.floor_map,
|
||||
initial_pointer = (self.entity.x, self.entity.y),
|
||||
callback=lambda x, y: self.use_at_range(x, y)
|
||||
initial_cursor = (self.entity.x, self.entity.y),
|
||||
callback=lambda x, y: self.use_at_range(x, y), #TODO: Radius is not rerolled on cancel
|
||||
cursor_radius=0 if not hasattr(useable, 'radius') else useable.radius,
|
||||
)
|
||||
else:
|
||||
#non-ranged items target the entity
|
||||
@ -390,7 +400,6 @@ class InventoryViewer(EventHandler):
|
||||
#TODO: For now, just target living entities
|
||||
targets: List[Entity] = list(filter(lambda entity: entity.is_alive() and entity.x == target_x and entity.y == target_y, self.engine.floor_map.entities))
|
||||
|
||||
#TODO: skip targeting for AOE
|
||||
#TODO: close the inventory if you've used a consumable?
|
||||
if len(targets) > 0:
|
||||
return UsageAction(self.entity, self.cursor, targets.pop(), lambda x: self.adjust_length(x))
|
||||
@ -497,6 +506,7 @@ class TileSelector(EventHandler):
|
||||
floor_map: FloorMap
|
||||
cursor_x: int
|
||||
cursor_y: int
|
||||
cursor_radius: int
|
||||
callback: function
|
||||
|
||||
def __init__(
|
||||
@ -505,12 +515,14 @@ class TileSelector(EventHandler):
|
||||
parent_handler,
|
||||
*,
|
||||
floor_map: FloorMap,
|
||||
initial_pointer: Tuple[int, int],
|
||||
initial_cursor: Tuple[int, int],
|
||||
callback: function,
|
||||
cursor_radius: int = 0,
|
||||
):
|
||||
super().__init__(engine, parent_handler)
|
||||
self.floor_map = floor_map
|
||||
self.cursor_x, self.cursor_y = initial_pointer
|
||||
self.cursor_x, self.cursor_y = initial_cursor
|
||||
self.cursor_radius = cursor_radius
|
||||
self.callback = callback
|
||||
|
||||
def render(self, console: tcod.console.Console) -> None:
|
||||
@ -521,9 +533,19 @@ class TileSelector(EventHandler):
|
||||
if parent:
|
||||
parent.render(console)
|
||||
|
||||
#highlight via inverting colors
|
||||
console.rgb["fg"][self.cursor_x, self.cursor_y] ^= 0xff
|
||||
console.rgb["bg"][self.cursor_x, self.cursor_y] ^= 0xff
|
||||
radius: int = max(self.cursor_radius, 0)
|
||||
|
||||
#radius is for display only - the item's actual effect is cacl'd elsewhere
|
||||
for i in range(self.cursor_x - radius, self.cursor_x + radius + 1):
|
||||
for j in range(self.cursor_y - radius, self.cursor_y + radius + 1):
|
||||
#highlight via inverting colors
|
||||
console.rgb["fg"][i, j] ^= 0xFF
|
||||
console.rgb["bg"][i, j] ^= 0xFF
|
||||
|
||||
if radius > 0:
|
||||
#re-apply an alteration, to show the center
|
||||
console.rgb["fg"][self.cursor_x, self.cursor_y] ^= 0x4F
|
||||
console.rgb["bg"][self.cursor_x, self.cursor_y] ^= 0x4F
|
||||
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
|
||||
key = event.sym #SDL stuff, neat.
|
||||
|
@ -95,8 +95,13 @@ def spawn_items(floor_map: FloorMap, room: RectangularRoom, room_items_max: int,
|
||||
|
||||
#if there's no entity at that position (not really needed for walkable entities)
|
||||
if not any(entity.x == x and entity.y == y for entity in floor_map.entities):
|
||||
if random.random() < 0.8:
|
||||
item_result = random.random()
|
||||
if item_result < 0.2:
|
||||
entity_types.scroll_of_fireball.spawn(x, y, floor_map)
|
||||
elif item_result < 0.4:
|
||||
entity_types.scroll_of_lightning.spawn(x, y, floor_map)
|
||||
elif item_result < 0.6:
|
||||
entity_types.scroll_of_confusion.spawn(x, y, floor_map)
|
||||
else:
|
||||
entity_types.potion_of_healing.spawn(x, y, floor_map)
|
||||
floor_map.procgen_cache["item_count"] += 1
|
||||
|
@ -47,7 +47,7 @@ class Stats:
|
||||
engine.message_log.add_message(f"The {self.entity.name} died", colors.green)
|
||||
|
||||
#transform into a dead body
|
||||
#TODO: dummied in a "usable" to let dead objects be treated like items
|
||||
#TODO: dummied in a "useable" to let dead objects be treated like items
|
||||
self.entity.char = "%"
|
||||
self.entity.color = (191, 0, 0)
|
||||
self.entity.walkable = True
|
||||
|
@ -4,7 +4,7 @@ from typing import Optional, TYPE_CHECKING
|
||||
from utils import roll_dice
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from stats import Stats
|
||||
from entity import Entity
|
||||
|
||||
class BaseUseable:
|
||||
"""
|
||||
@ -12,6 +12,7 @@ class BaseUseable:
|
||||
|
||||
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
|
||||
@ -31,28 +32,26 @@ class BaseUseable:
|
||||
self.minimum_range = minimum_range
|
||||
self.maximum_range = maximum_range
|
||||
|
||||
def apply(self, stats: Stats) -> bool:
|
||||
def apply(self, target: Entity) -> bool:
|
||||
"""
|
||||
Use this item's effects.
|
||||
Use this item's effects to the `target`.
|
||||
|
||||
Returns `True` if the item's state changed.
|
||||
Returns `True` if this item's state changed.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_used_msg(self, appearance: str) -> Optional[str]:
|
||||
def get_used_msg(self) -> Optional[str]:
|
||||
"""
|
||||
May return a string to display to the user.
|
||||
|
||||
`appearance` is what the item looks like, and can be substituted into the result.
|
||||
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 a stack by an amount.
|
||||
Reduce the size of this item stack by a given `amount`.
|
||||
|
||||
Returns `True` if this item should be deleted.
|
||||
Returns `True` if this entity should be deleted.
|
||||
"""
|
||||
if self.maximum_stack > 0:
|
||||
self.current_stack -= amount
|
||||
@ -79,11 +78,11 @@ class Unuseable(BaseUseable):
|
||||
def __init__(self):
|
||||
super().__init__() #enforce defaults
|
||||
|
||||
def apply(self, stats: Stats) -> bool:
|
||||
def apply(self, target: Entity) -> bool:
|
||||
return None
|
||||
|
||||
def get_used_msg(self, appearance: str) -> Optional[str]:
|
||||
return f"This {appearance} is utterly useless."
|
||||
def get_used_msg(self) -> Optional[str]:
|
||||
return f"This {self.entity.name} is utterly useless."
|
||||
|
||||
|
||||
class PotionOfHealing(BaseUseable):
|
||||
@ -95,14 +94,14 @@ class PotionOfHealing(BaseUseable):
|
||||
"""Restores 4d4 health when applied."""
|
||||
__last_roll: int = -1
|
||||
|
||||
def apply(self, stats: Stats) -> bool:
|
||||
def apply(self, target: Entity) -> bool:
|
||||
self.__last_roll = roll_dice(4, 4)
|
||||
stats.current_hp += self.__last_roll
|
||||
target.stats.current_hp += self.__last_roll
|
||||
return self.reduce_stack()
|
||||
|
||||
def get_used_msg(self, appearance: str) -> Optional[str]:
|
||||
def get_used_msg(self) -> Optional[str]:
|
||||
if self.__last_roll >= 0:
|
||||
return f"The {appearance} restored {self.__last_roll} health."
|
||||
return f"The {self.entity.name} restored {self.__last_roll} health."
|
||||
else:
|
||||
return None
|
||||
|
||||
@ -110,15 +109,74 @@ class PotionOfHealing(BaseUseable):
|
||||
class ScrollOfLightning(BaseUseable):
|
||||
"""Deals 2d4 damage when applied."""
|
||||
__last_roll: int = -1
|
||||
__last_target_name: str = ""
|
||||
|
||||
def apply(self, stats: Stats) -> bool:
|
||||
def apply(self, target: Entity) -> bool:
|
||||
self.__last_roll = roll_dice(2, 4)
|
||||
stats.current_hp -= self.__last_roll
|
||||
self.__last_target_name = target.name
|
||||
target.stats.current_hp -= self.__last_roll
|
||||
return self.reduce_stack()
|
||||
|
||||
def get_used_msg(self, appearance: str) -> Optional[str]:
|
||||
def get_used_msg(self) -> Optional[str]:
|
||||
if self.__last_roll >= 0:
|
||||
return f"The {appearance} dealt {self.__last_roll} damage."
|
||||
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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user