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
|
from typing import overload, Optional, Tuple, TYPE_CHECKING
|
||||||
|
|
||||||
import color
|
import color
|
||||||
|
import exceptions
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from engine import Engine
|
from engine import Engine
|
||||||
from entity import Actor, Entity
|
from entity import Actor, Entity, Item
|
||||||
|
|
||||||
|
|
||||||
class Action:
|
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):
|
class EscapeAction(Action):
|
||||||
def perform(self) -> None:
|
def perform(self) -> None:
|
||||||
raise SystemExit()
|
raise SystemExit()
|
||||||
@ -68,7 +92,7 @@ class MeleeAction(ActionWithDirection):
|
|||||||
def perform(self) -> None:
|
def perform(self) -> None:
|
||||||
target = self.target_actor
|
target = self.target_actor
|
||||||
if not target:
|
if not target:
|
||||||
return # No entity to attack.
|
raise exceptions.Impossible("Nothing to attack.")
|
||||||
|
|
||||||
damage = self.entity.fighter.power - target.fighter.defense
|
damage = self.entity.fighter.power - target.fighter.defense
|
||||||
|
|
||||||
@ -96,11 +120,14 @@ class MovementAction(ActionWithDirection):
|
|||||||
dest_x, dest_y = self.dest_xy
|
dest_x, dest_y = self.dest_xy
|
||||||
|
|
||||||
if not self.engine.game_map.in_bounds(dest_x, dest_y):
|
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]:
|
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):
|
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)
|
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)
|
player_die = (0xFF, 0x30, 0x30)
|
||||||
enemy_die = (0xFF, 0xA0, 0x30)
|
enemy_die = (0xFF, 0xA0, 0x30)
|
||||||
|
|
||||||
|
invalid = (0xFF, 0xFF, 0x00)
|
||||||
|
impossible = (0x80, 0x80, 0x80)
|
||||||
|
error = (0xFF, 0x40, 0x40)
|
||||||
|
|
||||||
welcome_text = (0x20, 0xA0, 0xFF)
|
welcome_text = (0x20, 0xA0, 0xFF)
|
||||||
|
health_recovered = (0x0, 0xFF, 0x0)
|
||||||
|
|
||||||
bar_text = white
|
bar_text = white
|
||||||
bar_filled = (0x0, 0x60, 0x0)
|
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.parent.render_order = RenderOrder.CORPSE
|
||||||
|
|
||||||
self.engine.message_log.add_message(death_message, death_message_color)
|
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.console import Console
|
||||||
from tcod.map import compute_fov
|
from tcod.map import compute_fov
|
||||||
|
|
||||||
|
import exceptions
|
||||||
from input_handlers import MainGameEventHandler
|
from input_handlers import MainGameEventHandler
|
||||||
from message_log import MessageLog
|
from message_log import MessageLog
|
||||||
from render_functions import render_bar, render_names_at_mouse_location
|
from render_functions import render_bar, render_names_at_mouse_location
|
||||||
@ -27,7 +28,10 @@ class Engine:
|
|||||||
def handle_enemy_turns(self) -> None:
|
def handle_enemy_turns(self) -> None:
|
||||||
for entity in set(self.game_map.actors) - {self.player}:
|
for entity in set(self.game_map.actors) - {self.player}:
|
||||||
if entity.ai:
|
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:
|
def update_fov(self) -> None:
|
||||||
"""Recompute the visible area based on the player's point of view."""
|
"""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:
|
if TYPE_CHECKING:
|
||||||
from components.ai import BaseAI
|
from components.ai import BaseAI
|
||||||
|
from components.consumable import Consumable
|
||||||
from components.fighter import Fighter
|
from components.fighter import Fighter
|
||||||
from game_map import GameMap
|
from game_map import GameMap
|
||||||
|
|
||||||
@ -105,3 +106,28 @@ class Actor(Entity):
|
|||||||
def is_alive(self) -> bool:
|
def is_alive(self) -> bool:
|
||||||
"""Returns True as long as this actor can perform actions."""
|
"""Returns True as long as this actor can perform actions."""
|
||||||
return bool(self.ai)
|
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.ai import HostileEnemy
|
||||||
|
from components.consumable import HealingConsumable
|
||||||
from components.fighter import Fighter
|
from components.fighter import Fighter
|
||||||
from entity import Actor
|
from entity import Actor, Item
|
||||||
|
|
||||||
player = Actor(
|
player = Actor(
|
||||||
char="@",
|
char="@",
|
||||||
@ -24,3 +25,10 @@ troll = Actor(
|
|||||||
ai_cls=HostileEnemy,
|
ai_cls=HostileEnemy,
|
||||||
fighter=Fighter(hp=16, defense=1, power=4)
|
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
|
import tcod.event
|
||||||
|
|
||||||
from actions import Action, BumpAction, EscapeAction, WaitAction
|
from actions import (
|
||||||
|
Action,
|
||||||
|
BumpAction,
|
||||||
|
EscapeAction,
|
||||||
|
WaitAction
|
||||||
|
)
|
||||||
|
import color
|
||||||
|
import exceptions
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from engine import Engine
|
from engine import Engine
|
||||||
@ -52,10 +59,28 @@ class EventHandler(tcod.event.EventDispatch[Action]):
|
|||||||
def __init__(self, engine: Engine):
|
def __init__(self, engine: Engine):
|
||||||
self.engine = engine
|
self.engine = engine
|
||||||
|
|
||||||
def handle_events(self, context: tcod.context.Context) -> None:
|
def handle_events(self, event: tcod.event.Event) -> None:
|
||||||
for event in tcod.event.wait():
|
self.handle_action(self.dispatch(event))
|
||||||
context.convert_event(event)
|
|
||||||
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:
|
def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None:
|
||||||
if self.engine.game_map.in_bounds(event.tile.x, event.tile.y):
|
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):
|
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]:
|
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||||||
action: Optional[Action] = None
|
action: Optional[Action] = None
|
||||||
|
|
||||||
@ -106,25 +117,9 @@ class MainGameEventHandler(EventHandler):
|
|||||||
|
|
||||||
|
|
||||||
class GameOverEventHandler(EventHandler):
|
class GameOverEventHandler(EventHandler):
|
||||||
def handle_events(self, context: tcod.context.Context) -> None:
|
def ev_keydown(self, event: tcod.event.KeyDown) -> None:
|
||||||
for event in tcod.event.wait():
|
if event.sym == tcod.event.K_ESCAPE:
|
||||||
action = self.dispatch(event)
|
raise SystemExit()
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
CURSOR_Y_KEYS = {
|
CURSOR_Y_KEYS = {
|
||||||
|
12
main.py
12
main.py
@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import copy
|
import copy
|
||||||
|
import traceback
|
||||||
|
|
||||||
import tcod
|
import tcod
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ def main() -> None:
|
|||||||
max_rooms = 30
|
max_rooms = 30
|
||||||
|
|
||||||
max_monsters_per_room = 2
|
max_monsters_per_room = 2
|
||||||
|
max_items_per_room = 2
|
||||||
|
|
||||||
tileset = tcod.tileset.load_tilesheet(
|
tileset = tcod.tileset.load_tilesheet(
|
||||||
"dejavu10x10_gs_tc.png",
|
"dejavu10x10_gs_tc.png",
|
||||||
@ -40,6 +42,7 @@ def main() -> None:
|
|||||||
map_width,
|
map_width,
|
||||||
map_height,
|
map_height,
|
||||||
max_monsters_per_room,
|
max_monsters_per_room,
|
||||||
|
max_items_per_room,
|
||||||
engine,
|
engine,
|
||||||
)
|
)
|
||||||
engine.update_fov()
|
engine.update_fov()
|
||||||
@ -62,7 +65,14 @@ def main() -> None:
|
|||||||
engine.event_handler.on_render(console=root_console)
|
engine.event_handler.on_render(console=root_console)
|
||||||
context.present(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__":
|
if __name__ == "__main__":
|
||||||
|
12
procgen.py
12
procgen.py
@ -46,8 +46,10 @@ def place_entities(
|
|||||||
room: RectangularRoom,
|
room: RectangularRoom,
|
||||||
dungeon: GameMap,
|
dungeon: GameMap,
|
||||||
maximum_monsters: int,
|
maximum_monsters: int,
|
||||||
|
maximum_items: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
number_of_monsters = random.randint(0, maximum_monsters)
|
number_of_monsters = random.randint(0, maximum_monsters)
|
||||||
|
number_of_items = random.randint(0, maximum_items)
|
||||||
|
|
||||||
for i in range(number_of_monsters):
|
for i in range(number_of_monsters):
|
||||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||||
@ -59,6 +61,13 @@ def place_entities(
|
|||||||
else:
|
else:
|
||||||
entity_factories.troll.spawn(dungeon, x, y)
|
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]]:
|
def tunnel_between(start: Tuple[int, int], end: Tuple[int, int]) -> Iterator[Tuple[int, int]]:
|
||||||
"""Return an L-shaped tunnel between these two points."""
|
"""Return an L-shaped tunnel between these two points."""
|
||||||
@ -86,6 +95,7 @@ def generate_dungeon(
|
|||||||
map_width: int,
|
map_width: int,
|
||||||
map_height: int,
|
map_height: int,
|
||||||
max_monsters_per_room: int,
|
max_monsters_per_room: int,
|
||||||
|
max_items_per_room: int,
|
||||||
engine: Engine,
|
engine: Engine,
|
||||||
) -> GameMap:
|
) -> GameMap:
|
||||||
"""Generate a new dungeon map."""
|
"""Generate a new dungeon map."""
|
||||||
@ -120,7 +130,7 @@ def generate_dungeon(
|
|||||||
for x, y in tunnel_between(rooms[-1].center, new_room.center):
|
for x, y in tunnel_between(rooms[-1].center, new_room.center):
|
||||||
dungeon.tiles[x, y] = tile_types.floor
|
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.
|
# Finally, append the new room to the list.
|
||||||
rooms.append(new_room)
|
rooms.append(new_room)
|
||||||
|
Loading…
Reference in New Issue
Block a user