Generate items and display on map, implement exception handling
This commit is contained in:
parent
f947338c2d
commit
35862fcc19
37
actions.py
37
actions.py
@ -3,10 +3,11 @@ from __future__ import annotations
|
||||
from typing import overload, Optional, Tuple, TYPE_CHECKING
|
||||
|
||||
import color
|
||||
import exceptions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from engine import Engine
|
||||
from entity import Actor, Entity
|
||||
from entity import Actor, Entity, Item
|
||||
|
||||
|
||||
class Action:
|
||||
@ -31,6 +32,29 @@ class Action:
|
||||
"""
|
||||
|
||||
|
||||
class ItemAction(Action):
|
||||
def __init__(
|
||||
self,
|
||||
entity: Actor,
|
||||
item: Item,
|
||||
target_xy: Optional[Tuple[int, int]] = None
|
||||
):
|
||||
super().__init__(entity)
|
||||
self.item = item
|
||||
if not target_xy:
|
||||
target_xy = entity.x, entity.y
|
||||
self.target_xy = target_xy
|
||||
|
||||
@property
|
||||
def target_actor(self) -> Optional[Actor]:
|
||||
"""Return the actor at this action's destination."""
|
||||
return self.engine.game_map.get_actor_at_location(*self.target_xy)
|
||||
|
||||
def perform(self) -> None:
|
||||
"""Invoke the item's ability, this action will be given to provide context."""
|
||||
self.item.consumable.activate(self)
|
||||
|
||||
|
||||
class EscapeAction(Action):
|
||||
def perform(self) -> None:
|
||||
raise SystemExit()
|
||||
@ -68,7 +92,7 @@ class MeleeAction(ActionWithDirection):
|
||||
def perform(self) -> None:
|
||||
target = self.target_actor
|
||||
if not target:
|
||||
return # No entity to attack.
|
||||
raise exceptions.Impossible("Nothing to attack.")
|
||||
|
||||
damage = self.entity.fighter.power - target.fighter.defense
|
||||
|
||||
@ -96,11 +120,14 @@ class MovementAction(ActionWithDirection):
|
||||
dest_x, dest_y = self.dest_xy
|
||||
|
||||
if not self.engine.game_map.in_bounds(dest_x, dest_y):
|
||||
return # Destination is out of bounds
|
||||
# Destination is out of bounds
|
||||
raise exceptions.Impossible("That way is blocked.")
|
||||
if not self.engine.game_map.tiles["walkable"][dest_x, dest_y]:
|
||||
return # Destination is blocked by a tile.
|
||||
# Destination is blocked by a tile.
|
||||
raise exceptions.Impossible("That way is blocked.")
|
||||
if self.engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
|
||||
return # Destination is blocked by an entity.
|
||||
# Destination is blocked by an entity.
|
||||
raise exceptions.Impossible("That way is blocked.")
|
||||
|
||||
self.entity.move(self.dx, self.dy)
|
||||
|
||||
|
5
color.py
5
color.py
@ -7,7 +7,12 @@ enemy_atk = (0xFF, 0xC0, 0xC0)
|
||||
player_die = (0xFF, 0x30, 0x30)
|
||||
enemy_die = (0xFF, 0xA0, 0x30)
|
||||
|
||||
invalid = (0xFF, 0xFF, 0x00)
|
||||
impossible = (0x80, 0x80, 0x80)
|
||||
error = (0xFF, 0x40, 0x40)
|
||||
|
||||
welcome_text = (0x20, 0xA0, 0xFF)
|
||||
health_recovered = (0x0, 0xFF, 0x0)
|
||||
|
||||
bar_text = white
|
||||
bar_filled = (0x0, 0x60, 0x0)
|
||||
|
43
components/consumable.py
Normal file
43
components/consumable.py
Normal file
@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
import actions
|
||||
import color
|
||||
from components.base_component import BaseComponent
|
||||
from exceptions import Impossible
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from entity import Actor, Item
|
||||
|
||||
|
||||
class Consumable(BaseComponent):
|
||||
parent: Item
|
||||
|
||||
def get_action(self, consumer: Actor) -> Optional[actions.Action]:
|
||||
"""Try to return the action for this item."""
|
||||
return actions.ItemAction(consumer, self.parent)
|
||||
|
||||
def activate(self, action: actions.ItemAction) -> None:
|
||||
"""Invoke this item's ability.
|
||||
|
||||
`action` is the context for this activation.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class HealingConsumable(Consumable):
|
||||
def __init__(self, amount: int):
|
||||
self.amount = amount
|
||||
|
||||
def activate(self, action: actions.ItemAction) -> None:
|
||||
consumer = action.entity
|
||||
amount_recovered = consumer.fighter.heal(self.amount)
|
||||
|
||||
if amount_recovered > 0:
|
||||
self.engine.message_log.add_message(
|
||||
f"You consume the {self.parent.name}, and recover {amount_recovered} HP!",
|
||||
color.health_recovered
|
||||
)
|
||||
else:
|
||||
raise Impossible(f"Your health is already full.")
|
@ -47,3 +47,21 @@ class Fighter(BaseComponent):
|
||||
self.parent.render_order = RenderOrder.CORPSE
|
||||
|
||||
self.engine.message_log.add_message(death_message, death_message_color)
|
||||
|
||||
def heal(self, amount: int) -> int:
|
||||
if self.hp == self.max_hp:
|
||||
return 0
|
||||
|
||||
new_hp_value = self.hp + amount
|
||||
|
||||
if new_hp_value > self.max_hp:
|
||||
new_hp_value = self.max_hp
|
||||
|
||||
amount_recovered = new_hp_value - self.hp
|
||||
|
||||
self.hp = new_hp_value
|
||||
|
||||
return amount_recovered
|
||||
|
||||
def take_damage(self, amount: int) -> None:
|
||||
self.hp -= amount
|
||||
|
@ -6,6 +6,7 @@ from tcod.context import Context
|
||||
from tcod.console import Console
|
||||
from tcod.map import compute_fov
|
||||
|
||||
import exceptions
|
||||
from input_handlers import MainGameEventHandler
|
||||
from message_log import MessageLog
|
||||
from render_functions import render_bar, render_names_at_mouse_location
|
||||
@ -27,7 +28,10 @@ class Engine:
|
||||
def handle_enemy_turns(self) -> None:
|
||||
for entity in set(self.game_map.actors) - {self.player}:
|
||||
if entity.ai:
|
||||
entity.ai.perform()
|
||||
try:
|
||||
entity.ai.perform()
|
||||
except exceptions.Impossible:
|
||||
pass # Ignore impossible action exceptions from AI.
|
||||
|
||||
def update_fov(self) -> None:
|
||||
"""Recompute the visible area based on the player's point of view."""
|
||||
|
26
entity.py
26
entity.py
@ -7,6 +7,7 @@ from render_order import RenderOrder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from components.ai import BaseAI
|
||||
from components.consumable import Consumable
|
||||
from components.fighter import Fighter
|
||||
from game_map import GameMap
|
||||
|
||||
@ -105,3 +106,28 @@ class Actor(Entity):
|
||||
def is_alive(self) -> bool:
|
||||
"""Returns True as long as this actor can perform actions."""
|
||||
return bool(self.ai)
|
||||
|
||||
|
||||
class Item(Entity):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
x: int = 0,
|
||||
y: int = 0,
|
||||
char: str = "?",
|
||||
color: Tuple[int, int, int] = (255, 255, 255),
|
||||
name: str = "<Unamed>",
|
||||
consumable: Consumable,
|
||||
):
|
||||
super().__init__(
|
||||
x=x,
|
||||
y=y,
|
||||
char=char,
|
||||
color=color,
|
||||
name=name,
|
||||
blocks_movement=False,
|
||||
render_order=RenderOrder.ITEM,
|
||||
)
|
||||
|
||||
self.consumable = consumable
|
||||
self.consumable.parent = self
|
||||
|
@ -1,6 +1,7 @@
|
||||
from components.ai import HostileEnemy
|
||||
from components.consumable import HealingConsumable
|
||||
from components.fighter import Fighter
|
||||
from entity import Actor
|
||||
from entity import Actor, Item
|
||||
|
||||
player = Actor(
|
||||
char="@",
|
||||
@ -24,3 +25,10 @@ troll = Actor(
|
||||
ai_cls=HostileEnemy,
|
||||
fighter=Fighter(hp=16, defense=1, power=4)
|
||||
)
|
||||
|
||||
health_potion = Item(
|
||||
char="!",
|
||||
color=(127, 0, 255),
|
||||
name="Health Potion",
|
||||
consumable=HealingConsumable(amount=4)
|
||||
)
|
||||
|
5
exceptions.py
Normal file
5
exceptions.py
Normal file
@ -0,0 +1,5 @@
|
||||
class Impossible(Exception):
|
||||
"""Exception raised when an action is impossible to be performed.
|
||||
|
||||
The reason is given as the exception message.
|
||||
"""
|
@ -4,7 +4,14 @@ from typing import overload, Optional, TYPE_CHECKING
|
||||
|
||||
import tcod.event
|
||||
|
||||
from actions import Action, BumpAction, EscapeAction, WaitAction
|
||||
from actions import (
|
||||
Action,
|
||||
BumpAction,
|
||||
EscapeAction,
|
||||
WaitAction
|
||||
)
|
||||
import color
|
||||
import exceptions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from engine import Engine
|
||||
@ -52,10 +59,28 @@ class EventHandler(tcod.event.EventDispatch[Action]):
|
||||
def __init__(self, engine: Engine):
|
||||
self.engine = engine
|
||||
|
||||
def handle_events(self, context: tcod.context.Context) -> None:
|
||||
for event in tcod.event.wait():
|
||||
context.convert_event(event)
|
||||
self.dispatch(event)
|
||||
def handle_events(self, event: tcod.event.Event) -> None:
|
||||
self.handle_action(self.dispatch(event))
|
||||
|
||||
def handle_action(self, action: Optional[Action]) -> bool:
|
||||
"""Handle actions returned from event methods.
|
||||
|
||||
Returns True if the action will advance a turn.
|
||||
"""
|
||||
if action is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
action.perform()
|
||||
except exceptions.Impossible as exc:
|
||||
self.engine.message_log.add_message(exc.args[0], color.impossible)
|
||||
return False # Skip enemy turn on exceptions.
|
||||
|
||||
self.engine.handle_enemy_turns()
|
||||
|
||||
self.engine.update_fov()
|
||||
|
||||
return True
|
||||
|
||||
def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None:
|
||||
if self.engine.game_map.in_bounds(event.tile.x, event.tile.y):
|
||||
@ -69,20 +94,6 @@ class EventHandler(tcod.event.EventDispatch[Action]):
|
||||
|
||||
|
||||
class MainGameEventHandler(EventHandler):
|
||||
def handle_events(self, context: tcod.context.Context) -> None:
|
||||
for event in tcod.event.wait():
|
||||
context.convert_event(event)
|
||||
|
||||
action = self.dispatch(event)
|
||||
|
||||
if action is None:
|
||||
continue
|
||||
|
||||
action.perform()
|
||||
|
||||
self.engine.handle_enemy_turns()
|
||||
self.engine.update_fov()
|
||||
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||||
action: Optional[Action] = None
|
||||
|
||||
@ -106,25 +117,9 @@ class MainGameEventHandler(EventHandler):
|
||||
|
||||
|
||||
class GameOverEventHandler(EventHandler):
|
||||
def handle_events(self, context: tcod.context.Context) -> None:
|
||||
for event in tcod.event.wait():
|
||||
action = self.dispatch(event)
|
||||
|
||||
if action is None:
|
||||
continue
|
||||
|
||||
action.perform()
|
||||
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||||
action: Optional[Action] = None
|
||||
|
||||
key = event.sym
|
||||
|
||||
if key == tcod.event.K_ESCAPE:
|
||||
action = EscapeAction(self.engine.player)
|
||||
|
||||
# No valid key was pressed
|
||||
return action
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> None:
|
||||
if event.sym == tcod.event.K_ESCAPE:
|
||||
raise SystemExit()
|
||||
|
||||
|
||||
CURSOR_Y_KEYS = {
|
||||
|
12
main.py
12
main.py
@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import copy
|
||||
import traceback
|
||||
|
||||
import tcod
|
||||
|
||||
@ -21,6 +22,7 @@ def main() -> None:
|
||||
max_rooms = 30
|
||||
|
||||
max_monsters_per_room = 2
|
||||
max_items_per_room = 2
|
||||
|
||||
tileset = tcod.tileset.load_tilesheet(
|
||||
"dejavu10x10_gs_tc.png",
|
||||
@ -40,6 +42,7 @@ def main() -> None:
|
||||
map_width,
|
||||
map_height,
|
||||
max_monsters_per_room,
|
||||
max_items_per_room,
|
||||
engine,
|
||||
)
|
||||
engine.update_fov()
|
||||
@ -62,7 +65,14 @@ def main() -> None:
|
||||
engine.event_handler.on_render(console=root_console)
|
||||
context.present(root_console)
|
||||
|
||||
engine.event_handler.handle_events(context)
|
||||
try:
|
||||
for event in tcod.event.wait():
|
||||
context.convert_event(event)
|
||||
engine.event_handler.handle_events(event)
|
||||
except Exception: # Handle exceptions in game.
|
||||
traceback.print_exc() # Print error to stderr.
|
||||
# Then print the error to the message log.
|
||||
engine.message_log.add_message(traceback.format_exc(), color.error)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
12
procgen.py
12
procgen.py
@ -46,8 +46,10 @@ def place_entities(
|
||||
room: RectangularRoom,
|
||||
dungeon: GameMap,
|
||||
maximum_monsters: int,
|
||||
maximum_items: int,
|
||||
) -> None:
|
||||
number_of_monsters = random.randint(0, maximum_monsters)
|
||||
number_of_items = random.randint(0, maximum_items)
|
||||
|
||||
for i in range(number_of_monsters):
|
||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||
@ -59,6 +61,13 @@ def place_entities(
|
||||
else:
|
||||
entity_factories.troll.spawn(dungeon, x, y)
|
||||
|
||||
for i in range(number_of_items):
|
||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||||
|
||||
if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
|
||||
entity_factories.health_potion.spawn(dungeon, x, y)
|
||||
|
||||
|
||||
def tunnel_between(start: Tuple[int, int], end: Tuple[int, int]) -> Iterator[Tuple[int, int]]:
|
||||
"""Return an L-shaped tunnel between these two points."""
|
||||
@ -86,6 +95,7 @@ def generate_dungeon(
|
||||
map_width: int,
|
||||
map_height: int,
|
||||
max_monsters_per_room: int,
|
||||
max_items_per_room: int,
|
||||
engine: Engine,
|
||||
) -> GameMap:
|
||||
"""Generate a new dungeon map."""
|
||||
@ -120,7 +130,7 @@ def generate_dungeon(
|
||||
for x, y in tunnel_between(rooms[-1].center, new_room.center):
|
||||
dungeon.tiles[x, y] = tile_types.floor
|
||||
|
||||
place_entities(new_room, dungeon, max_monsters_per_room)
|
||||
place_entities(new_room, dungeon, max_monsters_per_room, max_items_per_room)
|
||||
|
||||
# Finally, append the new room to the list.
|
||||
rooms.append(new_room)
|
||||
|
Loading…
Reference in New Issue
Block a user