Skip to content

Commit 0d376d0

Browse files
committed
parts 6 and 7 done!
1 parent 778e23b commit 0d376d0

File tree

15 files changed

+589
-46
lines changed

15 files changed

+589
-46
lines changed

actions.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
from typing import Optional, Tuple, TYPE_CHECKING
44

5+
import color
6+
57
if TYPE_CHECKING:
68
from engine import Engine
7-
from entity import Entity
9+
from entity import Actor, Entity
810

911

1012
class Action:
11-
def __init__(self, entity: Entity) -> None:
13+
def __init__(self, entity: Actor) -> None:
1214
super().__init__()
1315
self.entity = entity
1416

@@ -29,7 +31,7 @@ def perform(self) -> None:
2931
raise NotImplementedError()
3032

3133
class ActionWithDirection(Action):
32-
def __init__(self, entity: Entity, dx: int, dy: int):
34+
def __init__(self, entity: Actor, dx: int, dy: int):
3335
super().__init__(entity)
3436

3537
self.dx = dx
@@ -43,6 +45,11 @@ def dest_xy(self) -> Tuple[int, int]:
4345
"""Returns this actions destination."""
4446
return self.entity.x + self.dx, self.entity.y + self.dy
4547

48+
@property
49+
def target_actor(self) -> Optional[Actor]:
50+
"""Return the actor at this actions destination."""
51+
return self.engine.game_map.get_actor_at_location(*self.dest_xy)
52+
4653
@property
4754
def blocking_entity(self) -> Optional[Entity]:
4855
"""Return the blocking entity at this actions destination.."""
@@ -55,11 +62,28 @@ def perform(self) -> None:
5562

5663
class MeleeAction(ActionWithDirection):
5764
def perform(self) -> None:
58-
target = self.blocking_entity
65+
target = self.target_actor
5966
if not target:
6067
return # No entity to attack.
6168

62-
print(f"You punch the {target.name}, it does not seem to be doing much damage...")
69+
damage = self.entity.fighter.power - target.fighter.defense
70+
71+
attack_desc = f"{self.entity.name.capitalize()} attacks {target.name.capitalize()}"
72+
if self.entity is self.engine.player:
73+
attack_color = color.player_atk
74+
else:
75+
attack_color = color.enemy_atk
76+
if damage > 0:
77+
self.engine.message_log.add_message(
78+
f"{attack_desc} for {damage} hit points!",
79+
attack_color
80+
)
81+
target.fighter.hp -= damage
82+
else:
83+
self.engine.message_log.add_message(
84+
f"{attack_desc}, but does no damage.",
85+
attack_color
86+
)
6387

6488
class MovementAction(ActionWithDirection):
6589
def perform(self) -> None:
@@ -78,8 +102,12 @@ def perform(self) -> None:
78102

79103
class BumpAction(ActionWithDirection):
80104
def perform(self) -> None:
81-
if self.blocking_entity is not None:
105+
if self.target_actor:
82106
return MeleeAction(self.entity, self.dx, self.dy).perform()
83107

84108
else:
85-
return MovementAction(self.entity, self.dx, self.dy).perform()
109+
return MovementAction(self.entity, self.dx, self.dy).perform()
110+
111+
class WaitAction(Action):
112+
def perform(self) -> None:
113+
pass

color.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
white = (255, 255, 255)
2+
black = (0, 0, 0)
3+
4+
player_atk = (255, 153, 0)
5+
enemy_atk = (153, 0, 0)
6+
7+
player_die = (255, 0, 0)
8+
enemy_die = (255, 255, 0)
9+
10+
text_console = (204, 255, 51)
11+
12+
bar_text = white
13+
bar_filled = (255, 0, 0)
14+
bar_empty = (77, 0, 0)

components/__init__.py

Whitespace-only changes.

components/ai.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from __future__ import annotations
2+
3+
from typing import List, Tuple, TYPE_CHECKING
4+
5+
import numpy as np # type: ignore
6+
import tcod
7+
8+
from actions import Action, MeleeAction, MovementAction, WaitAction
9+
from components.base_component import BaseComponent
10+
11+
if TYPE_CHECKING:
12+
from entity import Actor
13+
14+
15+
class BaseAI(Action, BaseComponent):
16+
entity: Actor
17+
def perform(self) -> None:
18+
raise NotImplementedError()
19+
20+
def get_path_to(self, dest_x: int, dest_y: int) -> List[Tuple[int, int]]:
21+
"""Compute and return a path to the target position.
22+
23+
If there is no valid path then returns an empty list.
24+
"""
25+
# Copy the walkable array.
26+
cost = np.array(self.entity.gamemap.tiles["walkable"], dtype=np.int8)
27+
28+
for entity in self.entity.gamemap.entities:
29+
# Check that an enitiy blocks movement and the cost isn't zero (blocking.)
30+
if entity.blocks_movement and cost[entity.x, entity.y]:
31+
# Add to the cost of a blocked position.
32+
# A lower number means more enemies will crowd behind each other in
33+
# hallways. A higher number means enemies will take longer paths in
34+
# order to surround the player.
35+
cost[entity.x, entity.y] += 10
36+
37+
# Create a graph from the cost array and pass that graph to a new pathfinder.
38+
graph = tcod.path.SimpleGraph(cost=cost, cardinal=2, diagonal=3)
39+
pathfinder = tcod.path.Pathfinder(graph)
40+
41+
pathfinder.add_root((self.entity.x, self.entity.y)) # Start position.
42+
43+
# Compute the path to the destination and remove the starting point.
44+
path: List[List[int]] = pathfinder.path_to((dest_x, dest_y))[1:].tolist()
45+
46+
# Convert from List[List[int]] to List[Tuple[int, int]].
47+
return [(index[0], index[1]) for index in path]
48+
49+
class HostileEnemy(BaseAI):
50+
def __init__(self, entity: Actor):
51+
super().__init__(entity)
52+
self.path: List[Tuple[int, int]] = []
53+
54+
def perform(self) -> None:
55+
target = self.engine.player
56+
dx = target.x - self.entity.x
57+
dy = target.y - self.entity.y
58+
distance = max(abs(dx), abs(dy)) # Chebyshev distance.
59+
60+
if self.engine.game_map.visible[self.entity.x, self.entity.y]:
61+
if distance <= 1:
62+
return MeleeAction(self.entity, dx, dy).perform()
63+
64+
self.path = self.get_path_to(target.x, target.y)
65+
66+
if self.path:
67+
dest_x, dest_y = self.path.pop(0)
68+
return MovementAction(
69+
self.entity, dest_x - self.entity.x, dest_y - self.entity.y,
70+
).perform()
71+
72+
return WaitAction(self.entity).perform()

components/base_component.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
if TYPE_CHECKING:
6+
from engine import Engine
7+
from entity import Entity
8+
9+
10+
class BaseComponent:
11+
entity: Entity # Owning entity instance.
12+
13+
@property
14+
def engine(self) -> Engine:
15+
return self.entity.gamemap.engine

components/fighter.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
import color
6+
7+
from components.base_component import BaseComponent
8+
from input_handlers import GameOverEventHandler
9+
from render_order import RenderOrder
10+
11+
if TYPE_CHECKING:
12+
from entity import Actor
13+
14+
class Fighter(BaseComponent):
15+
entity: Actor
16+
def __init__(self, hp: int, defense: int, power: int):
17+
self.max_hp = hp
18+
self._hp = hp
19+
self.defense = defense
20+
self.power = power
21+
22+
@property
23+
def hp(self) -> int:
24+
return self._hp
25+
26+
@hp.setter
27+
def hp(self, value: int) -> None:
28+
self._hp = max(0, min(value, self.max_hp))
29+
if self._hp == 0 and self.entity.ai:
30+
self.die()
31+
32+
def die(self) -> None:
33+
if self.engine.player is self.entity:
34+
death_message = "You died!"
35+
death_message_color = color.player_die
36+
self.engine.event_handler = GameOverEventHandler(self.engine)
37+
else:
38+
death_message = f"{self.entity.name} is dead!"
39+
death_message_color = color.enemy_die
40+
41+
self.entity.char = "%"
42+
self.entity.color = (191, 0, 0)
43+
self.entity.blocks_movement = False
44+
self.entity.ai = None
45+
self.entity.name = f"Corpse of {self.entity.name}"
46+
self.entity.render_order = RenderOrder.CORPSE
47+
48+
self.engine.message_log.add_message(death_message, death_message_color)

engine.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,37 @@
22

33
from typing import TYPE_CHECKING
44

5-
from tcod.context import Context
65
from tcod.console import Console
76
from tcod.map import compute_fov
87
import tcod.constants
98

10-
from input_handlers import EventHandler
9+
from input_handlers import MainGameEventHandler
10+
from message_log import MessageLog
11+
from render_functions import render_bar, render_names_at_mouse_location
1112

1213
if TYPE_CHECKING:
13-
from entity import Entity
14+
from entity import Actor
1415
from game_map import GameMap
16+
from input_handlers import EventHandler
1517

1618
class Engine:
1719

1820
game_map: GameMap
1921

20-
def __init__(self, player: Entity):
21-
self.event_handler: EventHandler = EventHandler(self)
22+
def __init__(self, player: Actor):
23+
self.event_handler: EventHandler = MainGameEventHandler(self)
24+
self.message_log = MessageLog()
25+
self.mouse_location = (0, 0)
2226
self.player = player
2327

2428
def handle_enemy_turns(self) -> None:
25-
for entity in self.game_map.entities - {self.player}:
26-
print(f'The {entity.name} wonders when it will get to take a real turn.')
29+
for entity in set(self.game_map.actors) - {self.player}:
30+
if entity.ai:
31+
entity.ai.perform()
2732

2833
def update_fov(self) -> None:
2934
"""Recompute the visible area based on the players point of view."""
35+
3036
self.game_map.visible[:] = compute_fov(
3137
self.game_map.tiles["transparent"],
3238
(self.player.x, self.player.y),
@@ -38,9 +44,16 @@ def update_fov(self) -> None:
3844
self.game_map.explored |= self.game_map.visible
3945

4046

41-
def render(self, console: Console, context: Context) -> None:
47+
def render(self, console: Console) -> None:
4248
self.game_map.render(console)
4349

44-
context.present(console)
50+
self.message_log.render(console=console, x=21, y=45, width=40, height=5)
51+
52+
render_bar(
53+
console=console,
54+
current_value=self.player.fighter.hp,
55+
maximum_value=self.player.fighter.max_hp,
56+
total_width=20,
57+
)
4558

46-
console.clear()
59+
render_names_at_mouse_location(console=console, x=21, y=44, engine=self)

entity.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from __future__ import annotations
22

33
import copy
4-
from typing import Optional, Tuple, TypeVar, TYPE_CHECKING
4+
from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING
5+
6+
from render_order import RenderOrder
57

68
if TYPE_CHECKING:
9+
from components.ai import BaseAI
10+
from components.fighter import Fighter
711
from game_map import GameMap
812

913
T = TypeVar("T", bound="Entity")
@@ -23,6 +27,7 @@ def __init__(
2327
color: Tuple[int, int, int] = (255, 255, 255),
2428
name: str = "<Unnamed>",
2529
blocks_movement: bool = False,
30+
render_order: RenderOrder = RenderOrder.CORPSE,
2631
):
2732
"""
2833
Create a new entity with the given properties.
@@ -33,6 +38,7 @@ def __init__(
3338
self.color = color
3439
self.name = name
3540
self.blocks_movement = blocks_movement
41+
self.render_order = render_order
3642
if gamemap:
3743
# If gamemap isn't provided now then it will be set later.
3844
self.gamemap = gamemap
@@ -62,4 +68,41 @@ def move(self, dx: int, dy: int) -> None:
6268
Moves the entity by a given amount
6369
"""
6470
self.x += dx
65-
self.y += dy
71+
self.y += dy
72+
73+
74+
class Actor(Entity):
75+
"""
76+
A generic object to that can take damage, do turns, etc. for things like enemies, the player, etc.
77+
"""
78+
79+
def __init__(
80+
self,
81+
*,
82+
x: int = 0,
83+
y: int = 0,
84+
char: str = "?",
85+
color: Tuple[int, int, int] = (255, 255, 255),
86+
name: str = "<Unnamed>",
87+
ai_cls: Type[BaseAI],
88+
fighter: Fighter
89+
):
90+
super().__init__(
91+
x=x,
92+
y=y,
93+
char=char,
94+
color=color,
95+
name=name,
96+
blocks_movement=True,
97+
render_order=RenderOrder.ACTOR,
98+
)
99+
100+
self.ai: Optional[BaseAI] = ai_cls(self)
101+
102+
self.fighter = fighter
103+
self.fighter.entity = self
104+
105+
@property
106+
def is_alive(self) -> bool:
107+
"""Returns True as long as this actor can perform actions."""
108+
return bool(self.ai)

0 commit comments

Comments
 (0)