Skip to content

Commit bc011f9

Browse files
committed
finished parts 8 and 9!
1 parent 09f2d37 commit bc011f9

File tree

16 files changed

+803
-95
lines changed

16 files changed

+803
-95
lines changed

actions.py

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from typing import Optional, Tuple, TYPE_CHECKING
44

55
import color
6-
6+
import exceptions
77
if TYPE_CHECKING:
88
from engine import Engine
9-
from entity import Actor, Entity
9+
from entity import Actor, Entity, Item
1010

1111

1212
class Action:
@@ -17,7 +17,7 @@ def __init__(self, entity: Actor) -> None:
1717
@property
1818
def engine(self) -> Engine:
1919
"""Return the engine this action belongs to."""
20-
return self.entity.gamemap.engine
20+
return self.entity.parent.engine
2121

2222
def perform(self) -> None:
2323
"""Perform this action with the objects needed to determine its scope.
@@ -55,16 +55,30 @@ def blocking_entity(self) -> Optional[Entity]:
5555
"""Return the blocking entity at this actions destination.."""
5656
return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy)
5757

58-
class EscapeAction(Action):
58+
class ItemAction(Action):
59+
def __init__(
60+
self, entity: Actor, item: Item, target_xy: Optional[Tuple[int, int]] = None
61+
):
62+
super().__init__(entity)
63+
self.item = item
64+
if not target_xy:
65+
target_xy = entity.x, entity.y
66+
self.target_xy = target_xy
67+
68+
@property
69+
def target_actor(self) -> Optional[Actor]:
70+
"""Return the actor at this actions destination."""
71+
return self.engine.game_map.get_actor_at_location(*self.target_xy)
5972

6073
def perform(self) -> None:
61-
raise SystemExit()
74+
"""Invoke the items ability, this action will be given to provide context."""
75+
self.item.consumable.activate(self)
6276

6377
class MeleeAction(ActionWithDirection):
6478
def perform(self) -> None:
6579
target = self.target_actor
6680
if not target:
67-
return # No entity to attack.
81+
raise exceptions.Impossible("Nothing to attack.")
6882

6983
damage = self.entity.fighter.power - target.fighter.defense
7084

@@ -90,11 +104,14 @@ def perform(self) -> None:
90104
dest_x, dest_y = self.dest_xy
91105

92106
if not self.engine.game_map.in_bounds(dest_x, dest_y):
93-
return # Destination is out of bounds.
107+
# Destination is out of bounds.
108+
raise exceptions.Impossible("That way is blocked.")
94109
if not self.engine.game_map.tiles["walkable"][dest_x, dest_y]:
95-
return # Destination is blocked by a tile.
110+
# Destination is blocked by a tile.
111+
raise exceptions.Impossible("That way is blocked.")
96112
if self.engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
97-
return # Destination is blocked by an entity.
113+
# Destination is blocked by an entity.
114+
raise exceptions.Impossible("There's something blocking the way!")
98115

99116

100117
self.entity.move(self.dx, self.dy)
@@ -110,4 +127,42 @@ def perform(self) -> None:
110127

111128
class WaitAction(Action):
112129
def perform(self) -> None:
113-
pass
130+
pass
131+
132+
class PickupAction(Action):
133+
"""Pickup an item and add it to the inventory, if there is room for it."""
134+
135+
def __init__(self, entity: Actor):
136+
super().__init__(entity)
137+
138+
def perform(self) -> None:
139+
actor_location_x = self.entity.x
140+
actor_location_y = self.entity.y
141+
inventory = self.entity.inventory
142+
143+
for item in self.engine.game_map.items:
144+
if actor_location_x == item.x and actor_location_y == item.y:
145+
if len(inventory.items) >= inventory.capacity:
146+
raise exceptions.Impossible("Your inventory is full.")
147+
148+
self.engine.game_map.entities.remove(item)
149+
item.parent = self.entity.inventory
150+
inventory.items.append(item)
151+
152+
self.engine.message_log.add_message(f"You picked up the {item.name}!")
153+
return
154+
155+
raise exceptions.Impossible("There is nothing here to pick up.")
156+
157+
class DropItem(ItemAction):
158+
def perform(self) -> None:
159+
self.entity.inventory.drop(self.item)
160+
161+
class InspectAction(Action):
162+
def __init__(self, actor: Actor, target: Entity):
163+
super().__init__(actor)
164+
165+
self.message = target.inspect_message
166+
167+
def perform(self) -> None:
168+
self.engine.message_log.add_message(self.message.upper(), color.text_console)

color.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
white = (255, 255, 255)
22
black = (0, 0, 0)
3+
red = (255, 0, 0)
34

45
player_atk = (255, 153, 0)
56
enemy_atk = (153, 0, 0)
7+
needs_target = (51, 153, 255)
8+
status_effect_applied = (153, 0, 255)
69

710
player_die = (255, 0, 0)
811
enemy_die = (255, 255, 0)
912

13+
invalid = (255, 255, 0)
14+
impossible = (255, 153, 153)
15+
error = (204, 0, 0)
16+
1017
text_console = (204, 255, 51)
1118

1219
bar_text = white
1320
bar_filled = (255, 0, 0)
14-
bar_empty = (77, 0, 0)
21+
bar_empty = (77, 0, 0)
22+
23+
health_recovered = (102, 255, 102)

components/ai.py

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
from __future__ import annotations
22

3-
from typing import List, Tuple, TYPE_CHECKING
3+
import random
4+
from typing import List, Optional, Tuple, TYPE_CHECKING
45

56
import numpy as np # type: ignore
67
import tcod
78

8-
from actions import Action, MeleeAction, MovementAction, WaitAction
9-
from components.base_component import BaseComponent
9+
from actions import Action, BumpAction, MeleeAction, MovementAction, WaitAction
10+
1011

1112
if TYPE_CHECKING:
1213
from entity import Actor
1314

1415

15-
class BaseAI(Action, BaseComponent):
16-
entity: Actor
16+
class BaseAI(Action):
17+
1718
def perform(self) -> None:
1819
raise NotImplementedError()
1920

@@ -69,4 +70,46 @@ def perform(self) -> None:
6970
self.entity, dest_x - self.entity.x, dest_y - self.entity.y,
7071
).perform()
7172

72-
return WaitAction(self.entity).perform()
73+
return WaitAction(self.entity).perform()
74+
75+
class ConfusedEnemy(BaseAI):
76+
"""
77+
A confused enemy will stumble around aimlessly for a given number of turns, then revert back to its previous AI.
78+
If an actor occupies a tile it is randomly moving into, it will attack.
79+
"""
80+
81+
def __init__(
82+
self, entity: Actor, previous_ai: Optional[BaseAI], turns_remaining: int
83+
):
84+
super().__init__(entity)
85+
86+
self.previous_ai = previous_ai
87+
self.turns_remaining = turns_remaining
88+
89+
def perform(self) -> None:
90+
# Revert the AI back to the original state if the effect has run its course.
91+
if self.turns_remaining <= 0:
92+
self.engine.message_log.add_message(
93+
f"The {self.entity.name} is no longer confused."
94+
)
95+
self.entity.ai = self.previous_ai
96+
else:
97+
# Pick a random direction
98+
direction_x, direction_y = random.choice(
99+
[
100+
(-1, -1), # Northwest
101+
(0, -1), # North
102+
(1, -1), # Northeast
103+
(-1, 0), # West
104+
(1, 0), # East
105+
(-1, 1), # Southwest
106+
(0, 1), # South
107+
(1, 1), # Southeast
108+
]
109+
)
110+
111+
self.turns_remaining -= 1
112+
113+
# The actor will either try to move or attack in the chosen random direction.
114+
# Its possible the actor will just bump into the wall, wasting a turn.
115+
return BumpAction(self.entity, direction_x, direction_y,).perform()

components/base_component.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@
55
if TYPE_CHECKING:
66
from engine import Engine
77
from entity import Entity
8+
from game_map import GameMap
89

910

1011
class BaseComponent:
11-
entity: Entity # Owning entity instance.
12+
parent: Entity # Owning entity instance.
1213

14+
@property
15+
def gamemap(self) -> GameMap:
16+
return self.parent.gamemap
17+
1318
@property
1419
def engine(self) -> Engine:
15-
return self.entity.gamemap.engine
20+
return self.gamemap.engine

components/consumable.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
from __future__ import annotations
2+
3+
from typing import Optional, TYPE_CHECKING
4+
5+
import actions
6+
import color
7+
import components.ai
8+
import components.inventory
9+
from components.base_component import BaseComponent
10+
from exceptions import Impossible
11+
from input_handlers import AreaRangedAttackHandler, SingleRangedAttackHandler
12+
13+
if TYPE_CHECKING:
14+
from entity import Actor, Item
15+
16+
17+
class Consumable(BaseComponent):
18+
parent: Item
19+
20+
def get_action(self, consumer: Actor) -> Optional[actions.Action]:
21+
"""Try to return the action for this item."""
22+
return actions.ItemAction(consumer, self.parent)
23+
24+
def activate(self, action: actions.ItemAction) -> None:
25+
"""Invoke this items ability.
26+
27+
`action` is the context for this activation.
28+
"""
29+
raise NotImplementedError()
30+
31+
def consume(self) -> None:
32+
"""Remove the consumed item from its containing inventory."""
33+
entity = self.parent
34+
inventory = entity.parent
35+
if isinstance(inventory, components.inventory.Inventory):
36+
inventory.items.remove(entity)
37+
38+
39+
40+
class HealingConsumable(Consumable):
41+
def __init__(self, amount: int):
42+
self.amount = amount
43+
44+
def activate(self, action: actions.ItemAction) -> None:
45+
consumer = action.entity
46+
amount_recovered = consumer.fighter.heal(self.amount)
47+
48+
if amount_recovered > 0:
49+
self.engine.message_log.add_message(
50+
f"You consume the {self.parent.name}, and recover {amount_recovered} HP!",
51+
color.health_recovered,
52+
)
53+
self.consume()
54+
else:
55+
raise Impossible(f"Your health is already full.")
56+
57+
class EletricDamageConsumable(Consumable):
58+
def __init__(self, damage: int, maximum_range: int):
59+
self.damage = damage
60+
self.maximum_range = maximum_range
61+
62+
def activate(self, action: actions.ItemAction) -> None:
63+
consumer = action.entity
64+
target = None
65+
closest_distance = self.maximum_range + 1.0
66+
67+
for actor in self.engine.game_map.actors:
68+
if actor is not consumer and self.parent.gamemap.visible[actor.x, actor.y]:
69+
distance = consumer.distance(actor.x, actor.y)
70+
71+
if distance < closest_distance:
72+
target = actor
73+
closest_distance = distance
74+
75+
if target:
76+
self.engine.message_log.add_message(
77+
f"ZAP! A bright eletric bolt strikes the {target.name} for {self.damage} damage!"
78+
)
79+
target.fighter.take_damage(self.damage)
80+
self.consume()
81+
else:
82+
raise Impossible("No enemy is close enough to strike.")
83+
84+
class ConfusionConsumable(Consumable):
85+
def __init__(self, number_of_turns: int):
86+
self.number_of_turns = number_of_turns
87+
88+
def get_action(self, consumer: Actor) -> Optional[actions.Action]:
89+
self.engine.message_log.add_message(
90+
"Select a target location.", color.needs_target
91+
)
92+
self.engine.event_handler = SingleRangedAttackHandler(
93+
self.engine,
94+
callback=lambda xy: actions.ItemAction(consumer, self.parent, xy),
95+
)
96+
return None
97+
98+
def activate(self, action: actions.ItemAction) -> None:
99+
consumer = action.entity
100+
target = action.target_actor
101+
102+
if not self.engine.game_map.visible[action.target_xy]:
103+
raise Impossible("You cannot target an area that you cannot see.")
104+
if not target:
105+
raise Impossible("You must select an enemy to target.")
106+
if target is consumer:
107+
raise Impossible("You cannot confuse yourself!")
108+
109+
self.engine.message_log.add_message(
110+
f"The eyes of the {target.name} look vacant, as it starts to stumble around!",
111+
color.status_effect_applied,
112+
)
113+
target.ai = components.ai.ConfusedEnemy(
114+
entity=target, previous_ai=target.ai, turns_remaining=self.number_of_turns,
115+
)
116+
self.consume()
117+
118+
class FireballDamageConsumable(Consumable):
119+
def __init__(self, damage: int, radius: int):
120+
self.damage = damage
121+
self.radius = radius
122+
123+
def get_action(self, consumer: Actor) -> Optional[actions.Action]:
124+
self.engine.message_log.add_message(
125+
"Select a target location.", color.needs_target
126+
)
127+
self.engine.event_handler = AreaRangedAttackHandler(
128+
self.engine,
129+
radius=self.radius,
130+
callback=lambda xy: actions.ItemAction(consumer, self.parent, xy),
131+
)
132+
return None
133+
134+
def activate(self, action: actions.ItemAction) -> None:
135+
target_xy = action.target_xy
136+
137+
if not self.engine.game_map.visible[target_xy]:
138+
raise Impossible("You cannot target an area that you cannot see.")
139+
140+
targets_hit = False
141+
for actor in self.engine.game_map.actors:
142+
if actor.distance(*target_xy) <= self.radius:
143+
self.engine.message_log.add_message(
144+
f"The {actor.name} is engulfed in a fiery explosion, taking {self.damage} damage!"
145+
)
146+
actor.fighter.take_damage(self.damage)
147+
targets_hit = True
148+
149+
if not targets_hit:
150+
raise Impossible("There are no targets in the radius.")
151+
self.consume()

0 commit comments

Comments
 (0)