import pyxel as px   # pip install pyxel
import random as rd
from typing import Any


#################
# COMMENT JOUER #
#################
#- on bouge le vaisseau avec les touches : zqsd

#- on tire avec la touche: espace

#- la barre verte qui apparaît a gauche de l'écran représente la chaleur de votre canon
# elle augmente lorsqu'on tire et baisse avec le temps, lorsqu'elle est rouge vous ne pouvez plus tirer car votre canon est en surchauffe

#- on utilises les antimissiles (leurs nombres est représenté par les cercles en haut à gauche de l'écran) avec la touche: altgr

#- l'objectif est d'avoir le score maximum (le score est représenter par le nombre tout en haut à gauche de l'écran)

#- les ennemis: n'apparaissent que se le boss n'est pas là et donne 1 point

#- le boss: n'apparaît qu'après avoir tué un certain nombre d'ennemi et donne 50 points (sa vie est représenté par la barre en haut de l'écran)




#parfois il y a des *9 ou des /9, c'est parce que 1 pixel a l'écran représente 9 pixels dans mon code

class Vaisseau:
    """
    Un vaisseau possede une position x et y, des vecteurs directeurs dx et dy,
    une largeur et une hauteur et aussi une liste de ses missiles
    """
    def __init__(self, p_x: int, p_y: int) -> None:
        self.x = p_x
        self.y = p_y
        self.dx = 0
        self.dy = 0
        self.width = 4
        self.height = 4
        self.missiles = []
    
    def nouveau_missile(self):
        """
        Le vaisseau ajoute un nouveau missile dans sa liste missiles
        """
        self.missiles.append(Missile(self .x + 2, self.y, 0, -1))

    def update(self):
        """
        On update les positions du vaisseau et de ses missiles
        """
        self.x += self.dx
        self.y += self.dy

        #ce code permet au vaisseau de na pas sortir de l'écran
        #(je ne sais pas pourquoi je n'ai pas fais pareil qu'avec les ennemi)
        if self.x < 0:
            self.x = 0
        elif (self.x + self.width)*9 > px.width:
            self. x = (px.width - (self.width *9))/9
        if self.y < 0:
            self.y = 0
        elif (self.y + self.width)*9 > px.height:
            self. y = (px.height - (self.height *9))/9
        
        #on récupere chaque missile et on leur demande de s'update, et s'il sortent de l'écran, alors on les supprimes
        for e in self.missiles:
            e.update()
            if e.y + e.height < 0:
                if e in self.missiles: #si parfois je fais ça c'est parce que sinon ça m'arrivait d'avoir des erreurs (même si ça n'a aucun sens)
                    self.missiles.remove(e)

    def draw(self):
        """
        Le vaisseau se dessine et demande à chacun de ses missiles de se dessiner
        """

        #en fonction de la direction on change de sprite pour donner l'impression qu'il tourne vraiment
        #(j'ai passer beaucoup de temps à faire les sprites pour qu'au final ça ne se voit presque pas)
        if self.dx > 0:
            px.blt(self.x * 9, self.y * 9, 1, 5, 40, 37, 37, 0)
        elif self.dx < 0:
            px.blt(self.x * 9, self.y * 9, 1, 5, 40, -37, 37, 0)
        else:
            px.blt(self.x * 9, self.y * 9, 1, 5, 0, 37, 37, 0)

        for e in self.missiles:
            e.draw()

class Missile:
    """
    Un Missile possède une position x et y, des vecteurs directeurs dx et dy,
    une largeur et une hauteur
    """
    def __init__(self, x:int, y:int, dx:int, dy:int) -> None:
        self.x = x
        self.y = y
        self.dx = dx
        self.dy = dy
        self.width = 1
        self.height = 3
    
    def update(self):
        """
        Le missile bouge en fonction de ses vecteurs directeurs
        """
        self.x += self.dx * 2
        self.y += self.dy * 2
    
    def draw(self):
        """
        Le missile sait se dessiner et changer de sprite en fonction de sa direction
        """
        if self.dy < 0:
            px.blt(self.x * 9, self.y * 9, 1, 44, 0, 8, 23, 0)
        else:
            px.blt(self.x * 9, self.y * 9, 1, 60, 0, 8, 23, 0)

class Laser:
    """
    Un laser possede une position x et y, une largeur et une hauteur,
    une couleur (à la base elle servait à changer de couleur mais maintenant ça me sert a savoir quel sprite je dois mettre),
    """
    def __init__(self, x:int, y:int, col:px.colors) -> None:
        self.x = x
        self.y = y
        self.col = col
        self.width = 3
        self.height = px.height
    
    def draw(self):
        """
        Le laser sait se dessiner
        """
        #on affiche une partie du laser jusqu'à ce qu'il sorte de l'écran
        x = self.x* 9
        y = self.y* 9
        if self.col == px.COLOR_ORANGE: #on affiche le sprite du laser inoffensif
            for i in range(100):
                if i == 0:#le début de laser
                    px.blt(x, y, 1, 41, 168, 15, 16, 0)
                else:
                    px.blt(x, y, 1, 41, 176, 15, 16, 0)
                y += 16
        else: #on affiche le sprite du laser
            for i in range(100):
                if i == 0:#le début de laser
                    px.blt(x, y, 1, 61, 168, 29, 16, 0)
                else:
                    px.blt(x, y, 1, 61, 176, 29, 16, 0)
                y += 16

class Ennemi:
    """
    Un ennemi possède une position x et y, des vecteurs directeurs dx (choisi aléatoirement) et dy,
    une largeur et une hauteur et aussi une liste de ses missiles
    """
    def __init__(self, p_x: int, p_y: int) -> None:
        self.x = p_x
        self.y = p_y
        self.dx = rd.choice([-1,1])
        self.dy = 1
        self.width = 4
        self.height = 4
        self.missiles = []
    
    def update(self):
        """
        On update les positions de l'ennemi et de ses missiles
        """
        if px.frame_count % 15 == 0: #permet de faire descendre l'ennemi plus lentement
            self.y += self.dy
        if px.frame_count % 45 == 0: #on choisi une direction en x aléatoire: -1 -> gauche, 0 -> rien, 1 -> droite
            self.dx = rd.randint(-1,1)
        if px.frame_count % 2 == 0: #l'ennemi ne bouge qu'une fois sur 2 et que s'il ne sort pas de l'écran
            if (self.x + self.width)*9 < px.width and self.dx == 1:
                self.x += self.dx
            elif self.x > 0 and self.dx == -1:
                self.x += self.dx
        
        #on récupere chaque missile et on leur demande de s'update, et s'il sortent de l'écran, alors on les supprimes
        for e in self.missiles:
            e.update()
            if e.y > px.height/9:
                if e in self.missiles:
                    self.missiles.remove(e)
    
    def draw(self):
        """
        L'ennemi se dessine et demande à chacun de ses missiles de se dessiner
        """
        px.blt(self.x* 9, self.y* 9, 0, 4, 2, 35, 33, 0)
        for e in self.missiles:
            e.draw()

class Boss:
    """
    Un boss possède une position x et y, un vecteurs directeurs dx, des points de vie,
    une largeur et une hauteur et aussi une liste de ses missiles
    """
    def __init__(self, p_x: int, p_y: int) -> None:
        self.x = p_x
        self.y = p_y
        self.dx = 0
        self.hp = 70
        self.width = 7
        self.height = 6
        self.missiles = []

    def update(self):
        """
        On update les positions du boss et de ses missiles
        """
        #permet de faire descendre le boss lors de son apparition
        if self.y < 3:
            self.y += 1
        else:
            if px.frame_count % 45 == 0: #on choisi une direction en x aléatoire: -1 -> gauche, 0 -> rien, 1 -> droite
                self.dx = rd.randint(-1,1)
            if px.frame_count % 2 == 0: #l'ennemi ne bouge qu'une fois sur 2
                #(le boss ne va jamais sur les bords de l'écran pour qu'on puisse fuir s'il fait son laser)
                if (self.x + self.width)*9 < px.width - (self.width*9)/2 and self.dx == 1:
                    self.x += self.dx
                elif self.x > 5 and self.dx == -1:
                    self.x += self.dx
        
        #on récupere chaque missile et on leur demande de s'update, et s'il sortent de l'écran, alors on les supprimes
        for e in self.missiles:
            e.update()
            if e.x < 0 or e.x > px.width - 1 or e.y < 0 or e.y > px.height - 3:
                if e in self.missiles:
                    self.missiles.remove(e)
    
    def draw(self):
        """
        Le boss se dessine, affiche ses points de vie et demande à chacun de ses missiles de se dessiner
        """
        px.blt(self.x* 9, self.y* 9, 1, 21, 115, 61, 50, 0)
        for i in range(self.hp):
            px.rect(i* 6, 0, 1* 6, 2* 6, 2)
        for e in self.missiles:
            e.draw()

class Jeu:
    """
    un Jeu représente le chef d'orchestre.
    """
    def __init__(self) -> None:
        self.bgcolor = px.COLOR_BLACK
        self.vaisseau = Vaisseau(23, 50)
        self.ennemis = []
        self.perdu = True
        self.explosions = []
        self.can_shoot = True
        self.anti_missile = 3
        self.boss = None
        self.score = 0
        self.nombre_e = [0, rd.randint(10,25)]
        self.surchauffe = 0
        self.cooldown_surchauffe = None
        self.color_surchauffe = px.COLOR_GREEN
        self.laser = None
        self.is_laser = False
        self.etoiles = [[rd.randint(0, 416), rd.randint(0, 596)] for _ in range(rd.randint(5, 15))]
    
    def collision_vaisseau(self):
        """
        cette fonction gère les collisions avec le vaisseau
        """
        for ennemi in self.ennemis:
                if Jeu.collision(self.vaisseau, ennemi):
                    self.ennemis.remove(ennemi)
                    self.perdu = True
                    px.play(0, 7)
                for tir in ennemi.missiles:
                    if Jeu.collision(self.vaisseau, tir):
                        ennemi.missiles.remove(tir)
                        self.perdu = True
                        px.play(0, 7)
        if self.boss != None:
            if Jeu.collision(self.vaisseau, self.boss):
                    self.perdu = True
                    px.play(0, 7)
            for tir in self.boss.missiles:
                if Jeu.collision(self.vaisseau, tir):
                    self.perdu = True
                    px.play(0, 7)
        if self.laser != None and self.laser.col == px.COLOR_RED and Jeu.collision(self.vaisseau, self.laser):
                    self.perdu = True
                    px.play(0, 7)
    
    def restart(self):
        """
        cette fonction réinitalise toutes les variables du jeu si on appuie sur espace
        """
        if px.btnp(px.KEY_SPACE):
            px.play(0, 1)
            self.ennemis = []
            self.perdu = False
            self.explosions = []
            self.can_shoot = True
            self.anti_missile = 3
            self.boss = None
            self.score = 0
            self.nombre_e = [0, rd.randint(10,25)]
            self.surchauffe = 0
            self.cooldown_surchauffe = None
            self.color_surchauffe = px.COLOR_GREEN
            self.laser = None
            self.is_laser = False
            self.can_shoot = px.frame_count + 50
    
    def animation_explosion(self):
        """
        cette fonction sert à faire l'explosion de l'antimissile
        à chaque frame le cercle augmente de taille et change de couleur
        au bout de 15 frame elle s'arrête (car il y a en tout 16 couleur, dont le 0 -> noir)
        """
        for explosion in self.explosions:
            explosion[2] += 1
            if explosion[2] == 15:
                self.explosions.remove(explosion)
    
    def action_vaisseau(self):
        """
        cette fonction gère toutes les actions du vaisseau
        """

        #direction du vaisseau en fonction des touches appuyées
        if px.btn(px.KEY_Z) and self.vaisseau.dy != 1:
            self.vaisseau.dy = -1
        elif px.btn(px.KEY_S):
            self.vaisseau.dy = 1
        else: 
            self.vaisseau.dy = 0

        if px.btn(px.KEY_D) and self.vaisseau.dx != -1:
            self.vaisseau.dx = 1
        elif px.btn(px.KEY_Q):
            self.vaisseau.dx = -1
        else: 
            self.vaisseau.dx = 0
        
        #on dit au vaisseau de s'update
        self.vaisseau.update()

        if px.btn(px.KEY_SPACE) and self.can_shoot == True and self.cooldown_surchauffe == None:
            #on augmente la surchauffe si elle n'est pas au max, on tir et on joue le son du tir
            self.vaisseau.nouveau_missile()
            self.can_shoot = px.frame_count + 20 #permet de mettre un délai entre chaque tir (ici 20 frames)
            self.surchauffe += 2
            px.play(0, 0)
        elif px.frame_count % 60 == 0 and self.surchauffe > 0 and self.cooldown_surchauffe == None:
            #si on ne tire pas et qu'on est pas en surchauffe, alors la chaleur baisse
            self.surchauffe -= 1
        elif px.frame_count % 60 == 0 and self.surchauffe > 0:
            #si on ne tire pas et qu'on est en surchauffe, alors la chaleur baisse 2 fois plus vite
            self.surchauffe -= 2
            
        if self.surchauffe > 39:
            #si la surchauffe est plus grande que 39 (donc 40 car on va de 2 en 2),
            #alors on change la couleur de la barre et on met un cooldown pendant lequel on ne peut plus tirer
            self.color_surchauffe = px.COLOR_RED
            self.cooldown_surchauffe = px.frame_count + 60*4
        
        if self.cooldown_surchauffe == px.frame_count:
            #si le cooldown de la surchauffe est finit,
            #alors on remet la barre en vert et on met le cooldown à None
            self.color_surchauffe = px.COLOR_GREEN
            self.cooldown_surchauffe = None
        
        if self.can_shoot == px.frame_count:
            #si le cooldown pour tirer est finit, alors on peut tirer
            self.can_shoot = True
        
        if px.btnp(px.KEY_RALT) and self.anti_missile > 0:
            #si on a encore des antimissiles, alors on réduit le nombre d'antimissiles, on ajoute une explosions
            #et on supprime tous les missiles existant
            px.play(0, 8)
            self.anti_missile -= 1
            self.explosions.append([(self.vaisseau.x +1.5) *9, (self.vaisseau.y +1) *9, 0])
            if self.boss != None:
                self.boss.missiles = []
            for ennemi in self.ennemis:
                ennemi.missiles = []

    def action_ennemis(self):
        """
        cette fonction gère toutes les actions des ennemis
        """
        #la liste self.nombre_e contient le nombre d'ennemi tué et le nombre d'ennemi à tuer
        #fait spawn un ennemi selon un nombre aléatoire et si l'objectif d'ennemi est rempli
        if rd.randint(0, 80) == 0 and self.nombre_e[0] != self.nombre_e[1] :
            self.ennemis.append(Ennemi(rd.randint(0, 39), -5))
        
        #on prend chaque ennemi, on leur dit de s'update et s'il sorte de l'écran, alors on les supprime
        #ensuite on les fait tirer selon un nombre aléatoire et on joue le son du tir ennemi
        for ennemi in self.ennemis:
            ennemi.update()
            if (ennemi.y + 4)*9 > px.height:
                self.ennemis.remove(ennemi)
            if rd.randint(0, 100) == 0:
                px.play(0, 3)
                ennemi.missiles.append(Missile(ennemi.x + 2, ennemi.y, 0, 0.5))

            for tir in self.vaisseau.missiles:
                #on prend chaque tir du vaisseau et si un tir touche un ennemi, 
                # alors on les supprimes tous les deux, on joue le son d'explosion et on augmente le score de 1
                if Jeu.collision(ennemi, tir):
                    px.play(0, 6)
                    self.vaisseau.missiles.remove(tir)
                    if ennemi in self.ennemis:
                        self.ennemis.remove(ennemi)
                        self.score += 1
                        if self.nombre_e[0] != self.nombre_e[1]:
                            self.nombre_e[0] += 1
        if self.nombre_e[0] == self.nombre_e[1] and self.boss == None:
            #si l'objectif d'ennemi à tuer est rempli, alors on fait spawn un boss
            self.boss = Boss(30, -10)
    
    def action_boss(self):
        """
        cette fonction gère toutes les actions du boss
        """
        if self.boss.hp > 0:
            self.boss.update()
            if self.is_laser == False:
                if rd.randint(0, 800) > 0:
                    if rd.randint(0, 30) == 0:
                        #fait tirer le boss dans 3 direction selon un nombre aléatoire
                        px.play(0, 3) 
                        self.boss.missiles.append(Missile(self.boss.x + 2.5, self.boss.y + 5, 0, 0.5))
                        self.boss.missiles.append(Missile(self.boss.x + 2.5, self.boss.y + 5, 0.25, 0.25))
                        self.boss.missiles.append(Missile(self.boss.x + 2.5, self.boss.y + 5, -0.25, 0.25))
                else:
                    #on dit au jeu qu'un laser doit spawn en lui donnant un cooldown
                    self.is_laser = px.frame_count + 60*3
            
            if self.is_laser >= px.frame_count + 60*2:
                #pendant le 1er tiers on affiche un laser inofensifs afin d'indiquer au joueur ce qui va arriver
                px.play(0, 5)
                self.laser = Laser(self.boss.x + 2.5, self.boss.y + 6, px.COLOR_ORANGE)
            elif self.is_laser != False and self.is_laser >= px.frame_count:
                #ensuite on créer le laser qui peut détruire le vaisseau
                px.play(0, 4)
                self.laser = Laser(self.boss.x + 2, self.boss.y + 6, px.COLOR_RED)
            else:
                #on réinitialise les variables du laser si le cooldown est finit
                self.is_laser = False
                self.laser = None
        
            for tir in self.vaisseau.missiles:
                #on prend chaque tir du vaisseau et si un tir touche le boss, 
                #alors on les supprimes le tir et on baisse la vie du boss
                if Jeu.collision(self.boss, tir):
                    self.boss.hp -= 2
                    if tir in self.vaisseau.missiles:
                        self.vaisseau.missiles.remove(tir)
            if self.boss.hp <= 0:
                #si le boss n'a plus de vie, alors on le supprime, on crée un nouvel objecti d'ennemi a tuer et on augmante le score de 50
                px.play(0, 7)
                self.nombre_e = [0, rd.randint(10,25)]
                self.score += 50
                self.anti_missile += 1
                self.boss = None
                self.is_laser = False
                self.laser = None
    
    def affiche_nombres(x:int, y:int, nb:int):
        """
        permet d'afficher un nombre à l'écran à partir d'un int
        (car l'écran est trop grand par rapport à la taille du texte de pyxel)
        """
        nb = str(nb)
        for i in range(len(nb)):
            if nb[i] == '1':
                px.blt(x, y, 0, 0, 40, 16, 16, 0)
            elif nb[i] == '2':
                px.blt(x, y, 0, 0, 56, 16, 16, 0)
            elif nb[i] == '3':
                px.blt(x, y, 0, 0, 72, 16, 16, 0)
            elif nb[i] == '4':
                px.blt(x, y, 0, 0, 88, 16, 16, 0)
            elif nb[i] == '5':
                px.blt(x, y, 0, 0, 104, 16, 16, 0)
            elif nb[i] == '6':
                px.blt(x, y, 0, 0, 120, 16, 16, 0)
            elif nb[i] == '7':
                px.blt(x, y, 0, 0, 136, 16, 16, 0)
            elif nb[i] == '8':
                px.blt(x, y, 0, 0, 152, 16, 16, 0)
            elif nb[i] == '9':
                px.blt(x, y, 0, 0, 168, 16, 16, 0)
            elif nb[i] == '0':
                px.blt(x, y, 0, 0, 184, 16, 16, 0)
            x += 16
    
    def etoiles_update(self):
        """
        cette fonction ajoute des étoiles dans la liste etoiles,
        les fait bouger vers le bas et les supprimes si elles sortent de l'écran
        """
        if rd.randint(0, 50) == 0:
            self.etoiles.append([rd.randint(0, 416), -4])
        for e in self.etoiles:
            e[1] += 2
            if e[1] > 600:
                self.etoiles.remove(e)

    def collision(box1: Any, box2: Any) -> bool:
        """ Cette méthode détecte les collisions entre 2 boites
        rectangulaires. Les rectangles doivent avoir des attributs:
        x, y, width, height
        Elle renvoie True si les deux boîtes sont en collision.
        """
        return not (
            (box2.x >= box1.x + box1.width)
            or (box2.x + box2.width <= box1.x)
            or (box2.y >= box1.y + box1.height)
            or (box2.y + box2.height <= box1.y)
        )

    def update(self):
        """
        Le jeu sait demander à tout ce qu'il gère de se mettre à jour.
        """
        if not self.perdu:
            self.collision_vaisseau()
            if self.nombre_e[0] == self.nombre_e[1] and self.boss != None:
                self.action_boss()
            self.action_ennemis()
            self.animation_explosion()
        else:
            self.restart()
        self.action_vaisseau()
        self.etoiles_update()

    def draw(self):
        """
        Le jeu sait demander à tout ce qu'il gère de se dessiner après avoir effacé l'écran.
        """
        px.cls(self.bgcolor)
        #ETOILES
        for e in self.etoiles:
                px.blt(e[0], e[1], 0, 48, 0, 4, 4, 0)
        #SCORE
        Jeu.affiche_nombres(9, 18, self.score)

        if not self.perdu:
            #SURCHAUFFE
            if px.btn(px.KEY_SPACE) and self.can_shoot == True and self.cooldown_surchauffe != None:
                self.can_shoot = px.frame_count + 40
                px.play(0, 2)
                for i in range(self.surchauffe):
                    px.rect(3* 7, (-i + 51)* 7, 3* 7, 1* 7, self.color_surchauffe)
            else:
                for i in range(self.surchauffe):
                    px.rect(1* 7, (-i + 50)* 7, 3* 7, 1* 7, self.color_surchauffe)

            #ANTIMISSILES
            px.blt(1, 36, 0, 64, 0, 31, 30, 0)
            Jeu.affiche_nombres(40, 45, self.anti_missile)

            #BOSS, ENNEMIS ET LASER
            if self.nombre_e[0] == self.nombre_e[1] and self.boss != None:
                self.boss.draw()
            for e in self.ennemis:
                    e.draw()
            if self.is_laser != False:
                self.laser.draw()
            
            #EXPLOSIONS
            for explosion in self.explosions:
                px.circb(explosion[0]+ 3, explosion[1]+3, explosion[2] * 2, explosion[2])
            
        else:
            #LOGO
            px.blt(90, 0, 2, 0, 0, 255, 223, 0)
            #TEXTE: PRESS SPACE
            if px.frame_count % 30 > 10:
                px.blt(100, 250, 0, 0, 208, 247, 19, 0)
        self.vaisseau.draw()


px.init(420, 600, fps=45)
px.load("sprites.pyxres")#on importes les sprites et les sons
#Seul les sprites des vaisseaux ont été fait à partir de références
appli = Jeu()
px.run(appli.update, appli.draw)