Skip to content

Commit 3e6e459

Browse files
committed
Added Status Effects to the game.
1 parent e1b8dcd commit 3e6e459

File tree

8 files changed

+128
-10
lines changed

8 files changed

+128
-10
lines changed

actions.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
from entity import Actor
77
import exceptions
88
import random
9+
910
if TYPE_CHECKING:
1011
from engine import Engine
1112
from entity import Actor, Entity, Item, NPC
13+
from status_effect import StatusEffect
1214

1315

1416
class Action:
@@ -83,6 +85,11 @@ def perform(self) -> None:
8385
self.item.consumable.activate(self)
8486

8587
class MeleeAction(ActionWithDirection):
88+
89+
def __init__(self, entity: Actor, dx: int, dy: int, effect: Optional[StatusEffect] = None):
90+
super().__init__(entity, dx, dy)
91+
self.effect = effect
92+
8693
def perform(self) -> None:
8794
target = self.target_actor
8895
if not target:
@@ -110,6 +117,10 @@ def perform(self) -> None:
110117
attack_color
111118
)
112119
target.fighter.hp -= damage
120+
121+
# Apply the status effect to the target, if any.
122+
if self.effect is not None:
123+
target.fighter.apply_status_effect(self.effect)
113124
else:
114125
self.engine.message_log.add_message(
115126
f"{attack_desc}, but does no damage.",
@@ -137,8 +148,10 @@ def perform(self) -> None:
137148
class BumpAction(ActionWithDirection):
138149
def perform(self) -> None:
139150
if self.target_actor:
151+
self.entity.last_position = (self.entity.x, self.entity.y)
140152
return MeleeAction(self.entity, self.dx, self.dy).perform()
141153
elif self.target_NPC:
154+
self.entity.last_position = (self.entity.x, self.entity.y)
142155
return InteractNPCAction(self.entity, self.dx, self.dy).perform()
143156
else:
144157
return MovementAction(self.entity, self.dx, self.dy).perform()
@@ -157,7 +170,7 @@ def perform(self) -> None:
157170

158171
class WaitAction(Action):
159172
def perform(self) -> None:
160-
pass
173+
self.entity.last_position = (self.entity.x, self.entity.y)
161174

162175
class PickupAction(Action):
163176
"""Pickup an item and add it to the inventory, if there is room for it."""

components/ai.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,24 @@
66
import numpy as np # type: ignore
77
import tcod
88

9-
from actions import Action, BumpAction, MeleeAction, MovementAction, WaitAction
9+
from actions import Action, MeleeAction, MovementAction, WaitAction
10+
from entity import Actor
1011

1112

1213
if TYPE_CHECKING:
1314
from entity import Actor
15+
from status_effect import StatusEffect
1416

1517

1618
class BaseAI(Action):
1719

20+
def __init__(self, entity: Actor) -> None:
21+
super().__init__(entity)
22+
23+
def set_effect(self, effect: Optional[StatusEffect] = None) -> None:
24+
self.effect = effect
25+
26+
1827
def perform(self) -> None:
1928
raise NotImplementedError()
2029

@@ -52,6 +61,7 @@ def __init__(self, entity: Actor):
5261
super().__init__(entity)
5362
self.path: List[Tuple[int, int]] = []
5463

64+
5565
def perform(self) -> None:
5666
target = self.engine.player
5767
dx = target.x - self.entity.x
@@ -60,7 +70,7 @@ def perform(self) -> None:
6070

6171
if self.engine.game_map.visible[self.entity.x, self.entity.y]:
6272
if distance <= 1:
63-
return MeleeAction(self.entity, dx, dy).perform()
73+
return MeleeAction(self.entity, dx, dy, self.effect).perform()
6474

6575
self.path = self.get_path_to(target.x, target.y)
6676

@@ -110,9 +120,8 @@ def perform(self) -> None:
110120

111121
self.turns_remaining -= 1
112122

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()
123+
# The actor will either try to move in the randomly chosen direction.
124+
return MovementAction(self.entity, direction_x, direction_y,).perform()
116125

117126

118127
class SpawnerEnemy(BaseAI):

components/fighter.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
from typing import TYPE_CHECKING
55

66
import color
7+
import copy
78

89
from components.base_component import BaseComponent
910
from render_order import RenderOrder
1011

1112
if TYPE_CHECKING:
1213
from entity import Actor
14+
from status_effect import StatusEffect
1315

1416
class Fighter(BaseComponent):
1517
parent: Actor
@@ -20,6 +22,8 @@ def __init__(self, hp: int, base_dodge: int, base_defence, base_power: int):
2022
self.base_dodge = base_dodge
2123
self.base_defence = base_defence
2224
self.base_power = base_power
25+
self.status_effects = []
26+
2327

2428
@property
2529
def dodge(self) -> int:
@@ -79,6 +83,28 @@ def heal(self, amount: int) -> int:
7983

8084
return amount_recovered
8185

86+
def apply_status_effect(self, effect: StatusEffect) -> None:
87+
for status_effect in self.status_effects:
88+
if status_effect.name == effect.name:
89+
status_effect.duration += effect.duration
90+
return
91+
self.status_effects.append(copy.deepcopy(effect))
92+
93+
self.engine.message_log.add_message(f"{self.parent.name} is now {effect.name}!", color.status_effect_applied)
94+
95+
def update_status_effects(self) -> None:
96+
for status_effect in self.status_effects:
97+
status_effect.on_tick(self.parent)
98+
if status_effect.duration <= 0:
99+
self.status_effects.remove(status_effect)
100+
self.engine.message_log.add_message(f"{self.parent.name} is no longer {status_effect.name}.")
101+
102+
def has_status_effect(self, effect: StatusEffect) -> bool:
103+
for status_effect in self.status_effects:
104+
if status_effect.name == effect.name:
105+
return True
106+
return False
107+
82108
def take_damage(self, amount: int) -> None:
83109
self.hp -= amount
84110

@@ -108,4 +134,4 @@ def die(self) -> None:
108134
if roll <= 45:
109135
amount = self.max_hp // 4 # Give 25% of the entity's max HP as credits (rounded down).
110136
self.engine.game_world.credits += amount
111-
self.engine.message_log.add_message(f"You found {amount} credits!", color.health_recovered)
137+
self.engine.message_log.add_message(f"You found {amount} credits!", color.health_recovered)

engine.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ def handle_enemy_turns(self) -> None:
3737
except exceptions.Impossible:
3838
pass # Ignore impossible action exceptions from AI.
3939

40+
def handle_status_effects(self) -> None:
41+
for entity in set(self.game_map.actors):
42+
entity.fighter.update_status_effects()
43+
44+
4045
def update_fov(self) -> None:
4146
"""Recompute the visible area based on the players point of view."""
4247

entity.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from components.level import Level
1818
from game_map import GameMap
1919
from input_handlers import EventHandler
20+
from status_effect import StatusEffect
2021

2122

2223
T = TypeVar("T", bound="Entity")
@@ -52,6 +53,7 @@ def __init__(
5253
self.render_order = render_order
5354
self.inspect_message = inspect_message
5455
self.is_swarm = False
56+
self.last_position = (x, y)
5557
if parent:
5658
# If parent isn't provided now then it will be set later.
5759
self.parent = parent
@@ -95,6 +97,7 @@ def move(self, dx: int, dy: int) -> None:
9597
"""
9698
Moves the entity by a given amount
9799
"""
100+
self.last_position = (self.x, self.y)
98101
self.x += dx
99102
self.y += dy
100103

@@ -118,6 +121,7 @@ def __init__(
118121
fighter: Fighter,
119122
inventory: Inventory,
120123
level: Level,
124+
effect: Optional[StatusEffect] = None,
121125
):
122126
super().__init__(
123127
x=x,
@@ -131,6 +135,7 @@ def __init__(
131135
)
132136

133137
self.ai: Optional[BaseAI] = ai_cls(self)
138+
self.ai.set_effect(effect)
134139

135140
self.equipment: Equipment = equipment
136141
self.equipment.parent = self

entity_factories.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from entity import NPC, Actor, Item
77
from components.equipment import Equipment
88
from input_handlers import ShopkeepMenuEventHandler
9+
from status_effect import Poisoned, bleeding
10+
911

1012
player = Actor(
1113
char="@",
@@ -50,7 +52,9 @@
5052
inventory=Inventory(capacity=0),
5153
level=Level(xp_given=150),
5254
equipment=Equipment(),
53-
inspect_message="It's a mutated human, probably due to being exposed to irradiated places. It's now a reckless hunter, with sharp claws and skinny body, it wants to eat fresh flesh."
55+
inspect_message="It's a mutated human, probably due to being exposed to irradiated places. It's now a reckless hunter, with sharp claws and skinny body, it wants to eat fresh flesh.",
56+
effect=bleeding(duration=6, value=1),
57+
5458
)
5559
acid_mold = Actor(
5660
char="m",
@@ -61,7 +65,9 @@
6165
inventory=Inventory(capacity=0),
6266
level=Level(xp_given=50),
6367
equipment=Equipment(),
64-
inspect_message="It's a slime mold, but it's acidic. Don't touch it! It's pains to the touch."
68+
inspect_message="It's a slime mold, but it's acidic. Don't touch it! It's hurts to the touch.",
69+
effect=Poisoned(duration=4, value=1),
70+
6571
)
6672
mama_mold = Actor(
6773
char="M",

input_handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def handle_action(self, action: Optional[Action]) -> bool:
154154
return False # Skip enemy turn on exceptions.
155155

156156
self.engine.handle_enemy_turns()
157-
157+
self.engine.handle_status_effects()
158158
self.engine.update_fov()
159159
return True
160160

status_effect.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
if TYPE_CHECKING:
6+
from entity import Actor
7+
8+
class StatusEffect:
9+
"""
10+
A class to represent a status effect. Such as "poisoned", "stunned", etc.
11+
"""
12+
def __init__(self, name: str, duration: int, value: int):
13+
self.name = name
14+
self.duration = duration
15+
self.value = value
16+
17+
def on_tick(self, parent: Actor) -> None:
18+
"""
19+
Called every turn for the duration of the status effect.
20+
21+
This method must be overridden by subclasses.
22+
"""
23+
raise NotImplementedError()
24+
25+
class Poisoned(StatusEffect):
26+
"""
27+
A class to represent a poisoned status effect.
28+
29+
Actors will take damage every turn, based on the value of the status effect.
30+
31+
"""
32+
def __init__(self, duration: int, value: int):
33+
super().__init__("Poisoned", duration, value)
34+
35+
def on_tick(self, parent: Actor) -> None:
36+
parent.fighter.take_damage(self.value)
37+
self.duration -= 1
38+
39+
class bleeding(StatusEffect):
40+
"""
41+
A class to represent a bleeding status effect.
42+
43+
If the actor moved this turn, they will take damage based on the value of the status effect.
44+
"""
45+
def __init__(self, duration: int, value: int):
46+
super().__init__("Bleeding", duration, value)
47+
self.last_pos = (0, 0)
48+
49+
def on_tick(self, parent: Actor) -> None:
50+
self.last_pos = (parent.last_position[0], parent.last_position[1])
51+
52+
if parent.last_position != (parent.x, parent.y):
53+
parent.fighter.take_damage(self.value)
54+
self.duration -= 1

0 commit comments

Comments
 (0)