This commit is contained in:
2026-03-27 15:57:29 +01:00
18 changed files with 293 additions and 30 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 KiB

View File

@@ -1,24 +1,58 @@
import java.awt.*; import java.awt.*;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.swing.*; import javax.swing.*;
public class CadreDeConnexion extends JFrame { public class CadreDeConnexion extends JFrame {
//--------------------------------------------------------------------------
// Méthode peremttant de charger(récupérer) l'image îcone depuis Ressources
// -------------------------------------------------------------------------
private Image chargerImageDepuisRessource(String cheminRessource) {
// Cette méthode doit seulement charger l'image, pas rappeler elle-même.
try {
URL url = getClass().getResource(cheminRessource);
if (url != null) {
return ImageIO.read(url);
}
System.err.println("Ressource introuvable : " + cheminRessource);
} catch (Exception e) {
System.err.println("Erreur chargement image : " + e.getMessage());
}
return null;
}
public CadreDeConnexion() { public CadreDeConnexion() {
// 1. Configuration de base
setTitle("Connexion Linea"); // Image de fond de la page de connexion
Background panelBackground = null;
// 1. On change l'icône de CETTE fenêtre (this)
Image imageConnexion = chargerImageDepuisRessource("/images/icone.png");
if (imageConnexion != null) {
// On applique la même image comme icône de la fenêtre
this.setIconImage(imageConnexion);
// On prépare aussi le fond pour le panneau de connexion
panelBackground = new Background(imageConnexion);
}
// 2. Configuration de base
setTitle("ZENITH FLUX");
setSize(800, 600); setSize(800, 600);
setDefaultCloseOperation(EXIT_ON_CLOSE); setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocationRelativeTo(null); // Centre la fenêtre setLocationRelativeTo(null); // Centre la fenêtre
// 2. Style du panneau principal (noir ) // 2. Style du panneau principal (Gris foncé)
JPanel panel = new JPanel(new GridBagLayout()); // On utilise ZoneDessin pour pouvoir dessiner l'image en fond
panel.setBackground(new Color(30, 30, 30)); JPanel panel = new ZoneDessin(panelBackground);
add(panel); panel.setLayout(new GridBagLayout());
setContentPane(panel);
// Conteneur pour les éléments (pour les empiler verticalement) // Conteneur pour les éléments (pour les empiler verticalement)
Box box = Box.createVerticalBox(); Box box = Box.createVerticalBox();
// 3. Création des composants // 4. Création des composants
JLabel titre = new JLabel("CONNEXION"); JLabel titre = new JLabel("CONNEXION");
titre.setForeground(Color.WHITE); titre.setForeground(Color.WHITE);
titre.setFont(new Font("Arial", Font.BOLD, 24)); titre.setFont(new Font("Arial", Font.BOLD, 24));
@@ -32,7 +66,7 @@ public class CadreDeConnexion extends JFrame {
JButton loginBtn = BoutonsManager.creerBoutonConnexion(userField, passField, this); JButton loginBtn = BoutonsManager.creerBoutonConnexion(userField, passField, this);
// 4. Ajout des composants avec des espaces (Struts) // 5. Ajout des composants avec des espaces (Struts)
box.add(titre); box.add(titre);
box.add(Box.createVerticalStrut(30)); // Espace box.add(Box.createVerticalStrut(30)); // Espace
box.add(new JLabel("<html><font color='white'>Utilisateur :</font></html>")); box.add(new JLabel("<html><font color='white'>Utilisateur :</font></html>"));
@@ -49,6 +83,92 @@ public class CadreDeConnexion extends JFrame {
panel.add(box); // Ajoute la boîte au centre du GridBagLayout panel.add(box); // Ajoute la boîte au centre du GridBagLayout
// 5. Logique du bouton
loginBtn.addActionListener(e -> {
String user = userField.getText();
String pass = new String(passField.getPassword());
int userId = GestionBDD.verifierConnexion(user, pass);
if (userId != -1) {
dispose();
Jeu jeu = new Jeu(userId);
jeu.demarrer();
} else {
JOptionPane.showMessageDialog(this, "Acces refuse");
}
});
createBtn.addActionListener(evt -> {
JFrame createFrame = new JFrame("Creation de compte");
createFrame.setSize(800, 600);
createFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
createFrame.setLocationRelativeTo(null);
// On recharge la même image pour la fenêtre de création de compte
// afin de lui donner à la fois l'icône et le fond d'écran.
Background panelBackgroundCreation = null;
Image imageCreation = chargerImageDepuisRessource("/images/icone.png");
if (imageCreation != null) {
createFrame.setIconImage(imageCreation);
panelBackgroundCreation = new Background(imageCreation);
}
// Même principe que pour la page de connexion : le fond est dessiné par ZoneDessin
JPanel createPanel = new ZoneDessin(panelBackgroundCreation);
createPanel.setLayout(new GridBagLayout());
createFrame.setContentPane(createPanel);
Box createBox = Box.createVerticalBox();
JLabel createTitre = new JLabel("CREATION DE COMPTE");
createTitre.setForeground(Color.WHITE);
createTitre.setFont(new Font("Arial", Font.BOLD, 24));
createTitre.setAlignmentX(Component.CENTER_ALIGNMENT);
JTextField newUserField = new JTextField(15);
newUserField.setMaximumSize(new Dimension(250, 30));
JPasswordField newPassField = new JPasswordField(15);
newPassField.setMaximumSize(new Dimension(250, 30));
JButton createAccountBtn = new JButton("Créer");
createAccountBtn.setBackground(new Color(70, 130, 180));
createAccountBtn.setForeground(Color.WHITE);
createAccountBtn.setFocusPainted(false);
createAccountBtn.setAlignmentX(Component.CENTER_ALIGNMENT);
createBox.add(createTitre);
createBox.add(Box.createVerticalStrut(30));
createBox.add(new JLabel("<html><font color='white'>Nouvel utilisateur :</font></html>"));
createBox.add(newUserField);
createBox.add(Box.createVerticalStrut(15));
createBox.add(new JLabel("<html><font color='white'>Mot de passe :</font></html>"));
createBox.add(newPassField);
createBox.add(Box.createVerticalStrut(30));
createBox.add(createAccountBtn);
createPanel.add(createBox);
createAccountBtn.addActionListener(e -> {
String u = newUserField.getText().trim();
String p = new String(newPassField.getPassword());
if (u.isEmpty() || p.isEmpty()) {
JOptionPane.showMessageDialog(createFrame, "Utilisateur et mot de passe requis");
return;
}
boolean ok = GestionBDD.creerUtilisateur(u, p);
if (ok) {
JOptionPane.showMessageDialog(createFrame, "Compte cree avec succes.");
createFrame.dispose();
} else {
JOptionPane.showMessageDialog(createFrame, "Échec : le nom d'utilisateur existe deja ou erreur.");
}
});
createFrame.setVisible(true);
});
setVisible(true); setVisible(true);
} }
} }

View File

@@ -11,6 +11,7 @@ public class Cercle extends ObjetGraphique{ // il s'agit plutôt d'arcs de cercl
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
// PROPRIETES // PROPRIETES
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
private double gravite = 10.0;
protected double rayon = 30; protected double rayon = 30;
protected double depY = 0; // déplacement protected double depY = 0; // déplacement
@@ -77,6 +78,12 @@ public class Cercle extends ObjetGraphique{ // il s'agit plutôt d'arcs de cercl
return y; return y;
} }
//-------------------------------------------------------------------------
// Méthode pour mettre à jour la gravité du cercle depuis la classe
//-------------------------------------------------------------------------
public void setGravite(double nouvelleGravite) {
this.gravite = nouvelleGravite;
}
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
// Méthode qui note que up a été appuyée // Méthode qui note que up a été appuyée
// -> le booléen permet de ne plus être tributaire // -> le booléen permet de ne plus être tributaire
@@ -118,17 +125,15 @@ public class Cercle extends ObjetGraphique{ // il s'agit plutôt d'arcs de cercl
void Animer() { void Animer() {
// pas est à prendre comme un "delta t" // pas est à prendre comme un "delta t"
if (!modeTriche) { // chute libre
// chute libre vitesse = vitesse + gravite * pas;
vitesse = vitesse + 9.81 * pas;
}
// impulsion (toujours appliquée, même en triche ?) // impulsion (toujours appliquée, même en triche ?)
if (montee==true) { if (montee==true) {
vitesse = vitesse - impulsion *pas; vitesse = vitesse - impulsion *pas;
} }
depY = 1/2 * 9.81 + vitesse * pas; depY = 1/2 * gravite + vitesse * pas;
if (depY<-10) { if (depY<-10) {
depY=-10; depY=-10;
@@ -144,7 +149,7 @@ public class Cercle extends ObjetGraphique{ // il s'agit plutôt d'arcs de cercl
// Méthode pour mettre un plafond et un sol au cercle // Méthode pour mettre un plafond et un sol au cercle
public void verification_cercle_out(){ public void verification_cercle_out(){
if(y < plafond || (y + rayon) >= sol ) { if(y < plafond || (y + rayon) >= sol ) {
vitesse = -vitesse; vitesse =-vitesse;
} }
} }

View File

@@ -14,7 +14,7 @@ public class Jeu implements KeyListener, ActionListener{
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
// Ecran : instance de ZoneDessin, qui contiendra tous les objets graphiques // Ecran : instance de ZoneDessin, qui contiendra tous les objets graphiques
// On ne vapas créer directement l'écran car ça va créer un flash // On ne va pas créer directement l'écran car ça va créer un flash
// Dès le lancement du jeu avec l'image de fond par défaut et l'imga du niveau // Dès le lancement du jeu avec l'image de fond par défaut et l'imga du niveau
// et les animera // et les animera
protected ZoneDessin ecran; protected ZoneDessin ecran;
@@ -96,13 +96,14 @@ public class Jeu implements KeyListener, ActionListener{
if( n1 != null ){ if( n1 != null ){
try { try {
// On charge l'mage du niveau 1 // On charge l'image du niveau 1
java.net.URL url = Jeu.class.getResource("/" + n1.getImage()); java.net.URL url = Jeu.class.getResource("/" + n1.getImage());
if(url != null){ if(url != null){
java.awt.Image img = javax.imageio.ImageIO.read(url); java.awt.Image img = javax.imageio.ImageIO.read(url);
premierFond = new Background(img); premierFond = new Background(img);
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace();
} }
} }
@@ -268,6 +269,18 @@ public class Jeu implements KeyListener, ActionListener{
this.dernierSegmentValide = null; this.dernierSegmentValide = null;
this.ecran.partiePerdue = false; this.ecran.partiePerdue = false;
// Formule : Gravité de base - (Niveau * incrément)
// Niveau 1 : 10.0 + (1 * 1.50) = 8.5 (poids normal)
// Niveau 4 : 10.0 + (4 * 1.50) = 4.0 (Très léger et plane)
double graviteNiveau = 10.0 - (this.niveauEnCours.getId() * 1.5);
// Sécurité: La gravité du niveau ne doit pas descendre en dessous de 2
// Sinon le cercle ne retomberait plus jamais
if(graviteNiveau < 2.0){
graviteNiveau = 2.0;
}
this.demiCercleAvant.setGravite(graviteNiveau);
this.demiCercleArriere.setGravite(graviteNiveau);
// On corse le jeu avec son ID // On corse le jeu avec son ID
//On applique les paramètres aux options de jeu //On applique les paramètres aux options de jeu
@@ -302,7 +315,7 @@ public class Jeu implements KeyListener, ActionListener{
// Petite pause avec un message // Petite pause avec un message
horloge.stop(); horloge.stop();
JOptionPane.showMessageDialog(fenetre, "Niveau terminé ! Préparez-vous pour le niveau suivant ;)"); JOptionPane.showMessageDialog(fenetre, "Niveau terminé ! Préparez-vous pour le niveau suivant 😉 OU PERDEZ 😈");
chargerNiveau(idNiveauActuel); chargerNiveau(idNiveauActuel);
horloge.start(); horloge.start();
@@ -323,11 +336,14 @@ public class Jeu implements KeyListener, ActionListener{
itemCourant.y = this.laLigne.getYSurLigne(itemCourant.x) - 40; itemCourant.y = this.laLigne.getYSurLigne(itemCourant.x) - 40;
} }
// Mode triche : centrer le cercle sur le milieu du segment courant // Mode triche : Le cercle suit la ligne
// On utilisera le calcul pour déterminer si la ligne traverse le cercle
if (modeTriche && this.laLigne.getSegCourant() != null) { if (modeTriche && this.laLigne.getSegCourant() != null) {
double milieuY = this.laLigne.getSegCourant().getMilieuY(); // double yPoint = this.getSegCourant().y + (this.SegCourant.yLong / this.SegCourant.xLong) * (this.xCercle - this.SegCourant.x);
this.demiCercleAvant.y = milieuY; double yPointTricheArrire = this.laLigne.getSegCourant().getY() + (this.laLigne.getSegCourant().getYLong() / this.laLigne.getSegCourant().getXLong()) * (this.demiCercleArriere.getX() - this.laLigne.getSegCourant().getX());
this.demiCercleArriere.y = milieuY; double yPointTricheAvant = this.laLigne.getSegCourant().getY() + (this.laLigne.getSegCourant().getYLong() / this.laLigne.getSegCourant().getXLong()) * (this.demiCercleAvant.getX() - this.laLigne.getSegCourant().getX());
this.demiCercleAvant.y = yPointTricheArrire ;
this.demiCercleArriere.y = yPointTricheAvant;
} }
if (itemCourant.estTouche(demiCercleAvant)) { if (itemCourant.estTouche(demiCercleAvant)) {

View File

@@ -173,6 +173,7 @@ public class Ligne extends ObjetGraphique{// Hérite de la classe ObjetGraphique
} }
return dansCercle; return dansCercle;
} }
public Segment getSegCourant() { public Segment getSegCourant() {
return this.SegCourant; return this.SegCourant;
} }

View File

@@ -0,0 +1,113 @@
import javax.swing.*;
import java.awt.*;
// ★★★ CLASSE CREEE PAR L'UTILISATEUR ★★★
// Fichier : MenuPrincipal.java (nouveau fichier)
// Objectif : afficher un menu d'accueil APRES la connexion réussie
// Remplace : l'ancien démarrage direct du jeu après connexion
// Fonctionnalités : 2 boutons (Lancer le jeu / Voir mes scores)
// Fenêtre du menu principal affichée après la connexion
public class MenuPrincipal extends JFrame {
// ID de l'utilisateur connecté
private final int utilisateurId;
// Constructeur du menu principal
// Affiche un écran d'accueil avec deux options : "Lancer le jeu" et "Voir mes scores"
// ★★★ CONSTRUCTEUR CREE PAR L'UTILISATEUR ★★★
// Nouveau : il n'existait pas avant, crée le menu principal
public MenuPrincipal(int utilisateurId, String userName) {
// Sauvegarde l'ID pour l'utiliser lors des appels à la base de données
this.utilisateurId = utilisateurId;
// Configuration de la fenêtre
setTitle("Menu Principal - Linea"); // Titre de la fenêtre
setSize(800, 600); // Dimensions de la fenêtre
setDefaultCloseOperation(EXIT_ON_CLOSE); // Quitter l'app à la fermeture
setLocationRelativeTo(null); // Centre la fenêtre
// Crée le panneau principal avec un fond sombre
JPanel panel = new JPanel(new GridBagLayout()); // GridBagLayout pour centrage
panel.setBackground(new Color(30, 30, 30)); // Fond gris très foncé
add(panel);
// Crée une boîte verticale pour disposer les éléments de haut en bas
Box box = Box.createVerticalBox(); // Disposition verticale
box.setAlignmentX(Component.CENTER_ALIGNMENT); // Centré horizontalement
// Crée un titre de bienvenue personalisé avec le nom de l'utilisateur
JLabel titre = new JLabel("Bienvenue, " + userName + " !");
titre.setForeground(Color.WHITE); // Texte blanc
titre.setFont(new Font("Arial", Font.BOLD, 26)); // Police Arial gras 26pt
titre.setAlignmentX(Component.CENTER_ALIGNMENT); // Centré
// Crée le bouton "Lancer le jeu" pour démarrer une partie
JButton demarrer = new JButton("Lancer le jeu");
demarrer.setBackground(new Color(70, 130, 180)); // Couleur bleu acier
demarrer.setForeground(Color.WHITE); // Texte blanc
demarrer.setFocusPainted(false); // Pas de bordure au focus
demarrer.setMaximumSize(new Dimension(220, 40)); // Dimension du bouton
demarrer.setAlignmentX(Component.CENTER_ALIGNMENT); // Centré horizontalement
// Au clic, ferme ce menu et lance le jeu
// ★★★ ACTION MODIFIEE PAR L'UTILISATEUR ★★★
// Lance le jeu APRES fermeture du menu (ajout du menu principal)
demarrer.addActionListener(e -> {
// Ferme la fenêtre de menu
dispose();
// Crée une instance du jeu avec l'ID utilisateur (authentifié)
Jeu jeu = new Jeu(utilisateurId);
// Démarre l'affichage et l'initialisation du jeu
jeu.demarrer();
});
// Crée le bouton "Voir mes scores" pour afficher l'historique
// ★★★ NOUVEAU BOUTON PAR L'UTILISATEUR ★★★
// Nouvelle fonctionnalité : permet de voir les scores AVANT de lancer le jeu
// Utilise les méthodes : GestionBDD.recupererScoresUtilisateur()
// GestionBDD.recupererMeilleurScoreUtilisateur()
JButton scores = new JButton("Voir mes scores");
scores.setBackground(new Color(70, 130, 180)); // Couleur bleu acier (même que demarrer)
scores.setForeground(Color.WHITE); // Texte blanc
scores.setFocusPainted(false); // Pas de bordure au focus
scores.setMaximumSize(new Dimension(220, 40)); // Dimension du bouton
scores.setAlignmentX(Component.CENTER_ALIGNMENT); // Centré horizontalement
// Au clic, récupère et affiche les scores depuis la base de données
// ★★★ ACTION PAR L'UTILISATEUR ★★★
// Nouvelle action : afficher les scores historiques du joueur dans une popup
// Récupère : tous les scores + le meilleur score
scores.addActionListener(e -> {
// Récupère la liste de tous les scores de l'utilisateur depuis la BD
java.util.List<Integer> scoreList = GestionBDD.recupererScoresUtilisateur(utilisateurId);
// Récupère aussi le meilleur score (score maximum) depuis la BD
int meilleurScore = GestionBDD.recupererMeilleurScoreUtilisateur(utilisateurId);
// Construit un message texte avec tous les scores à afficher
StringBuilder message = new StringBuilder();
message.append("Meilleur score : ").append(meilleurScore).append("\n\n");
message.append("Tous les scores :\n");
// Ajoute chaque score à la liste, un par un
for (int s : scoreList) {
message.append(s).append("\n");
}
// Affiche les scores dans une boîte de dialogue popup
JOptionPane.showMessageDialog(this, message.toString(), "Mes Scores", JOptionPane.INFORMATION_MESSAGE);
});
// Ajoute tous les éléments dans la boîte
box.add(titre);
box.add(Box.createVerticalStrut(40)); // Espace de 40 pixels après le titre
box.add(demarrer);
box.add(Box.createVerticalStrut(15)); // Espace de 15 pixels entre les deux boutons
box.add(scores);
// Ajoute la boîte au panneau principal
panel.add(box);
}
}

View File

@@ -124,24 +124,24 @@ public class NiveauxDataConnect {
// le jeu est lancé // le jeu est lancé
// Pour corser le jeu, on utilisera l'id du niveau qui nous servira de difficulté // Pour corser le jeu, on utilisera l'id du niveau qui nous servira de difficulté
// --------------------------------Niveau 1 -------------------------------- // --------------------------------Niveau 1 --------------------------------
// nom = "Facile", vitesse_ligne = 5, nb_segments = 50, image = "images\ciel.jpeg", multiplicateur_score = 1, couleur_cercle = "bleu" // nom = "Facile", vitesse_ligne = 5, nb_segments = 50, image = "images\ciel.jpeg", multiplicateur_score = 10, couleur_cercle = "bleu"
// --------------------------------Niveau 2 -------------------------------- // --------------------------------Niveau 2 --------------------------------
// nom = "intermédaire", vitesse_ligne = 7, nb_segments = 100, image = "images\montagnes.jpg", multiplicateur_score = 2, couleur_cercle = "gris" // nom = "intermédaire", vitesse_ligne = 7, nb_segments = 100, image = "images\montagnes.jpg", multiplicateur_score = 20, couleur_cercle = "gris"
// --------------------------------Niveau 3 -------------------------------- // --------------------------------Niveau 3 --------------------------------
// nom = "Difficile", vitesse_ligne = 9, nb_segments = 100, image = "images\volcan.jpg", multiplicateur_score = 3, couleur_cercle = "marron" // nom = "Difficile", vitesse_ligne = 9, nb_segments = 100, image = "images\volcan.jpg", multiplicateur_score = 30, couleur_cercle = "marron"
// --------------------------------Niveau 4 -------------------------------- // --------------------------------Niveau 4 --------------------------------
// nom = "Ultime", vitesse_ligne = 10, nb_segments = 150, image = "images\espace.jpg", multiplicateur_score = 4, couleur_cercle = "violet" // nom = "Ultime", vitesse_ligne = 10, nb_segments = 150, image = "images\espace.jpg", multiplicateur_score = 50, couleur_cercle = "violet"
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
public static void insererNiveau(Connection conn) { public static void insererNiveau(Connection conn) {
String query1 = "INSERT INTO niveau(nom, vitesse_ligne, nb_segments, image, multiplicateur_score, couleur_cercle) " String query1 = "INSERT INTO niveau(nom, vitesse_ligne, nb_segments, image, multiplicateur_score, couleur_cercle) "
+ "VALUES('Facile', 5, 50, 'images/ciel.jpeg', 1, '#0000FF');"; + "VALUES('Facile', 5, 50, 'images/ciel.jpeg', 10, '#0000FF');";
String query2 = "INSERT INTO niveau(nom, vitesse_ligne, nb_segments, image, multiplicateur_score, couleur_cercle) " String query2 = "INSERT INTO niveau(nom, vitesse_ligne, nb_segments, image, multiplicateur_score, couleur_cercle) "
+ "VALUES('Intermédiaire', 7, 100, 'images/montagnes.jpg', 2, '#808080');"; + "VALUES('Intermédiaire', 7, 100, 'images/montagnes.jpg', 20, '#808080');";
String query3 = "INSERT INTO niveau(nom, vitesse_ligne, nb_segments, image, multiplicateur_score, couleur_cercle) " String query3 = "INSERT INTO niveau(nom, vitesse_ligne, nb_segments, image, multiplicateur_score, couleur_cercle) "
+ "VALUES('Difficile', 9, 100, 'images/volcan.jpg', 3, '#8B4513');"; + "VALUES('Difficile', 9, 100, 'images/volcan.jpg', 30, '#8B4513');";
String query4 = "INSERT INTO niveau(nom, vitesse_ligne, nb_segments, image, multiplicateur_score, couleur_cercle) " String query4 = "INSERT INTO niveau(nom, vitesse_ligne, nb_segments, image, multiplicateur_score, couleur_cercle) "
+ "VALUES('Ultime', 10, 150, 'images/espace.jpg', 4, '#9400D3');"; + "VALUES('Ultime', 10, 150, 'images/espace.jpg', 50, '#9400D3');";
try (Statement stmt = conn.createStatement()) { try (Statement stmt = conn.createStatement()) {
stmt.execute(query1); stmt.execute(query1);

View File

@@ -32,6 +32,14 @@ public class Segment extends ObjetGraphique { // Hérite de la classe ObjetGraph
yLong = yyLong; yLong = yyLong;
} }
public double getYLong(){
return this.yLong;
}
public double getXLong(){
return this.xLong;
}
// Méthode pour obtenir la position Y au milieu du segment // Méthode pour obtenir la position Y au milieu du segment
public double getMilieuY() { public double getMilieuY() {
return y + yLong / 2; return y + yLong / 2;

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 KiB