def __init__(self, start_pos, explosion_sheet, size, damage_amount, damage_type, collision_grid, *groups): super().__init__(*groups) self.collision_grid = collision_grid self.radius = size self.collide_radius = self.radius self.explosion_sheet = explosion_sheet self.explosion_frames = 16 self.explosion_images = [] random_explosion_int = random.randrange(0, 512, 64) for i in range(0, self.explosion_frames): x_start_index = (i * 64) explosion_frame = self.explosion_sheet.subsurface( pygame.Rect(x_start_index + 1, random_explosion_int + 1, 62, 62)) explosion_frame = pygame.transform.scale( explosion_frame, (self.radius * 2, self.radius * 2)) self.explosion_images.append(explosion_frame) self.image = self.explosion_images[0] self.rect = self.explosion_images[0].get_rect() self.rect.center = start_pos self.position = [ float(self.rect.center[0]), float(self.rect.center[1]) ] # handle collision shape setup self.collision_grid = collision_grid handlers_by_type = { GameCollisionType.MONSTER: self.collision_grid.no_handler } self.collision_shape = CollisionCircle(self.position[0], self.position[1], self.collide_radius, handlers_by_type, GameCollisionType.EXPLOSION, [GameCollisionType.MONSTER]) self.collision_shape.set_owner(self) self.collision_grid.add_new_shape_to_grid(self.collision_shape) self.should_die = False self.life_time = 0.45 self.time = self.life_time self.frame_time = self.life_time / self.explosion_frames self.frame = 1 self.damage = Damage(damage_amount, damage_type) self.should_kill_explosion_collision = False self.has_collision = True
def __init__(self, origin_centre_pos, facing_direction, fov=90.0, length=1000.0): self.facing_direction = Vector2(facing_direction[:]) self.origin_centre_position = Vector2(origin_centre_pos[:]) self.field_of_view = math.radians(fov) self.length = length self.length_squared = length ** 2 self.epsilon = 0.075 # to handle floating point inaccuracy at small values (I think) self.arc_epsilon = 1.5 # to handle small gaps between arc points self.halfAuxRayTilt = 8.72664625995e-3 # half degree in radians self.half_aux_ray_tilt_cos = math.cos(self.halfAuxRayTilt) # 0.999961923064 self.half_aux_ray_tilt_sin = math.sin(self.halfAuxRayTilt) # 8.72653549837e-3 self.collision_circle = CollisionCircle(self.origin_centre_position.x, self.origin_centre_position.y, self.length, {CollisionType.WORLD_SOLID: CollisionNoHandler(), CollisionType.WORLD_JUMP_THROUGH: CollisionNoHandler(), CollisionType.WORLD_PLATFORM_EDGE: CollisionNoHandler(), CollisionType.WORLD_JUMP_THROUGH_EDGE: CollisionNoHandler()}, CollisionType.VIEW_CONE, [CollisionType.WORLD_SOLID, CollisionType.WORLD_JUMP_THROUGH, CollisionType.WORLD_PLATFORM_EDGE, CollisionType.WORLD_JUMP_THROUGH_EDGE] ) self.neg_cos_fov = math.cos(-self.field_of_view / 2) self.neg_sin_fov = math.sin(-self.field_of_view / 2) self.sin_fov = math.sin(self.field_of_view / 2) self.cos_fov = math.cos(self.field_of_view / 2) self.perp_facing_cos = math.cos(math.radians(90)) self.perp_facing_sin = math.sin(math.radians(90)) self.angle_points_array = [] self.rays = [] self.hit_points = [] self.ctrl_points = [] self.blocking_edges = [] self.perp_facing_vec = None self.cone_extent_facings = None self.on_cone_changed_direction() self.end_positions = None self.on_cone_moved() self.fov_edges = [Edge("min", Vector2(self.origin_centre_position), Vector2(self.end_positions[0])), Edge("max", Vector2(self.origin_centre_position), Vector2(self.end_positions[1]))] self.timing_clock = pygame.time.Clock()
class PickUp(pygame.sprite.Sprite): def __init__(self, start_pos, image, type_name, collision_grid, *groups): super().__init__(*groups) self.collision_grid = collision_grid self.world_position = [start_pos[0], start_pos[1]] self.type_name = type_name self.image = image self.rect = self.image.get_rect() self.rect.center = start_pos self.position = [ float(self.rect.center[0]), float(self.rect.center[1]) ] self.should_die = False self.collide_radius = 8 self.collision_circle = CollisionCircle( self.position[0], self.position[1], self.collide_radius, {GameCollisionType.PLAYER: CollisionNoHandler()}, GameCollisionType.PICKUP, [GameCollisionType.PLAYER]) self.collision_circle.set_owner(self) self.collision_grid.add_new_shape_to_grid(self.collision_circle) def remove_from_grid(self): self.collision_grid.remove_shape_from_grid(self.collision_circle) def react_to_collision(self): for shape in self.collision_circle.collided_shapes_this_frame: if shape.game_type == GameCollisionType.PLAYER: self.should_die = True player = shape.owner if self.type_name == "health": player.add_health(0.25 * player.max_health) elif self.type_name == "mana": player.add_mana(25) def update(self, tiled_level): self.position[ 0] = self.world_position[0] - tiled_level.position_offset[0] self.position[ 1] = self.world_position[1] - tiled_level.position_offset[1] self.rect.center = self.position self.collision_circle.set_position(self.world_position) if self.should_die: self.collision_grid.remove_shape_from_grid(self.collision_circle) self.kill()
def __init__(self, start_pos, image, type_name, collision_grid, *groups): super().__init__(*groups) self.collision_grid = collision_grid self.world_position = [start_pos[0], start_pos[1]] self.type_name = type_name self.image = image self.rect = self.image.get_rect() self.rect.center = start_pos self.position = [ float(self.rect.center[0]), float(self.rect.center[1]) ] self.should_die = False self.collide_radius = 8 self.collision_circle = CollisionCircle( self.position[0], self.position[1], self.collide_radius, {GameCollisionType.PLAYER: CollisionNoHandler()}, GameCollisionType.PICKUP, [GameCollisionType.PLAYER]) self.collision_circle.set_owner(self) self.collision_grid.add_new_shape_to_grid(self.collision_circle)
class Explosion(pygame.sprite.DirtySprite): def __init__(self, start_pos, explosion_sheet, size, damage_amount, damage_type, collision_grid, *groups): super().__init__(*groups) self.collision_grid = collision_grid self.radius = size self.collide_radius = self.radius self.explosion_sheet = explosion_sheet self.explosion_frames = 16 self.explosion_images = [] random_explosion_int = random.randrange(0, 512, 64) for i in range(0, self.explosion_frames): x_start_index = (i * 64) explosion_frame = self.explosion_sheet.subsurface( pygame.Rect(x_start_index + 1, random_explosion_int + 1, 62, 62)) explosion_frame = pygame.transform.scale( explosion_frame, (self.radius * 2, self.radius * 2)) self.explosion_images.append(explosion_frame) self.image = self.explosion_images[0] self.rect = self.explosion_images[0].get_rect() self.rect.center = start_pos self.position = [ float(self.rect.center[0]), float(self.rect.center[1]) ] # handle collision shape setup self.collision_grid = collision_grid handlers_by_type = { GameCollisionType.MONSTER: self.collision_grid.no_handler } self.collision_shape = CollisionCircle(self.position[0], self.position[1], self.collide_radius, handlers_by_type, GameCollisionType.EXPLOSION, [GameCollisionType.MONSTER]) self.collision_shape.set_owner(self) self.collision_grid.add_new_shape_to_grid(self.collision_shape) self.should_die = False self.life_time = 0.45 self.time = self.life_time self.frame_time = self.life_time / self.explosion_frames self.frame = 1 self.damage = Damage(damage_amount, damage_type) self.should_kill_explosion_collision = False self.has_collision = True def update_sprite(self, all_explosion_sprites, time_delta): self.time -= time_delta if self.time < 0.0: self.should_die = True if self.has_collision: # destroy collision shape if it's not gone already self.collision_grid.remove_shape_from_grid( self.collision_shape) self.has_collision = False if self.frame < self.explosion_frames and ( self.life_time - self.time) > (self.frame_time * self.frame): self.image = self.explosion_images[self.frame] self.frame += 1 all_explosion_sprites.add(self) # for now we only one one frame's worth of collision tests from an explosion if self.should_kill_explosion_collision and self.has_collision: self.collision_grid.remove_shape_from_grid(self.collision_shape) self.has_collision = False else: self.should_kill_explosion_collision = True return all_explosion_sprites def update_movement_and_collision(self): pass def react_to_collision(self): pass
def __init__(self, monster_path, monster_id, loaded_image, point_cost, all_monster_sprites, offset, collision_grid, splat_loader, ui_manager, *groups): super().__init__(*groups) self.id = monster_id self.point_cost = point_cost self.cash_value = 0 self.move_speed = 0.0 self.monster_path = monster_path self.collision_grid = collision_grid self.splat_loader = splat_loader self.ui_manager = ui_manager self.splat_image = None self.original_image = loaded_image self.image = self.original_image.copy() self.rect = self.image.get_rect() self.collide_radius = int(self.rect[2] / 2.0) self.position = [0.0, 0.0] int_position = self.get_random_point_in_radius_of_point( self.monster_path.start_waypoint, self.monster_path.waypoint_radius) self.position[0] = float(int_position[0]) self.position[1] = float(int_position[1]) self.position[0] -= offset[0] self.position[1] -= offset[1] self.collision_grid = collision_grid handlers_by_type = { GameCollisionType.TURRET_PROJECTILE: self.collision_grid.no_handler } self.collision_shape = CollisionCircle( self.position[0], self.position[1], self.collide_radius, handlers_by_type, GameCollisionType.MONSTER, [GameCollisionType.TURRET_PROJECTILE]) self.collision_shape.set_owner(self) self.collision_grid.add_new_shape_to_grid(self.collision_shape) self.drawable_collision_circle = DrawableCollisionCircle( self.collision_shape) self.rect.centerx = self.position[0] self.rect.centery = self.position[1] self.change_direction_time = 5.0 self.change_direction_accumulator = 0.0 self.next_way_point_index = 0 next_way_point_centre = self.monster_path.waypoints[ self.next_way_point_index] self.next_way_point = self.get_random_point_in_radius_of_point( next_way_point_centre, self.monster_path.waypoint_radius) # apply screen position offset self.next_way_point[0] -= offset[0] self.next_way_point[1] -= offset[1] x_dist = float(self.next_way_point[0]) - float(self.position[0]) y_dist = float(self.next_way_point[1]) - float(self.position[1]) self.distance_to_next_way_point = math.sqrt((x_dist * x_dist) + (y_dist * y_dist)) self.current_vector = [ x_dist / self.distance_to_next_way_point, y_dist / self.distance_to_next_way_point ] direction_magnitude = math.sqrt(self.current_vector[0]**2 + self.current_vector[1]**2) if direction_magnitude > 0.0: unit_dir_vector = [ self.current_vector[0] / direction_magnitude, self.current_vector[1] / direction_magnitude ] self.oldFacingAngle = math.atan2( -unit_dir_vector[0], -unit_dir_vector[1]) * 180 / math.pi monster_centre_position = self.rect.center self.image = pygame.transform.rotate(self.original_image, self.oldFacingAngle) self.rect = self.image.get_rect() self.rect.center = monster_centre_position self.should_die = False self.sprite_needs_update = True self.all_monster_sprites = all_monster_sprites self.all_monster_sprites.add(self) self.health_capacity = 100 self.current_health = self.health_capacity self.slow_down_percentage = 1.0 self.health_bar = UIWorldSpaceHealthBar( pygame.Rect((0, 0), (self.rect.width + 4, 8)), self, self.ui_manager)
class BaseMonster(pygame.sprite.Sprite): def __init__(self, monster_path, monster_id, loaded_image, point_cost, all_monster_sprites, offset, collision_grid, splat_loader, ui_manager, *groups): super().__init__(*groups) self.id = monster_id self.point_cost = point_cost self.cash_value = 0 self.move_speed = 0.0 self.monster_path = monster_path self.collision_grid = collision_grid self.splat_loader = splat_loader self.ui_manager = ui_manager self.splat_image = None self.original_image = loaded_image self.image = self.original_image.copy() self.rect = self.image.get_rect() self.collide_radius = int(self.rect[2] / 2.0) self.position = [0.0, 0.0] int_position = self.get_random_point_in_radius_of_point( self.monster_path.start_waypoint, self.monster_path.waypoint_radius) self.position[0] = float(int_position[0]) self.position[1] = float(int_position[1]) self.position[0] -= offset[0] self.position[1] -= offset[1] self.collision_grid = collision_grid handlers_by_type = { GameCollisionType.TURRET_PROJECTILE: self.collision_grid.no_handler } self.collision_shape = CollisionCircle( self.position[0], self.position[1], self.collide_radius, handlers_by_type, GameCollisionType.MONSTER, [GameCollisionType.TURRET_PROJECTILE]) self.collision_shape.set_owner(self) self.collision_grid.add_new_shape_to_grid(self.collision_shape) self.drawable_collision_circle = DrawableCollisionCircle( self.collision_shape) self.rect.centerx = self.position[0] self.rect.centery = self.position[1] self.change_direction_time = 5.0 self.change_direction_accumulator = 0.0 self.next_way_point_index = 0 next_way_point_centre = self.monster_path.waypoints[ self.next_way_point_index] self.next_way_point = self.get_random_point_in_radius_of_point( next_way_point_centre, self.monster_path.waypoint_radius) # apply screen position offset self.next_way_point[0] -= offset[0] self.next_way_point[1] -= offset[1] x_dist = float(self.next_way_point[0]) - float(self.position[0]) y_dist = float(self.next_way_point[1]) - float(self.position[1]) self.distance_to_next_way_point = math.sqrt((x_dist * x_dist) + (y_dist * y_dist)) self.current_vector = [ x_dist / self.distance_to_next_way_point, y_dist / self.distance_to_next_way_point ] direction_magnitude = math.sqrt(self.current_vector[0]**2 + self.current_vector[1]**2) if direction_magnitude > 0.0: unit_dir_vector = [ self.current_vector[0] / direction_magnitude, self.current_vector[1] / direction_magnitude ] self.oldFacingAngle = math.atan2( -unit_dir_vector[0], -unit_dir_vector[1]) * 180 / math.pi monster_centre_position = self.rect.center self.image = pygame.transform.rotate(self.original_image, self.oldFacingAngle) self.rect = self.image.get_rect() self.rect.center = monster_centre_position self.should_die = False self.sprite_needs_update = True self.all_monster_sprites = all_monster_sprites self.all_monster_sprites.add(self) self.health_capacity = 100 self.current_health = self.health_capacity self.slow_down_percentage = 1.0 self.health_bar = UIWorldSpaceHealthBar( pygame.Rect((0, 0), (self.rect.width + 4, 8)), self, self.ui_manager) def set_starting_health(self, value): self.health_capacity = value self.current_health = self.health_capacity def kill(self): self.health_bar.kill() super().kill() def setup_splat(self, colour): self.splat_image = self.splat_loader.create_random_coloured_splat( colour) def draw_collision_circle(self, screen): self.drawable_collision_circle.update_collided_colours() self.drawable_collision_circle.draw(screen) def update_sprite(self): if self.sprite_needs_update: self.sprite_needs_update = False def react_to_collision(self): for shape in self.collision_shape.collided_shapes_this_frame: if shape.game_type == GameCollisionType.EXPLOSION: explosion = shape.owner self.take_damage(explosion.damage) def update_movement_and_collision(self, time_delta, player_resources, offset, splat_sprites): if self.current_health <= 0: self.should_die = True player_resources.current_cash += self.cash_value if self.distance_to_next_way_point <= 0.0: if self.next_way_point_index < (len(self.monster_path.waypoints) - 1): self.next_way_point_index += 1 next_way_point_centre = self.monster_path.waypoints[ self.next_way_point_index] self.next_way_point = self.get_random_point_in_radius_of_point( next_way_point_centre, self.monster_path.waypoint_radius) # apply screen position offset self.next_way_point[0] -= offset[0] self.next_way_point[1] -= offset[1] x_dist = float(self.next_way_point[0]) - float( self.position[0]) y_dist = float(self.next_way_point[1]) - float( self.position[1]) self.distance_to_next_way_point = math.sqrt((x_dist * x_dist) + (y_dist * y_dist)) self.current_vector = [ x_dist / self.distance_to_next_way_point, y_dist / self.distance_to_next_way_point ] # calc facing angle direction_magnitude = math.sqrt(self.current_vector[0]**2 + self.current_vector[1]**2) if direction_magnitude > 0.0: unit_dir_vector = [ self.current_vector[0] / direction_magnitude, self.current_vector[1] / direction_magnitude ] facing_angle = math.atan2( -unit_dir_vector[0], -unit_dir_vector[1]) * 180 / math.pi if facing_angle != self.oldFacingAngle: self.sprite_needs_update = True self.oldFacingAngle = facing_angle monster_centre_position = self.rect.center self.image = pygame.transform.rotate( self.original_image, facing_angle) self.rect = self.image.get_rect() self.rect.center = monster_centre_position else: # monster has reached base player_resources.current_base_health -= 10 self.should_die = True # move self.position[0] += (self.current_vector[0] * time_delta * self.move_speed * self.slow_down_percentage) self.position[1] += (self.current_vector[1] * time_delta * self.move_speed * self.slow_down_percentage) self.distance_to_next_way_point -= time_delta * self.move_speed * self.slow_down_percentage # reset any slowdown from turrets self.slow_down_percentage = 1.0 # set sprite & collision shape positions self.rect.center = self.position self.collision_shape.set_position(self.position) if self.should_die: self.collision_grid.remove_shape_from_grid(self.collision_shape) self.all_monster_sprites.remove(self) self.kill() if self.splat_image is not None: Splat(self.position, self.splat_image, splat_sprites) @staticmethod def get_random_point_in_radius_of_point(point, radius): t = 2 * math.pi * random.random() u = random.random() + random.random() if u > 1: r = 2 - u else: r = u return [ point[0] + radius * r * math.cos(t), point[1] + radius * r * math.sin(t) ] def guess_position_at_time(self, time): guess_position = [0.0, 0.0] # make sure we don't overshoot monster waypoints with our guesses, or turrets # will aim at impossible positions for monsters inside walls. if (time * self.move_speed * self.slow_down_percentage) > self.distance_to_next_way_point: guess_position[0] = self.next_way_point[0] guess_position[1] = self.next_way_point[1] else: x_move = self.current_vector[ 0] * time * self.move_speed * self.slow_down_percentage y_move = self.current_vector[ 1] * time * self.move_speed * self.slow_down_percentage guess_position[0] = self.position[0] + x_move guess_position[1] = self.position[1] + y_move return guess_position def set_average_speed(self, average_speed): self.move_speed = random.randint(int(average_speed * 0.75), int(average_speed * 1.25)) return self.move_speed def take_damage(self, damage): self.current_health -= damage.amount def set_slowdown(self, percentage): self.slow_down_percentage = percentage
def __init__(self, monster_id, start_pos, sprite_map, all_monster_sprites, play_area, tiled_level, collision_grid, *groups): super().__init__(*groups) self.id = monster_id self.start_pos = start_pos self.play_area = play_area self.tiled_level = tiled_level self.score = 100 self.xp = 25 self.collision_grid = collision_grid self.point_cost = 10 # point cost if self.id == "goblin": self.sprite_map_row = 0 self.walk_cycle_length = 4 elif self.id == "ent": self.sprite_map_row = 1 self.walk_cycle_length = 8 elif self.id == "spider": self.sprite_map_row = 2 self.walk_cycle_length = 7 self.anim_stand = sprite_map[0][self.sprite_map_row] self.walk_anim_speed = 64.0 / self.walk_cycle_length self.walk_cycle = [] for anim_index in range(0, self.walk_cycle_length): self.walk_cycle.append(sprite_map[anim_index][self.sprite_map_row]) self.attack_time_delay = 0.5 self.sprite_rot_centre_offset = [0.0, 0.0] self.image = self.anim_stand.copy() # self.sprite = pygame.sprite.Sprite() self.test_collision_sprite = pygame.sprite.Sprite() self.rect = self.image.get_rect() self.rect.center = self.start_pos self.position = [float(self.rect.center[0]), float(self.rect.center[1])] self.screen_position = [0, 0] self.screen_position[0] = self.position[0] self.screen_position[1] = self.position[1] self.collide_radius = 20 # we do collisions in world space self.collision_circle = CollisionCircle(self.position[0], self.position[1], self.collide_radius, {GameCollisionType.PLAYER_WEAPON: CollisionNoHandler(), GameCollisionType.TILE: CollisionRubHandler(), GameCollisionType.PLAYER: CollisionRubHandler(), GameCollisionType.MONSTER: CollisionRubHandler()}, GameCollisionType.MONSTER, [GameCollisionType.PLAYER_WEAPON, GameCollisionType.TILE, GameCollisionType.PLAYER, GameCollisionType.MONSTER]) self.collision_circle.set_owner(self) self.collision_grid.add_new_shape_to_grid(self.collision_circle) self.drawable_circle = DrawableCollisionCircle(self.collision_circle) self.update_screen_position(self.tiled_level.position_offset) self.change_direction_time = 5.0 self.change_direction_accumulator = 0.0 self.next_way_point = self.get_random_point_in_radius_of_point([500, 400], 96) x_dist = float(self.next_way_point[0]) - float(self.position[0]) y_dist = float(self.next_way_point[1]) - float(self.position[1]) self.distance_to_next_way_point = math.sqrt((x_dist * x_dist) + (y_dist * y_dist)) self.current_vector = [x_dist / self.distance_to_next_way_point, y_dist / self.distance_to_next_way_point] self.old_facing_angle = 0.0 self.rotate_sprite(self) self.should_die = False self.is_dead = False self.sprite_needs_update = True self.all_monster_sprites = all_monster_sprites self.is_on_screen = False self.health = 100 self.slow_down_percentage = 1.0 self.is_wandering_aimlessly = True self.random_target_change_time = random.uniform(3.0, 15.0) self.random_target_change_acc = 0.0 self.time_to_home_in_on_player = False self.monster_home_on_target_time = random.uniform(0.3, 1.5) self.monster_home_on_target_acc = 0.0 self.is_time_to_start_attack = True self.attack_time_acc = 0.0 self.attack_time_delay = 3.0 self.is_attacking = False self.should_do_attack_damage = False self.attack_anim_acc = 0.0 self.attack_anim_total_time = 0.8 self.attack_damage = 15 self.sprite_flash_acc = 0.0 self.sprite_flash_time = 0.15 self.should_flash_sprite = False self.active_flash_sprite = False self.flash_sprite = pygame.sprite.Sprite() self.player_distance = 1000 self.air_timer = 0.0 self.air_velocity_vector = [0.0, 0.0] self.attack_range = 86.0 self.collision_obj_rects = [] self.collision_obj_rect = pygame.Rect(0.0, 0.0, 2.0, 2.0) self.max_coll_handling_attempts = 10 self.move_accumulator = 0.0 self.move_speed = 0.0 self.idle_move_speed = 0.0 self.attack_move_speed = 0.0
class BaseMonster(pygame.sprite.Sprite): def __init__(self, monster_id, start_pos, sprite_map, all_monster_sprites, play_area, tiled_level, collision_grid, *groups): super().__init__(*groups) self.id = monster_id self.start_pos = start_pos self.play_area = play_area self.tiled_level = tiled_level self.score = 100 self.xp = 25 self.collision_grid = collision_grid self.point_cost = 10 # point cost if self.id == "goblin": self.sprite_map_row = 0 self.walk_cycle_length = 4 elif self.id == "ent": self.sprite_map_row = 1 self.walk_cycle_length = 8 elif self.id == "spider": self.sprite_map_row = 2 self.walk_cycle_length = 7 self.anim_stand = sprite_map[0][self.sprite_map_row] self.walk_anim_speed = 64.0 / self.walk_cycle_length self.walk_cycle = [] for anim_index in range(0, self.walk_cycle_length): self.walk_cycle.append(sprite_map[anim_index][self.sprite_map_row]) self.attack_time_delay = 0.5 self.sprite_rot_centre_offset = [0.0, 0.0] self.image = self.anim_stand.copy() # self.sprite = pygame.sprite.Sprite() self.test_collision_sprite = pygame.sprite.Sprite() self.rect = self.image.get_rect() self.rect.center = self.start_pos self.position = [float(self.rect.center[0]), float(self.rect.center[1])] self.screen_position = [0, 0] self.screen_position[0] = self.position[0] self.screen_position[1] = self.position[1] self.collide_radius = 20 # we do collisions in world space self.collision_circle = CollisionCircle(self.position[0], self.position[1], self.collide_radius, {GameCollisionType.PLAYER_WEAPON: CollisionNoHandler(), GameCollisionType.TILE: CollisionRubHandler(), GameCollisionType.PLAYER: CollisionRubHandler(), GameCollisionType.MONSTER: CollisionRubHandler()}, GameCollisionType.MONSTER, [GameCollisionType.PLAYER_WEAPON, GameCollisionType.TILE, GameCollisionType.PLAYER, GameCollisionType.MONSTER]) self.collision_circle.set_owner(self) self.collision_grid.add_new_shape_to_grid(self.collision_circle) self.drawable_circle = DrawableCollisionCircle(self.collision_circle) self.update_screen_position(self.tiled_level.position_offset) self.change_direction_time = 5.0 self.change_direction_accumulator = 0.0 self.next_way_point = self.get_random_point_in_radius_of_point([500, 400], 96) x_dist = float(self.next_way_point[0]) - float(self.position[0]) y_dist = float(self.next_way_point[1]) - float(self.position[1]) self.distance_to_next_way_point = math.sqrt((x_dist * x_dist) + (y_dist * y_dist)) self.current_vector = [x_dist / self.distance_to_next_way_point, y_dist / self.distance_to_next_way_point] self.old_facing_angle = 0.0 self.rotate_sprite(self) self.should_die = False self.is_dead = False self.sprite_needs_update = True self.all_monster_sprites = all_monster_sprites self.is_on_screen = False self.health = 100 self.slow_down_percentage = 1.0 self.is_wandering_aimlessly = True self.random_target_change_time = random.uniform(3.0, 15.0) self.random_target_change_acc = 0.0 self.time_to_home_in_on_player = False self.monster_home_on_target_time = random.uniform(0.3, 1.5) self.monster_home_on_target_acc = 0.0 self.is_time_to_start_attack = True self.attack_time_acc = 0.0 self.attack_time_delay = 3.0 self.is_attacking = False self.should_do_attack_damage = False self.attack_anim_acc = 0.0 self.attack_anim_total_time = 0.8 self.attack_damage = 15 self.sprite_flash_acc = 0.0 self.sprite_flash_time = 0.15 self.should_flash_sprite = False self.active_flash_sprite = False self.flash_sprite = pygame.sprite.Sprite() self.player_distance = 1000 self.air_timer = 0.0 self.air_velocity_vector = [0.0, 0.0] self.attack_range = 86.0 self.collision_obj_rects = [] self.collision_obj_rect = pygame.Rect(0.0, 0.0, 2.0, 2.0) self.max_coll_handling_attempts = 10 self.move_accumulator = 0.0 self.move_speed = 0.0 self.idle_move_speed = 0.0 self.attack_move_speed = 0.0 def draw_collision_shape(self, screen, camera_position, camera_half_dimensions): self.drawable_circle.update_collided_colours() self.drawable_circle.draw(screen, camera_position, camera_half_dimensions) def react_to_collision(self): for shape in self.collision_circle.collided_shapes_this_frame: if shape.game_type == GameCollisionType.TILE or shape.game_type == GameCollisionType.PLAYER \ or shape.game_type == GameCollisionType.MONSTER: self.position[0] = self.collision_circle.x self.position[1] = self.collision_circle.y def fling_monster(self, vector, time): self.air_timer = time self.air_velocity_vector = vector def update_sprite(self, time_delta): if self.sprite_needs_update: self.sprite_needs_update = False self.image = self.image if self.should_flash_sprite and not self.should_die and not self.is_dead: self.sprite_flash_acc += time_delta if self.sprite_flash_acc > self.sprite_flash_time: self.sprite_flash_acc = 0.0 self.should_flash_sprite = False self.active_flash_sprite = False self.all_monster_sprites.remove(self.flash_sprite) else: lerp_value = self.sprite_flash_acc / self.sprite_flash_time flash_alpha = self.lerp(255, 0, lerp_value) flash_image = self.image.copy() flash_image.fill((0, 0, 0, flash_alpha), None, pygame.BLEND_RGBA_MULT) flash_image.fill((255, 255, 255, 0), None, pygame.BLEND_RGBA_ADD) self.flash_sprite.image = flash_image self.flash_sprite.rect = self.flash_sprite.image.get_rect() self.flash_sprite.rect.center = self.screen_position if not self.active_flash_sprite: self.all_monster_sprites.add(self.flash_sprite) self.active_flash_sprite = True def attack(self, time_delta, player): self.attack_anim_acc += time_delta if self.attack_anim_acc > self.attack_anim_total_time: self.attack_anim_acc = 0.0 self.is_attacking = False elif self.attack_anim_acc > (self.attack_anim_total_time * 0.75): pass # finish attack frame elif self.attack_anim_acc > (self.attack_anim_total_time * 0.5): if self.should_do_attack_damage: self.should_do_attack_damage = False player.take_damage(self.attack_damage) elif self.attack_anim_acc > (self.attack_anim_total_time * 0.25): pass # start attack frame def update_movement_and_collision(self, time_delta, player, pick_up_spawner): if player is not None: player_x_dist = float(player.position[0]) - float(self.position[0]) player_y_dist = float(player.position[1]) - float(self.position[1]) self.player_distance = math.sqrt((player_x_dist ** 2) + (player_y_dist ** 2)) if self.health <= 0: self.should_die = True if self.air_timer > 0.0: self.distance_to_next_way_point = 0.0 self.air_timer -= time_delta if self.air_timer < 0.0: self.air_timer = 0.0 self.position[0] += (self.air_velocity_vector[0] * time_delta) self.position[1] += (self.air_velocity_vector[1] * time_delta) elif self.is_wandering_aimlessly and player is not None and not player.should_die: self.move_speed = self.idle_move_speed if self.player_distance < 256.0: self.is_wandering_aimlessly = False elif self.random_target_change_acc < self.random_target_change_time: self.random_target_change_acc += time_delta else: self.random_target_change_acc = 0.0 self.random_target_change_time = random.uniform(3.0, 15.0) self.next_way_point = self.get_random_point_in_world() x_dist = float(self.next_way_point[0]) - float(self.position[0]) y_dist = float(self.next_way_point[1]) - float(self.position[1]) self.distance_to_next_way_point = math.sqrt((x_dist * x_dist) + (y_dist * y_dist)) self.current_vector = [x_dist / self.distance_to_next_way_point, y_dist / self.distance_to_next_way_point] self.rotate_sprite(self) elif not self.is_wandering_aimlessly and player is not None and not player.should_die: self.move_speed = self.attack_move_speed if self.monster_home_on_target_acc < self.monster_home_on_target_time: self.monster_home_on_target_acc += time_delta else: self.monster_home_on_target_acc = 0.0 self.monster_home_on_target_time = random.uniform(0.3, 1.5) self.time_to_home_in_on_player = True if self.time_to_home_in_on_player: self.time_to_home_in_on_player = False if self.player_distance > 384.0: self.is_wandering_aimlessly = True else: x_dist = float(player.position[0]) - float(self.position[0]) y_dist = float(player.position[1]) - float(self.position[1]) self.distance_to_next_way_point = (math.sqrt((x_dist * x_dist) + (y_dist * y_dist))) self.current_vector = [x_dist / self.distance_to_next_way_point, y_dist / self.distance_to_next_way_point] # calculate a position in attack range minus the rough size # of the sprites radius so we are going from edge to edge self.distance_to_next_way_point -= (self.attack_range - self.collide_radius - player.collide_radius) self.rotate_sprite(self) if self.attack_time_acc < self.attack_time_delay: self.attack_time_acc += time_delta else: self.attack_time_acc = 0.0 self.is_time_to_start_attack = True if self.player_distance <= self.attack_range and self.is_time_to_start_attack: self.is_time_to_start_attack = False self.is_attacking = True self.should_do_attack_damage = True if self.air_timer == 0.0 and self.is_attacking: self.attack(time_delta, player) if self.air_timer == 0.0 and self.distance_to_next_way_point > 0.0: self.position[0] += (self.current_vector[0] * time_delta * self.move_speed) self.position[1] += (self.current_vector[1] * time_delta * self.move_speed) self.distance_to_next_way_point -= time_delta * self.move_speed self.move_accumulator += time_delta * self.move_speed # move if self.on_screen(): if not self.is_on_screen: self.is_on_screen = True self.all_monster_sprites.add(self) self.update_screen_position(self.tiled_level.position_offset) # if walking about walk_cycle_index = int(self.move_accumulator / self.walk_anim_speed) if walk_cycle_index >= len(self.walk_cycle): self.move_accumulator = 0.0 walk_cycle_index = 0 self.image = pygame.transform.rotate(self.walk_cycle[walk_cycle_index], self.old_facing_angle) self.rect = self.image.get_rect() self.rect.center = self.rot_point([self.screen_position[0], self.screen_position[1] + self.sprite_rot_centre_offset[1]], self.screen_position, -self.old_facing_angle) else: if self.is_on_screen: self.is_on_screen = False self.all_monster_sprites.remove(self) if self.active_flash_sprite: self.all_monster_sprites.remove(self.flash_sprite) self.active_flash_sprite = False self.collision_circle.set_position(self.position) if self.should_die: self.should_die = False self.is_dead = True if self.active_flash_sprite: self.all_monster_sprites.remove(self.flash_sprite) self.active_flash_sprite = False self.all_monster_sprites.remove(self) self.try_pick_up_spawn(pick_up_spawner) player.add_score(self.score) player.add_xp(self.xp) self.collision_grid.remove_shape_from_grid(self.collision_circle) def remove_from_grid(self): self.collision_grid.remove_shape_from_grid(self.collision_circle) def on_screen(self): if self.position[0] + self.rect[2] / 2 > self.tiled_level.position_offset[0] and self.position[0] - \ self.rect[2] / 2 < self.tiled_level.position_offset[0] + self.play_area[0]: if self.position[1] + self.rect[3] / 2 > self.tiled_level.position_offset[1] and self.position[1] - \ self.rect[3] / 2 < self.tiled_level.position_offset[1] + self.play_area[1]: return True return False def update_screen_position(self, world_offset): self.screen_position[0] = self.position[0] - world_offset[0] self.screen_position[1] = self.position[1] - world_offset[1] self.collision_circle.x = self.screen_position[0] self.collision_circle.y = self.screen_position[1] @staticmethod def get_random_point_in_radius_of_point(point, radius): t = 2 * math.pi * random.random() u = random.random() + random.random() if u > 1: r = 2 - u else: r = u return [point[0] + radius * r * math.cos(t), point[1] + radius * r * math.sin(t)] def set_average_speed(self, average_speed): self.move_speed = random.randint(int(average_speed * 0.75), int(average_speed * 1.25)) return self.move_speed def take_damage(self, damage): self.health -= damage.amount self.should_flash_sprite = True def get_random_point_in_world(self): random_x = random.randint(32, self.tiled_level.level_pixel_size[0] - 32) random_y = random.randint(32, self.tiled_level.level_pixel_size[1] - 32) return [random_x, random_y] def rotate_sprite(self, sprite_to_rot): direction_magnitude = math.sqrt( self.current_vector[0] * self.current_vector[0] + self.current_vector[1] * self.current_vector[1]) if direction_magnitude > 0.0: unit_dir_vector = [self.current_vector[0] / direction_magnitude, self.current_vector[1] / direction_magnitude] self.old_facing_angle = math.atan2(-unit_dir_vector[0], -unit_dir_vector[1]) * 180 / math.pi return sprite_to_rot def try_pick_up_spawn(self, pickup_spawner): pickup_spawner.try_spawn(self.position) @staticmethod def rot_point(point, axis, ang): """ Orbit. calculates the new loc for a point that rotates a given num of degrees around an axis point, +clockwise, -anticlockwise -> tuple x,y """ ang -= 90 x, y = point[0] - axis[0], point[1] - axis[1] radius = math.sqrt(x * x + y * y) # get the distance between points r_ang = math.radians(ang) # convert ang to radians. h = axis[0] + (radius * math.cos(r_ang)) v = axis[1] + (radius * math.sin(r_ang)) return [h, v] @staticmethod def lerp(a, b, c): return (c * b) + ((1.0 - c) * a) @staticmethod def get_distance(from_x, from_y, to_x, to_y): dx = from_x - to_x dy = from_y - to_y return math.sqrt((dx ** 2) + (dy ** 2))
class ViewCone: class PointInConeStatus: FRONT_SEMICIRCLE = 0, # point within sector - containing semicircle but outside sector BEHIND = 1, # point behind sector OUTSIDE = 2, # point outside bounding circle WITHIN = 4 class LineSegConfig: DISJOINT = 0, PARALLEL = 1, INTERSECT = 2 def __init__(self, origin_centre_pos, facing_direction, fov=90.0, length=1000.0): self.facing_direction = Vector2(facing_direction[:]) self.origin_centre_position = Vector2(origin_centre_pos[:]) self.field_of_view = math.radians(fov) self.length = length self.length_squared = length ** 2 self.epsilon = 0.075 # to handle floating point inaccuracy at small values (I think) self.arc_epsilon = 1.5 # to handle small gaps between arc points self.halfAuxRayTilt = 8.72664625995e-3 # half degree in radians self.half_aux_ray_tilt_cos = math.cos(self.halfAuxRayTilt) # 0.999961923064 self.half_aux_ray_tilt_sin = math.sin(self.halfAuxRayTilt) # 8.72653549837e-3 self.collision_circle = CollisionCircle(self.origin_centre_position.x, self.origin_centre_position.y, self.length, {CollisionType.WORLD_SOLID: CollisionNoHandler(), CollisionType.WORLD_JUMP_THROUGH: CollisionNoHandler(), CollisionType.WORLD_PLATFORM_EDGE: CollisionNoHandler(), CollisionType.WORLD_JUMP_THROUGH_EDGE: CollisionNoHandler()}, CollisionType.VIEW_CONE, [CollisionType.WORLD_SOLID, CollisionType.WORLD_JUMP_THROUGH, CollisionType.WORLD_PLATFORM_EDGE, CollisionType.WORLD_JUMP_THROUGH_EDGE] ) self.neg_cos_fov = math.cos(-self.field_of_view / 2) self.neg_sin_fov = math.sin(-self.field_of_view / 2) self.sin_fov = math.sin(self.field_of_view / 2) self.cos_fov = math.cos(self.field_of_view / 2) self.perp_facing_cos = math.cos(math.radians(90)) self.perp_facing_sin = math.sin(math.radians(90)) self.angle_points_array = [] self.rays = [] self.hit_points = [] self.ctrl_points = [] self.blocking_edges = [] self.perp_facing_vec = None self.cone_extent_facings = None self.on_cone_changed_direction() self.end_positions = None self.on_cone_moved() self.fov_edges = [Edge("min", Vector2(self.origin_centre_position), Vector2(self.end_positions[0])), Edge("max", Vector2(self.origin_centre_position), Vector2(self.end_positions[1]))] self.timing_clock = pygame.time.Clock() def on_cone_changed_direction(self): self.cone_extent_facings = [Vector2(self.facing_direction.x * self.cos_fov - self.facing_direction.y * self.sin_fov, self.facing_direction.x * self.sin_fov + self.facing_direction.y * self.cos_fov), Vector2(self.facing_direction.x * self.neg_cos_fov - self.facing_direction.y * self.neg_sin_fov, self.facing_direction.x * self.neg_sin_fov + self.facing_direction.y * self.neg_cos_fov) ] self.perp_facing_vec = Vector2( self.facing_direction.x * self.perp_facing_cos - self.facing_direction.y * self.perp_facing_sin, self.facing_direction.x * self.perp_facing_sin + self.facing_direction.y * self.perp_facing_cos) def on_cone_moved(self): self.end_positions = [[self.origin_centre_position.x + self.cone_extent_facings[0].x * self.length, self.origin_centre_position.y + self.cone_extent_facings[0].y * self.length], [self.origin_centre_position.x + self.cone_extent_facings[1].x * self.length, self.origin_centre_position.y + self.cone_extent_facings[1].y * self.length] ] self.fov_edges = [Edge("min", Vector2(self.origin_centre_position), Vector2(self.end_positions[0])), Edge("max", Vector2(self.origin_centre_position), Vector2(self.end_positions[1]))] def set_facing_direction(self, direction): if direction[0] != self.facing_direction.x or direction.y != self.facing_direction.y: self.facing_direction.x = direction.x self.facing_direction.y = direction.y self.on_cone_changed_direction() self.on_cone_moved() def set_position(self, position): if position[0] != self.origin_centre_position.x or position.y != self.origin_centre_position.y: self.origin_centre_position.x = position.x self.origin_centre_position.y = position.y self.on_cone_moved() self.collision_circle.set_position((self.origin_centre_position.x, self.origin_centre_position.y)) def is_point_in_sector(self, point_to_test): """ Tests if a point is inside our cone and, if not roughly where it is in relation to our circle for future processing. :param point_to_test: :return: status class variable """ vector_to_point = point_to_test - self.origin_centre_position dot = self.facing_direction.dot(vector_to_point) if dot < 0: return ViewCone.PointInConeStatus.BEHIND if (vector_to_point.length_squared() - self.length_squared) > self.epsilon: return ViewCone.PointInConeStatus.OUTSIDE self.cone_extent_facings[0].cross(vector_to_point) cross_1 = self.cone_extent_facings[0].cross(vector_to_point) # self.cone_extent_facings[0][0] * min_vector_to_point[1] - self.cone_extent_facings[0][1] * min_vector_to_point[0] cross_2 = self.cone_extent_facings[1].cross(vector_to_point) # self.cone_extent_facings[1][0] * max_vector_to_point[1] - self.cone_extent_facings[1][1] * max_vector_to_point[0] if cross_1 < 0 < cross_2 or cross_1 > 0 > cross_2: return ViewCone.PointInConeStatus.WITHIN return ViewCone.PointInConeStatus.FRONT_SEMICIRCLE def draw(self, surface, camera=None): if camera is not None: view_top_left_position = (camera.position[0] - (camera.dimensions[0] / 2), camera.position[1] - (camera.dimensions[1] / 2)) else: view_top_left_position = [0.0, 0.0] arc_rect = pygame.Rect((0, 0), (self.length * 2, self.length * 2)) arc_rect.center = [self.origin_centre_position[0] - view_top_left_position[0], self.origin_centre_position[1] - view_top_left_position[1]] arc_angle = math.atan2(-self.facing_direction[1], self.facing_direction[0]) #pygame.draw.arc(surface, pygame.Color("#FFFFFF"), arc_rect, arc_angle, arc_angle + (self.field_of_view / 2)) #pygame.draw.arc(surface, pygame.Color("#FFFFFF"), arc_rect, arc_angle - (self.field_of_view / 2), arc_angle) view_top_left_vec = pygame.math.Vector2(view_top_left_position) #pygame.draw.line(surface, pygame.Color("#FFFFFF"), self.origin_centre_position - view_top_left_vec, # self.origin_centre_position + (self.facing_direction * self.length) - view_top_left_vec) #for ray in self.rays: # pygame.draw.line(surface, pygame.Color("#999999"), self.origin_centre_position - view_top_left_vec, self.origin_centre_position + ray - view_top_left_vec) for j in range(0, len(self.hit_points)): hit_point = self.hit_points[j] pygame.draw.circle(surface, pygame.Color("#FF7700"), [int(hit_point.x - view_top_left_position[0]), int(hit_point.y - view_top_left_position[1])], 5) # draw los cone shape if len(self.hit_points) > 0: pygame.draw.line(surface, pygame.Color("#FFFF00"), self.origin_centre_position - view_top_left_vec, self.hit_points[0] - view_top_left_vec) for i in range(0, len(self.hit_points) - 1): if self.ctrl_points[i+1] is None: pygame.draw.line(surface, pygame.Color("#FFFF00"), self.hit_points[i] - view_top_left_vec, self.hit_points[i+1] - view_top_left_vec) else: start_angle_vec = self.hit_points[i] - self.origin_centre_position finish_angle_vec = self.hit_points[i+1] - self.origin_centre_position start_arc_angle = math.atan2(-start_angle_vec[1], start_angle_vec[0]) finish_arc_angle = math.atan2(-finish_angle_vec[1], finish_angle_vec[0]) x_diff = abs(start_angle_vec.x - finish_angle_vec.x) y_diff = abs(start_angle_vec.y - finish_angle_vec.y) if not (x_diff <= self.arc_epsilon and y_diff <= self.arc_epsilon): pygame.draw.arc(surface, pygame.Color("#FFFF00"), arc_rect, start_arc_angle, finish_arc_angle) # else: # pygame.draw.arc(surface, pygame.Color("#FFFF00"), arc_rect, finish_arc_angle, start_arc_angle) if len(self.hit_points) > 0: pygame.draw.line(surface, pygame.Color("#FFFF00"), self.hit_points[len(self.hit_points)-1] - view_top_left_vec, self.origin_centre_position - view_top_left_vec) # end draw los cone shape def edge_within_radius(self, edge): return self.closest_point_on_edge(edge).distance_squared_to(self.origin_centre_position) <= self.length_squared def is_active_facing_edge(self, polygon, edge): edge_normal = polygon.normals[edge.name] if edge_normal.should_skip: return False return True def check_polygon(self, polygon, angle_points, blocking_edges): edges_list = [edge for edge in polygon.edges.values() if self.edge_within_radius(edge) and self.is_active_facing_edge(polygon, edge)] n = len(edges_list) if n > 0: prev_edge = edges_list[n - 1] # TODO: this might be wrong? else: prev_edge = None for i in range(0, len(edges_list)): edge = edges_list[i] point_a_status = self.is_point_in_sector(edge.a) point_b_status = self.is_point_in_sector(edge.b) if point_a_status == ViewCone.PointInConeStatus.BEHIND and point_b_status == ViewCone.PointInConeStatus.BEHIND: continue if point_a_status == ViewCone.PointInConeStatus.WITHIN and point_b_status == ViewCone.PointInConeStatus.WITHIN: self.add_angle_point_with_aux(edge.a, prev_edge, edge, angle_points) # for the last edge, send undefined as nextEdge to # addAnglePointWithAux; it should never get used since # both endpoints of the last edge would be handled by now # due to edges 0 and n − 2 if i < len(edges_list) - 1: self.add_angle_point_with_aux(edge.b, edge, edges_list[i + 1], angle_points) else: self.add_angle_point_with_aux(edge.b, edge, None, angle_points) blocking_edges.append(edge) else: """ ANGLE POINTS Either one or both the points are outside the sector; add the one which is inside. Perform edge – arc intersection test, if this edge has a possibility of intersecting the arc, add resultant intersection point(s) to angle_points. BLOCKING EDGE If one of the points is inside, then the edge is blocking, add it without any checks. If one or both are out, and the edge cuts the sector's arc then too the edge is blocking, add it to blocking_edges. If both are out and edge doesn't cut the arc, check if it cuts one of the sector's edges and add to blocking_edges if it does. """ blocking = False if point_a_status == ViewCone.PointInConeStatus.WITHIN: self.add_angle_point_with_aux(edge.a, prev_edge, edge, angle_points) blocking = True if point_b_status == ViewCone.PointInConeStatus.WITHIN: if i < len(edges_list) - 1: self.add_angle_point_with_aux(edge.b, edge, edges_list[i+1], angle_points) else: self.add_angle_point_with_aux(edge.b, edge, None, angle_points) blocking = True edge_may_intersect_arc = point_a_status == ViewCone.PointInConeStatus.OUTSIDE or point_b_status == ViewCone.PointInConeStatus.OUTSIDE test_seg_seg_xsect = True if edge_may_intersect_arc: # perform line segment – sector arc intersection test to # check if there're more angle points i.e. if the edge # intersects the sector's arc then the intersection points # would also become angle points. arc_xsect_result = self.line_seg_arc_x_sect(edge) if arc_xsect_result is not None: if arc_xsect_result['config'] == ViewCone.PointInConeStatus.WITHIN: # just add intersection point to Set without any # auxiliarys as it's an intersection angle point for point in arc_xsect_result['points']: angle_points.add(point) blocking = True # edge – edge intersection test is not needed when the # intersection point(s) are within or behind; the # within case is ignored since it's already blocking # and hence won't reach the lineSegLineSegXsect code test_seg_seg_xsect = arc_xsect_result['config'] != ViewCone.PointInConeStatus.BEHIND # If there was an angle point added due to this edge, then it # is blocking; add and continue to avoid further processing. if blocking: blocking_edges.append(edge) elif test_seg_seg_xsect and \ any([fov_edge for fov_edge in self.fov_edges if self.edges_intersect(fov_edge, edge)]): blocking_edges.append(edge) """ If any angle point(s) would occur because of this edge, they would have been found by now and the edge would have been tagged as a blocking one. Even if no angle points were found due to this edge it still may be a blocking, or not. Perform a couple of segment – segment intersection tests with the sector's edges to check if the edge is indeed blocking. This is worth the expenditure incurred; say we have 10 angle points, for every redundant, non-blocking edge added without such a check means we waste time in performing 10 futile line segment intersection tests. Prune them early on by performing the tests beforehand. Perform segment – segment testing if testSegSegXsect is true; this will be so if the arc intersection was never performed (say when both points are in FrontSemicircle and their edge occluding vision) or if the intersection points aren't behind the sector; there can be cases where not both points are behind (if so they'd have gotten pruned by now), but the intersection points are behind, prune them. """ prev_edge = edge def is_zero(self, vec): return (abs(vec.x) + abs(vec.y)) <= self.epsilon def are_parallel(self, vec_a, vec_b): return abs(vec_a.cross(vec_b)) <= self.epsilon def is_point_on_line(self, pt_vec, line): v = pt_vec - line.a return self.are_parallel(v, line.vec) @staticmethod def rotate_dir(direction, cos_a, sin_a): """ rotates dir based on cosA and sinA both counter-clockwise and clockwise doing it manually instead of using mat2d as these can be reused for rotation in both directions, avoiding their recalculation""" x_c = direction.x * cos_a y_c = direction.y * cos_a x_s = direction.x * sin_a y_s = direction.y * sin_a return [Vector2(x_c - y_s, x_s + y_c), Vector2(x_c + y_s, -x_s + y_c)] @staticmethod def point_on_line(line, t): return line.a + (line.b - line.a) * t @staticmethod def perp2d(v, clockwise=False): if clockwise: return Vector2(v.y, -v.x) return Vector2(-v.y, v.x) @staticmethod def inv_lerp(line, point): t = point - line.a return t.dot(line.vec) / line.length_squared def edges_intersect(self, edge_a, edge_b): """ Simple wrapper function to turn complicated intersection of line segment test into a boolean True or False result. :param edge_a: first edge to test :param edge_b: second edge to test :return: True or False if the two segments intersect or not """ return ViewCone.LineSegConfig.INTERSECT == self.line_seg_line_seg_x_sect(edge_a, edge_b)['config'] def line_seg_line_seg_x_sect(self, line_1, line_2, should_compute_point=False, is_line_1_ray=False): """ converted from here: https://github.com/legends2k/2d-fov/blob/gh-pages/index.html originally from §16.16.1, Real-Time Rendering, 3rd Edition with Antonio's optimisation. Code gets around eh? The intent of this function is to determine if two line segments intersect, it also provides additional information (on request) giving the point of intersection (if any) and if the non-intersecting lines are parallel or entirely disjoint. :param line_1: The first line segment to test :param line_2: The second line segment to test :param should_compute_point: Should we compute the point of intersection or not :param is_line_1_ray: Is line 1 a 'ray' which I believe in this context is asking if it is a view cone ray :return: """ result = {'config': ViewCone.LineSegConfig.DISJOINT, 't': None, 'point': None} line_1_vec = line_1.b - line_1.a line_2_vec = line_2.b - line_2.a l1p = self.perp2d(line_1_vec) f = line_2_vec.dot(l1p) if abs(f) <= self.epsilon: result['config'] = ViewCone.LineSegConfig.PARALLEL # if line1 is a ray and an intersection point is needed, then filter # cases where line and ray are parallel, but the line isn't part of # ray e.g. ---> ____ should be filterd, but ---> ----- should not be. if is_line_1_ray and should_compute_point and self.is_point_on_line(line_2.a, line_1): # find the ray origin position with regards to the line segment alpha = self.inv_lerp(line_2, line_1.a) if 0 <= alpha <= 1: result['t'] = 0 result['point'] = Vector2(line_1.a) elif alpha < 0: result['point'] = Vector2(line_2.a) result['t'] = self.inv_lerp(line_1, result['point']) else: c = line_1.a - line_2.a e = c.dot(l1p) # t = e ÷ f, but computing t isn't necessary, just checking the values # of e and f we deduce if t ∈ [0, 1], if not the division and further # calculations are avoided: Antonio's optimisation. # f should never be zero here which means they're parallel if (f > 0 and 0 <= e <= f) or (f < 0 and 0 >= e >= f): l2p = self.perp2d(line_2_vec) d = c.dot(l2p) # if line 1 is a ray, checks relevant to restricting s to 1 # isn't needed, just check if it wouldn't become < 0 if (is_line_1_ray and ((f > 0 and d >= 0) or (f < 0 and d <= 0))) or ( (f > 0 and 0 <= d <= f) or (f < 0 and 0 >= d >= f)): result['config'] = ViewCone.LineSegConfig.INTERSECT if should_compute_point: s = d / f result['t'] = s result['point'] = self.point_on_line(line_1, s) return result def line_seg_arc_x_sect(self, line): """ Checks for intersection between a line and the 'arc' of a view cone/sector. Should be a fairly rarely used test as blocking edges have to be in a very specific place to intersect the cone at this point. :param line: the line to test against the view cone's sector for intersection :return: None if no intersection at all, or {'config': ViewCone.PointInConeStatus.BEHIND} if both points of line are behind the sector/cone. If there is an intersection returns a dictionary like so - {'config': ViewCone.PointInConeStatus.WITHIN, 'points': points} """ delta = line.a - self.origin_centre_position b = line.vec.dot(delta) d_2 = line.length_squared c = delta.length_squared() - self.length_squared det = (b * b) - (d_2 * c) if det > 0: det_sqrt = math.sqrt(det) if b >= 0: t = b + det_sqrt t1 = -t / d_2 t2 = -c / t else: t = det_sqrt - b t1 = c / t t2 = t / d_2 p1 = None p2 = None points = [] p1_in_sector = None p2_in_sector = None if 0 <= t1 <= 1: p1 = self.point_on_line(line, t1) p1_in_sector = self.is_point_in_sector(p1) if p1_in_sector == ViewCone.PointInConeStatus.WITHIN: points.append(p1) if 0 <= t2 <= 1: p2 = self.point_on_line(line, t2) p2_in_sector = self.is_point_in_sector(p2) if p2_in_sector == ViewCone.PointInConeStatus.WITHIN: points.append(p2) # line segment is contained within circle; it may be cutting # the sector, but not the arc, so return false, as there're # no angle points if p1 is None and p2 is None: return None # both intersection points are behind, the edge has no way of cutting # the sector if p1_in_sector == ViewCone.PointInConeStatus.BEHIND and p2_in_sector == ViewCone.PointInConeStatus.BEHIND: return {'config': ViewCone.PointInConeStatus.BEHIND} if len(points) > 0: return {'config': ViewCone.PointInConeStatus.WITHIN, 'points': points} return None def add_angle_point_with_aux(self, point, prev_edge, next_edge, angle_points): """ /* * Auxiliary rays (primary ray rotated both counter-clockwise and clockwise by * an iota angle). These are needed for cases where a primary ray would get hit * an edge's vertex and get past it to hit things behind it too. * * ALLOW PENETRATION DISALLOW PENETRATION * * ----------X \ polygon / * polygon / \ <- ray X---------X * / \ \ <- ray * \ * References: * 1: http://ncase.me/sight-and-light * 2: http://www.redblobgames.com/articles/visibility */ :param angle_points: :param next_edge: :param prev_edge: :param point: the point to add :return: """ current_size = len(angle_points.points) point_index = angle_points.add(point) # Add aux points only if the addition of the primary point was successful. # When a corner vertex of a polygon is added twice for edges A and B, # although the primary point would not be added since constructEdges would # have used the same vec2 object to make the end points of both edges, # this isn't case for the auxiliary points created in this function afresh # on each call. This check avoids redundant auxiliary point addition. if current_size != len(angle_points.points): ray = point - self.origin_centre_position auxiliaries = self.rotate_dir(ray, self.half_aux_ray_tilt_cos, self.half_aux_ray_tilt_sin) auxiliaries[0] += self.origin_centre_position auxiliaries[1] += self.origin_centre_position proj_axis = self.perp2d(ray) if (next_edge is None) or (next_edge == prev_edge): # line_vec should originate from the added endpoint going to the # other end; if added point is second in edge, flip edge's vector if point == prev_edge.a: line_vec = prev_edge.b - prev_edge.a else: prev_edge_vec = prev_edge.b - prev_edge.a line_vec = Vector2(-prev_edge_vec.x, -prev_edge_vec.y) p = line_vec.dot(proj_axis) # if line_vec is in −ve halfspace of proj_axis, add the auxiliary # ray that would be in the +ve halfspace (i.e. the auxiliary ray # due to rotating ray counter-clockwise by iota) and vice-versa if p <= 0: angle_points.add_aux(point_index, auxiliaries[0]) # use if instead of else if to deal with the case where ray and # edge are parallel, in which case both auxiliary rays are needed if p >= 0: angle_points.add_aux(point_index, auxiliaries[1]) else: # refer to vision_beyond.html workout to understand in which # situation vision can extend beyond corners and auxiliary rays # are needed (we'll take that on trust since I'm not including that file) prev_edge_vec = prev_edge.b - prev_edge.a next_edge_vec = next_edge.b - next_edge.a p1 = prev_edge_vec.dot(proj_axis) p2 = next_edge_vec.dot(proj_axis) if (p1 >= 0) and (p2 <= 0): angle_points.add_aux(point_index, auxiliaries[0]) elif (p1 <= 0) and (p2 >= 0): angle_points.add_aux(point_index, auxiliaries[1]) def make_rays(self, angle_points): ray = angle_points[0] - self.origin_centre_position rays = [ray] # i for angle_points, j for rays to avoid doing len(angle_points) - 1 j = 0 for i in range(1, len(angle_points)): ray = angle_points[i] - self.origin_centre_position if not self.are_parallel(ray, rays[j]): rays.append(ray) j += 1 return rays def angular_points_sorter(self, a, b): a_v = a[0] - self.origin_centre_position b_v = b[0] - self.origin_centre_position # sort expects a negative value when a should come before b; since # cross2d gives a negative value when the rotation from a to b is # counter-clockwise we use it as-is; see comment in isPointInSector return a_v.cross(b_v) def aux_angular_points_sorter(self, a, b): a_v = a - self.origin_centre_position b_v = b - self.origin_centre_position # sort expects a negative value when a should come before b; since # cross2d gives a negative value when the rotation from a to b is # counter-clockwise we use it as-is; see comment in isPointInSector return a_v.cross(b_v) def sort_angular_points(self, angle_points, blocking_edges): # need to determine if two co-planar edges share a point so we can strip it points_to_remove = [] for edge in blocking_edges: for other_edge in blocking_edges: if edge != other_edge: if 1.0 - abs(edge.vec.dot(other_edge.vec)) <= self.epsilon: # edges are co_planar, do they share a point? shared_point = None if abs(edge.a.x - other_edge.a.x) <= self.epsilon and abs(edge.a.y - other_edge.a.y) <= self.epsilon: shared_point = edge.a elif abs(edge.a.x - other_edge.b.x) <= self.epsilon and abs(edge.a.y - other_edge.b.y) <= self.epsilon: shared_point = edge.a elif abs(edge.b.x - other_edge.a.x) <= self.epsilon and abs(edge.b.y - other_edge.a.y) <= self.epsilon: shared_point = edge.b elif abs(edge.b.x - other_edge.b.x) <= self.epsilon and abs(edge.b.y - other_edge.b.y) <= self.epsilon: shared_point = edge.b if shared_point is not None: points_to_remove.append(shared_point) angle_points.points = [angle_points.points[i] for i in range(0, len(angle_points.points)) if angle_points.points[i][0] not in points_to_remove] angle_points.points.sort(key=functools.cmp_to_key(self.angular_points_sorter)) final_angle_points = [] for point_group in angle_points.points: point_group.sort(key=functools.cmp_to_key(self.aux_angular_points_sorter)) for point in point_group: final_angle_points.append(point) return final_angle_points @staticmethod def calc_quad_bez_curve_ctrl_point(v1, v2, centre, radius): ctrl_point = v1 + v2 ctrl_point.normalize_ip() scale = radius * (2 - v1.dot(ctrl_point)) ctrl_point = centre + (ctrl_point * scale) return ctrl_point def shoot_rays(self, rays, blocking_edges): line_1_is_ray = True should_compute_point = True n = len(rays) hit_points = [None for _ in range(0, n)] ctrl_points = [None for _ in range(0, n)] # rays is an array of vectors only, however the intersection functions # work on edges i.e. it also needs the end points and square length; hence # this_ray would act as the ray with additional edge data prev_point_on_arc = False prev_unit_ray = Vector2() for i in range(0, n): # set edge data on this_ray specific to the ray currently shot this_ray = Edge("ray", self.origin_centre_position, self.origin_centre_position + rays[i]) hit_point = Vector2() t = None blocker = None hit_dist_2 = None for j in range(0, len(blocking_edges)): res = self.line_seg_line_seg_x_sect(this_ray, blocking_edges[j], should_compute_point, line_1_is_ray) # both parallel and intersecting cases are valid for inspection; # both have the parameter and point defined if (res['t'] is not None) and ((t is None) or (res['t'] < t)): # This is needed when the observer is exactly at a polygon's # vertex, from where both worlds (outside and inside the # polygon/building) are visible as the observer is standing at # a pillar point where two walls meet. In such case, all rays # emanating from the centre would hit one of these edges with # t = 0 but this point should be discounted from calculations. # However, the value of t can vary depending on the length of # the ray, hence using the distance between the points as a # better measure of proximity hit_dist_2 = res['point'].distance_squared_to(self.origin_centre_position) if hit_dist_2 > self.epsilon: t = res['t'] hit_point = Vector2(res['point']) blocker = blocking_edges[j] """ the ray could've hit i. nothing (no blocking edge was in its way; t is None) ii. blocking edge(s) of which the closest intersecting point is a. within the sector b. on the sector's arc c. beyond the sector's arc For (ii.c) t may be defined but the point would be beyond the sector's radius. For everything except (ii.a), the hit point would be on the arc and the unit vector along the ray would be needed to draw the Bézier curve, if the next point is also going to be on the arc. For cases (i) and (ii.c), a new hit point needs to be calculated too, which can use the unit vector. One can avoid sqrt and call atan2 to get the angle directly which would also help in drawing the actual arc (using ctx.arc) and not an approximation of the arc using ctx.quadraticCurveTo. However, sqrt is chosen over atan2 since it's usually faster: http://stackoverflow.com/a/9318108. """ point_on_arc = (t is None) or ((hit_dist_2 + self.epsilon - self.length_squared) >= 0) if point_on_arc: unit_ray = rays[i].normalize() # for cases (i), (ii.b) and (ii.c) set the hit point; this would # be redundant for case (ii.b) but checking for it would be # cumbersome, so just reassign hit_point = self.origin_centre_position + (unit_ray * self.length) if prev_point_on_arc: needs_arc = True """ the case where part of the arc is cut by a blocking edge needs to be handled differently: /--- +----------+ /--- \-| | /--- X | /-- |\ | /--- | \ | o | | | ---\ | / | --\ |/ | ---\ X | ---\ /-| | ---- +----------+ although both hit points would be on the arc, they shouldn't be connected by an arc since the blocking edge wouldn't allow vision beyond; hence check if this ray hit a blocking edge, if yes, then check if it's parallel to the edge formed by the connection between this and the previous hit points, if so don't make an arc. the check i > 0 isn't needed since if that was the case the variable prevPointOnArc would be false and the control would've not reached here, so doing i - 1 is safe here """ if blocker: connector = hit_points[i - 1] - hit_point needs_arc = not self.are_parallel(blocker.b - blocker.a, connector) if needs_arc: ctrl_points[i] = self.calc_quad_bez_curve_ctrl_point(unit_ray, prev_unit_ray, self.origin_centre_position, self.length) prev_unit_ray = Vector2(unit_ray) prev_point_on_arc = point_on_arc hit_points[i] = hit_point return {'hit_points': hit_points, 'ctrl_points': ctrl_points} def los_blocked_test(self, ray, edge): res = self.line_seg_line_seg_x_sect(ray, edge, True) if ViewCone.LineSegConfig.INTERSECT == res['config']: return res['point'].distance_squared_to(self.origin_centre_position) > self.epsilon def is_subject_visible(self, target): if self.is_point_in_sector(target) == ViewCone.PointInConeStatus.WITHIN: ray = Edge("ray", self.origin_centre_position, target) return not any([True for edge in self.blocking_edges if self.los_blocked_test(ray, edge)]) return False def update(self): """ Based on method established here: https://legends2k.github.io/2d-fov/design.html :return: """ #tick_1 = self.timing_clock.tick() # each loop we first grab the edges that are attached to world objects in our cone length/radius # then we cull down only to the edges whose closest point is in our radius if len(self.collision_circle.collided_shapes_this_frame) != 0: blocking_edges = [] angle_points = AnglePointSet() for shape in self.collision_circle.collided_shapes_this_frame: if shape.type == BaseCollisionShape.RECT: self.check_polygon(shape, angle_points, blocking_edges) sorted_angle_points = self.sort_angular_points(angle_points, blocking_edges) angle_points_array = [Vector2(self.end_positions[0])] angle_points_array.extend(sorted_angle_points) angle_points_array.append(Vector2(self.end_positions[1])) rays = self.make_rays(angle_points_array) result = self.shoot_rays(rays, blocking_edges) self.angle_points_array = angle_points_array self.rays = rays self.hit_points = result['hit_points'] self.ctrl_points = result['ctrl_points'] self.blocking_edges = blocking_edges #tick_2 = self.timing_clock.tick() #if tick_2 > 7: # print("view_cone_timing:", tick_2, "ms") def clear(self): self.angle_points_array = [] self.rays = [] self.hit_points = [] self.ctrl_points = [] self.blocking_edges = [] def closest_point_on_edge(self, edge): """ Calculate closest point on a line segment to another point, in this case our cone's origin. Gathered from explanation here: http://doswa.com/2009/07/13/circle-segment-intersectioncollision.html :param edge: a line segment or an 'edge' in this case of a polygon or rectangle :return: the closest point on the segment to the origin point of our view cone """ seg_v = edge.vec pt_v = self.origin_centre_position - edge.a seg_length_squared = edge.length_squared if seg_length_squared <= 0: raise ValueError("Segment length is less than or equal to zero") if seg_length_squared != 1: seg_len = math.sqrt(seg_length_squared) seg_v_unit = seg_v / seg_len else: seg_len = 1 seg_v_unit = seg_v proj = pt_v.dot(seg_v_unit) if proj <= 0: return Vector2(edge.a) if proj >= seg_len: return Vector2(edge.b) return (seg_v_unit * proj) + edge.a
def __init__(self, character, start_pos, tiled_level, control_scheme, hud_buttons, collision_grid, projectile_sprite_group, player_sprites): super().__init__(player_sprites) self.collision_grid = collision_grid self.player_sprites = player_sprites self.character = character self.xp = 0 self.level = 1 self.score = 0 self.scheme = control_scheme self.image_name = "images/player_2.png" self.original_image = pygame.image.load(self.image_name) self.sprite_sheet = self.original_image.copy() self.hud_buttons = hud_buttons self.player_world_target = [0.0, 0.0] # ------------------------------------------ # Add new weapon objects here # ------------------------------------------ self.bow_weapon = BowWeapon(self, self.sprite_sheet, self.collision_grid, projectile_sprite_group) self.sword_weapon = SwordWeapon(self, self.sprite_sheet, self.collision_grid, projectile_sprite_group) self.magic_weapon = MagicWeapon(self, self.sprite_sheet) self.active_weapon = self.bow_weapon for button in self.hud_buttons: if button.button_image_name == "bow_icon": button.set_selected() else: button.clear_selected() # self.sprite = pygame.sprite.Sprite() self.test_collision_sprite = pygame.sprite.Sprite() self.flash_sprite = pygame.sprite.Sprite() self.image = self.active_weapon.anim_set.stand self.rect = self.active_weapon.anim_set.stand.get_rect() self.rect.center = start_pos self.sprite_rot_centre_offset = [0.0, 11.0] self.speed = 0.0 self.acceleration = 200.0 self.max_speed = 250.0 self.collide_radius = 18 self.max_health = 100 + (50 * self.level * (self.character.strength / 20)) self.health = self.max_health self.max_mana = 50.0 + (50.0 * self.level * (self.character.magic / 20)) self.mana = self.max_mana self.mana_recharge = 1.0 + (1.0 * self.level * (self.character.magic / 20)) self.should_die = False self.move_accumulator = 0.0 self.position = [float(self.rect.center[0]), float(self.rect.center[1])] self.player_move_target = self.position self.distance_to_move_target = 0.0 self.current_vector = [0.0, -1.0] self.new_facing_angle = 0 self.screen_position = [0, 0] self.screen_position[0] = self.position[0] self.screen_position[1] = self.position[1] self.update_screen_position(tiled_level.position_offset) direction_magnitude = math.sqrt(self.current_vector[0] ** 2 + self.current_vector[1] ** 2) if direction_magnitude > 0.0: unit_dir_vector = [self.current_vector[0] / direction_magnitude, self.current_vector[1] / direction_magnitude] self.new_facing_angle = math.atan2(-unit_dir_vector[0], -unit_dir_vector[1]) * 180 / math.pi self.old_facing_angle = self.new_facing_angle self.rect.center = self.rot_point([self.screen_position[0], self.screen_position[1] + self.sprite_rot_centre_offset[1]], self.screen_position, -self.new_facing_angle) self.left_mouse_held = False self.right_mouse_held = False self.per_bullet_damage = 25 self.player_fire_target = [10000, 10000] self.switch_to_bow = False self.switch_to_sword = False self.switch_to_magic = False self.sprite_flash_acc = 0.0 self.sprite_flash_time = 0.15 self.should_flash_sprite = False self.sprite_flashing = False self.is_collided = False self.firing = False self.firing_timer = 0.0 self.has_new_high_score = False self.should_draw_collision_obj = False self.collision_obj_rects = [] self.collision_obj_rect = pygame.Rect(0.0, 0.0, 2.0, 2.0) self.world_click_pos = [0, 0] # we do collisions in world space self.collision_circle = CollisionCircle(self.position[0], self.position[1], self.collide_radius, {GameCollisionType.MONSTER_WEAPON: CollisionNoHandler(), GameCollisionType.TILE: CollisionRubHandler(), GameCollisionType.MONSTER: CollisionRubHandler(), GameCollisionType.PICKUP: CollisionNoHandler()}, GameCollisionType.PLAYER, [GameCollisionType.MONSTER_WEAPON, GameCollisionType.TILE, GameCollisionType.MONSTER, GameCollisionType.PICKUP]) self.collision_circle.set_owner(self) self.collision_grid.add_new_shape_to_grid(self.collision_circle) self.drawable_circle = DrawableCollisionCircle(self.collision_circle)
class Player(pygame.sprite.Sprite): def __init__(self, character, start_pos, tiled_level, control_scheme, hud_buttons, collision_grid, projectile_sprite_group, player_sprites): super().__init__(player_sprites) self.collision_grid = collision_grid self.player_sprites = player_sprites self.character = character self.xp = 0 self.level = 1 self.score = 0 self.scheme = control_scheme self.image_name = "images/player_2.png" self.original_image = pygame.image.load(self.image_name) self.sprite_sheet = self.original_image.copy() self.hud_buttons = hud_buttons self.player_world_target = [0.0, 0.0] # ------------------------------------------ # Add new weapon objects here # ------------------------------------------ self.bow_weapon = BowWeapon(self, self.sprite_sheet, self.collision_grid, projectile_sprite_group) self.sword_weapon = SwordWeapon(self, self.sprite_sheet, self.collision_grid, projectile_sprite_group) self.magic_weapon = MagicWeapon(self, self.sprite_sheet) self.active_weapon = self.bow_weapon for button in self.hud_buttons: if button.button_image_name == "bow_icon": button.set_selected() else: button.clear_selected() # self.sprite = pygame.sprite.Sprite() self.test_collision_sprite = pygame.sprite.Sprite() self.flash_sprite = pygame.sprite.Sprite() self.image = self.active_weapon.anim_set.stand self.rect = self.active_weapon.anim_set.stand.get_rect() self.rect.center = start_pos self.sprite_rot_centre_offset = [0.0, 11.0] self.speed = 0.0 self.acceleration = 200.0 self.max_speed = 250.0 self.collide_radius = 18 self.max_health = 100 + (50 * self.level * (self.character.strength / 20)) self.health = self.max_health self.max_mana = 50.0 + (50.0 * self.level * (self.character.magic / 20)) self.mana = self.max_mana self.mana_recharge = 1.0 + (1.0 * self.level * (self.character.magic / 20)) self.should_die = False self.move_accumulator = 0.0 self.position = [float(self.rect.center[0]), float(self.rect.center[1])] self.player_move_target = self.position self.distance_to_move_target = 0.0 self.current_vector = [0.0, -1.0] self.new_facing_angle = 0 self.screen_position = [0, 0] self.screen_position[0] = self.position[0] self.screen_position[1] = self.position[1] self.update_screen_position(tiled_level.position_offset) direction_magnitude = math.sqrt(self.current_vector[0] ** 2 + self.current_vector[1] ** 2) if direction_magnitude > 0.0: unit_dir_vector = [self.current_vector[0] / direction_magnitude, self.current_vector[1] / direction_magnitude] self.new_facing_angle = math.atan2(-unit_dir_vector[0], -unit_dir_vector[1]) * 180 / math.pi self.old_facing_angle = self.new_facing_angle self.rect.center = self.rot_point([self.screen_position[0], self.screen_position[1] + self.sprite_rot_centre_offset[1]], self.screen_position, -self.new_facing_angle) self.left_mouse_held = False self.right_mouse_held = False self.per_bullet_damage = 25 self.player_fire_target = [10000, 10000] self.switch_to_bow = False self.switch_to_sword = False self.switch_to_magic = False self.sprite_flash_acc = 0.0 self.sprite_flash_time = 0.15 self.should_flash_sprite = False self.sprite_flashing = False self.is_collided = False self.firing = False self.firing_timer = 0.0 self.has_new_high_score = False self.should_draw_collision_obj = False self.collision_obj_rects = [] self.collision_obj_rect = pygame.Rect(0.0, 0.0, 2.0, 2.0) self.world_click_pos = [0, 0] # we do collisions in world space self.collision_circle = CollisionCircle(self.position[0], self.position[1], self.collide_radius, {GameCollisionType.MONSTER_WEAPON: CollisionNoHandler(), GameCollisionType.TILE: CollisionRubHandler(), GameCollisionType.MONSTER: CollisionRubHandler(), GameCollisionType.PICKUP: CollisionNoHandler()}, GameCollisionType.PLAYER, [GameCollisionType.MONSTER_WEAPON, GameCollisionType.TILE, GameCollisionType.MONSTER, GameCollisionType.PICKUP]) self.collision_circle.set_owner(self) self.collision_grid.add_new_shape_to_grid(self.collision_circle) self.drawable_circle = DrawableCollisionCircle(self.collision_circle) def draw_collision_shape(self, screen, camera_position, camera_half_dimensions): self.drawable_circle.update_collided_colours() self.drawable_circle.draw(screen, camera_position, camera_half_dimensions) def remove_from_grid(self): self.collision_grid.remove_shape_from_grid(self.collision_circle) def react_to_collision(self): for shape in self.collision_circle.collided_shapes_this_frame: if shape.game_type == GameCollisionType.MONSTER: self.position[0] = self.collision_circle.x self.position[1] = self.collision_circle.y elif shape.game_type == GameCollisionType.TILE: self.position[0] = self.collision_circle.x self.position[1] = self.collision_circle.y def add_xp(self, xp): self.xp += xp def add_score(self, score): self.score += score def xp_for_next_level(self): # 2 = 100 # 3 = 250 # 4 = 500 # 5 = 850 # 6 = 1300 # 7 = 1850 return 50 + (100 * ((self.level * self.level)/2)) def update_screen_position(self, world_offset): self.screen_position[0] = self.position[0] - world_offset[0] self.screen_position[1] = self.position[1] - world_offset[1] def update_sprite(self, time_delta): if self.should_flash_sprite: self.should_flash_sprite = False self.player_sprites.add(self.flash_sprite) self.sprite_flashing = True if self.sprite_flashing: self.sprite_flash_acc += time_delta if self.sprite_flash_acc > self.sprite_flash_time: self.sprite_flash_acc = 0.0 self.sprite_flashing = False self.flash_sprite.kill() else: lerp_value = self.sprite_flash_acc / self.sprite_flash_time flash_alpha = self.lerp(255, 0, lerp_value) flash_image = self.image.copy() flash_image.fill((0, 0, 0, flash_alpha), None, pygame.BLEND_RGBA_MULT) flash_image.fill((255, 255, 255, 0), None, pygame.BLEND_RGBA_ADD) self.flash_sprite.image = flash_image self.flash_sprite.rect = self.flash_sprite.image.get_rect() anim_centre_offset = self.active_weapon.anim_set.centre_offset if self.firing: anim_centre_offset = self.active_weapon.anim_set.firing_centre_offset y_pos = self.screen_position[1]+self.sprite_rot_centre_offset[1]+anim_centre_offset[1] self.flash_sprite.rect.center = self.rot_point([self.screen_position[0], y_pos], self.screen_position, -self.new_facing_angle) def process_event(self, event): if event.type == MOUSEBUTTONDOWN: if event.button == 1: self.left_mouse_held = True if event.button == 3: self.right_mouse_held = True if event.type == MOUSEBUTTONUP: if event.button == 1: self.left_mouse_held = False if event.button == 3: self.right_mouse_held = False if event.type == KEYDOWN: if event.key == self.scheme.bow: self.switch_to_bow = True if event.key == self.scheme.sword: self.switch_to_sword = True if event.key == self.scheme.magic: self.switch_to_magic = True def check_and_save_high_score(self): if self.score > self.character.score: self.has_new_high_score = True self.character.score = self.score self.character.save() def on_level_up(self): self.level += 1 old_max_health = self.max_health self.max_health = 100 + (50 * self.level * (self.character.strength / 20)) self.health += (self.max_health - old_max_health) # add change in health to current health self.max_mana = 50 + (50 * self.level * (self.character.magic / 20)) self.acceleration = 200.0 + (25 * self.level * (self.character.dexterity/20)) self.max_speed = 200.0 + (25 * self.level * (self.character.dexterity / 20)) self.bow_weapon.on_level_up() self.sword_weapon.on_level_up() self.magic_weapon.on_level_up() def update_movement_and_collision(self, time_delta, tiled_level, monsters): if self.xp_for_next_level() <= self.xp: self.on_level_up() if self.health == 0: self.should_die = True self.check_and_save_high_score() self.mana += self.mana_recharge * time_delta if self.mana > self.max_mana: self.mana = self.max_mana if self.switch_to_bow: self.switch_to_bow = False self.active_weapon = self.bow_weapon for button in self.hud_buttons: if button.button_image_name == "bow_icon": button.set_selected() else: button.clear_selected() self.image = pygame.transform.rotate(self.active_weapon.anim_set.stand, self.new_facing_angle) self.rect = self.image.get_rect() anim_centre_offset = self.active_weapon.anim_set.centre_offset y_pos = self.screen_position[1]+self.sprite_rot_centre_offset[1]+anim_centre_offset[1] self.rect.center = self.rot_point([self.screen_position[0], y_pos], self.screen_position, -self.new_facing_angle) if self.switch_to_sword: self.switch_to_sword = False self.active_weapon = self.sword_weapon for button in self.hud_buttons: if button.button_image_name == "sword_icon": button.set_selected() else: button.clear_selected() self.image = pygame.transform.rotate(self.active_weapon.anim_set.stand, self.new_facing_angle) self.rect = self.image.get_rect() anim_centre_offset = self.active_weapon.anim_set.centre_offset y_pos = self.screen_position[1]+self.sprite_rot_centre_offset[1]+anim_centre_offset[1] self.rect.center = self.rot_point([self.screen_position[0], y_pos], self.screen_position, -self.new_facing_angle) if self.switch_to_magic: self.switch_to_magic = False self.active_weapon = self.magic_weapon for button in self.hud_buttons: if button.button_image_name == "magic_icon": button.set_selected() else: button.clear_selected() self.image = pygame.transform.rotate(self.active_weapon.anim_set.stand, self.new_facing_angle) self.rect = self.image.get_rect() anim_centre_offset = self.active_weapon.anim_set.centre_offset y_pos = self.screen_position[1] + self.sprite_rot_centre_offset[1] + anim_centre_offset[1] self.rect.center = self.rot_point([self.screen_position[0], y_pos], self.screen_position, -self.new_facing_angle) fire_this_update = False if self.active_weapon.can_fire and self.right_mouse_held: fire_this_update = True self.player_move_target = self.position self.distance_to_move_target = 0.0 self.active_weapon.fire_rate_acc = 0.0 self.active_weapon.can_fire = False if self.player_fire_target != pygame.mouse.get_pos(): new_target = pygame.mouse.get_pos() x_dist = float(new_target[0]) - float(self.screen_position[0]) y_dist = float(new_target[1]) - float(self.screen_position[1]) distance = math.sqrt((x_dist * x_dist) + (y_dist * y_dist)) if distance > 0.0: self.player_fire_target = new_target self.current_vector = [x_dist / distance, y_dist / distance] direction_magnitude = math.sqrt(self.current_vector[0] ** 2 + self.current_vector[1] ** 2) if direction_magnitude > 0.0: unit_dir_vector = [self.current_vector[0] / direction_magnitude, self.current_vector[1] / direction_magnitude] self.new_facing_angle = math.atan2(-unit_dir_vector[0], -unit_dir_vector[1]) * 180 / math.pi self.image = pygame.transform.rotate(self.active_weapon.anim_set.stand, self.new_facing_angle) self.rect = self.image.get_rect() anim_centre_offset = self.active_weapon.anim_set.centre_offset y_pos = self.screen_position[1]+self.sprite_rot_centre_offset[1]+anim_centre_offset[1] self.rect.center = self.rot_point([self.screen_position[0], y_pos], self.screen_position, -self.new_facing_angle) self.active_weapon.update(time_delta, self.position, self.current_vector) barrel_screen_x = float(self.active_weapon.barrel_exit_pos[0] - tiled_level.position_offset[0]) barrel_screen_y = float(self.active_weapon.barrel_exit_pos[1] - tiled_level.position_offset[1]) x_dist = float(self.player_fire_target[0]) - barrel_screen_x y_dist = float(self.player_fire_target[1]) - barrel_screen_y distance = math.sqrt(x_dist**2 + y_dist**2) if distance > 0.0: projectile_vector = [x_dist/distance, y_dist/distance] self.active_weapon.set_projectile_vector(projectile_vector) if fire_this_update: self.active_weapon.fire(monsters) self.firing_timer = self.active_weapon.fire_anim_speed self.firing = True if self.firing: anim_progress = self.active_weapon.fire_anim_speed - self.firing_timer fire_anim_percentage = anim_progress / self.active_weapon.fire_anim_speed number_of_anims = len(self.active_weapon.anim_set.fire_sprites) anim_index = max(1, int(fire_anim_percentage * number_of_anims)) - 1 current_anim = self.active_weapon.anim_set.fire_sprites[anim_index] anim_centre_offset = self.active_weapon.anim_set.firing_centre_offset self.image = pygame.transform.rotate(current_anim, self.new_facing_angle) self.rect = self.image.get_rect() y_pos = self.screen_position[1] + self.sprite_rot_centre_offset[1] + anim_centre_offset[1] self.rect.center = self.rot_point([self.screen_position[0], y_pos], self.screen_position, -self.new_facing_angle) self.firing_timer -= time_delta if self.firing_timer <= 0.0: self.firing_timer = 0.0 self.firing = False if self.left_mouse_held: self.firing = False self.firing_timer = 0.0 self.world_click_pos = [pygame.mouse.get_pos()[0] + tiled_level.position_offset[0], pygame.mouse.get_pos()[1] + tiled_level.position_offset[1]] # fake quick distance, just to check we are away from target click_x_dist = abs(self.world_click_pos[0] - self.player_world_target[0]) click_y_dist = abs(self.world_click_pos[1] - self.player_world_target[1]) click_target_dist = click_x_dist + click_y_dist if click_target_dist > 2.0: new_target = pygame.mouse.get_pos() x_dist = float(new_target[0]) - float(self.screen_position[0]) y_dist = float(new_target[1]) - float(self.screen_position[1]) distance = math.sqrt((x_dist * x_dist) + (y_dist * y_dist)) if distance > 0.0: self.player_move_target = new_target self.player_world_target = [self.player_move_target[0] + tiled_level.position_offset[0], self.player_move_target[1] + tiled_level.position_offset[1]] self.distance_to_move_target = distance self.current_vector = [x_dist / self.distance_to_move_target, y_dist / self.distance_to_move_target] direction_magnitude = math.sqrt(self.current_vector[0] ** 2 + self.current_vector[1] ** 2) if direction_magnitude > 0.0: unit_dir_vector = [self.current_vector[0] / direction_magnitude, self.current_vector[1] / direction_magnitude] self.new_facing_angle = math.atan2(-unit_dir_vector[0], -unit_dir_vector[1]) * 180 / math.pi if self.distance_to_move_target > 0.0: self.speed += self.acceleration * time_delta if self.speed > self.max_speed: self.speed = self.max_speed self.position[0] += (self.current_vector[0] * time_delta * self.speed) self.position[1] += (self.current_vector[1] * time_delta * self.speed) self.move_accumulator += self.speed * time_delta self.distance_to_move_target -= self.speed * time_delta self.update_screen_position(tiled_level.position_offset) if not self.firing: y_pos = self.screen_position[1] + self.sprite_rot_centre_offset[1] if abs(self.move_accumulator) > 64.0: self.image = pygame.transform.rotate(self.active_weapon.anim_set.stand, self.new_facing_angle) self.rect = self.image.get_rect() self.rect.center = self.rot_point([self.screen_position[0], y_pos], self.screen_position, -self.new_facing_angle) self.move_accumulator = 0.0 elif abs(self.move_accumulator) > 48.0: self.image = pygame.transform.rotate(self.active_weapon.anim_set.step_left, self.new_facing_angle) self.rect = self.image.get_rect() self.rect.center = self.rot_point([self.screen_position[0], y_pos], self.screen_position, -self.new_facing_angle) elif abs(self.move_accumulator) > 32.0: self.image = pygame.transform.rotate(self.active_weapon.anim_set.stand, self.new_facing_angle) self.rect = self.image.get_rect() self.rect.center = self.rot_point([self.screen_position[0], y_pos], self.screen_position, -self.new_facing_angle) elif abs(self.move_accumulator) > 16.0: self.image = pygame.transform.rotate(self.active_weapon.anim_set.step_right, self.new_facing_angle) self.rect = self.image.get_rect() self.rect.center = self.rot_point([self.screen_position[0], y_pos], self.screen_position, -self.new_facing_angle) else: self.image = pygame.transform.rotate(self.active_weapon.anim_set.stand, self.new_facing_angle) self.rect = self.image.get_rect() self.rect.center = self.rot_point([self.screen_position[0], y_pos], self.screen_position, -self.new_facing_angle) else: self.update_screen_position(tiled_level.position_offset) self.speed = 0.0 if not self.firing: self.image = pygame.transform.rotate(self.active_weapon.anim_set.stand, self.new_facing_angle) self.rect = self.image.get_rect() y_pos = self.screen_position[1] + self.sprite_rot_centre_offset[1] self.rect.center = self.rot_point([self.screen_position[0], y_pos], self.screen_position, -self.new_facing_angle) self.collision_circle.set_position(self.position) if self.should_die: self.flash_sprite.kill() self.kill() self.collision_grid.remove_shape_from_grid(self.collision_circle) def add_health(self, health): self.health += health if self.health > self.max_health: self.health = self.max_health def add_mana(self, mana): self.mana += mana if self.mana > self.max_mana: self.mana = self.max_mana def take_damage(self, damage): self.health -= damage if self.health < 0: self.health = 0 self.should_flash_sprite = True @staticmethod def rot_point(point, axis, ang): """ Orbit. calculates the new loc for a point that rotates a given num of degrees around an axis point, +clockwise, -anticlockwise -> tuple x,y """ ang -= 90 x, y = point[0] - axis[0], point[1] - axis[1] radius = math.sqrt(x*x + y*y) # get the distance between points r_ang = math.radians(ang) # convert ang to radians. h = axis[0] + (radius * math.cos(r_ang)) v = axis[1] + (radius * math.sin(r_ang)) return [h, v] @staticmethod def lerp(a, b, c): return (c * b) + ((1.0 - c) * a)