From 621d4780e8887ae600979863db2e2486d18fca27 Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Fri, 14 Jan 2022 16:45:07 -0500 Subject: [PATCH] Implement confusion scrolls --- components/ai.py | 50 ++++++++++++++++++++++++++++++++++++++-- components/consumable.py | 42 +++++++++++++++++++++++++++++++++ entity_factories.py | 7 +++++- input_handlers.py | 18 ++++++++++++++- procgen.py | 2 ++ 5 files changed, 115 insertions(+), 4 deletions(-) diff --git a/components/ai.py b/components/ai.py index 51d7828..6633d61 100644 --- a/components/ai.py +++ b/components/ai.py @@ -1,11 +1,12 @@ from __future__ import annotations -from typing import List, Tuple, TYPE_CHECKING +import random +from typing import List, Optional, Tuple, TYPE_CHECKING import numpy as np # type: ignore import tcod -from actions import Action, MeleeAction, MovementAction, WaitAction +from actions import Action, BumpAction, MeleeAction, MovementAction, WaitAction if TYPE_CHECKING: from entity import Actor @@ -43,6 +44,51 @@ class BaseAI(Action): return [(index[0], index[1]) for index in path] +class ConfusedEnemy(BaseAI): + """ + A confused enemy will stumble around aimlessly for a given number of turns, then + reverts back to its previous AI. If an actor occupies a tile it is randomly + moving into, it will attack. + """ + + def __init__( + self, + entity: Actor, + previous_ai: Optional[BaseAI], + turns_remaining: int + ): + super().__init__(entity) + + self.previous_ai = previous_ai + self.turns_remaining = turns_remaining + + def perform(self) -> None: + # Rever the AI back to the original state if the effect has run its course. + if self.turns_remaining <= 0: + self.engine.message_log.add_message(f"The {self.entity.name} is no longer confused.") + self.entity.ai = self.previous_ai + else: + # Pick a random direction + direction_x, direction_y = random.choice( + [ + (-1, -1), # Northwest + (0, -1), # North + (1, -1), # Northeast + (-1, 0), # West + (1, 0), # East + (-1, 1), # Southwest + (0, 1), # South + (1, 1), # Southeast + ] + ) + + self.turns_remaining -= 1 + + # The actor will either try to move or attack in the chosen random direction. + # It's possible the actor will just bump into the wall, wasting a turn. + return BumpAction(self.entity, direction_x, direction_y).perform() + + class HostileEnemy(BaseAI): def __init__(self, entity: Actor): super().__init__(entity) diff --git a/components/consumable.py b/components/consumable.py index 9128bd7..f09e6a9 100644 --- a/components/consumable.py +++ b/components/consumable.py @@ -4,9 +4,11 @@ from typing import Optional, TYPE_CHECKING import actions import color +import components.ai import components.inventory from components.base_component import BaseComponent from exceptions import Impossible +from input_handlers import SingleRangedAttackHandler if TYPE_CHECKING: from entity import Actor, Item @@ -34,6 +36,46 @@ class Consumable(BaseComponent): inventory.items.remove(entity) +class ConfusionConsumable(Consumable): + def __init__(self, number_of_turns: int): + self.number_of_turns = number_of_turns + + def get_action(self, consumer: Actor) -> Optional[actions.Action]: + self.engine.message_log.add_message( + "Select a target location.", + color.needs_target + ) + self.engine.event_handler = SingleRangedAttackHandler( + self.engine, + callback=lambda xy: actions.ItemAction(consumer, self.parent, xy), + ) + + return None + + def activate(self, action: actions.ItemAction) -> None: + consumer = action.entity + target = action.target_actor + + if not self.engine.game_map.visible[action.target_xy]: + raise Impossible("You cannot target an area that you cannot see.") + if not target: + raise Impossible("You must select an enemy to target.") + if target is consumer: + raise Impossible("You cannot confuse yourself!") + + self.engine.message_log.add_message( + f"The eyes of the {target.name} look vacant, as it starts to stumble around!", + color.status_effect_applied, + ) + target.ai = components.ai.ConfusedEnemy( + entity=target, + previous_ai=target.ai, + turns_remaining=self.number_of_turns, + ) + + self.consume() + + class HealingConsumable(Consumable): def __init__(self, amount: int): self.amount = amount diff --git a/entity_factories.py b/entity_factories.py index 4297b8f..250aa22 100644 --- a/entity_factories.py +++ b/entity_factories.py @@ -30,13 +30,18 @@ troll = Actor( inventory=Inventory(capacity=0), ) +confusion_scroll = Item( + char="~", + color=(207, 63, 255), + name="Confusion Scroll", + consumable=consumable.ConfusionConsumable(number_of_turns=10), +) health_potion = Item( char="!", color=(127, 0, 255), name="Health Potion", consumable=consumable.HealingConsumable(amount=4), ) - lightning_scroll = Item( char="~", color=(255, 255, 0), diff --git a/input_handlers.py b/input_handlers.py index 99e1a10..dd1e2b6 100644 --- a/input_handlers.py +++ b/input_handlers.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import overload, Optional, TYPE_CHECKING +from typing import overload, Callable, Optional, Tuple, TYPE_CHECKING import tcod.event @@ -294,6 +294,22 @@ class LookHandler(SelectIndexHandler): self.engine.event_handler = MainGameEventHandler(self.engine) +class SingleRangedAttackHandler(SelectIndexHandler): + """Handles targeting a single enemy. Only the enemy selected will be affected.""" + + def __init__( + self, + engine: Engine, + callback: Callable[[Tuple[int, int]], Optional[Action]] + ): + super().__init__(engine) + + self.callback = callback + + def on_index_selected(self, x: int, y: int) -> Optional[Action]: + return self.callback((x, y)) + + class MainGameEventHandler(EventHandler): def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]: action: Optional[Action] = None diff --git a/procgen.py b/procgen.py index abb49ef..7fe948e 100644 --- a/procgen.py +++ b/procgen.py @@ -70,6 +70,8 @@ def place_entities( if item_chance < 0.7: entity_factories.health_potion.spawn(dungeon, x, y) + elif item_chance < 0.9: + entity_factories.confusion_scroll.spawn(dungeon, x, y) else: entity_factories.lightning_scroll.spawn(dungeon, x, y)