Stepwise/source/actions.py

284 lines
7.6 KiB
Python

from __future__ import annotations
from typing import List, Optional, TYPE_CHECKING
import colors
from floor_map import FloorMap
from inventory import Inventory
from useable import BaseUseable
if TYPE_CHECKING:
from engine import Engine
from entity import Entity
class BaseAction:
"""Base type for the various actions to apply to a specified entity."""
entity: Entity
def __init__(self, entity: Entity):
self.entity = entity
def perform(self) -> bool:
"""return True if the game state should be progressed"""
raise NotImplementedError()
class QuitAction(BaseAction):
def __init__(self):
"""override the base __init__, as no entity is needed"""
pass
def perform(self) -> bool:
raise SystemExit()
class WaitAction(BaseAction):
def perform(self) -> bool:
return True
class MovementAction(BaseAction):
"""Move an Entity within the map"""
def __init__(self, entity, xdir: int, ydir: int):
super().__init__(entity)
self.xdir = xdir
self.ydir = ydir
def perform(self) -> bool:
dest_x = self.entity.x + self.xdir
dest_y = self.entity.y + self.ydir
floor_map: FloorMap = self.entity.floor_map
#bounds and collision checks
if not floor_map.in_bounds(dest_x, dest_y):
return False
if not floor_map.tiles["walkable"][dest_x, dest_y]:
return False
if floor_map.get_all_entities_at(dest_x, dest_y, unwalkable_only=True):
return False
self.entity.set_pos(dest_x, dest_y)
return True
class MeleeAction(BaseAction):
"""Melee attack from the Entity towards a target"""
def __init__(self, entity, xdir: int, ydir: int):
super().__init__(entity)
self.xdir = xdir
self.ydir = ydir
def perform(self) -> bool:
dest_x = self.entity.x + self.xdir
dest_y = self.entity.y + self.ydir
targets = self.entity.floor_map.get_all_entities_at(dest_x, dest_y, unwalkable_only=True)
if not targets:
return False
target = targets.pop()
if not target or not target.stats:
return False
#TODO: better combat system
#calculate damage
damage = self.entity.stats.attack - target.stats.defense
#calculate message output
engine: Engine = self.entity.floor_map.engine
msg: str = f"{self.entity.name} attacked {target.name}"
if damage > 0:
msg += f" for {damage} damage"
else:
msg += f" but was ineffective"
engine.message_log.add_message(msg)
#performing the actual change here, so the player's death event is at the bottom of the message log
target.stats.current_hp -= damage
return True
class BumpAction(BaseAction):
"""Move an Entity within the map, or attack a target if one is found"""
def __init__(self, entity, xdir: int, ydir: int):
super().__init__(entity)
self.xdir = xdir
self.ydir = ydir
def perform(self) -> bool:
dest_x = self.entity.x + self.xdir
dest_y = self.entity.y + self.ydir
if self.entity.floor_map.get_all_entities_at(dest_x, dest_y, unwalkable_only=True):
return MeleeAction(self.entity, self.xdir, self.ydir).perform()
else:
return MovementAction(self.entity, self.xdir, self.ydir).perform()
class PickupAction(BaseAction):
"""Pickup an item at the entity's location"""
def perform(self) -> bool:
x = self.entity.x
y = self.entity.y
floor_map: FloorMap = self.entity.floor_map
engine: Engine = floor_map.engine
item_pile: List[Entity] = floor_map.get_all_entities_at(x, y, items_only=True)
if len(item_pile) == 0:
return False
elif len(item_pile) == 1:
item: Entity = item_pile.pop()
msg: str = f"you picked up a(n) {item.name}"
if item.useable.current_stack > 1:
msg = f"you picked up a stack of {item.useable.current_stack} {item.name}"
floor_map.entities.remove(item)
self.entity.inventory.insert(item)
engine.message_log.add_message(msg, color=colors.terminal_light)
else:
from event_handlers import OptionSelector #circular imports are a pain
#build an options list
options: List[str] = []
for item in item_pile:
options.append(item.name)
engine.event_handler = OptionSelector(
engine=floor_map.engine,
parent_handler=engine.event_handler,
title="Pick Up Item",
options=options,
callback=lambda x: self.pickup_callback(engine, floor_map, self.entity, item_pile[x])
)
return True
#utils
def pickup_callback(self, engine: Engine, floor_map: FloorMap, entity: Entity, item: Entity) -> None:
msg: str = f"you picked up a(n) {item.name}"
if item.useable.current_stack > 1:
msg = f"you picked up a stack of {item.useable.current_stack} {item.name}"
floor_map.entities.remove(item)
entity.inventory.insert(item)
engine.message_log.add_message(msg, color=colors.terminal_light)
class DropAction(BaseAction):
"""Drop an item from an entity's inventory at the entity's location"""
index: int
display_callback: function
def __init__(self, entity: Entity, index: int, display_callback: function):
"""override the base __init__"""
super().__init__(entity)
self.index = index
self.display_callback = display_callback
def perform(self) -> bool:
x = self.entity.x
y = self.entity.y
inventory: Inventory = self.entity.inventory
floor_map: FloorMap = self.entity.floor_map
engine: Engine = floor_map.engine
item: Entity = inventory.withdraw(self.index)
item.x = x
item.y = y
#TODO: Check for floorpile stack merging
floor_map.entities.add(item)
if self.display_callback: #adjust the cursor
self.display_callback(-1)
msg: str = f"you dropped a(n) {item.name}"
if item.useable.current_stack > 1:
msg = f"you dropped a stack of {item.useable.current_stack} {item.name}"
engine.message_log.add_message(msg, color=colors.terminal_light)
return True
class DropPartialStackAction(BaseAction):
"""Drop part of a stack from an entity's inventory at the entity's location"""
index: int
amount: int
display_callback: function
def __init__(self, entity: Entity, index: int, amount: int, display_callback: function):
"""override the base __init__"""
super().__init__(entity)
self.index = index
self.amount = amount
self.display_callback = display_callback
def perform(self) -> bool:
x = self.entity.x
y = self.entity.y
inventory: Inventory = self.entity.inventory
floor_map: FloorMap = self.entity.floor_map
engine: Engine = floor_map.engine
item: Entity = inventory.access(self.index)
#TODO: Check for floorpile stack merging
new_item: Entity = item.spawn(x, y, floor_map)
item.useable.current_stack -= self.amount
new_item.useable.current_stack = self.amount
if self.display_callback: #adjust the cursor
self.display_callback(0) #by zero
msg: str = f"you dropped a partial stack of {new_item.useable.current_stack} {new_item.name}"
engine.message_log.add_message(msg, color=colors.terminal_light)
return True
class UsageAction(BaseAction):
"""Use an item from an entity's inventory, removing it if needed"""
index: int
target: Entity
display_callback: function
def __init__(self, entity: Entity, index: int, target: Entity, display_callback: function):
"""override the base __init__"""
super().__init__(entity)
self.index = index
self.target = target
self.display_callback = display_callback
def perform(self) -> bool:
inventory: Inventory = self.entity.inventory
engine: Engine = self.entity.floor_map.engine
item: Entity = inventory.access(self.index)
usable: BaseUseable = item.useable
if usable.apply(self.target.stats) and usable.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)
if not msg:
msg = f"you used a(n) {item.name}"
engine.message_log.add_message(msg, color=colors.terminal_light)
return True