Part 9 - Ranged Scrolls and Targeting
Adding health potions was a big step, but we won’t stop there. Let’s continue adding a few items, this time with a focus on offense. We’ll add a few scrolls, which will give the player a one-time ranged attack. This gives the player a lot more tactical options to work with, and is definitely something you’ll want to expand upon in your own game.
Let’s start simple, with a spell that just hits the closest enemy. We’ll
create a scroll of lightning, which automatically targets an enemy
nearby the player. Start by adding the function to item_functions.py
:
def heal(*args, **kwargs):
...
+def cast_lightning(*args, **kwargs):
+ caster = args[0]
+ entities = kwargs.get('entities')
+ fov_map = kwargs.get('fov_map')
+ damage = kwargs.get('damage')
+ maximum_range = kwargs.get('maximum_range')
+
+ results = []
+
+ target = None
+ closest_distance = maximum_range + 1
+
+ for entity in entities:
+ if entity.fighter and entity != caster and libtcod.map_is_in_fov(fov_map, entity.x, entity.y):
+ distance = caster.distance_to(entity)
+
+ if distance < closest_distance:
+ target = entity
+ closest_distance = distance
+
+ if target:
+ results.append({'consumed': True, 'target': target, 'message': Message('A lighting bolt strikes the {0} with a loud thunder! The damage is {1}'.format(target.name, damage))})
+ results.extend(target.fighter.take_damage(damage))
+ else:
+ results.append({'consumed': False, 'target': None, 'message': Message('No enemy is close enough to strike.', libtcod.red)})
+
+ return results
def heal(*args, **kwargs):
...
def cast_lightning(*args, **kwargs):
caster = args[0]
entities = kwargs.get('entities')
fov_map = kwargs.get('fov_map')
damage = kwargs.get('damage')
maximum_range = kwargs.get('maximum_range')
results = []
target = None
closest_distance = maximum_range + 1
for entity in entities:
if entity.fighter and entity != caster and libtcod.map_is_in_fov(fov_map, entity.x, entity.y):
distance = caster.distance_to(entity)
if distance < closest_distance:
target = entity
closest_distance = distance
if target:
results.append({'consumed': True, 'target': target, 'message': Message('A lighting bolt strikes the {0} with a loud thunder! The damage is {1}'.format(target.name, damage))})
results.extend(target.fighter.take_damage(damage))
else:
results.append({'consumed': False, 'target': None, 'message': Message('No enemy is close enough to strike.', libtcod.red)})
return results
Now let’s add a chance for this scroll to drop on the map. Most of the
items will still be health potions, but we’ll sprinkle in a few
lightning scrolls as well. In game_map.py
:
...
if not any([entity for entity in entities if entity.x == x and entity.y == y]):
+ item_chance = randint(0, 100)
- item_component = Item(use_function=heal, amount=4)
- item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM,
- item=item_component)
+
+ if item_chance < 70:
+ item_component = Item(use_function=heal, amount=4)
+ item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM,
+ item=item_component)
+ else:
+ item_component = Item(use_function=cast_lightning, damage=20, maximum_range=5)
+ item = Entity(x, y, '#', libtcod.yellow, 'Lightning Scroll', render_order=RenderOrder.ITEM,
+ item=item_component)
... if not any([entity for entity in entities if entity.x == x and entity.y == y]): item_chance = randint(0, 100) if item_chance < 70: item_component = Item(use_function=heal, amount=4) item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM, item=item_component) else: item_component = Item(use_function=cast_lightning, damage=20, maximum_range=5) item = Entity(x, y, '#', libtcod.yellow, 'Lightning Scroll', render_order=RenderOrder.ITEM, item=item_component)
Be sure to import cast_lightning
at the top of the file.
...
from entity import Entity
-from item_functions import heal
+from item_functions import cast_lightning, heal
from map_objects.rectangle import Rect
...
...
from entity import Entity
from item_functions import cast_lightning, heal
from map_objects.rectangle import Rect
...
Lastly, we’ll need to adjust our “use” call in engine.py
, since our
lightning spell is expecting more keyword arguments than we’re currently
passing.
...
if game_state == GameStates.SHOW_INVENTORY:
- player_turn_results.extend(player.inventory.use(item))
+ player_turn_results.extend(player.inventory.use(item, entities=entities, fov_map=fov_map))
elif game_state == GameStates.DROP_INVENTORY:
player_turn_results.extend(player.inventory.drop_item(item))
... if game_state == GameStates.SHOW_INVENTORY: player_turn_results.extend(player.inventory.use(item)) player_turn_results.extend(player.inventory.use(item, entities=entities, fov_map=fov_map)) elif game_state == GameStates.DROP_INVENTORY: player_turn_results.extend(player.inventory.drop_item(item))
Run the project now, and you should have a working lightning scroll. That was pretty easy!
*Tip: For testing, you may want to increase the maximum amount of items per room.
Needless to say, the spell would be much more usable if we were allowed to select the target. While we won’t change the lightning spell, we should have another type of spell that allows targeting. Let’s focus on creating a fireball spell, which will not only ask for a target, but also hit multiple enemies in a set radius.
We’ll work backwards in this case, by starting with the end result (the
“fireball” spell) and modifying everything else to make this work.
Here’s the fireball spell, which should go in item_functions.py
:
...
def cast_lightning(*args, **kwargs):
...
+def cast_fireball(*args, **kwargs):
+ entities = kwargs.get('entities')
+ fov_map = kwargs.get('fov_map')
+ damage = kwargs.get('damage')
+ radius = kwargs.get('radius')
+ target_x = kwargs.get('target_x')
+ target_y = kwargs.get('target_y')
+
+ results = []
+
+ if not libtcod.map_is_in_fov(fov_map, target_x, target_y):
+ results.append({'consumed': False, 'message': Message('You cannot target a tile outside your field of view.', libtcod.yellow)})
+ return results
+
+ results.append({'consumed': True, 'message': Message('The fireball explodes, burning everything within {0} tiles!'.format(radius), libtcod.orange)})
+
+ for entity in entities:
+ if entity.distance(target_x, target_y) <= radius and entity.fighter:
+ results.append({'message': Message('The {0} gets burned for {1} hit points.'.format(entity.name, damage), libtcod.orange)})
+ results.extend(entity.fighter.take_damage(damage))
+
+ return results
...
def cast_lightning(*args, **kwargs):
...
def cast_fireball(*args, **kwargs):
entities = kwargs.get('entities')
fov_map = kwargs.get('fov_map')
damage = kwargs.get('damage')
radius = kwargs.get('radius')
target_x = kwargs.get('target_x')
target_y = kwargs.get('target_y')
results = []
if not libtcod.map_is_in_fov(fov_map, target_x, target_y):
results.append({'consumed': False, 'message': Message('You cannot target a tile outside your field of view.', libtcod.yellow)})
return results
results.append({'consumed': True, 'message': Message('The fireball explodes, burning everything within {0} tiles!'.format(radius), libtcod.orange)})
for entity in entities:
if entity.distance(target_x, target_y) <= radius and entity.fighter:
results.append({'message': Message('The {0} gets burned for {1} hit points.'.format(entity.name, damage), libtcod.orange)})
results.extend(entity.fighter.take_damage(damage))
return results
What do we need to do to make this function work? The most obvious thing
is to pass the damage, radius, and target location. Damage and radius
are easy; we can do those when we create the item in place_entities
.
The target is trickier, because we don’t know that is until the player
selects a tile after using the item.
We’re going to need another game state for targeting. When the player selects a certain type of item, the game will ask him or her to select a location before proceeding. The player then can left-click on a location, or right-click to cancel, so we’ll need a new set of input handlers as well.
Start with the easy part: Add a new game state to GameStates
:
class GameStates(Enum):
PLAYERS_TURN = 1
ENEMY_TURN = 2
PLAYER_DEAD = 3
SHOW_INVENTORY = 4
DROP_INVENTORY = 5
+ TARGETING = 6
class GameStates(Enum):
PLAYERS_TURN = 1
ENEMY_TURN = 2
PLAYER_DEAD = 3
SHOW_INVENTORY = 4
DROP_INVENTORY = 5
TARGETING = 6
Now let’s modify the input handlers. We’ll add a function for the keys while we’re targeting, and also add a generalized mouse handler, to know where the player clicks.
def handle_keys(key, game_state):
if game_state == GameStates.PLAYERS_TURN:
return handle_player_turn_keys(key)
elif game_state == GameStates.PLAYER_DEAD:
return handle_player_dead_keys(key)
+ elif game_state == GameStates.TARGETING:
+ return handle_targeting_keys(key)
elif game_state in (GameStates.SHOW_INVENTORY, GameStates.DROP_INVENTORY):
return handle_inventory_keys(key)
...
+def handle_targeting_keys(key):
+ if key.vk == libtcod.KEY_ESCAPE:
+ return {'exit': True}
+
+ return {}
def handle_player_dead_keys(key):
...
+def handle_mouse(mouse):
+ (x, y) = (mouse.cx, mouse.cy)
+
+ if mouse.lbutton_pressed:
+ return {'left_click': (x, y)}
+ elif mouse.rbutton_pressed:
+ return {'right_click': (x, y)}
+
+ return {}
def handle_keys(key, game_state): if game_state == GameStates.PLAYERS_TURN: return handle_player_turn_keys(key) elif game_state == GameStates.PLAYER_DEAD: return handle_player_dead_keys(key) elif game_state == GameStates.TARGETING: return handle_targeting_keys(key) elif game_state in (GameStates.SHOW_INVENTORY, GameStates.DROP_INVENTORY): return handle_inventory_keys(key) ... def handle_targeting_keys(key): if key.vk == libtcod.KEY_ESCAPE: return {'exit': True} return {} def handle_player_dead_keys(key): ... def handle_mouse(mouse): (x, y) = (mouse.cx, mouse.cy) if mouse.lbutton_pressed: return {'left_click': (x, y)} elif mouse.rbutton_pressed: return {'right_click': (x, y)} return {}
If the player is in targeting mode, the only key we’ll accept is Escape,
which cancels the targeting. The mouse handler doesn’t take the game
state into account; it just tells the engine if the left or right mouse
button was clicked. The engine will have to decide what to do with that.
Modify engine.py
to accept the mouse inputs:
...
action = handle_keys(key, game_state)
+ mouse_action = handle_mouse(mouse)
move = action.get('move')
pickup = action.get('pickup')
show_inventory = action.get('show_inventory')
inventory_index = action.get('inventory_index')
exit = action.get('exit')
fullscreen = action.get('fullscreen')
+ left_click = mouse_action.get('left_click')
+ right_click = mouse_action.get('right_click')
player_turn_results = []
... action = handle_keys(key, game_state) mouse_action = handle_mouse(mouse) move = action.get('move') pickup = action.get('pickup') show_inventory = action.get('show_inventory') inventory_index = action.get('inventory_index') exit = action.get('exit') fullscreen = action.get('fullscreen') left_click = mouse_action.get('left_click') right_click = mouse_action.get('right_click') player_turn_results = []
Of course, we need to import handle_mouse
into engine.py
:
...
from game_states import GameStates
-from input_handlers import handle_keys
+from input_handlers import handle_keys, handle_mouse
from map_objects.game_map import GameMap
...
...
from game_states import GameStates
from input_handlers import handle_keys, handle_mouse
from map_objects.game_map import GameMap
...
So how do we even know what types of items need to select a target? We
can add an attribute to the Item
component which will tell us. We
should also add a message, which will display when the user activates
the item, to inform the user that a target needs to be selected. Modify
the __init__
function in Item
like this:
class Item:
- def __init__(self, use_function=None, **kwargs):
+ def __init__(self, use_function=None, targeting=False, targeting_message=None, **kwargs):
self.use_function = use_function
+ self.targeting = targeting
+ self.targeting_message = targeting_message
self.function_kwargs = kwargs
class Item: def __init__(self, use_function=None, targeting=False, targeting_message=None, **kwargs): self.use_function = use_function self.targeting = targeting self.targeting_message = targeting_message self.function_kwargs = kwargs
Because we’re setting the values of targeting
and targeting_message
to None
by default, we don’t have to worry about changing the items
we’ve already made.
We’ll need to change our use
function in Inventory
to take the
targeting variable into account. If the item needs a target, we should
return a result that tells the engine that, and not use the item. If
not, we proceed as before. Add a new “if” statement to use
, and wrap
the previous code section in the “else” clause, like this:
def use(self, item_entity, **kwargs):
results = []
item_component = item_entity.item
if item_component.use_function is None:
results.append({'message': Message('The {0} cannot be used'.format(item_entity.name), libtcod.yellow)})
else:
- kwargs = {**item_component.function_kwargs, **kwargs}
- item_use_results = item_component.use_function(self.owner, **kwargs)
- for item_use_result in item_use_results:
- if item_use_result.get('consumed'):
- self.remove_item(item_entity)
-
- results.extend(item_use_results)
+ if item_component.targeting and not (kwargs.get('target_x') or kwargs.get('target_y')):
+ results.append({'targeting': item_entity})
+ else:
+ kwargs = {**item_component.function_kwargs, **kwargs}
+ item_use_results = item_component.use_function(self.owner, **kwargs)
+
+ for item_use_result in item_use_results:
+ if item_use_result.get('consumed'):
+ self.remove_item(item_entity)
+
+ results.extend(item_use_results)
return results
def use(self, item_entity, **kwargs): results = [] item_component = item_entity.item if item_component.use_function is None: results.append({'message': Message('The {0} cannot be used'.format(item_entity.name), libtcod.yellow)}) else: if item_component.targeting and not (kwargs.get('target_x') or kwargs.get('target_y')): results.append({'targeting': item_entity}) else: kwargs = {**item_component.function_kwargs, **kwargs} item_use_results = item_component.use_function(self.owner, **kwargs) for item_use_result in item_use_results: if item_use_result.get('consumed'): self.remove_item(item_entity) results.extend(item_use_results) return results
So basically, we check if the item has “targeting” set to True, and if
it does, whether or not we received the target_x
and target_y
variables. If we didn’t we can assume that the target has not yet been
selected, and the game state needs to switch to targeting. If it did, we
can use the item like normal.
Now let’s modify the engine to handle this new result type. Note that this result returns the item entity to the engine. That’s because the engine will need to “remember” which item was selected in the first place. Therefore, we’ll need a new variable right before the main game loop to keep track of the targeting item that was selected.
...
game_state = GameStates.PLAYERS_TURN
previous_game_state = game_state
+ targeting_item = None
while not libtcod.console_is_window_closed():
...
message = player_turn_result.get('message')
dead_entity = player_turn_result.get('dead')
item_added = player_turn_result.get('item_added')
item_consumed = player_turn_result.get('consumed')
item_dropped = player_turn_result.get('item_dropped')
+ targeting = player_turn_result.get('targeting')
...
if item_consumed:
game_state = GameStates.ENEMY_TURN
+ if targeting:
+ previous_game_state = GameStates.PLAYERS_TURN
+ game_state = GameStates.TARGETING
+
+ targeting_item = targeting
+
+ message_log.add_message(targeting_item.item.targeting_message)
... game_state = GameStates.PLAYERS_TURN previous_game_state = game_state targeting_item = None while not libtcod.console_is_window_closed(): ... message = player_turn_result.get('message') dead_entity = player_turn_result.get('dead') item_added = player_turn_result.get('item_added') item_consumed = player_turn_result.get('consumed') item_dropped = player_turn_result.get('item_dropped') targeting = player_turn_result.get('targeting') ... if item_consumed: game_state = GameStates.ENEMY_TURN if targeting: previous_game_state = GameStates.PLAYERS_TURN game_state = GameStates.TARGETING targeting_item = targeting message_log.add_message(targeting_item.item.targeting_message)
Now our game state will switch to targeting when we select an item from the inventory that needs it. Note that we’re doing something a little strange with the previous game state; we’re setting it to the player’s turn rather than the actual previous state. This is so that cancelling the targeting will not reopen the inventory screen.
Let’s now do something with the left and right clicks we added in before. If the player left clicks while in targeting, we’ll activate the use function again, this time with the target variables. If the user right clicks, we’ll cancel the targeting. We can also add the cancel targeting on Escape now.
...
if inventory_index is not None and previous_game_state != GameStates.PLAYER_DEAD and inventory_index < len(
player.inventory.items):
...
+ if game_state == GameStates.TARGETING:
+ if left_click:
+ target_x, target_y = left_click
+
+ item_use_results = player.inventory.use(targeting_item, entities=entities, fov_map=fov_map,
+ target_x=target_x, target_y=target_y)
+ player_turn_results.extend(item_use_results)
+ elif right_click:
+ player_turn_results.append({'targeting_cancelled': True})
if exit:
if game_state in (GameStates.SHOW_INVENTORY, GameStates.DROP_INVENTORY):
game_state = previous_game_state
+ elif game_state == GameStates.TARGETING:
+ player_turn_results.append({'targeting_cancelled': True})
else:
return True
if fullscreen:
...
... if inventory_index is not None and previous_game_state != GameStates.PLAYER_DEAD and inventory_index < len( player.inventory.items): ... if game_state == GameStates.TARGETING: if left_click: target_x, target_y = left_click item_use_results = player.inventory.use(targeting_item, entities=entities, fov_map=fov_map, target_x=target_x, target_y=target_y) player_turn_results.extend(item_use_results) elif right_click: player_turn_results.append({'targeting_cancelled': True}) if exit: if game_state in (GameStates.SHOW_INVENTORY, GameStates.DROP_INVENTORY): game_state = previous_game_state elif game_state == GameStates.TARGETING: player_turn_results.append({'targeting_cancelled': True}) else: return True if fullscreen: ...
Add the following to make the target cancellation revert the game state:
targeting = player_turn_result.get('targeting')
+ targeting_cancelled = player_turn_result.get('targeting_cancelled')
if message:
...
+ if targeting_cancelled:
+ game_state = previous_game_state
+
+ message_log.add_message(Message('Targeting cancelled'))
targeting = player_turn_result.get('targeting') targeting_cancelled = player_turn_result.get('targeting_cancelled') if message: ... if targeting_cancelled: game_state = previous_game_state message_log.add_message(Message('Targeting cancelled'))
Finally, let’s add the fireball scroll to the map. Modify
place_entities
like this:
...
item_chance = randint(0, 100)
if item_chance < 70:
item_component = Item(use_function=heal, amount=4)
item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM,
item=item_component)
+ elif item_chance < 85:
+ item_component = Item(use_function=cast_fireball, targeting=True, targeting_message=Message(
+ 'Left-click a target tile for the fireball, or right-click to cancel.', libtcod.light_cyan),
+ damage=12, radius=3)
+ item = Entity(x, y, '#', libtcod.red, 'Fireball Scroll', render_order=RenderOrder.ITEM,
+ item=item_component)
else:
item_component = Item(use_function=cast_lightning, damage=20, maximum_range=5)
item = Entity(x, y, '#', libtcod.yellow, 'Lightning Scroll', render_order=RenderOrder.ITEM,
item=item_component)
...
item_chance = randint(0, 100)
if item_chance < 70:
item_component = Item(use_function=heal, amount=4)
item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM,
item=item_component)
elif item_chance < 85:
item_component = Item(use_function=cast_fireball, targeting=True, targeting_message=Message(
'Left-click a target tile for the fireball, or right-click to cancel.', libtcod.light_cyan),
damage=12, radius=3)
item = Entity(x, y, '#', libtcod.red, 'Fireball Scroll', render_order=RenderOrder.ITEM,
item=item_component)
else:
item_component = Item(use_function=cast_lightning, damage=20, maximum_range=5)
item = Entity(x, y, '#', libtcod.yellow, 'Lightning Scroll', render_order=RenderOrder.ITEM,
item=item_component)
You’ll need to import both cast_fireball
and Message
:
...
from entity import Entity
+from game_messages import Message
-from item_functions import cast_lightning, heal
+from item_functions import cast_fireball, cast_lightning, heal
from map_objects.rectangle import Rect
...
... from entity import Entity from game_messages import Message from item_functions import cast_fireball, cast_lightning, heal from map_objects.rectangle import Rect ...
One change we need to make for cast_fireball
to work: We need a
distance
function in Entity
, to get the distance between the entity
and an arbitrary point.
def move_towards(self, target_x, target_y, game_map, entities):
...
+ def distance(self, x, y):
+ return math.sqrt((x - self.x) ** 2 + (y - self.y) ** 2)
def distance_to(self, other):
...
def move_towards(self, target_x, target_y, game_map, entities):
...
def distance(self, x, y):
return math.sqrt((x - self.x) ** 2 + (y - self.y) ** 2)
def distance_to(self, other):
...
Run the project now, and you should have a functioning fireball spell! Be careful though, the player can get damaged by this spell if you cast it too close to yourself!
Let’s add one more spell for fun: confusion. This will involve modifying the target’s AI for a few turns, and setting it back to normal once the spell ends.
We’ll begin by adding the confused AI, to ai.py
:
import tcod as libtcod
+from random import randint
+
+from game_messages import Message
class BasicMonster:
...
+class ConfusedMonster:
+ def __init__(self, previous_ai, number_of_turns=10):
+ self.previous_ai = previous_ai
+ self.number_of_turns = number_of_turns
+
+ def take_turn(self, target, fov_map, game_map, entities):
+ results = []
+
+ if self.number_of_turns > 0:
+ random_x = self.owner.x + randint(0, 2) - 1
+ random_y = self.owner.y + randint(0, 2) - 1
+
+ if random_x != self.owner.x and random_y != self.owner.y:
+ self.owner.move_towards(random_x, random_y, game_map, entities)
+
+ self.number_of_turns -= 1
+ else:
+ self.owner.ai = self.previous_ai
+ results.append({'message': Message('The {0} is no longer confused!'.format(self.owner.name), libtcod.red)})
+
+ return results
import tcod as libtcod from random import randint from game_messages import Message class BasicMonster: ... class ConfusedMonster: def __init__(self, previous_ai, number_of_turns=10): self.previous_ai = previous_ai self.number_of_turns = number_of_turns def take_turn(self, target, fov_map, game_map, entities): results = [] if self.number_of_turns > 0: random_x = self.owner.x + randint(0, 2) - 1 random_y = self.owner.y + randint(0, 2) - 1 if random_x != self.owner.x and random_y != self.owner.y: self.owner.move_towards(random_x, random_y, game_map, entities) self.number_of_turns -= 1 else: self.owner.ai = self.previous_ai results.append({'message': Message('The {0} is no longer confused!'.format(self.owner.name), libtcod.red)}) return results
The class gets initialized with a number of turns that the entity is
confused for. It also keeps track of what the entity’s actual AI is, so
that it can be switched back when the confusion wears off. For the
take_turn
method, the entity moves randomly (or not at all), and one
turn gets taken off the timer. Once the timer hits 0, the entity is no
longer confused, and goes back to its previous AI.
Now for the confusion spell. Add the following to item_functions.py
def cast_fireball(*args, **kwargs):
...
+def cast_confuse(*args, **kwargs):
+ entities = kwargs.get('entities')
+ fov_map = kwargs.get('fov_map')
+ target_x = kwargs.get('target_x')
+ target_y = kwargs.get('target_y')
+
+ results = []
+
+ if not libtcod.map_is_in_fov(fov_map, target_x, target_y):
+ results.append({'consumed': False, 'message': Message('You cannot target a tile outside your field of view.', libtcod.yellow)})
+ return results
+
+ for entity in entities:
+ if entity.x == target_x and entity.y == target_y and entity.ai:
+ confused_ai = ConfusedMonster(entity.ai, 10)
+
+ confused_ai.owner = entity
+ entity.ai = confused_ai
+
+ results.append({'consumed': True, 'message': Message('The eyes of the {0} look vacant, as he starts to stumble around!'.format(entity.name), libtcod.light_green)})
+
+ break
+ else:
+ results.append({'consumed': False, 'message': Message('There is no targetable enemy at that location.', libtcod.yellow)})
+
+ return results
+
def cast_fireball(*args, **kwargs):
...
def cast_confuse(*args, **kwargs):
entities = kwargs.get('entities')
fov_map = kwargs.get('fov_map')
target_x = kwargs.get('target_x')
target_y = kwargs.get('target_y')
results = []
if not libtcod.map_is_in_fov(fov_map, target_x, target_y):
results.append({'consumed': False, 'message': Message('You cannot target a tile outside your field of view.', libtcod.yellow)})
return results
for entity in entities:
if entity.x == target_x and entity.y == target_y and entity.ai:
confused_ai = ConfusedMonster(entity.ai, 10)
confused_ai.owner = entity
entity.ai = confused_ai
results.append({'consumed': True, 'message': Message('The eyes of the {0} look vacant, as he starts to stumble around!'.format(entity.name), libtcod.light_green)})
break
else:
results.append({'consumed': False, 'message': Message('There is no targetable enemy at that location.', libtcod.yellow)})
return results
You’ll need to import the ConfusedMonster
class to the top of the
file:
import tcod as libtcod
+from components.ai import ConfusedMonster
from game_messages import Message
...
import tcod as libtcod
from components.ai import ConfusedMonster
from game_messages import Message
...
Finally, we’ll put the scroll on the map. First, import the
cast_confuse
function:
...
from game_messages import Message
-from item_functions import cast_fireball, cast_lightning, heal
+from item_functions import cast_confuse, cast_fireball, cast_lightning, heal
from map_objects.rectangle import Rect
...
...
from game_messages import Message
from item_functions import cast_confuse, cast_fireball, cast_lightning, heal
from map_objects.rectangle import Rect
...
We’ll also modify the chances of our scrolls, so that each one has a 10% chance of spawning.
if item_chance < 70:
item_component = Item(use_function=heal, amount=4)
item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM,
item=item_component)
- elif item_chance < 85:
+ elif item_chance < 80:
item_component = Item(use_function=cast_fireball, targeting=True, targeting_message=Message(
'Left-click a target tile for the fireball, or right-click to cancel.', libtcod.light_cyan),
damage=12, radius=3)
item = Entity(x, y, '#', libtcod.red, 'Fireball Scroll', render_order=RenderOrder.ITEM,
item=item_component)
+ elif item_chance < 90:
+ item_component = Item(use_function=cast_confuse, targeting=True, targeting_message=Message(
+ 'Left-click an enemy to confuse it, or right-click to cancel.', libtcod.light_cyan))
+ item = Entity(x, y, '#', libtcod.light_pink, 'Confusion Scroll', render_order=RenderOrder.ITEM,
+ item=item_component)
if item_chance < 70: item_component = Item(use_function=heal, amount=4) item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM, item=item_component) elif item_chance < 85: elif item_chance < 80: item_component = Item(use_function=cast_fireball, targeting=True, targeting_message=Message( 'Left-click a target tile for the fireball, or right-click to cancel.', libtcod.light_cyan), damage=12, radius=3) item = Entity(x, y, '#', libtcod.red, 'Fireball Scroll', render_order=RenderOrder.ITEM, item=item_component) elif item_chance < 90: item_component = Item(use_function=cast_confuse, targeting=True, targeting_message=Message( 'Left-click an enemy to confuse it, or right-click to cancel.', libtcod.light_cyan)) item = Entity(x, y, '#', libtcod.light_pink, 'Confusion Scroll', render_order=RenderOrder.ITEM, item=item_component)
Run the project, and you should be able to cast confusion on enemies. Enemies who are confused will waste their turns either moving randomly, or staying in one spot.
That’s all for today. We now have 3 different types of scrolls the player can utilize against enemies. Feel free to try adding more scrolls and spells as you see fit.
If you want to see the code so far in its entirety, click here.