-
Notifications
You must be signed in to change notification settings - Fork 0
/
character.py
223 lines (183 loc) · 9.77 KB
/
character.py
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
import pdb
import math
from enum import Enum
import pygame
from pygame.locals import *
import entity
from entity import Entity
from timerhandler import KnockbackTimer
enemies = pygame.sprite.Group()
#wall_colliders = pygame.sprite.Group()
class Character(Entity):
Direction = Enum("Direction", "UP RIGHT DOWN LEFT");
def __init__(self):
Entity.__init__(self)
self.collide_walls = True
self.walk_speed = 100
self._movement_vectors = []
self._walking = {
Character.Direction.UP : False,
Character.Direction.RIGHT : False,
Character.Direction.DOWN : False,
Character.Direction.LEFT : False
}
self.max_hitpoints = self.current_hitpoints = 10
self.hitbar_health_color = 0, 255, 0
self.hitbar_damage_color = 255, 0, 0
self.sprites = {
Character.Direction.UP : 0,
Character.Direction.RIGHT : 0,
Character.Direction.DOWN : 0,
Character.Direction.LEFT : 0
}
self.facing = Character.Direction.DOWN
#This should typically be called by a subclass's update function.
def makeMove(self, dt):
"""Combines movement and walking vectors into a single vector, checks collisions,
moves the character, and calls the wall collision handler if relevant.
"""
#Remove empty vectors. These are vectors that have timed out/become irrelevant.
for vector in self._movement_vectors:
if len(vector) == 0:
self._movement_vectors.remove(vector)
walking_vector = self.getWalkingVector()
move_x = sum(vector[0] for vector in self._movement_vectors) + walking_vector[0]
move_y = sum(vector[1] for vector in self._movement_vectors) + walking_vector[1]
movement = (move_x, move_y)
x_change = movement[0] * (dt/1000.0)
y_change = movement[1] * (dt/1000.0)
walls = self._getMovementCollisionWalls(x_change, y_change)
x_change, y_change = self._truncateMovementByCollisions(x_change, y_change, walls)
self.rect.x += x_change
self.rect.y += y_change
def _getMovementCollisionWalls(self, x_change, y_change):
"""Returns a list of walls that the character might collide with if it is translated x_change pixels
horizontally and y_change pixels vertically.
"""
def moveCollide(source, wall):
source_move = source.rect.copy()
#First, expand the source rect's width based on the movement distance.
#This will move the right side of the rect out while keeping the left stationary.
source_move.width += abs(x_change)
#Then, if the movement is to the left, move the rect left.
#The result will be a rect that covers both the original bounding box, and all the
#space that the rect will pass over or touch when it moves
source_move.x = min(source.rect.x, source.rect.x + x_change)
#Do the same for vertical movement, of course.
#NOTE: We'll get slop here, won't we? Diagonal movement
#makes a box that covers more than just the movement.
source_move.height += abs(y_change)
source_move.y = min(source.rect.y, source.rect.y + y_change)
#Finally, check if the movement box intersects with the wall and return the result.
return source_move.colliderect(wall) #pygame.sprite.collide_rect(source_move, wall)
return pygame.sprite.spritecollide(self, entity.walls, False, moveCollide)
def _truncateMovementByCollisions(self, x_change, y_change, walls):
"""Returns a x, y tuple representing the portion of x_change and y_change that the character can travel
before colliding with the nearest wall in walls.
"""
collision = None
#Compare the difference between the character and the wall with the distance they're going to move.
#If the latter is greater than the former, set the latter to the former and get the wall that's being collided with.
#Note that "walls" is only walls which the previous function call caught as potential collisions.
for wall in walls:
if wall.rect.top <= self.rect.top < wall.rect.bottom or wall.rect.top < self.rect.bottom <= wall.rect.bottom:
if x_change > 0 and wall.rect.left - self.rect.right <= x_change:
collision = wall
x_change = wall.rect.left - self.rect.right
elif x_change < 0 and wall.rect.right - self.rect.left >= x_change:
collision = wall
x_change = wall.rect.right - self.rect.left
if wall.rect.left <= self.rect.left < wall.rect.right or wall.rect.left < self.rect.right <= wall.rect.right:
if y_change > 0 and wall.rect.top - self.rect.bottom <= y_change:
collision = wall
y_change = wall.rect.top - self.rect.bottom
elif y_change < 0 and wall.rect.bottom - self.rect.top >= y_change:
collision = wall
y_change = wall.rect.bottom - self.rect.top
#TODO: Is it bad that I call the wall collision method before actually moving the character?
if collision:
self.onWallCollision(collision)
return (x_change, y_change)
def addMovementVector(self, x, y):
"""Add a vector to the character's movement that will impact the direction/speed he moves when makeMove() is called"""
self._movement_vectors.append([x, y])
def setWalking(self, direction, is_walking = True):
"""Set the character's walking state for the given direction.
Every direction the character is walking will add a vector to movement in the given direction, at the character's max speed.
"""
self._walking[direction] = is_walking
def haltWalking(self):
"""Stop the character from walking"""
self._walking = self._walking.fromkeys(self._walking, False)
def getWalkingVector(self):
"""Return a vector to represent the character's current attempt to move itself"""
x = 0
y = 0
if self._walking[Character.Direction.UP]:
y -= self.walk_speed
if self._walking[Character.Direction.RIGHT]:
x += self.walk_speed
if self._walking[Character.Direction.DOWN]:
y += self.walk_speed
if self._walking[Character.Direction.LEFT]:
x -= self.walk_speed
return (x, y)
def isWalking(self):
return self._walking[Character.Direction.UP] or self._walking[Character.Direction.RIGHT] or self._walking[Character.Direction.DOWN] or self._walking[Character.Direction.LEFT]
def addKnockbackVector(self, source_position, force):
"""Add a movement vector away from source_position, then set a timer to remove it."""
#Get the direction from source_position to the location of self.
hit_direction = {'x': self.rect.centerx - source_position[0], 'y': self.rect.centery - source_position[1]}
#Normalize
length = math.sqrt(math.pow(hit_direction['x'], 2) + math.pow(hit_direction['y'], 2))
if length == 0:
length = 1
hit_direction['x'] /= length
hit_direction['y'] /= length
#Combine the hit direction with the force to get a knockback movement vector
self.knockback_vector = [hit_direction['x'] * force, hit_direction['y'] * force]
#self.addMovementVector(self.knockback_vector[0], self.knockback_vector[1])
self._movement_vectors.append(self.knockback_vector)
#Set a timer
hittimer = KnockbackTimer(200, self.knockback_vector, self.endWeaponHit)
def endWeaponHit(self, timer):
pass
#By default, charactes will move when updated.
def update(self, dt):
self.makeMove(dt)
def updateImage(self):
'''Set the sprite's image property based on its current state.
This is what will be drawn to the screen for the current frame.
'''
self.image = self.sprites[self.facing].getCurrentFrame()
#Draw the hitpoint bar:
bar_height = self.image.get_height()
pygame.draw.rect(self.image, self.hitbar_health_color, Rect(0,0,3,bar_height))
#Only draw damage if there is damage. (A bar with 0 height still shows as one pixel.)
if self.current_hitpoints < self.max_hitpoints:
damage_bar_height = (bar_height/self.max_hitpoints)*(self.max_hitpoints - self.current_hitpoints)
pygame.draw.rect(self.image, self.hitbar_damage_color, Rect(0,0,3,damage_bar_height))
def setHostile(self, is_hostile = True):
"""Add or remove this character from the group that can collide with the player for damage"""
if is_hostile:
enemies.add(self)
else:
enemies.remove(self)
#def setWallCollider(self, collides_walls = True):
#"""Add or remove this character from the group that can collide with the player for damage"""
#if collides_walls:
#wall_colliders.add(self)
#else:
#wall_colliders.remove(self)
#TODO: Should be abstracted out into an interface?
def onPlayerCollision(self, player):
pass
def onWallCollision(self, wall):
pass
def takeDamage(self, damage_amount):
self.current_hitpoints -= damage_amount
if self.current_hitpoints <= 0:
self.die()
def die(self):
'''Play a death animation and remove this object from the game'''
self.kill()