forked from assamidanov/python_programming_class
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprojectiles.py
More file actions
309 lines (261 loc) · 10 KB
/
projectiles.py
File metadata and controls
309 lines (261 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
from abstract import Drawable, Killable, Moveable
from color import Color
from artist import Artist
import random
from math import cos, sin
from pygame import Surface
class ProjectileMaster:
"""
Implements methods for cannon-wide projectile checking
Introduces methods for creating random projectiles and maintaining existing
projectiles (drawing them and moving them)
Attributes
----------
projectile_list : list[Target]
A list of all the projectiles created by this ProjectileMaster
projectile_types : list
A list of the possible types of projectiles
"""
def __init__(self) -> None:
"""Initializes the empty projectile list"""
self.projectile_list: list[Projectile] = []
# The possible projectile types and their chosen_type denotion
self.projectile_types = {
'c': CircleProjectile,
's': SquareProjectile,
't': TriangleProjectile
}
def create_projectile(
self,
x: int,
y: int,
vel: int,
angle: int,
chosen_type: str = None) -> None:
"""
Creates a projectile based on the cannon's parameters
If a type is not provided, a random projectile will be fired.
Adds the projectile to the projectile list, does not return it
Parameters
----------
x : int
The x position of the projectile.
y : int
The y position of the projectile.
vel : int
The velocity of the projectile (determined by the cannon's power)
angle : int
The angle of the cannon (affects the v_x and v_y distribution)
chosen_type : str
A string of characters 's', 't', or 'c' denoting whether the object
is a square, triangle, or circle. If it is not provided, it will be
random
"""
# The params to create the projectile with
params: dict = {
'x': x,
'y': y,
'size': 20,
'v_x': int(vel * cos(angle)),
'v_y': int(vel * sin(angle))
}
# A projectile of the chosen type, or a random projectile
if chosen_type:
chosen_type = self.projectile_types[chosen_type]
else:
chosen_type = random.choice(list(self.projectile_types.values()))
# Create and store the projectile
created_projectile = chosen_type(**params)
self.projectile_list.append(created_projectile)
def draw_all(self, surface: Surface) -> None:
"""
Simply loops through all the projectiles and draws them to the surface
Simply calls the projectiles.draw function on each target
Parameters
----------
surface : pygame.Surface
The surface to draw the projectiles to
"""
[projectile.draw(surface) for projectile in self.projectile_list]
def move_all(self, screen_size: tuple) -> None:
"""
Simply loops through all the projectiles and moves them based on their
velocity
Simply calls the projectile.move function on each projectile
Parameters
----------
screen_size : tuple
The size of the screen
"""
[
projectile.move(screen_size, grav = 2)
for projectile in self.projectile_list
]
def remove_dead(self) -> None:
"""Removes dead projectiles from the projectile list"""
for projectile in self.projectile_list:
if not projectile.is_alive:
self.projectile_list.remove(projectile)
class Projectile(Drawable, Killable, Moveable):
"""A class representing a projectile
A projectile (which can be one of a square, circle, or triangle) can hit
a target of the same shape, or the cannon.
A Projectile is Drawable, Killable, and Moveable, meaning it needs
all the attributes for all of those abstract classes.
Attributes
----------
x : int
The x coordinate of the object
y : int
The y coordinate of the object
v_x : int
The object's velocity in the x direction
v_y : int
The object's velocity in the y direction
color : tuple
A tuple representing the (R, G, B) values of the object's color
size : int
An int representing the size of the object
For a square, it will be the length of a side
For a circle, it will be its radius
For a triangle, it will be the length of a side
health : int
An int denoting the object's health. A health value of 1 means the object
is killed after a single hit.
shape : str
A string of characters 's', 't', or 'c' denoting whether the object is a
square, triangle, or circle.
"""
def __init__(
self,
x: int,
y: int,
v_x: int,
v_y: int,
color: int = None,
size: int = 5,
health: int = 1,
shape: int = 'c') -> None:
"""
Intiailizes the necessary values for a Moveable, Killable, Drawable,
object using those classes' init functions
While the default value for color, size, health, and shape
looks to be None, the function defaults those to a random color, 5, 1,
and 'c' for circle. This is due to behavior in Python with passing attributes
through super functions.
"""
color = color or Color.rand_color()
# Parent class initialization
Drawable.__init__(self, x, y, color=color, size=size)
Killable.__init__(self, health=health)
Moveable.__init__(self, v_x, v_y)
# Shape initialization
self.shape = shape
def move(
self,
screen_size: tuple,
time: int = 1, grav: int = 0) -> None:
"""
Moves the projectile based on its velocity and the effect of gravity
Parameters
----------
screen_size : tuple
The size of the screen
time : int
The time step multiplier for the velocity (default 1)
gravity : int
The force of gravity (default 0)
"""
# Add gravity
self.v_y += grav
# Change position based on velocity
self.x += time * self.v_x
self.y += time * self.v_y
# Check screen collisions to make sure we don't go off-screen
self.check_corners(screen_size)
# If the projectile is moving slowly at the bottom of the screen
# it has lost its health
if self.v_x**2 + self.v_y**2 < 2**2:
if self.y > screen_size[1] - 2 * self.size:
self.kill()
def draw(self, surface: Surface) -> None:
"""
Uses a static Artist draw function to draw the object to the given
surface
Parameters
----------
surface : pygame.Surface
A surface object to draw the Drawable onto
"""
Artist.draw(
surface,
self.x, self.y,
self.color, self.size, self.shape)
def check_corners(
self,
screen_size: tuple,
refl_ort: float = 0.6, refl_par: float = 0.7) -> None:
"""
Implements inelastic rebound when the projectile hits the screen's edge
Parameters
----------
screen_size : tuple
The size of the screen
refl_ort : float
The coefficient of restitution orthogonal to the surface (default 0.9)
refl_par : float
The coefficient of restitution parallel to the surface (default 0.8)
"""
# If the projectile hits the left edge of the screen
if self.x < self.size:
# Make sure we don't go off-screen
self.x = self.size
# Reverse the x velocity and calculate the new velocities (decreased)
# based on the reflection parameters
self.v_x = -int(self.v_x * refl_ort)
self.v_y = int(self.v_y * refl_par)
# If the projectile hits the right edge of the scrteen
elif self.x > screen_size[0] - self.size:
# Make sure we don't go off-screen
self.x = screen_size[0] - self.size
# Reverse the x velocity and calculate the new velocities (decreased)
# based on the reflection parameters
self.v_x = -int(self.v_x * refl_ort)
self.v_y = int(self.v_y * refl_par)
# If the projectile hits the top of the screen
if self.y < self.size:
# Make sure we don't go off-screen
self.y = self.size
# Reverse the y velocity and calculate the new velocities (decreased)
# based on the reflection parameters
self.v_x = int(self.v_x * refl_par)
self.v_y = -int(self.v_y * refl_ort)
# If the projectile hits the bottom of the screen
elif self.y > screen_size[1] - self.size:
# Make sure we don't go off-screen
self.y = screen_size[1] - self.size
# Reverse the y velocity and calculate the new velocities (decreased)
# based on the reflection parameters
self.v_x = int(self.v_x * refl_par)
self.v_y = -int(self.v_y * refl_ort)
class CircleProjectile(Projectile):
"""A Projectile of shape Circle. Refer to `Projectile`"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(
*args,
**kwargs,
shape = 'c')
class SquareProjectile(Projectile):
"""A Projectile of shape Square. Refer to `Projectile`"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(
*args,
**kwargs,
shape = 's')
class TriangleProjectile(Projectile):
"""A Projectile of shape Triangle. Refer to `Projectile`"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(
*args,
**kwargs,
shape = 't')