class Background(pygame.sprite.Sprite):

    containers = None   # pygame group
    image = None        # surface to display (can be a list of Surface or a single pygame.Surface)

    def __init__(self,
                 vector_: pygame.math.Vector2,       # background speed vector (pygame.math.Vector2)
                 position_: pygame.math.Vector2,          # original position (tuple)
                 gl_,                    # global variables  (GL class)
                 layer_: int = -8,       # layer used default is -8 (int <= 0)
                 blend_: int = 0,        # pygame blend effect (e.g pygame.BLEND_RGB_ADD, or int)
                 event_name_: str = '',  # event name (str)
                 timing_=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 = Background.image.copy()
        self.image = self.images_copy[0] if isinstance(Background.image, list) else self.images_copy
        self.rect = self.image.get_rect(topleft=position_)
        self.position = position_
        self.vector = vector_
        self.gl = gl_
        self.blend = blend_
        self.event_name = event_name_
        self.id_ = id(self)
        self.rotation = 0
        self.timing = timing_
        self.dt = 0
        if self.event_name == 'STATION':
            self.rotation = 0
            self.background_object = Broadcast(self.make_rotation_object())
        else:
            self.background_object = Broadcast(self.make_object())

    def make_object(self) -> StaticSprite:
        return StaticSprite(frame_=self.gl.FRAME, id_=self.id_, surface_=self.event_name,
                            layer_=self.layer, blend_=self.blend, rect_=self.rect)

    def make_rotation_object(self) -> RotateSprite:
        return RotateSprite(frame_=self.gl.FRAME, id_=self.id_, surface_=self.event_name,
                            layer_=self.layer, blend_=self.blend, rect_=self.rect, rotation_=self.rotation)

    def process(self):

        if self.event_name == 'CL1':
            self.rect.move_ip(self.vector)
            if self.rect.y > 1023:
                self.rect.y = randint(-1024, - CL1.get_height())
                self.rect.x = randint(-400, 400)
            self.background_object.update({'frame': self.gl.FRAME, 'rect': self.rect})
            self.background_object.queue()

        elif self.event_name == 'CL2':
            self.rect.move_ip(self.vector)
            if self.rect.y > 1023:
                self.rect.y = randint(-1024, - CL2.get_height())
                self.rect.x = randint(-400, 400)
            self.background_object.update({'frame': self.gl.FRAME, 'rect': self.rect})
            self.background_object.queue()

        elif self.event_name == 'BLUE_PLANET':
            self.rect.move_ip(self.vector)
            if self.rect.y > 1023:
                self.rect.y = randint(-1024, - BLUE_PLANET.get_height())
                self.rect.x = randint(-400, 400)
            self.background_object.update({'frame': self.gl.FRAME,
                                           'rect': self.rect})
            self.background_object.queue()

        elif self.event_name == 'STATION':
            # below 8192 frames the station is not on sight

            if self.gl.FRAME < 12280:
                self.rect.move_ip(self.vector)

            # no need to rotate station if not on sight
            if self.rect.colliderect(self.gl.SCREENRECT):
                centre = self.rect.center
                self.image = pygame.transform.rotate(self.images_copy.copy(), self.rotation)
                self.rect = self.image.get_rect(center=centre)
                self.background_object.update({'frame': self.gl.FRAME,
                                               'rect': self.rect,
                                               'rotation': self.rotation})
                self.background_object.queue()
                self.rotation += 0.2

        else:
            if self.event_name in ('BACK1_S', 'BACK2_S'):
                self.rect.move_ip(self.vector)
                if self.rect.y > 1023:
                    self.rect.y = -1024

                self.background_object.update({'frame': self.gl.FRAME, 'rect': self.rect})
                self.background_object.queue()

            elif self.event_name == 'BACK3':
                if self.gl.FRAME < 12288:
                    self.rect.move_ip(self.vector)

                # if self.gl.FRAME > 12288:
                #    self.rect.y = 0

                self.background_object.update({'frame': self.gl.FRAME, 'rect': self.rect})
                self.background_object.queue()

            # Any other background type
            else:
                self.rect.move_ip(self.vector)
                if self.rect.y > 1023:
                    self.rect.y = -1024
                    self.rect.x = 0
                self.background_object.update({'frame': self.gl.FRAME, 'rect': self.rect})
                self.background_object.queue()

    def update(self):

        if self.timing != 0:
            # update frequently
            if self.dt > self.timing:
                self.process()
                self.dt = 0
            else:
                self.dt += self.gl.TIME_PASSED_SECONDS

        # update every frames
        else:
            self.process()
예제 #2
0
class AfterBurner(pygame.sprite.Sprite):

    containers = None
    images = None

    def __init__(self,
                 parent_,
                 gl_,
                 offset_: tuple,
                 timing_: int = 8,
                 blend_: int = 0,
                 layer_: int = 0,
                 texture_name_='EXHAUST'):
        """
        Create an exhaust effect for the player's

        :param parent_: Player's instance (MirroredPlayer1Class or MirroredPlayer2Class)
        :param gl_: Class GL (contains all the game constants
        :param offset_: tuple, offset location of the afterburner sprite (offset from the center)
        :param timing_: integer; Sprite refreshing time must be > 0
        :param blend_: integer; Sprite blending effect, must be > 0 or any of the following BLEND_RGBA_ADD,
        BLEND_RGBA_SUB, BLEND_RGBA_MULT, BLEND_RGBA_MIN, BLEND_RGBA_MAX BLEND_RGB_ADD,
        BLEND_RGB_SUB, BLEND_RGB_MULT, BLEND_RGB_MIN, BLEND_RGB_MAX
        :param layer_: integer; must be <= 0 (0 is the top layer)
        :param texture_name_: string corresponding to the texture used.
        """

        if parent_ is None:
            raise ValueError('Positional argument <parent_> cannot be None.')
        if gl_ is None:
            raise ValueError('Positional argument <gl_> cannot be None.')
        if offset_ is None:
            raise ValueError('Positional argument <offset_> cannot be None.')

        assert isinstance(offset_, tuple), \
            "Positional argument <offset_> is type %s , expecting tuple." % type(offset_)
        assert isinstance(timing_, int), \
            "Positional argument <timing_> is type %s , expecting integer." % type(timing_)
        assert isinstance(blend_, int), \
            "Positional argument <blend_> is type %s , expecting integer." % type(blend_)
        assert isinstance(layer_, int), \
            "Positional argument <layer_> is type %s , expecting integer." % type(layer_)
        assert isinstance(texture_name_, str), \
            "Positional argument <texture_name_> is type %s , expecting str." % type(texture_name_)

        if self.containers is None:
            raise ValueError(
                'AfterBurner.containers is not initialised.\nMake sure to assign the containers to'
                ' a pygame group prior instantiation.\ne.g: AfterBurner.containers = '
                'pygame.sprite.Group()')
        if self.images is None:
            raise ValueError(
                "AfterBurner.images is not initialised.\nMake sure to assign a texture to "
                "prior instantiation.\ne.g: AfterBurner.images = 'EXHAUST'")

        if timing_ < 0:
            raise ValueError('Positional argument timing_ cannot be < 0')

        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 = AfterBurner.images
        self.image = self.images[0] if isinstance(self.images,
                                                  list) else self.images
        self.parent = parent_
        self.offset = offset_
        x, y = self.parent.rect.centerx + self.offset[
            0], self.parent.rect.centery + self.offset[1]
        self.rect = self.image.get_rect(center=(x, y))
        self.timing = timing_
        self.dt = 0
        self.index = 0
        self.gl = gl_
        self.blend = blend_
        self.texture_name = texture_name_
        self.id_ = id(self)
        self.afterburner_object = Broadcast(self.make_object())

        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 make_object(self) -> AnimatedSprite:
        """
        Create a network object (AnimatedSprite)
        :return: 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) -> None:
        """
        Update the sprite.

        :return: None
        """
        if self.dt > self.timing:

            # checking if MirroredPlayer1Class is still alive
            if self.parent.alive():

                # display animation if self.images is a list.
                if isinstance(self.images, list):
                    self.image = self.images[self.index % len(self.images) - 1]

                x, y = self.parent.rect.centerx + self.offset[
                    0], self.parent.rect.centery + self.offset[1]
                self.rect.center = (x, y)

                if self.rect.colliderect(self.gl.SCREENRECT):
                    self.afterburner_object.update({
                        'frame': self.gl.FRAME,
                        'rect': self.rect,
                        'index': self.index
                    })
                    self.afterburner_object.queue()

                self.dt = 0
                self.index += 1
            else:
                self.quit()
                return
        else:
            self.dt += self.gl.TIME_PASSED_SECONDS
class ShootingStar(pygame.sprite.Sprite):

    image = None  # sprite surface (single surface)
    containers = None  # sprite group to use

    def __init__(
            self,
            gl_,  # global variables
            layer_=-4,  # layer where the shooting sprite will be display
            timing_=16,  # refreshing rate, default is 16ms (60 fps)
            surface_name_=''):

        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 = ShootingStar.image.copy()
        self.image = self.images_copy[0] if isinstance(
            ShootingStar.image, list) else self.images_copy
        self.w, self.h = pygame.display.get_surface().get_size()
        self.position = pygame.math.Vector2(randint(0, self.w),
                                            randint(-self.h, 0))
        self.rect = self.image.get_rect(midbottom=self.position)
        self.speed = pygame.math.Vector2(uniform(-30, 30), 60)
        self.rotation = -270 - int(degrees(atan2(self.speed.y, self.speed.x)))
        self.image = pygame.transform.rotozoom(self.image, self.rotation, 1)
        self.blend = pygame.BLEND_RGB_ADD
        self.timing = timing_
        self.gl = gl_
        self.dt = 0
        self.surface_name = surface_name_
        self.id_ = id(self)
        self.shooting_star_object = Broadcast(self.make_object())

        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.surface_name})

    def make_object(self) -> RotateSprite:
        return RotateSprite(frame_=self.gl.FRAME,
                            id_=self.id_,
                            surface_=self.surface_name,
                            layer_=self.layer,
                            blend_=self.blend,
                            rect_=self.rect,
                            rotation_=self.rotation)

    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.centery > self.h:
                self.quit()
                return
            self.rect = self.image.get_rect(center=self.position)
            self.position += self.speed
            if self.rect.colliderect(self.gl.SCREENRECT):
                self.shooting_star_object.update({
                    'frame': self.gl.FRAME,
                    'rect': self.rect,
                    'rotation': self.rotation
                })
                self.shooting_star_object.queue()

            self.dt = 0

        else:
            self.dt += self.gl.TIME_PASSED_SECONDS
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 Debris(pygame.sprite.Sprite):

    containers = None
    image = None

    def __init__(self,
                 asteroid_name_: str,
                 pos_: tuple,
                 gl_: GL,
                 blend_: int = 0,
                 timing_: int = 16,
                 layer_: int = -2,
                 particles_: bool = True):
        """
        Create debris after asteroid explosion or collision

        :param asteroid_name_: string; Parent name (not used)
        :param pos_: tuple, representing the impact position (x, y)
        :param gl_: class GL, global constants
        :param blend_: integer; Sprite blend effect, must be > 0 or any of the following BLEND_RGBA_ADD,
        BLEND_RGBA_SUB, BLEND_RGBA_MULT, BLEND_RGBA_MIN, BLEND_RGBA_MAX BLEND_RGB_ADD,
        BLEND_RGB_SUB, BLEND_RGB_MULT, BLEND_RGB_MIN, BLEND_RGB_MAX
        :param timing_: integer; Sprite refreshing time in milliseconds, must be >=0
        :param layer_: integer; Sprite layer must be <=0 (0 is the top layer)
        :param particles_: bool; Particles effect after asteroid desintegration
        """

        assert isinstance(asteroid_name_, str), \
            "Positional argument <asteroid_name_> is type %s , expecting string." % type(asteroid_name_)
        assert isinstance(pos_, tuple), \
            "Positional argument <pos_> is type %s , expecting tuple." % type(pos_)
        assert isinstance(timing_, int), \
            "Positional argument <timing_> is type %s , expecting integer." % type(timing_)
        assert isinstance(blend_, int), \
            "Positional argument <blend_> is type %s , expecting integer." % type(blend_)
        assert isinstance(layer_, int), \
            "Positional argument <layer_> is type %s , expecting integer." % type(layer_)
        assert isinstance(particles_, bool), \
            "Positional argument <particles_> is type %s , expecting boolean." % type(particles_)

        if self.containers is None:
            raise ValueError(
                'Debris.containers is not initialised.\nMake sure to assign the containers to'
                ' a pygame group prior instantiation.\ne.g: Debris.containers = '
                'pygame.sprite.Group()')
        if self.image is None:
            raise ValueError(
                "Debris.image is not initialised.\nMake sure to assign a texture to "
                "prior instantiation.\ne.g: Debris.image = 'CHOOSE_YOUR_TEXTURE'"
            )

        if timing_ < 0:
            raise ValueError('Positional argument timing_ cannot be < 0')
        if blend_ < 0:
            raise ValueError('Positional argument blend_ cannot be < 0')
        if layer_ > 0:
            raise ValueError('Positional argument layer_ cannot 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.image = Debris.image
        self.rect = self.image.get_rect(center=pos_)
        self.speed = pygame.math.Vector2(uniform(-10, +10), uniform(-8, +8))
        self.damage = randint(5, 15)
        self.timing = timing_
        self.dt = 0
        self.fxdt = 0
        self.gl = gl_
        self.asteroid_name = asteroid_name_  # not used
        self.blend = blend_
        self.layer = layer_

        self.life = self.damage
        self.points = self.life
        self.rotation = 0
        self.scale = 1.0
        self.id_ = id(self)
        self.asteroid_object = Broadcast(self.make_object())

        self.vertex_array = []

        # todo create instances in the vertex_array (declare in global)
        #  before instanciating debris.
        #  make a copy of the vertex_array and goes through the list changing arguments:
        #  position_, vector_. Assign the list to self.vertex_array
        if particles_:
            angle = math.radians(uniform(0, 359))
            self.asteroid_particles_fx(position_=pygame.math.Vector2(
                self.rect.center),
                                       vector_=pygame.math.Vector2(
                                           math.cos(angle) * randint(5, 10),
                                           math.sin(angle) * randint(5, 10)),
                                       images_=FIRE_PARTICLES.copy(),
                                       layer_=self.layer,
                                       blend_=pygame.BLEND_RGB_ADD)
        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_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 remove_particle(self, p_) -> None:
        """
        Remove the sprite from the group it belongs to and remove
        the object from the vertex_array
        :param p_: pygame.sprite.Sprite
        :return: None
        """
        p_.kill()
        if p_ in self.vertex_array:
            self.vertex_array.remove(p_)

    def display_asteroid_particles_fx(self) -> None:
        # Display asteroid tail debris.
        if self.fxdt > self.timing - 8:  # 8 ms

            for p_ in self.vertex_array:

                p_.image = p_.images[p_.index %
                                     p_.length]  # load the next surface
                p_.rect.move_ip(p_.vector)  # Move the particle
                p_.vector *= 0.9999  # particle deceleration / attenuation
                rect_centre = p_.rect.center  # rect centre after deceleration

                next_image = p_.images[(p_.index + 1) %
                                       p_.length]  # Load the next image
                # Decrease image dimensions (re-scale)
                try:
                    particle_size = pygame.math.Vector2(
                        p_.w - p_.index * 4, p_.h - p_.index * 4)
                    # transform next image (re-scale)
                    p_.images[(p_.index + 1) % p_.length] = \
                        pygame.transform.scale(next_image, (int(particle_size.x), int(particle_size.y)))
                    # Redefine the rectangle after transformation to avoid a collapsing movement
                    # to the right
                    p_.rect = p_.images[(p_.index + 1) %
                                        p_.length].get_rect(center=rect_centre)

                    # delete the particle before exception
                    if particle_size.length() < 25:
                        self.remove_particle(p_)

                except (ValueError, IndexError):
                    self.remove_particle(p_)

                if not p_.rect.colliderect(
                        self.gl.SCREENRECT) or p_.vector.length() < 1:
                    self.remove_particle(p_)

                p_.index += 1
            self.fxdt = 0
        else:
            self.fxdt += self.gl.TIME_PASSED_SECONDS

    def asteroid_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 bright debris after explosion or asteroid collision
        """
        # 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_.w, sprite_.h = sprite_.image.get_size()
        sprite_.index = 0
        sprite_.length = len(sprite_.images) - 1
        # assign update method to self.display_fire_particle_fx
        # (local method to display the particles)
        sprite_.update = self.display_asteroid_particles_fx
        self.vertex_array.append(sprite_)

    def collide(self, player_=None, damage_: int = 0) -> None:
        """

        :return:
        """
        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
        """
        self.quit()
        ...

    def quit(self) -> None:
        Broadcast.remove_object_id(self.id_)
        obj = self.delete_object()
        broadcast_object = Broadcast(obj)
        broadcast_object.queue()
        self.kill()
        ...

    def update(self) -> None:
        """
        Update debris sprite
        :return:
        """
        # Inside the 60FPS Area
        if self.dt > self.timing:

            if self.rect.colliderect(self.gl.SCREENRECT):
                self.rect.move_ip(self.speed)
            else:
                self.quit()
                return

            self.asteroid_object.update({
                'frame': self.gl.FRAME,
                'id_': self.id_,
                'rect': self.rect,
                'life': self.life
            })
            self.asteroid_object.queue()
            self.dt = 0
        else:
            self.dt += self.gl.TIME_PASSED_SECONDS
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 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