2022-05-07 12:25:44 -07:00
|
|
|
'''
|
|
|
|
This module defines all of the actions that can be performed by the game. These actions can come from the player (e.g.
|
|
|
|
via keyboard input), or from non-player entities (e.g. AI deciboard input), or from non-player entities (e.g. AI
|
2022-05-07 11:16:17 -07:00
|
|
|
decisions).
|
|
|
|
|
|
|
|
Class Hierarchy
|
|
|
|
---------------
|
|
|
|
|
|
|
|
Action : Base class of all actions
|
|
|
|
MoveAction : Base class for all actions that are performed with a direction
|
2022-05-07 12:25:44 -07:00
|
|
|
BumpAction
|
2022-05-07 11:16:17 -07:00
|
|
|
WalkAction
|
|
|
|
MeleeAction
|
|
|
|
ExitAction
|
2022-05-08 08:55:08 -07:00
|
|
|
WaitAction
|
2022-05-07 11:16:17 -07:00
|
|
|
'''
|
|
|
|
|
2023-02-12 16:34:37 -08:00
|
|
|
import random
|
2022-05-28 08:52:54 -07:00
|
|
|
from typing import TYPE_CHECKING
|
2022-05-08 23:41:54 -07:00
|
|
|
|
2022-05-28 08:52:54 -07:00
|
|
|
from .. import items
|
|
|
|
from .. import log
|
2023-02-09 21:05:05 -08:00
|
|
|
from ..geometry import Vector
|
2022-05-28 08:52:54 -07:00
|
|
|
from ..object import Actor, Item
|
2023-02-12 16:34:37 -08:00
|
|
|
from .action import Action, ActionWithActor
|
2022-05-28 08:52:54 -07:00
|
|
|
from .result import ActionResult
|
2022-05-03 18:21:24 -07:00
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
2022-05-28 08:52:54 -07:00
|
|
|
from ..engine import Engine
|
2022-05-01 09:51:22 -07:00
|
|
|
|
2023-02-09 16:11:33 -08:00
|
|
|
|
2022-04-30 21:59:01 -07:00
|
|
|
class ExitAction(Action):
|
2022-05-07 11:16:17 -07:00
|
|
|
'''Exit the game.'''
|
|
|
|
|
2022-05-07 12:25:46 -07:00
|
|
|
def perform(self, engine: 'Engine') -> ActionResult:
|
2022-05-01 09:29:30 -07:00
|
|
|
raise SystemExit()
|
2022-04-30 21:59:01 -07:00
|
|
|
|
2023-02-09 16:11:33 -08:00
|
|
|
|
2022-04-30 21:59:01 -07:00
|
|
|
class RegenerateRoomsAction(Action):
|
2022-05-07 11:16:17 -07:00
|
|
|
'''Regenerate the dungeon map'''
|
|
|
|
|
2022-05-07 12:25:46 -07:00
|
|
|
def perform(self, engine: 'Engine') -> ActionResult:
|
2023-02-12 16:34:37 -08:00
|
|
|
return self.failure()
|
2023-02-09 16:11:33 -08:00
|
|
|
|
|
|
|
|
2023-02-12 16:34:37 -08:00
|
|
|
class MoveAction(ActionWithActor):
|
2022-05-07 11:16:17 -07:00
|
|
|
'''An abstract Action that requires a direction to complete.'''
|
2022-04-30 21:59:01 -07:00
|
|
|
|
2023-02-09 21:05:05 -08:00
|
|
|
def __init__(self, actor: Actor, direction: Vector):
|
2022-05-08 23:40:33 -07:00
|
|
|
super().__init__(actor)
|
2022-05-01 09:29:30 -07:00
|
|
|
self.direction = direction
|
|
|
|
|
2022-05-07 11:16:17 -07:00
|
|
|
def __repr__(self):
|
2022-05-08 23:40:33 -07:00
|
|
|
return f'{self.__class__.__name__}({self.actor!r}, {self.direction!r})'
|
2022-05-07 11:16:17 -07:00
|
|
|
|
2022-05-12 08:48:28 -07:00
|
|
|
def __str__(self) -> str:
|
|
|
|
return f'{self.__class__.__name__} toward {self.direction} by {self.actor!s}'
|
|
|
|
|
2023-02-09 16:11:33 -08:00
|
|
|
|
2022-05-07 11:16:17 -07:00
|
|
|
class BumpAction(MoveAction):
|
|
|
|
'''Attempt to perform a movement action in a direction.
|
|
|
|
|
|
|
|
This action tests if an action in the direction is possible and returns the action that can be completed.
|
|
|
|
|
|
|
|
Attributes
|
|
|
|
----------
|
2023-02-15 08:25:40 -08:00
|
|
|
direction : Vector
|
2022-05-07 11:16:17 -07:00
|
|
|
The direction to test
|
|
|
|
'''
|
|
|
|
|
2022-05-07 12:25:46 -07:00
|
|
|
def perform(self, engine: 'Engine') -> ActionResult:
|
2022-05-08 23:40:33 -07:00
|
|
|
new_position = self.actor.position + self.direction
|
2022-05-07 11:16:17 -07:00
|
|
|
|
2023-03-08 08:57:20 -08:00
|
|
|
position_is_in_bounds = engine.map.point_is_in_bounds(new_position)
|
|
|
|
position_is_walkable = engine.map.point_is_walkable(new_position)
|
2022-05-07 11:16:17 -07:00
|
|
|
|
|
|
|
for ent in engine.entities:
|
2022-05-15 19:58:39 -07:00
|
|
|
if new_position != ent.position or not ent.blocks_movement:
|
2022-05-07 11:16:17 -07:00
|
|
|
continue
|
|
|
|
entity_occupying_position = ent
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
entity_occupying_position = None
|
|
|
|
|
2023-02-09 16:11:33 -08:00
|
|
|
log.ACTIONS.info(
|
|
|
|
'Bumping %s into %s (in_bounds:%s walkable:%s overlaps:%s)',
|
2022-05-12 20:40:06 -07:00
|
|
|
self.actor,
|
|
|
|
new_position,
|
|
|
|
position_is_in_bounds,
|
|
|
|
position_is_walkable,
|
|
|
|
entity_occupying_position)
|
2022-05-07 11:16:17 -07:00
|
|
|
|
|
|
|
if not position_is_in_bounds or not position_is_walkable:
|
2022-05-08 23:34:09 -07:00
|
|
|
return self.failure()
|
2022-05-07 11:16:17 -07:00
|
|
|
|
2023-02-12 16:34:37 -08:00
|
|
|
# TODO: I'm passing entity_occupying_position into the ActionResult below, but the type checker doesn't
|
|
|
|
# understand that the entity is an Actor. I think I need some additional checks here.
|
2022-05-15 19:58:39 -07:00
|
|
|
if entity_occupying_position:
|
|
|
|
assert entity_occupying_position.blocks_movement
|
2022-05-08 23:40:33 -07:00
|
|
|
return ActionResult(self, alternate=MeleeAction(self.actor, self.direction, entity_occupying_position))
|
2022-05-07 11:16:17 -07:00
|
|
|
|
2022-05-08 23:40:33 -07:00
|
|
|
return ActionResult(self, alternate=WalkAction(self.actor, self.direction))
|
2022-05-07 11:16:17 -07:00
|
|
|
|
|
|
|
|
|
|
|
class WalkAction(MoveAction):
|
|
|
|
'''Walk one step in the given direction.'''
|
|
|
|
|
2022-05-07 12:25:46 -07:00
|
|
|
def perform(self, engine: 'Engine') -> ActionResult:
|
2023-02-12 16:34:37 -08:00
|
|
|
actor = self.actor
|
|
|
|
|
|
|
|
assert actor.fighter
|
|
|
|
|
|
|
|
new_position = actor.position + self.direction
|
2022-05-07 11:16:17 -07:00
|
|
|
|
2022-05-12 20:40:06 -07:00
|
|
|
log.ACTIONS.debug('Moving %s to %s', self.actor, new_position)
|
2023-02-12 16:34:37 -08:00
|
|
|
actor.position = new_position
|
2022-05-07 11:16:17 -07:00
|
|
|
|
2022-05-16 20:50:23 -07:00
|
|
|
try:
|
2023-02-12 16:34:37 -08:00
|
|
|
should_recover_hit_points = actor.fighter.passively_recover_hit_points(5)
|
2022-05-16 20:50:23 -07:00
|
|
|
if should_recover_hit_points:
|
2023-02-12 16:34:37 -08:00
|
|
|
return ActionResult(self, alternate=HealAction(actor, random.randint(1, 3)))
|
2022-05-16 20:50:23 -07:00
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
|
2022-05-08 23:34:09 -07:00
|
|
|
return self.success()
|
2022-05-07 11:16:17 -07:00
|
|
|
|
2023-02-09 16:11:33 -08:00
|
|
|
|
2022-05-07 11:16:17 -07:00
|
|
|
class MeleeAction(MoveAction):
|
2022-05-08 23:40:33 -07:00
|
|
|
'''Perform a melee attack on another Actor'''
|
2022-05-01 09:29:30 -07:00
|
|
|
|
2023-02-09 21:05:05 -08:00
|
|
|
def __init__(self, actor: Actor, direction: Vector, target: Actor):
|
2022-05-08 23:40:33 -07:00
|
|
|
super().__init__(actor, direction)
|
2022-05-07 11:16:17 -07:00
|
|
|
self.target = target
|
2022-05-01 09:29:30 -07:00
|
|
|
|
2022-05-07 12:25:46 -07:00
|
|
|
def perform(self, engine: 'Engine') -> ActionResult:
|
2023-02-12 16:34:37 -08:00
|
|
|
assert self.actor.fighter and self.target.fighter
|
|
|
|
|
|
|
|
fighter = self.actor.fighter
|
|
|
|
target_fighter = self.target.fighter
|
|
|
|
|
2022-05-16 20:51:53 -07:00
|
|
|
try:
|
2023-02-12 16:34:37 -08:00
|
|
|
damage = fighter.attack_power - target_fighter.defense
|
2022-05-16 20:51:53 -07:00
|
|
|
if damage > 0 and self.target:
|
|
|
|
log.ACTIONS.debug('%s attacks %s for %d damage!', self.actor, self.target, damage)
|
|
|
|
self.target.fighter.hit_points -= damage
|
|
|
|
|
|
|
|
if self.actor == engine.hero:
|
2023-02-09 16:11:33 -08:00
|
|
|
engine.message_log.add_message(
|
|
|
|
f'You attack the {self.target.name} for {damage} damage!',
|
|
|
|
fg=(127, 255, 127))
|
2022-05-16 20:51:53 -07:00
|
|
|
elif self.target == engine.hero:
|
2023-02-09 16:11:33 -08:00
|
|
|
engine.message_log.add_message(
|
|
|
|
f'The {self.actor.name} attacks you for {damage} damage!',
|
|
|
|
fg=(255, 127, 127))
|
2022-05-16 20:51:53 -07:00
|
|
|
else:
|
|
|
|
log.ACTIONS.debug('%s attacks %s but does no damage!', self.actor, self.target)
|
|
|
|
|
|
|
|
if self.target.fighter.is_dead:
|
|
|
|
log.ACTIONS.info('%s is dead!', self.target)
|
|
|
|
return ActionResult(self, alternate=DieAction(self.target))
|
|
|
|
except AttributeError:
|
2022-05-08 23:41:54 -07:00
|
|
|
return self.failure()
|
|
|
|
else:
|
2022-05-16 20:51:53 -07:00
|
|
|
return self.success()
|
2022-05-07 22:34:43 -07:00
|
|
|
|
2023-02-09 16:11:33 -08:00
|
|
|
|
2023-02-12 16:34:37 -08:00
|
|
|
class WaitAction(ActionWithActor):
|
2022-05-07 22:34:43 -07:00
|
|
|
'''Wait a turn'''
|
|
|
|
|
|
|
|
def perform(self, engine: 'Engine') -> ActionResult:
|
2022-05-12 20:40:06 -07:00
|
|
|
log.ACTIONS.debug('%s is waiting a turn', self.actor)
|
2022-05-15 16:19:03 -07:00
|
|
|
|
|
|
|
if self.actor == engine.hero:
|
2023-02-12 16:34:37 -08:00
|
|
|
assert self.actor.fighter
|
|
|
|
|
|
|
|
fighter = self.actor.fighter
|
|
|
|
should_recover_hit_points = fighter.passively_recover_hit_points(20)
|
2022-05-15 16:19:03 -07:00
|
|
|
if should_recover_hit_points:
|
2023-02-12 16:34:37 -08:00
|
|
|
return ActionResult(self, alternate=HealAction(self.actor, random.randint(1, 3)))
|
2022-05-15 16:19:03 -07:00
|
|
|
|
2022-05-08 23:34:09 -07:00
|
|
|
return self.success()
|
2022-05-08 23:41:54 -07:00
|
|
|
|
2023-02-09 16:11:33 -08:00
|
|
|
|
2023-02-12 16:34:37 -08:00
|
|
|
class DieAction(ActionWithActor):
|
2022-05-08 23:41:54 -07:00
|
|
|
'''Kill an Actor'''
|
|
|
|
|
|
|
|
def perform(self, engine: 'Engine') -> ActionResult:
|
2022-05-14 23:43:38 -07:00
|
|
|
engine.kill_actor(self.actor)
|
2022-05-08 23:41:54 -07:00
|
|
|
|
2022-05-15 13:58:37 -07:00
|
|
|
if self.actor == engine.hero:
|
|
|
|
engine.message_log.add_message('You die...', fg=(255, 127, 127))
|
|
|
|
else:
|
|
|
|
engine.message_log.add_message(f'The {self.actor.name} dies', fg=(127, 255, 127))
|
|
|
|
|
2022-05-08 23:41:54 -07:00
|
|
|
if self.actor.yields_corpse_on_death:
|
2022-05-12 20:40:06 -07:00
|
|
|
log.ACTIONS.debug('%s leaves a corpse behind', self.actor)
|
2022-05-08 23:41:54 -07:00
|
|
|
corpse = Item(kind=items.Corpse, name=f'{self.actor.name} Corpse', position=self.actor.position)
|
|
|
|
return ActionResult(self, alternate=DropItemAction(self.actor, corpse))
|
|
|
|
|
|
|
|
return self.success()
|
|
|
|
|
2023-02-09 16:11:33 -08:00
|
|
|
|
2023-02-12 16:34:37 -08:00
|
|
|
class DropItemAction(ActionWithActor):
|
2022-05-08 23:41:54 -07:00
|
|
|
'''Drop an item'''
|
|
|
|
|
|
|
|
def __init__(self, actor: 'Actor', item: 'Item'):
|
|
|
|
super().__init__(actor)
|
|
|
|
self.item = item
|
|
|
|
|
|
|
|
def perform(self, engine: 'Engine') -> ActionResult:
|
|
|
|
engine.entities.add(self.item)
|
2022-05-12 08:48:28 -07:00
|
|
|
return self.success()
|
2022-05-15 16:19:03 -07:00
|
|
|
|
2023-02-09 16:11:33 -08:00
|
|
|
|
2023-02-12 16:34:37 -08:00
|
|
|
class HealAction(ActionWithActor):
|
2022-05-15 16:19:03 -07:00
|
|
|
'''Heal a target actor some number of hit points'''
|
|
|
|
|
|
|
|
def __init__(self, actor: 'Actor', hit_points_to_recover: int):
|
|
|
|
super().__init__(actor)
|
|
|
|
self.hit_points_to_recover = hit_points_to_recover
|
|
|
|
|
|
|
|
def perform(self, engine: 'Engine') -> ActionResult:
|
|
|
|
fighter = self.actor.fighter
|
|
|
|
if not fighter:
|
|
|
|
log.ACTIONS.error('Attempted to heal %s but it has no hit points', self.actor)
|
|
|
|
return self.failure()
|
|
|
|
|
|
|
|
fighter.hit_points += self.hit_points_to_recover
|
|
|
|
|
2022-05-16 20:50:23 -07:00
|
|
|
return self.success()
|