class Asteroid(pygame.sprite.Sprite): containers = None image = None def __init__(self, asteroid_name_: str, gl_, blend_: int = 0, rotation_: int = 0, scale_: int = 1, timing_: int = 8, layer_: int = 0): """ :param asteroid_name_: strings, MirroredAsteroidClass name :param gl_: class GL (contains all the game constant) :param blend_: integer, Sprite blend effect, must be > 0 :param rotation_: integer, Object rotation in degrees :param scale_: integer, Object scale value, default 1 -> no transformation. must be > 0 :param timing_: integer; Refreshing time in milliseconds, must be > 0 :param layer_: integer; Layer used. must be <= 0 (0 is the top layer) """ """ assert isinstance(asteroid_name_, str), \ "Positional argument <asteroid_name_> is type %s , expecting string." % type(asteroid_name_) assert isinstance(blend_, int), \ "Positional argument <blend_> is type %s , expecting integer." % type(blend_) if blend_ < 0: raise ValueError('Positional attribute blend_ must be > 0') assert isinstance(rotation_, (float, int)), \ "Positional argument <rotation_> is type %s , expecting float or integer." % type(rotation_) assert isinstance(scale_, (float, int)), \ "Positional argument <scale_> is type %s , expecting float or integer." % type(scale_) if scale_ < 0: raise ValueError('Positional attribute scale_ must be > 0') assert isinstance(timing_, int), \ "Positional argument <timing_> is type %s , expecting integer." % type(timing_) if timing_ < 0: raise ValueError('Positional attribute timing_ must be >= 0') assert isinstance(layer_, int), \ "Positional argument <layer_> is type %s , expecting integer." % type(layer_) if layer_ > 0: raise ValueError('Positional attribute layer_ must be <= 0') """ self.layer = layer_ pygame.sprite.Sprite.__init__(self, self.containers) # change sprite layer if isinstance(gl_.All, pygame.sprite.LayeredUpdates): gl_.All.change_layer(self, layer_) self.images_copy = Asteroid.image.copy() self.image = self.images_copy[0] if isinstance( Asteroid.image, list) else self.images_copy self.w, self.h = pygame.display.get_surface().get_size() self.appearance_frame = 0 # randint(100, 100240) # When the asteroid start moving if asteroid_name_ == 'MAJOR_ASTEROID': self.speed = pygame.math.Vector2(0, 1) self.rect = self.image.get_rect(center=((self.w >> 1), -self.image.get_height())) # MirroredAsteroidClass life points (proportional to its size) self.life = 5000 self.damage = self.life * 2 else: self.rect = self.image.get_rect(center=(randint(0, self.w), -randint(0, self.h) - self.image.get_height())) # self.rect = self.image.get_rect(center=(self.w // 2, - self.image.get_height())) self.speed = pygame.math.Vector2(0, uniform( +4, +8)) # * round(self.appearance_frame / 4000)) if self.speed.length() == 0: self.speed = pygame.math.Vector2(0, 1) # MirroredAsteroidClass life points (proportional to its size) self.life = randint(self.rect.w, max(self.rect.w, (self.rect.w >> 1) * 10)) # Collision damage, how much life point # will be removed from players and transport in case of collision self.damage = self.life >> 1 self.timing = timing_ self.rotation = rotation_ self.scale = scale_ self.dt = 0 self.gl = gl_ self.asteroid_name = asteroid_name_ self.blend = blend_ # No need to pre-calculate the mask as all asteroid instanciation is # done before the main loop self.mask = pygame.mask.from_surface(self.image) # MirroredAsteroidClass value in case of destruction. self.points = self.life self.layer = layer_ self.index = 0 self.has_been_hit = False self.id_ = id(self) self.asteroid_object = Broadcast(self.make_object()) self.impact_sound_object = Broadcast(self.make_sound_object('IMPACT')) Broadcast.add_object_id(self.id_) def delete_object(self) -> DeleteSpriteCommand: """ Send a command to kill an object on client side. :return: DetectCollisionSprite object """ return DeleteSpriteCommand(frame_=self.gl.FRAME, to_delete_={self.id_: self.asteroid_name}) def make_sound_object(self, sound_name_: str) -> SoundAttr: """ Create a network sound object :param sound_name_: string; represent the sound name e.g 'EXPLOSION_SOUND" :return: SoundAttr object """ assert isinstance(sound_name_, str), \ "Positional argument <sound_name_> is type %s , expecting string." % type(sound_name_) if sound_name_ not in globals(): raise NameError('Sound %s is not define.' % sound_name_) return SoundAttr(frame_=self.gl.FRAME, id_=self.id_, sound_name_=sound_name_, rect_=self.rect) def make_object(self) -> DetectCollisionSprite: """ Create a network sprite object :return: DetectCollisionSprite object """ return DetectCollisionSprite(frame_=self.gl.FRAME, id_=self.id_, surface_=self.asteroid_name, layer_=self.layer, blend_=self.blend, rect_=self.rect, rotation_=self.rotation, scale_=self.scale, damage_=self.damage, life_=self.life, points_=self.points) def create_gems(self, player_) -> None: """ Create collectable gems after asteroid disintegration. :param player_: player causing asteroid explosion :return: None """ if player_ is None: raise ValueError("Argument player_ cannot be none!.") if hasattr(player_, 'alive'): if player_.alive(): number = randint(3, 15) for _ in range(number): if _ < number: MakeGems.inventory = set() MakeGems(gl_=self.gl, player_=player_, object_=self, ratio_=1.0, timing_=8, offset_=pygame.Rect(self.rect.centerx, self.rect.centery, randint(-100, 100), randint(-100, 20))) def make_debris(self) -> None: """ Create sprite debris (different sizes 32x32, 64x64 pixels) :return:None """ if not globals().__contains__('MULT_ASTEROID_64'): raise NameError( "Texture MULT_ASTEROID_64 is missing!" "\nCheck file Texture.py for MULT_ASTEROID_64 assigment. ") if not globals().__contains__('MULT_ASTEROID_32'): raise NameError( "Texture MULT_ASTEROID_32 is missing!" "\nCheck file Texture.py for MULT_ASTEROID_32 assigment. ") size_x, size_y = self.image.get_size() if size_x > 128: aster = MULT_ASTEROID_64 name = 'MULT_ASTEROID_64' else: aster = MULT_ASTEROID_32 name = 'MULT_ASTEROID_32' length = len(aster) - 1 if self.asteroid_name != 'MAJOR_ASTEROID': Debris.containers = self.gl.All, self.gl.ASTEROID for _ in range(8 if size_x > 255 else 6): element = randint(0, length) Debris.image = aster[element] Debris(asteroid_name_=name + '[' + str(element) + ']', pos_=self.rect.center, gl_=self.gl, blend_=0, timing_=16, layer_=-2, particles_=True) else: Debris.containers = self.gl.All, self.gl.ASTEROID aster = MULT_ASTEROID_64 name = 'MULT_ASTEROID_64' length = len(aster) - 1 for _ in range(10): element = randint(0, length) Debris.image = aster[element] Debris(asteroid_name_=name + '[' + str(element) + ']', pos_=(self.rect.centerx + randint(-size_x, size_y), self.rect.centery + randint(-size_x, size_y)), gl_=self.gl, blend_=0, timing_=20, layer_=randint(-2, 0), particles_=False) aster = MULT_ASTEROID_32 name = 'MULT_ASTEROID_32' length = len(aster) - 1 for _ in range(10): element = randint(0, length) Debris.image = aster[element] Debris(asteroid_name_=name + '[' + str(element) + ']', pos_=(self.rect.centerx + randint(-size_x, size_y), self.rect.centery + randint(-size_x, size_y)), gl_=self.gl, blend_=0, timing_=8, layer_=randint(-2, 0), particles_=False) def explode(self, player_) -> None: """ Create an explosion sprite when asteroid life points < 1 :param player_: Player causing asteroid explosion :return: None """ if not globals().__contains__('EXPLOSION1'): raise NameError( "Texture EXPLOSION1 is missing!" "\nCheck file Texture.py for EXPLOSION1 assigment. ") # Create queue sprite Explosion.images = EXPLOSION1 Explosion(self, self.rect.center, self.gl, 8, 0, texture_name_='EXPLOSION1') # self.layer) if not globals().__contains__('HALO_SPRITE12'): raise NameError( "Texture HALO_SPRITE12 is missing!" "\nCheck file Texture.py for HALO_SPRITE12 assigment. ") if not globals().__contains__('HALO_SPRITE14'): raise NameError( "Texture HALO_SPRITE14 is missing!" "\nCheck file Texture.py for HALO_SPRITE14 assigment. ") # Create Halo sprite AsteroidHalo.images = choice([HALO_SPRITE12, HALO_SPRITE14]) AsteroidHalo.containers = self.gl.All AsteroidHalo(texture_name_='HALO_SPRITE12' if AsteroidHalo.images is HALO_SPRITE12 else 'HALO_SPRITE14', object_=self, timing_=8) self.make_debris() if player_ is not None: self.create_gems(player_) self.quit() def hit(self, player_=None, damage_: int = 0) -> None: """ Check asteroid life after laser collision. :param player_: Player instance :param damage_: integer; Damage received :return: None """ assert isinstance(damage_, int), \ "Positional argument <damage_> is type %s , expecting integer." % type(damage_) if damage_ < 0: raise ValueError('positional argument damage_ cannot be < 0') self.life -= damage_ self.has_been_hit = True if self.asteroid_name != 'MAJOR_ASTEROID' else False if self.life < 1: if player_ is not None: if hasattr(player_, 'update_score'): player_.update_score(self.points) self.explode(player_) def collide(self, player_=None, damage_: int = 0) -> None: """ Check asteroid life after collision with players or transport :param player_: Player instance or transport :param damage_: integer; Damage received :return: None """ assert isinstance(damage_, int), \ "Positional argument <damage_> is type %s , expecting integer." % type(damage_) if damage_ < 0: raise ValueError('positional argument damage_ cannot be < 0') if not globals().__contains__('IMPACT1'): raise NameError("Sound IMPACT1 is missing!" "\nCheck file Sounds.py for IMPACT1 assigment. ") if hasattr(self, 'life'): self.life -= damage_ # transfer damage to the asteroid (decrease life) else: raise AttributeError('self %s, %s does not have attribute life ' % (self, type(self))) # play asteroid burst sound locally self.gl.MIXER.play(sound_=IMPACT1, loop_=False, priority_=0, volume_=1.0, fade_out_ms=0, panning_=True, name_='IMPACT1', x_=self.rect.centerx, object_id_=id(IMPACT1), screenrect_=self.gl.SCREENRECT) # broadcast asteroid burst sound self.impact_sound_object.play() # check if asteroid life is still > 0 if self.life < 1: # check who is colliding with the asteroid # if not colliding with transport, transfer score to player. if not type(player_).__name__ == 'Transport': if player_ is not None: # player has collide with asteroid, player1 or player 2 get the points player_.update_score(self.points) else: # Transport does not get points ... # Split asteroid self.make_debris() self.quit() ... def quit(self) -> None: Broadcast.remove_object_id(self.id_) obj = Broadcast(self.delete_object()) obj.queue() self.kill() def update(self) -> None: """ Update asteroid sprites. :return: None """ if self.gl.FRAME > self.appearance_frame: # start to move asteroid when frame number is over # self.appearance_frame (random frame number) self.rect.move_ip(self.speed) # asteroid is moving but not visible yet? # The rectangle bottom edge must be > 0 to start the code below if self.rect.midbottom[1] > 0: # Inside the 60 FPS Area if self.dt > self.timing: # self.image = self.images_copy.copy() if self.has_been_hit: if not globals().__contains__('LAVA'): raise NameError("Texture LAVA not available") self.image.blit(LAVA[self.index % len(LAVA) - 1], (0, 0), special_flags=pygame.BLEND_RGB_ADD) self.index += 1 self.has_been_hit = False # if self.rotation != 0: # self.mask = pygame.mask.from_surface(self.image) self.asteroid_object.update({ 'frame': self.gl.FRAME, 'rect': self.rect, 'life': self.life }) self.asteroid_object.queue() self.dt = 0 else: self.dt += self.gl.TIME_PASSED_SECONDS if self.rect.midtop[1] > self.gl.SCREENRECT.h: self.quit()
class Explosion(pygame.sprite.Sprite): images = None containers = None def __init__(self, parent_, pos_, gl_, timing_, layer_, texture_name_, mute_=False): self.layer = layer_ pygame.sprite.Sprite.__init__(self, self.containers) if isinstance(gl_.All, pygame.sprite.LayeredUpdates): if layer_: gl_.All.change_layer(self, layer_) self.images_copy = Explosion.images.copy() self.image = self.images_copy[0] if isinstance( self.images_copy, list) else self.images_copy self.timing = timing_ self.length = len(self.images) - 1 self.pos = pos_ self.gl = gl_ self.position = pygame.math.Vector2(*self.pos) self.rect = self.image.get_rect(center=self.pos) self.dt = 0 self.blend = pygame.BLEND_RGB_ADD self.parent = parent_ self.index = 0 self.id_ = id(self) self.texture_name = texture_name_ self.mute = mute_ # Create the network object self.explosion_object = Broadcast(self.make_object()) # Create sound object self.explosion_sound_object = Broadcast( self.make_sound_object('EXPLOSION_SOUND')) Broadcast.add_object_id(self.id_) def delete_object(self) -> DeleteSpriteCommand: """ Send a command to kill an object on client side. :return: DetectCollisionSprite object """ return DeleteSpriteCommand(frame_=self.gl.FRAME, to_delete_={self.id_: self.texture_name}) def play_explosion_sound(self) -> None: """ Play the sound explosion locally and forward the sound object to the client(s). :return: None """ # play the sound locally self.gl.MIXER.play(sound_=EXPLOSION_SOUND, loop_=False, priority_=0, volume_=1.0, fade_out_ms=0, panning_=True, name_='EXPLOSION_SOUND', x_=self.rect.centerx, object_id_=id(EXPLOSION_SOUND), screenrect_=self.gl.SCREENRECT) # Add the sound object to the queue self.explosion_sound_object.play() def make_sound_object(self, sound_name_: str) -> SoundAttr: """ Create a network sound object :param sound_name_: string; represent the sound name e.g 'EXPLOSION_SOUND" :return: SoundAttr object """ assert isinstance(sound_name_, str), \ "Positional argument <sound_name_> is type %s , expecting string." % type(sound_name_) if sound_name_ not in globals(): raise NameError('Sound %s is not define.' % sound_name_) return SoundAttr(frame_=self.gl.FRAME, id_=self.id_, sound_name_=sound_name_, rect_=self.rect) def make_object(self) -> AnimatedSprite: return AnimatedSprite(frame_=self.gl.FRAME, id_=self.id_, surface_=self.texture_name, layer_=self.layer, blend_=self.blend, rect_=self.rect, index_=self.index) def quit(self) -> None: Broadcast.remove_object_id(self.id_) obj = Broadcast(self.delete_object()) obj.queue() self.kill() def update(self): if self.dt > self.timing: if self.rect.colliderect(self.gl.SCREENRECT): if self.index == 0 and not self.mute: self.play_explosion_sound() self.image = self.images_copy[self.index] self.rect = self.image.get_rect(center=self.rect.center) self.index += 1 if self.index > self.length: self.quit() return self.dt = 0 self.explosion_object.update({ 'frame': self.gl.FRAME, 'rect': self.rect, 'index': self.index, 'blend': self.blend }) self.explosion_object.queue() else: self.quit() return else: self.dt += self.gl.TIME_PASSED_SECONDS
class Transport(pygame.sprite.Sprite): containers = None image = None def __init__(self, gl_, timing_, pos_, surface_name_, layer_=0): pygame.sprite.Sprite.__init__(self, self.containers) if isinstance(gl_.All, pygame.sprite.LayeredUpdates): if layer_: gl_.All.change_layer(self, layer_) self.image = Transport.image self.image_copy = self.image.copy() self.rect = self.image.get_rect(center=pos_) self.timing = timing_ self.gl = gl_ self.dt = 0 self.fxdt = 0 self.layer = layer_ self.blend = 0 self.previous_pos = pygame.math.Vector2() # previous position self.max_life = 5000 self.life = 5000 # MirroredTransportClass max hit points self.damage = 10000 # Damage transfer after collision self.mask = pygame.mask.from_surface(self.image) # Image have to be convert_alpha compatible self.pos = pos_ self.index = 0 self.impact = False self.vertex_array = [] self.engine = self.engine_on() self.surface_name = surface_name_ self.id_ = id(self) self.transport_object = Broadcast(self.make_object()) self.impact_sound_object = Broadcast(self.make_sound_object('IMPACT')) half = self.gl.SCREENRECT.w >> 1 self.safe_zone = pygame.Rect(half - 200, half, 400, self.gl.SCREENRECT.bottom - half) self.half_life = (self.max_life >> 1) Broadcast.add_object_id(self.id_) # this is now obsolete since it is done from main loop def delete_object(self) -> DeleteSpriteCommand: """ Send a command to kill an object on client side. :return: DetectCollisionSprite object """ return DeleteSpriteCommand(frame_=self.gl.FRAME, to_delete_={self.id_: self.surface_name}) def make_sound_object(self, sound_name_: str) -> SoundAttr: return SoundAttr(frame_=self.gl.FRAME, id_=self.id_, sound_name_=sound_name_, rect_=self.rect) def make_object(self) -> StaticSprite: return StaticSprite(frame_=self.gl.FRAME, id_=self.id_, surface_=self.surface_name, layer_=self.layer, blend_=self.blend, rect_=self.rect, damage=self.damage, life=self.life, impact=self.impact) def engine_on(self) -> AfterBurner: AfterBurner.images = EXHAUST2 calc_pos = (self.pos[0] - 400, self.pos[1] - 205) # Top left corner position return AfterBurner(self, self.gl, calc_pos, 8, pygame.BLEND_RGB_ADD, self.layer - 1, texture_name_='EXHAUST2') def player_lost(self) -> None: PlayerLost.containers = self.gl.All PlayerLost.DIALOGBOX_READOUT_RED = DIALOGBOX_READOUT_RED PlayerLost.SKULL = SKULL font = freetype.Font('Assets\\Fonts\\Gtek Technology.ttf', size=14) PlayerLost(gl_=self.gl, font_=font, image_=FINAL_MISSION, layer_=0) # todo kill player 1 and 2 game is over def explode(self): Explosion.images = EXPLOSION2 for i in range(10): Explosion(self, (self.rect.centerx + randint(-400, 400), self.rect.centery + randint(-400, 400)), self.gl, 8, self.layer, texture_name_='EXPLOSION2', mute_=False if i > 0 else True) PlayerHalo.images = HALO_SPRITE13 PlayerHalo.containers = self.gl.All PlayerHalo(texture_name_='HALO_SPRITE13', object_=self, timing_=8) self.quit() def collide(self, damage_: int)-> None: """ Asteroid collide with object (e.g Asteroids) :param damage_: int; damage transfer to the transport (damage must be positive) :return: None """ assert isinstance(damage_, int), \ 'Positional arguement damage_, expecting int type got %s ' % type(damage_) if self.alive(): if damage_ is None or damage_ < 0: raise ValueError('positional arguement damage_ cannot be None or < 0.') self.impact = True # variable used for blending, (electric effect on the transport's hull ) self.index = 0 self.life -= damage_ # Transport life decrease # Play an impact sound locally self.gl.MIXER.play(sound_=IMPACT1, loop_=False, priority_=0, volume_=1.0, fade_out_ms=0, panning_=True, name_='IMPACT1', x_=self.rect.centerx, object_id_=id(IMPACT1), screenrect_=self.gl.SCREENRECT) # Play impact sound on client computer. self.impact_sound_object.play() else: self.quit() def hit(self, damage_): if self.alive(): self.life -= damage_ else: self.quit() def get_centre(self) -> tuple: return self.rect.center def display_fire_particle_fx(self) -> None: # Display fire particles when the player has taken bad hits # Use the additive blend mode. if self.fxdt > self.timing: for p_ in self.vertex_array: # queue the particle in the vector direction p_.rect.move_ip(p_.vector) p_.image = p_.images[p_.index] if p_.index > len(p_.images) - 2: p_.kill() self.vertex_array.remove(p_) p_.index += 1 self.fxdt = 0 else: self.fxdt += self.gl.TIME_PASSED_SECONDS def fire_particles_fx(self, position_, # particle starting location (tuple or pygame.math.Vector2) vector_, # particle speed, pygame.math.Vector2 images_, # surface used for the particle, (list of pygame.Surface) layer_=0, # Layer used to display the particles (int) blend_=pygame.BLEND_RGB_ADD # Blend mode (int) ) -> None: # Create fire particles around the aircraft hull when player is taking serious damages # Cap the number of particles to avoid lag # if len(self.gl.FIRE_PARTICLES_FX) > 100: # return # Create fire particles when the aircraft is disintegrating sprite_ = pygame.sprite.Sprite() self.gl.All.add(sprite_) # self.gl.FIRE_PARTICLES_FX.add(sprite__) # assign the particle to a specific layer if isinstance(self.gl.All, pygame.sprite.LayeredUpdates): self.gl.All.change_layer(sprite_, layer_) sprite_.layer = layer_ sprite_.blend = blend_ # use the additive mode sprite_.images = images_ sprite_.image = images_[0] sprite_.rect = sprite_.image.get_rect(center=position_) sprite_.vector = vector_ # vector sprite_.index = 0 # assign update method to self.display_fire_particle_fx # (local method to display the particles) sprite_.update = self.display_fire_particle_fx self.vertex_array.append(sprite_) def quit(self) -> None: Broadcast.remove_object_id(self.id_) obj = Broadcast(self.delete_object()) obj.queue() self.kill() def update(self): self.rect.clamp_ip(self.safe_zone) self.image = self.image_copy.copy() # in the 16ms area (60 FPS) if self.dt > self.timing: if self.life < self.half_life: position = pygame.math.Vector2(randint(-50, 50), randint(-100, 100)) self.fire_particles_fx(position_=position + pygame.math.Vector2(self.rect.center), vector_=pygame.math.Vector2(uniform(-1, 1), uniform(+1, +3)), images_=FIRE_PARTICLES, layer_=0, blend_=pygame.BLEND_RGB_ADD) if self.life < 1: self.explode() return if self.previous_pos == self.rect.center: self.rect.centerx += randint(-1, 1) self.rect.centery += randint(-1, 1) if self.gl.FRAME < 100: self.rect.centery -= 3 self.previous_pos = self.rect.center self.engine.update() self.transport_object.update( {'frame': self.gl.FRAME, 'rect': self.rect, 'damage': self.damage, 'life': self.life, 'impact': self.impact}) # Broadcast the spaceship position self.transport_object.queue() self.dt = 0 else: self.dt += self.gl.TIME_PASSED_SECONDS # outside the 60 FPS area. # Below code processed every frames. if self.impact: self.image.blit(DISRUPTION_ORG[self.index % len(DISRUPTION_ORG) - 1], (0, 0), special_flags=pygame.BLEND_RGB_ADD) self.index += 1 if self.index > len(DISRUPTION_ORG) - 2: self.impact = False self.index = 0