Skip to content

Commit 54b3b49

Browse files
committed
added shop keeper to the game
1 parent 3a1da28 commit 54b3b49

File tree

11 files changed

+405
-26
lines changed

11 files changed

+405
-26
lines changed

actions.py

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

55
import color
6+
from entity import Actor
67
import exceptions
78
import random
89
if TYPE_CHECKING:
910
from engine import Engine
10-
from entity import Actor, Entity, Item
11+
from entity import Actor, Entity, Item, NPC
1112

1213

1314
class Action:
@@ -50,6 +51,11 @@ def dest_xy(self) -> Tuple[int, int]:
5051
def target_actor(self) -> Optional[Actor]:
5152
"""Return the actor at this actions destination."""
5253
return self.engine.game_map.get_actor_at_location(*self.dest_xy)
54+
55+
@property
56+
def target_NPC(self) -> Optional[NPC]:
57+
"""Return the NPC at this actions destination."""
58+
return self.engine.game_map.get_NPC_at_location(*self.dest_xy)
5359

5460
@property
5561
def blocking_entity(self) -> Optional[Entity]:
@@ -131,11 +137,24 @@ def perform(self) -> None:
131137
class BumpAction(ActionWithDirection):
132138
def perform(self) -> None:
133139
if self.target_actor:
134-
return MeleeAction(self.entity, self.dx, self.dy).perform()
135-
140+
return MeleeAction(self.entity, self.dx, self.dy).perform()
141+
elif self.target_NPC:
142+
return InteractNPCAction(self.entity, self.dx, self.dy).perform()
136143
else:
137144
return MovementAction(self.entity, self.dx, self.dy).perform()
138145

146+
class InteractNPCAction(ActionWithDirection):
147+
def __init__(self, entity: Actor, dx: int, dy: int):
148+
super().__init__(entity, dx, dy)
149+
150+
def perform(self) -> None:
151+
if self.target_NPC:
152+
input_handler = self.target_NPC.interact()
153+
if input_handler:
154+
self.engine.future_event_handler = input_handler
155+
else:
156+
raise exceptions.Impossible("There's nothing to interact with.")
157+
139158
class WaitAction(Action):
140159
def perform(self) -> None:
141160
pass

components/ai.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ def perform(self) -> None:
161161
return WaitAction(self.entity).perform() # If the spawner is not able to find a valid location, it will wait.
162162

163163
# Spawn the new enemy.
164-
self.spawned_entity.spawn(self.engine.game_map, x, y)
164+
self.spawned_entity.spawn(self.engine.game_map, x, y, True) # Make sure to set the spawned entity to be a swarm entity.
165+
165166
# Add a message to the message log.
166167
self.engine.message_log.add_message(
167168
f"The {self.entity.name} spawned a new {self.spawned_entity.name}!"

components/fighter.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import random
34
from typing import TYPE_CHECKING
45

56
import color
@@ -99,4 +100,12 @@ def die(self) -> None:
99100

100101
self.engine.message_log.add_message(death_message, death_message_color)
101102

102-
self.engine.player.level.add_xp(self.parent.level.xp_given)
103+
if not self.parent.is_swarm: # If this is a swarm, then it will not give XP or Credits when it dies.
104+
self.engine.player.level.add_xp(self.parent.level.xp_given)
105+
106+
# Have a chance to find credits when the enemy dies.
107+
roll = random.randint(1, 100)
108+
if roll <= 45:
109+
amount = self.max_hp // 4 # Give 25% of the entity's max HP as credits (rounded down).
110+
self.engine.game_world.credits += amount
111+
self.engine.message_log.add_message(f"You found {amount} credits!", color.health_recovered)

components/inventory.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from typing import List, TYPE_CHECKING
44

5+
import color
56
from components.base_component import BaseComponent
67

78
if TYPE_CHECKING:
@@ -22,4 +23,23 @@ def drop(self, item: Item) -> None:
2223
self.items.remove(item)
2324
item.place(self.parent.x, self.parent.y, self.gamemap)
2425

25-
self.engine.message_log.add_message(f"You dropped the {item.name}.")
26+
self.engine.message_log.add_message(f"You dropped the {item.name}.")
27+
28+
def remove(self, item: Item) -> None:
29+
"""
30+
Removes an item from the inventory.
31+
"""
32+
self.items.remove(item)
33+
34+
def add(self, item: Item) -> None:
35+
"""
36+
Adds an item to the inventory.
37+
"""
38+
if len(self.items) >= self.capacity:
39+
self.engine.message_log.add_message(
40+
"You cannot carry any more, your inventory is full!", fg=color.impossible
41+
)
42+
return
43+
44+
item.parent = self
45+
self.items.append(item)

engine.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def __init__(self, player: Actor):
2727
self.message_log = MessageLog()
2828
self.mouse_location = (0, 0)
2929
self.player = player
30+
self.future_event_handler = None # Use this to set the next event handler of the engine once the next player turn is started.
3031

3132
def handle_enemy_turns(self) -> None:
3233
for entity in set(self.game_map.actors) - {self.player}:
@@ -67,6 +68,11 @@ def render(self, console: Console) -> None:
6768
dungeon_level=self.game_world.current_floor,
6869
location=(0, 47),
6970
)
71+
render_functions.render_credit_amount(
72+
console=console,
73+
credit_amount=self.game_world.credits,
74+
location=(0, 48),
75+
)
7076

7177
render_functions.render_names_at_mouse_location(
7278
console=console, x=21, y=44, engine=self

entity.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import math
55
from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING, Union
66

7+
from engine import Engine
78
from render_order import RenderOrder
89

910
if TYPE_CHECKING:
@@ -15,6 +16,8 @@
1516
from components.inventory import Inventory
1617
from components.level import Level
1718
from game_map import GameMap
19+
from input_handlers import EventHandler
20+
1821

1922
T = TypeVar("T", bound="Entity")
2023

@@ -48,6 +51,7 @@ def __init__(
4851
self.blocks_movement = blocks_movement
4952
self.render_order = render_order
5053
self.inspect_message = inspect_message
54+
self.is_swarm = False
5155
if parent:
5256
# If parent isn't provided now then it will be set later.
5357
self.parent = parent
@@ -57,11 +61,12 @@ def __init__(
5761
def gamemap(self) -> GameMap:
5862
return self.parent.gamemap
5963

60-
def spawn(self: T, gamemap: GameMap, x: int, y: int) -> T:
64+
def spawn(self: T, gamemap: GameMap, x: int, y: int, is_swarm: Optional[bool] = False) -> T:
6165
"""Spawn a copy of this instance at the given location."""
6266
clone = copy.deepcopy(self)
6367
clone.x = x
6468
clone.y = y
69+
clone.is_swarm = is_swarm
6570
clone.parent = gamemap
6671
gamemap.entities.add(clone)
6772
return clone
@@ -73,7 +78,7 @@ def distance(self, x: int, y: int) -> float:
7378
return math.sqrt((x - self.x) ** 2 + (y - self.y) ** 2)
7479

7580

76-
def place(self, x: int, y: int, gamemap: Optional[GameMap] = None) -> None:
81+
def place(self, x: int, y: int, gamemap: Optional[GameMap] = None, is_swarm: Optional[bool] = False) -> None:
7782
"""Place this entity at a new location. Handles moving across GameMaps."""
7883
self.x = x
7984
self.y = y
@@ -82,7 +87,9 @@ def place(self, x: int, y: int, gamemap: Optional[GameMap] = None) -> None:
8287
if self.parent is self.gamemap:
8388
self.gamemap.entities.remove(self)
8489
self.parent = gamemap
90+
self.is_swarm = is_swarm # If this is a swarm, then it will not give XP when it dies.
8591
gamemap.entities.add(self)
92+
self.is_swarm = False
8693

8794
def move(self, dx: int, dy: int) -> None:
8895
"""
@@ -142,6 +149,49 @@ def is_alive(self) -> bool:
142149
"""Returns True as long as this actor can perform actions."""
143150
return bool(self.ai)
144151

152+
class NPC(Entity):
153+
"""
154+
A NPC (Non-player character) is used for freindly npcs the player can interact with. Such as merchants, quest givers, etc.
155+
"""
156+
157+
def __init__(
158+
self,
159+
*,
160+
x: int = 0,
161+
y: int = 0,
162+
char: str = "?",
163+
color: Tuple[int, int, int] = (255, 255, 255),
164+
name: str = "<Unnamed>",
165+
inspect_message: Optional[str] = "I don't know what this thing is.",
166+
interact_input_handler_cls: Type[EventHandler]
167+
):
168+
super().__init__(
169+
x=x,
170+
y=y,
171+
char=char,
172+
color=color,
173+
name=name,
174+
blocks_movement=True,
175+
render_order=RenderOrder.ACTOR,
176+
inspect_message=inspect_message,
177+
)
178+
self.is_npc = True
179+
self.input_handler_class = interact_input_handler_cls
180+
181+
def init_handler(self, engine: Engine) -> None:
182+
self.input_handler = self.input_handler_class(engine)
183+
184+
def interact(self) -> EventHandler:
185+
"""
186+
Interact with this NPC.
187+
"""
188+
self.parent.engine.message_log.add_message(f"You interact with the {self.name}.")
189+
190+
return self.input_handler
191+
192+
193+
194+
145195
class Item(Entity):
146196
"""
147197
An item that can be picked up and used, uuses item especific components.
@@ -157,6 +207,7 @@ def __init__(
157207
inspect_message: Optional[str] = "I don't know what this thing is.",
158208
consumable: Optional[Consumable] = None,
159209
equippable: Optional[Equippable] = None,
210+
value: int = 0,
160211
):
161212
super().__init__(
162213
x=x,
@@ -169,6 +220,7 @@ def __init__(
169220
inspect_message=inspect_message,
170221
)
171222

223+
self.value = value
172224
self.consumable = consumable
173225
if self.consumable:
174226
self.consumable.parent = self

entity_factories.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
from components.fighter import Fighter
44
from components.inventory import Inventory
55
from components.level import Level
6-
from entity import Actor, Item
6+
from entity import NPC, Actor, Item
77
from components.equipment import Equipment
8+
from input_handlers import ShopkeepMenuEventHandler
89

910
player = Actor(
1011
char="@",
@@ -81,46 +82,53 @@
8182
name="Healing Gel",
8283
consumable=consumable.HealingConsumable(amount=7),
8384
inspect_message="A Small, green-glowing piece of smile. It has a some-what apple taste.",
85+
value=10,
8486
)
8587
XL_healing_gel = Item(
8688
char="!",
8789
color=(0, 255, 0),
8890
name="XL Healing Gel",
8991
consumable=consumable.HealingConsumable(amount=18),
9092
inspect_message="A Extra Large healing Gel. Don't choke while gobbling it up!",
93+
value=25,
9194
)
9295
taser = Item(
9396
char="~",
9497
color=(255, 255, 0),
9598
name="Taser",
9699
consumable=consumable.EletricDamageConsumable(damage=20, maximum_range=5),
97100
inspect_message="It's a bit old, and it's bolts go to whatever they want, but it's still effective.",
101+
value=30,
98102
)
99103
stun_gun = Item(
100104
char="~",
101105
color=(153, 0, 255),
102106
name="Stun Gun",
103107
consumable=consumable.ConfusionConsumable(number_of_turns=10),
104108
inspect_message="Bright flashes always makes everyone disoriented. I'd get confused if i were to look at it.",
109+
value=10,
105110
)
106111
fireball_gun = Item(
107112
char="~",
108113
color=(255, 0, 0),
109114
name="Fireball Gun",
110115
consumable=consumable.FireballDamageConsumable(damage=12, radius=3),
111116
inspect_message="It's a gun that shoots fireballs. Like the ones from fantasy games. It's pretty effective!",
117+
value=50,
112118
)
113119

114120
pocket_kinfe = Item( char="/", color=(102, 255, 255), name="Pocket kinfe", equippable=equippable.PocketKinfe(),
115-
inspect_message="The most pesonal kinfe you'll ever find. Use it if you're in a pinch."
121+
inspect_message="The most pesonal kinfe you'll ever find. Use it if you're in a pinch.",
122+
value=10,
116123
)
117-
118124
old_kinfe = Item(
119125
char="/", color=(102, 255, 255), name="Old kinfe", equippable=equippable.OldKinfe(),
120-
inspect_message="It's a old rusty kitchen kinfe. It's not very sharp, but it's still effective."
126+
inspect_message="It's a old rusty kitchen kinfe. It's not very sharp, but it's still effective.",
127+
value=50,
121128
)
122129
sharp_kinfe = Item(char="/", color=(102, 255, 255), name="Sharp kinfe", equippable=equippable.SharpKinfe(),
123-
inspect_message="It's a kitchen kinfe that was not let outside at least."
130+
inspect_message="It's a kitchen kinfe that was not let outside at least.",
131+
value=100,
124132
)
125133

126134
scrap_chest_plate = Item(
@@ -129,15 +137,26 @@
129137
name="Scrap chest plate",
130138
equippable=equippable.ScrapChestPlate(),
131139
inspect_message="It's a chest plate made of scrap metal. It's not very strong, but it's still effective.",
140+
value=25,
132141
)
133142
iron_chest_plate = Item(
134143
char="[",
135144
color=(102, 255, 255),
136145
name="Iron chest plate",
137146
equippable=equippable.IronChestPlate(),
138-
inspect_message="It's a chest plate made of iron. Put it on and you'll be able to take a beating.")
139-
147+
inspect_message="It's a chest plate made of iron. Put it on and you'll be able to take a beating.",
148+
value=50,
149+
)
140150
steel_chest_plate = Item(
141151
char="[", color=(102, 255, 255), name="Steel chest plate", equippable=equippable.SteelChestPlate(),
142-
inspect_message="It's a chest plate, now made of steel. You'll be able to take quite the beating!"
152+
inspect_message="It's a chest plate, now made of steel. You'll be able to take quite the beating!",
153+
value=100,
154+
)
155+
156+
shopkeep_npc = NPC(
157+
char="@",
158+
color=(102, 255, 102),
159+
name="Shop Keeper",
160+
interact_input_handler_cls=ShopkeepMenuEventHandler,
161+
inspect_message="It's a shop keeper. He's a friendly humanoid robot. He's selling some items. At least some freind in this world.",
143162
)

0 commit comments

Comments
 (0)