- **Cleanup:** Suppression logs de débogage superflus, commentaires obsolètes. Standardisation logging. - **Documentation:** Ajout/Amélioration significative des commentaires JavaDoc (Game, GameStats, MainActivity, etc.). - **Game Logic:** Refactoring des méthodes pushX en processMove. Ajout validation et helpers (isIndexValid). Amélioration robustesse (deserialize, win/loss checks). Classe rendue totalement indépendante du contexte/prefs. - **Stats:** Suppression stat 'perfectGames'. Amélioration formatage temps/moyennes. - **Notifications:** Logique de cooldown/intervalle affinée dans NotificationService. Utilisation centralisée de NotificationHelper. Gestion permission robuste. - **MainActivity:** Refactoring affichage plateau (syncBoardView avec fonds + tuiles). Animation simplifiée (apparition/fusion seulement). Gestion améliorée des états de jeu (PLAYING, WON, GAME_OVER). Logique onResume/onPause/load/save affinée. Placeholders pour Menu/Multi clarifiés. - **UI:** Layouts de dialogues standardisés avec LinearLayout. Permissions AndroidManifest nettoyées. Ajustements mineurs (marges, couleurs, strings, style police).
3570 lines
162 KiB
Plaintext
3570 lines
162 KiB
Plaintext
<?xml version="1.0" encoding="utf-8"?>
|
||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||
xmlns:tools="http://schemas.android.com/tools">
|
||
|
||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||
|
||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||
<uses-permission android:name="android.permission.INTERNET" />
|
||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||
|
||
<application
|
||
android:allowBackup="true"
|
||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||
android:fullBackupContent="@xml/backup_rules"
|
||
android:icon="@mipmap/ic_launcher"
|
||
android:label="@string/app_name"
|
||
android:roundIcon="@mipmap/ic_launcher_round"
|
||
android:supportsRtl="true"
|
||
android:theme="@style/Theme.Best2048">
|
||
<activity
|
||
android:name=".MainActivity"
|
||
android:exported="true"
|
||
android:screenOrientation="portrait">
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.MAIN" />
|
||
<category android:name="android.intent.category.LAUNCHER" />
|
||
</intent-filter>
|
||
</activity>
|
||
<service
|
||
android:name=".NotificationService"
|
||
android:enabled="true"
|
||
android:exported="false" />
|
||
</application>
|
||
|
||
</manifest>// Fichier Game.java
|
||
/**
|
||
* Représente la logique métier du jeu 2048. Gère l'état du plateau de jeu,
|
||
* les déplacements, les fusions, le score de la partie en cours, et les conditions
|
||
* de victoire ou de défaite. Cette classe est conçue pour être indépendante du
|
||
* framework Android (pas de dépendance au Contexte ou aux SharedPreferences).
|
||
*/
|
||
package legion.muyue.best2048;
|
||
|
||
import androidx.annotation.NonNull;
|
||
import androidx.annotation.VisibleForTesting; // Pour les méthodes de test éventuelles
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
import java.util.Random;
|
||
|
||
public class Game {
|
||
|
||
/** Le plateau de jeu, une matrice 2D d'entiers. 0 représente une case vide. */
|
||
private int[][] board;
|
||
/** Générateur de nombres aléatoires pour l'ajout de nouvelles tuiles. */
|
||
private final Random randomNumberGenerator;
|
||
/** Score de la partie actuellement en cours. */
|
||
private int currentScore = 0;
|
||
/** Meilleur score global (reçu et stocké, mais non géré logiquement ici). */
|
||
private int highestScore = 0;
|
||
/** Taille du plateau de jeu (nombre de lignes/colonnes). */
|
||
private static final int BOARD_SIZE = 4;
|
||
/** Indicateur si la condition de victoire (>= 2048) a été atteinte. */
|
||
private boolean gameWon = false;
|
||
/** Indicateur si la partie est terminée (plus de mouvements possibles). */
|
||
private boolean gameOver = false;
|
||
|
||
/**
|
||
* Constructeur pour démarrer une nouvelle partie.
|
||
* Initialise un plateau vide, le score à 0, et ajoute deux tuiles initiales.
|
||
*/
|
||
public Game() {
|
||
this.randomNumberGenerator = new Random();
|
||
initializeNewBoard();
|
||
}
|
||
|
||
/**
|
||
* Constructeur pour restaurer une partie à partir d'un état sauvegardé.
|
||
* Le meilleur score (`highestScore`) doit être défini séparément via {@link #setHighestScore(int)}.
|
||
* Recalcule les états `gameWon` et `gameOver` en fonction du plateau fourni.
|
||
*
|
||
* @param board Le plateau de jeu restauré.
|
||
* @param score Le score courant restauré.
|
||
*/
|
||
public Game(int[][] board, int score) {
|
||
// Valider les dimensions du plateau fourni ? Pourrait être ajouté.
|
||
this.board = board;
|
||
this.currentScore = score;
|
||
this.randomNumberGenerator = new Random();
|
||
checkWinCondition();
|
||
checkGameOverCondition();
|
||
}
|
||
|
||
// --- Getters / Setters ---
|
||
|
||
/**
|
||
* Retourne la valeur de la tuile aux coordonnées spécifiées.
|
||
* @param row Ligne (0 à BOARD_SIZE-1).
|
||
* @param column Colonne (0 à BOARD_SIZE-1).
|
||
* @return Valeur de la tuile, ou 0 si les coordonnées sont invalides.
|
||
*/
|
||
public int getCellValue(int row, int column) {
|
||
if (isIndexValid(row, column)) {
|
||
return this.board[row][column];
|
||
}
|
||
return 0; // Retourne 0 pour indice invalide
|
||
}
|
||
|
||
/**
|
||
* Définit la valeur d'une tuile aux coordonnées spécifiées.
|
||
* Ne fait rien si les coordonnées sont invalides.
|
||
* @param row Ligne (0 à BOARD_SIZE-1).
|
||
* @param col Colonne (0 à BOARD_SIZE-1).
|
||
* @param value Nouvelle valeur de la tuile.
|
||
*/
|
||
public void setCellValue(int row, int col, int value) {
|
||
if (isIndexValid(row, col)) {
|
||
this.board[row][col] = value;
|
||
}
|
||
}
|
||
|
||
/** @return Le score actuel de la partie. */
|
||
public int getCurrentScore() { return currentScore; }
|
||
|
||
/** @return Le meilleur score connu par cet objet (défini via setHighestScore). */
|
||
public int getHighestScore() { return highestScore; }
|
||
|
||
/**
|
||
* Met à jour la valeur du meilleur score stockée dans cet objet Game.
|
||
* Typiquement appelé par la classe gérant la persistance (MainActivity).
|
||
* @param highScore Le meilleur score global à stocker.
|
||
*/
|
||
public void setHighestScore(int highScore) { this.highestScore = highScore; }
|
||
|
||
/** @return true si une tuile 2048 (ou plus) a été atteinte, false sinon. */
|
||
public boolean isGameWon() { return gameWon; }
|
||
|
||
/** @return true si aucune case n'est vide ET aucun mouvement/fusion n'est possible, false sinon. */
|
||
public boolean isGameOver() { return gameOver; }
|
||
|
||
/** Met à jour la valeur de gameWon si partie gagné **/
|
||
private void setGameWon(boolean won) {this.gameWon = won;}
|
||
|
||
/** Met à jour la valeur de gameWon si partie gagné **/
|
||
private void setGameOver(boolean over) {this.gameOver = over;}
|
||
|
||
/**
|
||
* Retourne une copie profonde du plateau de jeu actuel.
|
||
* Utile pour la sérialisation ou pour éviter des modifications externes non désirées.
|
||
* @return Une nouvelle matrice 2D représentant l'état actuel du plateau.
|
||
*/
|
||
public int[][] getBoard() {
|
||
int[][] copy = new int[BOARD_SIZE][BOARD_SIZE];
|
||
for(int i=0; i<BOARD_SIZE; i++) {
|
||
System.arraycopy(this.board[i], 0, copy[i], 0, BOARD_SIZE);
|
||
}
|
||
return copy;
|
||
}
|
||
|
||
/**
|
||
* (Ré)Initialise le plateau de jeu pour une nouvelle partie.
|
||
* Remplit le plateau de zéros, réinitialise le score et les états `gameWon`/`gameOver`,
|
||
* puis ajoute deux tuiles initiales.
|
||
*/
|
||
private void initializeNewBoard() {
|
||
this.board = new int[BOARD_SIZE][BOARD_SIZE]; // Crée une nouvelle matrice vide
|
||
this.currentScore = 0;
|
||
this.gameWon = false;
|
||
this.gameOver = false;
|
||
addNewTile(); // Ajoute la première tuile
|
||
addNewTile(); // Ajoute la seconde tuile
|
||
}
|
||
|
||
// --- Logique du Jeu ---
|
||
|
||
/**
|
||
* Ajoute une nouvelle tuile (2, 4, 8, etc., selon probabilités) sur une case vide aléatoire.
|
||
* Si le plateau est plein, cette méthode ne fait rien.
|
||
*/
|
||
public void addNewTile() {
|
||
List<int[]> emptyCells = findEmptyCells();
|
||
if (!emptyCells.isEmpty()) {
|
||
int[] randomCell = emptyCells.get(randomNumberGenerator.nextInt(emptyCells.size()));
|
||
int value = generateRandomTileValue();
|
||
setCellValue(randomCell[0], randomCell[1], value);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Trouve toutes les cellules vides sur le plateau.
|
||
* @return Une liste de tableaux d'entiers `[row, col]` pour chaque cellule vide.
|
||
*/
|
||
private List<int[]> findEmptyCells() {
|
||
List<int[]> emptyCells = new ArrayList<>();
|
||
for (int row = 0; row < BOARD_SIZE; row++) {
|
||
for (int col = 0; col < BOARD_SIZE; col++) {
|
||
if (board[row][col] == 0) {
|
||
emptyCells.add(new int[]{row, col});
|
||
}
|
||
}
|
||
}
|
||
return emptyCells;
|
||
}
|
||
|
||
/**
|
||
* Génère la valeur pour une nouvelle tuile en utilisant des probabilités prédéfinies.
|
||
* @return La valeur (2, 4, 8, ...).
|
||
*/
|
||
private int generateRandomTileValue() {
|
||
int randomValue = randomNumberGenerator.nextInt(10000); // Base 10000 pour pourcentages fins
|
||
if (randomValue < 8540) return 2; // 85.40%
|
||
if (randomValue < 9740) return 4; // 12.00%
|
||
if (randomValue < 9940) return 8; // 2.00%
|
||
if (randomValue < 9990) return 16; // 0.50%
|
||
if (randomValue < 9995) return 32; // 0.05%
|
||
if (randomValue < 9998) return 64; // 0.03%
|
||
if (randomValue < 9999) return 128;// 0.01%
|
||
return 256; // 0.01%
|
||
}
|
||
|
||
/**
|
||
* Tente de déplacer et fusionner les tuiles vers le HAUT.
|
||
* Met à jour le score interne et vérifie les états win/gameOver.
|
||
* @return true si le plateau a été modifié, false sinon.
|
||
*/
|
||
public boolean pushUp() { return processMove(MoveDirection.UP); }
|
||
|
||
/**
|
||
* Tente de déplacer et fusionner les tuiles vers le BAS.
|
||
* @return true si le plateau a été modifié, false sinon.
|
||
*/
|
||
public boolean pushDown() { return processMove(MoveDirection.DOWN); }
|
||
|
||
/**
|
||
* Tente de déplacer et fusionner les tuiles vers la GAUCHE.
|
||
* @return true si le plateau a été modifié, false sinon.
|
||
*/
|
||
public boolean pushLeft() { return processMove(MoveDirection.LEFT); }
|
||
|
||
/**
|
||
* Tente de déplacer et fusionner les tuiles vers la DROITE.
|
||
* @return true si le plateau a été modifié, false sinon.
|
||
*/
|
||
public boolean pushRight() { return processMove(MoveDirection.RIGHT); }
|
||
|
||
/** Énumération interne pour clarifier le traitement des mouvements. */
|
||
private enum MoveDirection { UP, DOWN, LEFT, RIGHT }
|
||
|
||
/**
|
||
* Méthode générique pour traiter un mouvement (déplacement et fusion) dans une direction donnée.
|
||
* Contient la logique de base partagée par les méthodes pushX.
|
||
* @param direction La direction du mouvement.
|
||
* @return true si le plateau a été modifié, false sinon.
|
||
*/
|
||
private boolean processMove(MoveDirection direction) {
|
||
boolean boardChanged = false;
|
||
// Itère sur l'axe perpendiculaire au mouvement
|
||
for (int i = 0; i < BOARD_SIZE; i++) {
|
||
boolean[] hasMerged = new boolean[BOARD_SIZE]; // Pour éviter double fusion sur l'axe de mouvement
|
||
// Itère sur l'axe du mouvement, dans le bon sens
|
||
int start = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? BOARD_SIZE - 2 : 1;
|
||
int end = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? -1 : BOARD_SIZE;
|
||
int step = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? -1 : 1;
|
||
|
||
for (int j = start; j != end; j += step) {
|
||
int row = (direction == MoveDirection.UP || direction == MoveDirection.DOWN) ? j : i;
|
||
int col = (direction == MoveDirection.LEFT || direction == MoveDirection.RIGHT) ? j : i;
|
||
|
||
if (getCellValue(row, col) != 0) {
|
||
int currentValue = getCellValue(row, col);
|
||
int currentRow = row;
|
||
int currentCol = col;
|
||
|
||
// Calcule la position cible après déplacement dans les cases vides
|
||
int targetRow = currentRow;
|
||
int targetCol = currentCol;
|
||
int nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
|
||
int nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);
|
||
|
||
while (isIndexValid(nextRow, nextCol) && getCellValue(nextRow, nextCol) == 0) {
|
||
targetRow = nextRow;
|
||
targetCol = nextCol;
|
||
nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
|
||
nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);
|
||
}
|
||
|
||
// Déplace la tuile si sa position cible est différente
|
||
if (targetRow != currentRow || targetCol != currentCol) {
|
||
setCellValue(targetRow, targetCol, currentValue);
|
||
setCellValue(currentRow, currentCol, 0);
|
||
boardChanged = true;
|
||
}
|
||
|
||
// Vérifie la fusion potentielle avec la case suivante dans la direction du mouvement
|
||
int mergeTargetRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
|
||
int mergeTargetCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);
|
||
int mergeIndex = (direction == MoveDirection.UP || direction == MoveDirection.DOWN) ? mergeTargetRow : mergeTargetCol;
|
||
|
||
if (isIndexValid(mergeTargetRow, mergeTargetCol) &&
|
||
getCellValue(mergeTargetRow, mergeTargetCol) == currentValue &&
|
||
!hasMerged[mergeIndex])
|
||
{
|
||
int newValue = currentValue * 2;
|
||
setCellValue(mergeTargetRow, mergeTargetCol, newValue);
|
||
setCellValue(targetRow, targetCol, 0); // La tuile qui fusionne disparaît
|
||
currentScore += newValue;
|
||
hasMerged[mergeIndex] = true;
|
||
boardChanged = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Vérifie les conditions de fin après chaque type de mouvement complet
|
||
checkWinCondition();
|
||
checkGameOverCondition();
|
||
return boardChanged;
|
||
}
|
||
|
||
/**
|
||
* Vérifie si les indices de ligne et colonne sont valides pour le plateau.
|
||
* @param row Ligne.
|
||
* @param col Colonne.
|
||
* @return true si les indices sont dans les limites [0, BOARD_SIZE-1].
|
||
*/
|
||
private boolean isIndexValid(int row, int col) {
|
||
return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
|
||
}
|
||
|
||
|
||
/**
|
||
* Sérialise l'état essentiel du jeu (plateau et score courant) pour sauvegarde.
|
||
* Format: "val,val,...,val,score"
|
||
* @return Chaîne représentant l'état du jeu.
|
||
*/
|
||
@NonNull
|
||
@Override
|
||
public String toString() {
|
||
StringBuilder sb = new StringBuilder();
|
||
for (int row = 0; row < BOARD_SIZE; row++) {
|
||
for (int col = 0; col < BOARD_SIZE; col++) { sb.append(board[row][col]).append(","); }
|
||
}
|
||
sb.append(currentScore);
|
||
return sb.toString();
|
||
}
|
||
|
||
/**
|
||
* Crée un objet Game à partir de sa représentation sérialisée (plateau + score).
|
||
* Le meilleur score doit être défini séparément après la création.
|
||
* @param serializedState La chaîne issue de {@link #toString()}.
|
||
* @return Une nouvelle instance de Game, ou null en cas d'erreur de format.
|
||
*/
|
||
public static Game deserialize(String serializedState) {
|
||
if (serializedState == null || serializedState.isEmpty()) return null;
|
||
String[] values = serializedState.split(",");
|
||
if (values.length != (BOARD_SIZE * BOARD_SIZE + 1)) return null; // +1 pour le score
|
||
int[][] newBoard = new int[BOARD_SIZE][BOARD_SIZE]; int index = 0;
|
||
try {
|
||
for (int row = 0; row < BOARD_SIZE; row++) {
|
||
for (int col = 0; col < BOARD_SIZE; col++) { newBoard[row][col] = Integer.parseInt(values[index++]); }
|
||
}
|
||
int score = Integer.parseInt(values[index]); // Le dernier élément est le score
|
||
return new Game(newBoard, score);
|
||
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { return null; }
|
||
}
|
||
|
||
/**
|
||
* Vérifie si la condition de victoire (une tuile >= 2048) est atteinte.
|
||
* Met à jour l'état interne `gameWon`.
|
||
*/
|
||
private void checkWinCondition() {
|
||
if (!gameWon) {
|
||
for (int r=0; r<BOARD_SIZE; r++) for (int c=0; c<BOARD_SIZE; c++) if (getCellValue(r,c) >= 2048) {
|
||
setGameWon(true); return;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vérifie si la condition de fin de partie est atteinte (plateau plein ET aucun mouvement possible).
|
||
* Met à jour l'état interne `gameOver`.
|
||
*/
|
||
private void checkGameOverCondition() {
|
||
if (hasEmptyCell()) { setGameOver(false); return; } // Pas game over si case vide
|
||
// Vérifie s'il existe au moins une fusion possible
|
||
for (int r=0; r<BOARD_SIZE; r++) for (int c=0; c<BOARD_SIZE; c++) {
|
||
int current = getCellValue(r,c);
|
||
if ((r>0 && getCellValue(r-1,c)==current) || (r<BOARD_SIZE-1 && getCellValue(r+1,c)==current) ||
|
||
(c>0 && getCellValue(r,c-1)==current) || (c<BOARD_SIZE-1 && getCellValue(r,c+1)==current)) {
|
||
setGameOver(false); return; // Fusion possible -> pas game over
|
||
}
|
||
}
|
||
setGameOver(true); // Aucune case vide et aucune fusion -> game over
|
||
}
|
||
|
||
/**
|
||
* Vérifie si le plateau contient au moins une case vide (valeur 0).
|
||
* @return true si une case vide existe, false sinon.
|
||
*/
|
||
private boolean hasEmptyCell() {
|
||
for (int r=0; r<BOARD_SIZE; r++) for (int c=0; c<BOARD_SIZE; c++) if (getCellValue(r,c)==0) return true;
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Retourne la valeur de la plus haute tuile présente sur le plateau.
|
||
* @return La valeur maximale trouvée, ou 0 si le plateau est vide.
|
||
*/
|
||
public int getHighestTileValue() {
|
||
int maxTile = 0;
|
||
for (int r=0; r<BOARD_SIZE; r++) for (int c=0; c<BOARD_SIZE; c++) if (board[r][c] > maxTile) maxTile = board[r][c];
|
||
return maxTile;
|
||
}
|
||
}// Fichier GameStats.java
|
||
/**
|
||
* Gère la collecte, la persistance (via SharedPreferences) et l'accès
|
||
* aux statistiques du jeu 2048, pour les modes solo et multijoueur (si applicable).
|
||
*/
|
||
package legion.muyue.best2048;
|
||
|
||
import android.annotation.SuppressLint;
|
||
import android.content.Context;
|
||
import android.content.SharedPreferences;
|
||
import java.util.concurrent.TimeUnit;
|
||
|
||
public class GameStats {
|
||
|
||
// --- Constantes pour SharedPreferences ---
|
||
private static final String PREFS_NAME = "Best2048_Prefs";
|
||
private static final String HIGH_SCORE_KEY = "high_score"; // Clé partagée avec Game/MainActivity
|
||
// Clés spécifiques aux statistiques
|
||
private static final String STATS_TOTAL_GAMES_PLAYED = "totalGamesPlayed";
|
||
private static final String STATS_TOTAL_GAMES_STARTED = "totalGamesStarted";
|
||
private static final String STATS_TOTAL_MOVES = "totalMoves";
|
||
private static final String STATS_TOTAL_PLAY_TIME_MS = "totalPlayTimeMs";
|
||
private static final String STATS_TOTAL_MERGES = "totalMerges";
|
||
private static final String STATS_HIGHEST_TILE = "highestTile";
|
||
private static final String STATS_OBJECTIVE_REACHED_COUNT = "numberOfTimesObjectiveReached";
|
||
private static final String STATS_PERFECT_GAMES = "perfectGames";
|
||
private static final String STATS_BEST_WINNING_TIME_MS = "bestWinningTimeMs";
|
||
private static final String STATS_WORST_WINNING_TIME_MS = "worstWinningTimeMs";
|
||
// ... (autres clés stats) ...
|
||
private static final String STATS_MP_GAMES_WON = "multiplayerGamesWon";
|
||
private static final String STATS_MP_GAMES_PLAYED = "multiplayerGamesPlayed";
|
||
private static final String STATS_MP_BEST_WINNING_STREAK = "multiplayerBestWinningStreak";
|
||
private static final String STATS_MP_TOTAL_SCORE = "multiplayerTotalScore";
|
||
private static final String STATS_MP_TOTAL_TIME_MS = "multiplayerTotalTimeMs";
|
||
private static final String STATS_MP_LOSSES = "totalMultiplayerLosses";
|
||
private static final String STATS_MP_HIGH_SCORE = "multiplayerHighScore";
|
||
|
||
|
||
// --- Champs de Statistiques ---
|
||
// Générales & Solo
|
||
private int totalGamesPlayed;
|
||
private int totalGamesStarted;
|
||
private int totalMoves;
|
||
private long totalPlayTimeMs;
|
||
private int totalMerges;
|
||
private int highestTile;
|
||
private int numberOfTimesObjectiveReached; // Nombre de victoires (>= 2048)
|
||
private int perfectGames; // Concept non défini ici
|
||
private long bestWinningTimeMs;
|
||
private long worstWinningTimeMs;
|
||
private int overallHighScore; // Meilleur score global
|
||
|
||
// Partie en cours (non persistées telles quelles)
|
||
private int currentMoves;
|
||
private long currentGameStartTimeMs;
|
||
private int mergesThisGame;
|
||
|
||
// Multijoueur
|
||
private int multiplayerGamesWon;
|
||
private int multiplayerGamesPlayed;
|
||
private int multiplayerBestWinningStreak;
|
||
private long multiplayerTotalScore;
|
||
private long multiplayerTotalTimeMs;
|
||
private int totalMultiplayerLosses;
|
||
private int multiplayerHighestScore;
|
||
|
||
/** Contexte nécessaire pour accéder aux SharedPreferences. */
|
||
private final Context context;
|
||
|
||
/**
|
||
* Constructeur. Initialise l'objet et charge immédiatement les statistiques
|
||
* depuis les SharedPreferences.
|
||
* @param context Contexte de l'application.
|
||
*/
|
||
public GameStats(Context context) {
|
||
this.context = context.getApplicationContext(); // Utilise le contexte applicatif
|
||
loadStats();
|
||
}
|
||
|
||
// --- Persistance (SharedPreferences) ---
|
||
|
||
/**
|
||
* Charge toutes les statistiques persistantes depuis les SharedPreferences.
|
||
* Appelé par le constructeur.
|
||
*/
|
||
public void loadStats() {
|
||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||
overallHighScore = prefs.getInt(HIGH_SCORE_KEY, 0);
|
||
totalGamesPlayed = prefs.getInt(STATS_TOTAL_GAMES_PLAYED, 0);
|
||
totalGamesStarted = prefs.getInt(STATS_TOTAL_GAMES_STARTED, 0);
|
||
// ... (chargement de toutes les autres clés persistantes) ...
|
||
totalMoves = prefs.getInt(STATS_TOTAL_MOVES, 0);
|
||
totalPlayTimeMs = prefs.getLong(STATS_TOTAL_PLAY_TIME_MS, 0);
|
||
totalMerges = prefs.getInt(STATS_TOTAL_MERGES, 0);
|
||
highestTile = prefs.getInt(STATS_HIGHEST_TILE, 0);
|
||
numberOfTimesObjectiveReached = prefs.getInt(STATS_OBJECTIVE_REACHED_COUNT, 0);
|
||
perfectGames = prefs.getInt(STATS_PERFECT_GAMES, 0);
|
||
bestWinningTimeMs = prefs.getLong(STATS_BEST_WINNING_TIME_MS, Long.MAX_VALUE); // MAX_VALUE comme défaut pour 'best'
|
||
worstWinningTimeMs = prefs.getLong(STATS_WORST_WINNING_TIME_MS, 0);
|
||
multiplayerGamesWon = prefs.getInt(STATS_MP_GAMES_WON, 0);
|
||
multiplayerGamesPlayed = prefs.getInt(STATS_MP_GAMES_PLAYED, 0);
|
||
multiplayerBestWinningStreak = prefs.getInt(STATS_MP_BEST_WINNING_STREAK, 0);
|
||
multiplayerTotalScore = prefs.getLong(STATS_MP_TOTAL_SCORE, 0);
|
||
multiplayerTotalTimeMs = prefs.getLong(STATS_MP_TOTAL_TIME_MS, 0);
|
||
totalMultiplayerLosses = prefs.getInt(STATS_MP_LOSSES, 0);
|
||
multiplayerHighestScore = prefs.getInt(STATS_MP_HIGH_SCORE, 0);
|
||
}
|
||
|
||
/**
|
||
* Sauvegarde toutes les statistiques persistantes dans les SharedPreferences.
|
||
* Appelé typiquement dans `onPause` de l'activité.
|
||
*/
|
||
public void saveStats() {
|
||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||
SharedPreferences.Editor editor = prefs.edit();
|
||
editor.putInt(HIGH_SCORE_KEY, overallHighScore); // Sauvegarde le HS global
|
||
editor.putInt(STATS_TOTAL_GAMES_PLAYED, totalGamesPlayed);
|
||
editor.putInt(STATS_TOTAL_GAMES_STARTED, totalGamesStarted);
|
||
// ... (sauvegarde de toutes les autres clés persistantes) ...
|
||
editor.putInt(STATS_TOTAL_MOVES, totalMoves);
|
||
editor.putLong(STATS_TOTAL_PLAY_TIME_MS, totalPlayTimeMs);
|
||
editor.putInt(STATS_TOTAL_MERGES, totalMerges);
|
||
editor.putInt(STATS_HIGHEST_TILE, highestTile);
|
||
editor.putInt(STATS_OBJECTIVE_REACHED_COUNT, numberOfTimesObjectiveReached);
|
||
editor.putInt(STATS_PERFECT_GAMES, perfectGames);
|
||
editor.putLong(STATS_BEST_WINNING_TIME_MS, bestWinningTimeMs);
|
||
editor.putLong(STATS_WORST_WINNING_TIME_MS, worstWinningTimeMs);
|
||
editor.putInt(STATS_MP_GAMES_WON, multiplayerGamesWon);
|
||
editor.putInt(STATS_MP_GAMES_PLAYED, multiplayerGamesPlayed);
|
||
editor.putInt(STATS_MP_BEST_WINNING_STREAK, multiplayerBestWinningStreak);
|
||
editor.putLong(STATS_MP_TOTAL_SCORE, multiplayerTotalScore);
|
||
editor.putLong(STATS_MP_TOTAL_TIME_MS, multiplayerTotalTimeMs);
|
||
editor.putInt(STATS_MP_LOSSES, totalMultiplayerLosses);
|
||
editor.putInt(STATS_MP_HIGH_SCORE, multiplayerHighestScore);
|
||
|
||
editor.apply(); // Applique les changements de manière asynchrone
|
||
}
|
||
|
||
// --- Méthodes de Mise à Jour des Statistiques ---
|
||
|
||
/**
|
||
* Doit être appelée au début de chaque nouvelle partie.
|
||
* Incrémente le compteur de parties démarrées et réinitialise les stats de la partie en cours.
|
||
*/
|
||
public void startGame() {
|
||
totalGamesStarted++;
|
||
currentMoves = 0;
|
||
mergesThisGame = 0;
|
||
currentGameStartTimeMs = System.currentTimeMillis();
|
||
}
|
||
|
||
/**
|
||
* Enregistre un mouvement réussi (qui a modifié le plateau).
|
||
*/
|
||
public void recordMove() {
|
||
currentMoves++;
|
||
totalMoves++;
|
||
}
|
||
|
||
/**
|
||
* Enregistre une ou plusieurs fusions survenues lors d'un mouvement.
|
||
* @param numberOfMerges Le nombre de fusions (si connu, sinon 1 par défaut).
|
||
*/
|
||
public void recordMerge(int numberOfMerges) {
|
||
if (numberOfMerges > 0) {
|
||
mergesThisGame += numberOfMerges;
|
||
totalMerges += numberOfMerges;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Met à jour la statistique de la plus haute tuile atteinte globalement.
|
||
* @param tileValue La valeur de la tuile la plus haute de la partie en cours.
|
||
*/
|
||
public void updateHighestTile(int tileValue) {
|
||
if (tileValue > this.highestTile) {
|
||
this.highestTile = tileValue;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Enregistre une victoire et met à jour les temps associés.
|
||
* @param timeTakenMs Temps écoulé pour cette partie gagnante.
|
||
*/
|
||
public void recordWin(long timeTakenMs) {
|
||
numberOfTimesObjectiveReached++;
|
||
if (timeTakenMs < bestWinningTimeMs) { bestWinningTimeMs = timeTakenMs; }
|
||
if (timeTakenMs > worstWinningTimeMs) { worstWinningTimeMs = timeTakenMs; }
|
||
endGame(timeTakenMs); // Finalise aussi le temps total et parties jouées
|
||
}
|
||
|
||
/**
|
||
* Enregistre une défaite.
|
||
*/
|
||
public void recordLoss() {
|
||
// Calcule le temps écoulé avant de finaliser
|
||
endGame(System.currentTimeMillis() - currentGameStartTimeMs);
|
||
}
|
||
|
||
/**
|
||
* Finalise les statistiques générales à la fin d'une partie (victoire ou défaite).
|
||
* @param timeTakenMs Temps total de la partie terminée.
|
||
*/
|
||
public void endGame(long timeTakenMs) {
|
||
totalGamesPlayed++;
|
||
addPlayTime(timeTakenMs);
|
||
}
|
||
|
||
/**
|
||
* Ajoute une durée (en ms) au temps de jeu total enregistré.
|
||
* Typiquement appelé dans `onPause`.
|
||
* @param durationMs Durée à ajouter.
|
||
*/
|
||
public void addPlayTime(long durationMs) {
|
||
if (durationMs > 0) {
|
||
this.totalPlayTimeMs += durationMs;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Réinitialise toutes les statistiques (solo, multijoueur, high score global)
|
||
* à leurs valeurs par défaut.
|
||
*/
|
||
public void resetStats() {
|
||
// Réinitialise toutes les variables membres à 0 ou valeur initiale
|
||
overallHighScore = 0;
|
||
totalGamesPlayed = 0;
|
||
totalGamesStarted = 0;
|
||
totalMoves = 0;
|
||
totalPlayTimeMs = 0;
|
||
totalMerges = 0;
|
||
highestTile = 0;
|
||
numberOfTimesObjectiveReached = 0;
|
||
perfectGames = 0;
|
||
bestWinningTimeMs = Long.MAX_VALUE;
|
||
worstWinningTimeMs = 0;
|
||
|
||
multiplayerGamesWon = 0;
|
||
multiplayerGamesPlayed = 0;
|
||
multiplayerBestWinningStreak = 0;
|
||
multiplayerTotalScore = 0;
|
||
multiplayerTotalTimeMs = 0;
|
||
totalMultiplayerLosses = 0;
|
||
multiplayerHighestScore = 0;
|
||
|
||
saveStats();
|
||
}
|
||
|
||
// --- Getters pour l'affichage ---
|
||
public int getTotalGamesPlayed() { return totalGamesPlayed; }
|
||
public int getTotalGamesStarted() { return totalGamesStarted; }
|
||
public int getTotalMoves() { return totalMoves; }
|
||
public int getCurrentMoves() { return currentMoves; }
|
||
public long getTotalPlayTimeMs() { return totalPlayTimeMs; }
|
||
public long getCurrentGameStartTimeMs() { return currentGameStartTimeMs; } // Utile pour calcul durée en cours
|
||
public int getMergesThisGame() { return mergesThisGame; }
|
||
public int getTotalMerges() { return totalMerges; }
|
||
public int getHighestTile() { return highestTile; }
|
||
public int getNumberOfTimesObjectiveReached() { return numberOfTimesObjectiveReached; }
|
||
public int getPerfectGames() { return perfectGames; }
|
||
public long getBestWinningTimeMs() { return bestWinningTimeMs; }
|
||
public long getWorstWinningTimeMs() { return worstWinningTimeMs; }
|
||
public int getOverallHighScore() { return overallHighScore; } // Getter pour HS global
|
||
// Getters Multiplayer
|
||
public int getMultiplayerGamesWon() { return multiplayerGamesWon; }
|
||
public int getMultiplayerGamesPlayed() { return multiplayerGamesPlayed; }
|
||
public int getMultiplayerBestWinningStreak() { return multiplayerBestWinningStreak; }
|
||
public long getMultiplayerTotalScore() { return multiplayerTotalScore; }
|
||
public long getMultiplayerTotalTimeMs() { return multiplayerTotalTimeMs; }
|
||
public int getTotalMultiplayerLosses() { return totalMultiplayerLosses; }
|
||
public int getMultiplayerHighestScore() { return multiplayerHighestScore; }
|
||
|
||
// --- Setters ---
|
||
/** Met à jour la valeur interne du high score global. */
|
||
public void setHighestScore(int highScore) {
|
||
// Met à jour si la nouvelle valeur est meilleure
|
||
if (highScore > this.overallHighScore) {
|
||
this.overallHighScore = highScore;
|
||
// La sauvegarde se fait via saveStats() globalement
|
||
}
|
||
}
|
||
/** Définit le timestamp de début de la partie en cours. */
|
||
public void setCurrentGameStartTimeMs(long timeMs) { this.currentGameStartTimeMs = timeMs; }
|
||
|
||
// --- Méthodes Calculées ---
|
||
/** @return Temps moyen par partie terminée en millisecondes. */
|
||
public long getAverageGameTimeMs() { return (totalGamesPlayed > 0) ? totalPlayTimeMs / totalGamesPlayed : 0; }
|
||
/** @return Score moyen par partie multijoueur terminée. */
|
||
public int getMultiplayerAverageScore() { return (multiplayerGamesPlayed > 0) ? (int)(multiplayerTotalScore / multiplayerGamesPlayed) : 0; }
|
||
/** @return Temps moyen par partie multijoueur terminée en millisecondes. */
|
||
public long getMultiplayerAverageTimeMs() { return (multiplayerGamesPlayed > 0) ? multiplayerTotalTimeMs / multiplayerGamesPlayed : 0; }
|
||
|
||
/**
|
||
* Formate une durée en millisecondes en chaîne "hh:mm:ss" ou "mm:ss".
|
||
* @param milliseconds Durée en millisecondes.
|
||
* @return Chaîne de temps formatée.
|
||
*/
|
||
@SuppressLint("DefaultLocale")
|
||
public static String formatTime(long milliseconds) {
|
||
long hours = TimeUnit.MILLISECONDS.toHours(milliseconds);
|
||
long minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60;
|
||
long seconds = TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60;
|
||
if (hours > 0) { return String.format("%02d:%02d:%02d", hours, minutes, seconds); }
|
||
else { return String.format("%02d:%02d", minutes, seconds); }
|
||
}
|
||
}// Fichier MainActivity.java
|
||
/**
|
||
* Activité principale de l'application 2048.
|
||
* Gère l'interface utilisateur (plateau, scores, boutons), coordonne les interactions
|
||
* avec la logique du jeu (classe Game) et la gestion des statistiques (classe GameStats).
|
||
* Gère également le cycle de vie de l'application et la persistance de l'état du jeu.
|
||
*/
|
||
package legion.muyue.best2048;
|
||
|
||
import android.annotation.SuppressLint;
|
||
import android.app.AlertDialog;
|
||
import android.content.Context;
|
||
import android.app.NotificationChannel;
|
||
import android.app.NotificationManager;
|
||
import android.app.PendingIntent;
|
||
import android.content.ActivityNotFoundException;
|
||
import android.content.pm.PackageManager;
|
||
import android.os.Build;
|
||
import android.provider.Settings;
|
||
import com.google.android.material.switchmaterial.SwitchMaterial;
|
||
import android.content.Intent;
|
||
import android.content.SharedPreferences;
|
||
import android.net.Uri;
|
||
import android.os.Bundle;
|
||
import android.util.TypedValue;
|
||
import android.view.Gravity;
|
||
import android.view.LayoutInflater;
|
||
import android.view.View;
|
||
import android.view.ViewStub;
|
||
import android.view.animation.AnimationUtils;
|
||
import android.widget.TextView;
|
||
import androidx.activity.EdgeToEdge;
|
||
import androidx.activity.result.ActivityResultLauncher;
|
||
import androidx.activity.result.contract.ActivityResultContracts;
|
||
import androidx.appcompat.app.AppCompatActivity;
|
||
import androidx.core.app.ActivityCompat;
|
||
import androidx.core.app.NotificationCompat;
|
||
import androidx.core.app.NotificationManagerCompat;
|
||
import androidx.core.content.ContextCompat;
|
||
import androidx.gridlayout.widget.GridLayout;
|
||
import android.widget.Button;
|
||
import android.widget.Toast;
|
||
import android.animation.Animator;
|
||
import android.animation.AnimatorListenerAdapter;
|
||
import android.animation.AnimatorSet;
|
||
import android.animation.ObjectAnimator;
|
||
import android.view.ViewTreeObserver;
|
||
import android.media.AudioAttributes; // Pour SoundPool
|
||
import android.media.SoundPool; // Pour SoundPool
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
|
||
|
||
public class MainActivity extends AppCompatActivity {
|
||
|
||
// --- UI Elements ---
|
||
private GridLayout boardGridLayout;
|
||
private TextView currentScoreTextView;
|
||
private TextView highestScoreTextView;
|
||
private Button newGameButton;
|
||
private Button multiplayerButton;
|
||
private Button statisticsButton;
|
||
private Button menuButton;
|
||
private ViewStub statisticsViewStub;
|
||
private View inflatedStatsView;
|
||
|
||
// --- Game Logic & Stats ---
|
||
private Game game;
|
||
private GameStats gameStats;
|
||
private static final int BOARD_SIZE = 4;
|
||
private static final String NOTIFICATION_CHANNEL_ID = "BEST_2048_CHANNEL";
|
||
private boolean notificationsEnabled = false;
|
||
private static final String LAST_PLAYED_TIME_KEY = "last_played_time";
|
||
|
||
// --- State Management ---
|
||
private boolean statisticsVisible = false;
|
||
private enum GameFlowState { PLAYING, WON_DIALOG_SHOWN, GAME_OVER }
|
||
private GameFlowState currentGameState = GameFlowState.PLAYING;
|
||
|
||
/** Références aux TextViews des tuiles actuellement affichées. */
|
||
private TextView[][] tileViews = new TextView[BOARD_SIZE][BOARD_SIZE];
|
||
|
||
// --- Preferences ---
|
||
private SharedPreferences preferences;
|
||
private static final String PREFS_NAME = "Best2048_Prefs";
|
||
private static final String HIGH_SCORE_KEY = "high_score";
|
||
private static final String GAME_STATE_KEY = "game_state";
|
||
|
||
// --- Champs Son ---
|
||
private SoundPool soundPool;
|
||
private int soundMoveId = -1; // Initialise à -1 pour savoir s'ils sont chargés
|
||
private int soundMergeId = -1;
|
||
private int soundWinId = -1;
|
||
private int soundGameOverId = -1;
|
||
private boolean soundPoolLoaded = false; // Flag pour savoir si les sons sont prêts
|
||
private boolean soundEnabled = true; // Son activé par défaut
|
||
|
||
// --- Activity Lifecycle ---
|
||
|
||
private final ActivityResultLauncher<String> requestPermissionLauncher =
|
||
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
|
||
if (isGranted) {
|
||
// La permission est accordée. On peut activer/planifier les notifications.
|
||
notificationsEnabled = true;
|
||
saveNotificationPreference(true);
|
||
Toast.makeText(this, R.string.notifications_enabled, Toast.LENGTH_SHORT).show();
|
||
// Ici, on pourrait (re)planifier les notifications périodiques avec WorkManager/AlarmManager
|
||
} else {
|
||
// La permission est refusée. L'utilisateur ne recevra pas de notifications.
|
||
notificationsEnabled = false;
|
||
saveNotificationPreference(false);
|
||
// Désactive le switch dans les paramètres si l'utilisateur vient de refuser
|
||
updateNotificationSwitchState(false);
|
||
Toast.makeText(this, R.string.notifications_disabled, Toast.LENGTH_SHORT).show();
|
||
// Afficher une explication si nécessaire
|
||
// showNotificationPermissionRationale();
|
||
}
|
||
});
|
||
|
||
@Override
|
||
protected void onCreate(Bundle savedInstanceState) {
|
||
EdgeToEdge.enable(this);
|
||
super.onCreate(savedInstanceState);
|
||
setContentView(R.layout.activity_main);
|
||
NotificationHelper.createNotificationChannel(this);
|
||
findViews();
|
||
initializeSoundPool();
|
||
initializeGameAndStats();
|
||
setupListeners();
|
||
if (notificationsEnabled) {
|
||
startNotificationService();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Synchronise COMPLETEMENT le GridLayout avec l'état actuel de 'game.board'.
|
||
* Étape 1: Ajoute 16 vues de fond pour "fixer" la structure de la grille.
|
||
* Étape 2: Ajoute les TextViews des tuiles réelles (valeur > 0) par-dessus.
|
||
* Stocke les références des tuiles réelles dans tileViews.
|
||
*/
|
||
private void syncBoardView() {
|
||
// Vérifications de sécurité
|
||
if (game == null || boardGridLayout == null) {
|
||
System.err.println("syncBoardView: Game ou GridLayout est null !");
|
||
return;
|
||
}
|
||
// Log.d("SyncDebug", "Syncing board view (Background + Tiles)...");
|
||
|
||
// --- Réinitialisation ---
|
||
boardGridLayout.removeAllViews(); // Vide complètement le GridLayout visuel
|
||
// Réinitialise le tableau qui stocke les références aux *vraies* tuiles (pas les fonds)
|
||
for (int r = 0; r < BOARD_SIZE; r++) {
|
||
for (int c = 0; c < BOARD_SIZE; c++) {
|
||
tileViews[r][c] = null;
|
||
}
|
||
}
|
||
|
||
// Récupère la marge une seule fois
|
||
int gridMargin = (int) getResources().getDimension(R.dimen.tile_margin);
|
||
|
||
// --- Étape 1: Ajouter les 16 vues de fond pour définir la grille ---
|
||
for (int r = 0; r < BOARD_SIZE; r++) {
|
||
for (int c = 0; c < BOARD_SIZE; c++) {
|
||
// Utiliser un simple View pour le fond est suffisant
|
||
View backgroundCell = new View(this);
|
||
|
||
// Appliquer le style d'une cellule vide
|
||
// Utilise le même drawable que les tuiles pour les coins arrondis, mais avec la couleur de fond vide
|
||
backgroundCell.setBackgroundResource(R.drawable.tile_background);
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||
// Utiliser setTintList pour la compatibilité et éviter de recréer des drawables
|
||
backgroundCell.getBackground().setTintList(ContextCompat.getColorStateList(this, R.color.tile_empty));
|
||
} else {
|
||
// Pour les versions plus anciennes, une approche différente pourrait être nécessaire si setTintList n'est pas dispo
|
||
// ou utiliser un drawable spécifique pour le fond. Ici on suppose API 21+ pour setTintList.
|
||
// Alternative simple mais moins propre : backgroundCell.setBackgroundColor(ContextCompat.getColor(this, R.color.tile_empty)); (perd les coins arrondis)
|
||
}
|
||
|
||
|
||
// Définir les LayoutParams pour positionner cette vue de fond dans la grille
|
||
GridLayout.LayoutParams params = new GridLayout.LayoutParams();
|
||
params.width = 0;
|
||
params.height = 0;
|
||
// Spécifier ligne, colonne, span=1, et poids=1 pour occuper la cellule
|
||
params.rowSpec = GridLayout.spec(r, 1, 1f);
|
||
params.columnSpec = GridLayout.spec(c, 1, 1f);
|
||
params.setMargins(gridMargin, gridMargin, gridMargin, gridMargin);
|
||
backgroundCell.setLayoutParams(params);
|
||
|
||
// Ajouter cette vue de fond au GridLayout
|
||
boardGridLayout.addView(backgroundCell);
|
||
}
|
||
}
|
||
|
||
// --- Étape 2: Ajouter les TextViews des tuiles réelles (par-dessus les fonds) ---
|
||
for (int r = 0; r < BOARD_SIZE; r++) {
|
||
for (int c = 0; c < BOARD_SIZE; c++) {
|
||
int value = game.getCellValue(r, c); // Récupère la valeur logique
|
||
|
||
// Si la case logique contient une tuile réelle
|
||
if (value > 0) {
|
||
// Crée la TextView stylisée pour cette tuile via notre méthode helper
|
||
// createTileTextView utilise les mêmes LayoutParams (row, col, span 1, weight 1, margins)
|
||
TextView tileTextView = createTileTextView(value, r, c);
|
||
|
||
// Stocke la référence à cette TextView de tuile (pas la vue de fond)
|
||
tileViews[r][c] = tileTextView;
|
||
|
||
// Ajoute la TextView de la tuile au GridLayout.
|
||
// Comme elle est ajoutée après la vue de fond pour la même cellule (r,c)
|
||
// et utilise les mêmes paramètres de positionnement, elle s'affichera par-dessus.
|
||
boardGridLayout.addView(tileTextView);
|
||
// Log.d("SyncDebug", "Added TILE view at ["+r+","+c+"] with value "+value);
|
||
}
|
||
}
|
||
}
|
||
// Log.d("SyncDebug", "Board sync finished.");
|
||
|
||
// Met à jour l'affichage textuel des scores
|
||
updateScores();
|
||
}
|
||
|
||
/**
|
||
* Crée et configure une TextView pour une tuile.
|
||
* @param value Valeur de la tuile.
|
||
* @param row Ligne.
|
||
* @param col Colonne.
|
||
* @return La TextView configurée.
|
||
*/
|
||
private TextView createTileTextView(int value, int row, int col) {
|
||
TextView tileTextView = new TextView(this);
|
||
setTileStyle(tileTextView, value); // Applique le style visuel
|
||
|
||
GridLayout.LayoutParams params = new GridLayout.LayoutParams();
|
||
params.width = 0; // Largeur gérée par GridLayout basé sur la colonne/poids
|
||
params.height = 0; // Hauteur gérée par GridLayout basé sur la ligne/poids
|
||
|
||
// --- Modification : Spécifier explicitement le span (taille) à 1 ---
|
||
// Utilisation de spec(start, size, weight)
|
||
params.rowSpec = GridLayout.spec(row, 1, 1f); // Commence à 'row', occupe 1 ligne, poids 1
|
||
params.columnSpec = GridLayout.spec(col, 1, 1f); // Commence à 'col', occupe 1 colonne, poids 1
|
||
// --- Fin Modification ---
|
||
|
||
int margin = (int) getResources().getDimension(R.dimen.tile_margin);
|
||
params.setMargins(margin, margin, margin, margin);
|
||
tileTextView.setLayoutParams(params);
|
||
|
||
return tileTextView;
|
||
}
|
||
|
||
@Override
|
||
protected void onResume() {
|
||
super.onResume();
|
||
// Redémarre le timer seulement si le jeu est en cours (pas gagné/perdu)
|
||
if (game != null && gameStats != null && currentGameState == GameFlowState.PLAYING) {
|
||
gameStats.setCurrentGameStartTimeMs(System.currentTimeMillis());
|
||
}
|
||
// Gère le réaffichage potentiel des stats si l'activité reprend
|
||
if (statisticsVisible) {
|
||
if (inflatedStatsView != null) { // Si déjà gonflé
|
||
updateStatisticsTextViews(); // Met à jour les données affichées
|
||
inflatedStatsView.setVisibility(View.VISIBLE);
|
||
multiplayerButton.setVisibility(View.GONE);
|
||
} else {
|
||
// Si pas encore gonflé (cas rare mais possible), on le fait afficher
|
||
toggleStatistics();
|
||
}
|
||
}
|
||
}
|
||
|
||
@Override
|
||
protected void onPause() {
|
||
super.onPause();
|
||
// Sauvegarde l'état et les stats si le jeu existe
|
||
if (game != null && gameStats != null) {
|
||
// Met à jour le temps total SI la partie était en cours
|
||
if (currentGameState == GameFlowState.PLAYING) {
|
||
gameStats.addPlayTime(System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs()); // Utilise méthode GameStats
|
||
}
|
||
saveGame(); // Sauvegarde l'état du jeu (plateau + score courant) et le HS
|
||
gameStats.saveStats(); // Sauvegarde toutes les stats via GameStats
|
||
}
|
||
}
|
||
|
||
/** Sauvegarde le timestamp actuel comme dernier moment joué. */
|
||
private void saveLastPlayedTime() {
|
||
if (preferences != null) {
|
||
preferences.edit().putLong(LAST_PLAYED_TIME_KEY, System.currentTimeMillis()).apply();
|
||
}
|
||
}
|
||
|
||
@Override
|
||
protected void onDestroy() { // Important de libérer SoundPool
|
||
super.onDestroy();
|
||
if (soundPool != null) {
|
||
soundPool.release();
|
||
soundPool = null;
|
||
}
|
||
}
|
||
|
||
// --- Initialisation ---
|
||
|
||
/**
|
||
* Récupère les références des vues du layout principal via leur ID.
|
||
*/
|
||
private void findViews() {
|
||
boardGridLayout = findViewById(R.id.gameBoard);
|
||
currentScoreTextView = findViewById(R.id.scoreLabel);
|
||
highestScoreTextView = findViewById(R.id.highScoreLabel);
|
||
newGameButton = findViewById(R.id.restartButton);
|
||
statisticsButton = findViewById(R.id.statsButton);
|
||
menuButton = findViewById(R.id.menuButton);
|
||
multiplayerButton = findViewById(R.id.multiplayerButton);
|
||
statisticsViewStub = findViewById(R.id.statsViewStub);
|
||
}
|
||
|
||
/**
|
||
* Initialise les objets Game et GameStats.
|
||
* Charge l'état du jeu sauvegardé (s'il existe) et le meilleur score.
|
||
* Met à jour l'interface utilisateur initiale.
|
||
*/
|
||
private void initializeGameAndStats() {
|
||
preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||
gameStats = new GameStats(this);
|
||
loadNotificationPreference();
|
||
loadSoundPreference();
|
||
loadGame(); // Charge jeu et met à jour high score
|
||
updateUI();
|
||
if (game == null) {
|
||
// Si loadGame échoue ou aucune sauvegarde, startNewGame gère l'initialisation
|
||
startNewGame();
|
||
}
|
||
// L'état (currentGameState) est défini dans loadGame ou startNewGame
|
||
}
|
||
|
||
/** Initialise le SoundPool et charge les effets sonores. */
|
||
private void initializeSoundPool() {
|
||
// Configuration pour les effets sonores de jeu
|
||
AudioAttributes attributes = new AudioAttributes.Builder()
|
||
.setUsage(AudioAttributes.USAGE_GAME)
|
||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||
.build();
|
||
|
||
// Crée le SoundPool
|
||
soundPool = new SoundPool.Builder()
|
||
.setMaxStreams(3) // Nombre max de sons joués simultanément
|
||
.setAudioAttributes(attributes)
|
||
.build();
|
||
|
||
// Listener pour savoir quand les sons sont chargés
|
||
soundPool.setOnLoadCompleteListener((soundPool, sampleId, status) -> {
|
||
if (status == 0) {
|
||
// Vérifie si TOUS les sons sont chargés (ou gère individuellement)
|
||
// Ici, on met juste un flag général pour simplifier
|
||
soundPoolLoaded = true;
|
||
}
|
||
});
|
||
|
||
// Charge les sons depuis res/raw
|
||
// Le 3ème argument (priority) est 1 (priorité normale)
|
||
try {
|
||
soundMoveId = soundPool.load(this, R.raw.move, 1);
|
||
soundMergeId = soundPool.load(this, R.raw.merge, 1);
|
||
soundWinId = soundPool.load(this, R.raw.win, 1);
|
||
soundGameOverId = soundPool.load(this, R.raw.game_over, 1);
|
||
} catch (Exception e) {
|
||
// Gérer l'erreur, peut-être désactiver le son
|
||
soundEnabled = false;
|
||
}
|
||
}
|
||
|
||
// --- Préférences Son ---
|
||
|
||
/** Sauvegarde la préférence d'activation du son. */
|
||
private void saveSoundPreference(boolean enabled) {
|
||
if (preferences != null) {
|
||
preferences.edit().putBoolean("sound_enabled", enabled).apply();
|
||
}
|
||
}
|
||
|
||
/** Charge la préférence d'activation du son. */
|
||
private void loadSoundPreference() {
|
||
if (preferences != null) {
|
||
soundEnabled = preferences.getBoolean("sound_enabled", true); // Son activé par défaut
|
||
} else {
|
||
soundEnabled = true;
|
||
}
|
||
}
|
||
|
||
// --- Lecture Son ---
|
||
|
||
/**
|
||
* Joue un effet sonore si le SoundPool est chargé et si le son est activé.
|
||
* @param soundId L'ID du son retourné par soundPool.load().
|
||
*/
|
||
private void playSound(int soundId) {
|
||
if (soundPoolLoaded && soundEnabled && soundPool != null && soundId > 0) {
|
||
// Arguments: soundID, leftVolume, rightVolume, priority, loop, rate
|
||
soundPool.play(soundId, 1.0f, 1.0f, 1, 0, 1.0f);
|
||
}
|
||
}
|
||
|
||
/** Crée le canal de notification nécessaire pour Android 8.0+. */
|
||
private void createNotificationChannel() {
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||
CharSequence name = getString(R.string.notification_channel_name);
|
||
String description = getString(R.string.notification_channel_description);
|
||
int importance = NotificationManager.IMPORTANCE_DEFAULT;
|
||
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance);
|
||
channel.setDescription(description);
|
||
// Enregistre le canal avec le système; ne peut pas être changé après ça
|
||
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||
if (notificationManager != null) {
|
||
notificationManager.createNotificationChannel(channel);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Configure les listeners pour les boutons et le plateau de jeu (swipes).
|
||
* Mise à jour pour le bouton Menu.
|
||
*/
|
||
private void setupListeners() {
|
||
newGameButton.setOnClickListener(v -> {
|
||
v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press));
|
||
showRestartConfirmationDialog();
|
||
});
|
||
statisticsButton.setOnClickListener(v -> {
|
||
v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press));
|
||
toggleStatistics();
|
||
});
|
||
// Modifié pour appeler la nouvelle méthode showMenu()
|
||
menuButton.setOnClickListener(v -> {
|
||
v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press));
|
||
showMenu(); // Appelle la méthode du menu
|
||
});
|
||
multiplayerButton.setOnClickListener(v -> {
|
||
v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press));
|
||
showMultiplayerScreen(); // Affiche dialogue placeholder
|
||
});
|
||
setupSwipeListener();
|
||
}
|
||
|
||
// --- Mise à jour UI ---
|
||
|
||
/**
|
||
* Met à jour complètement l'interface utilisateur (plateau et scores).
|
||
*/
|
||
private void updateUI() {
|
||
if (game == null) return;
|
||
updateBoard();
|
||
updateScores();
|
||
}
|
||
|
||
/**
|
||
* Redessine le plateau de jeu en créant/mettant à jour les TextViews des tuiles.
|
||
*/
|
||
private void updateBoard() {
|
||
boardGridLayout.removeAllViews();
|
||
for (int row = 0; row < BOARD_SIZE; row++) {
|
||
for (int col = 0; col < BOARD_SIZE; col++) {
|
||
TextView tileTextView = new TextView(this);
|
||
int value = game.getCellValue(row, col);
|
||
setTileStyle(tileTextView, value);
|
||
// Définit les LayoutParams pour que la tuile remplisse la cellule du GridLayout
|
||
GridLayout.LayoutParams params = new GridLayout.LayoutParams();
|
||
params.width = 0; params.height = 0; // Poids gère la taille
|
||
params.rowSpec = GridLayout.spec(row, 1f); // Prend 1 fraction de l'espace en hauteur
|
||
params.columnSpec = GridLayout.spec(col, 1f); // Prend 1 fraction de l'espace en largeur
|
||
int margin = (int) getResources().getDimension(R.dimen.tile_margin);
|
||
params.setMargins(margin, margin, margin, margin);
|
||
tileTextView.setLayoutParams(params);
|
||
boardGridLayout.addView(tileTextView);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Met à jour les TextViews affichant le score courant et le meilleur score.
|
||
*/
|
||
private void updateScores() {
|
||
currentScoreTextView.setText(getString(R.string.score_placeholder, game.getCurrentScore()));
|
||
highestScoreTextView.setText(getString(R.string.high_score_placeholder, game.getHighestScore()));
|
||
}
|
||
|
||
/**
|
||
* Applique le style visuel (fond, texte, taille) à une TextView représentant une tuile.
|
||
* @param tileTextView La TextView de la tuile.
|
||
* @param value La valeur numérique de la tuile (0 pour vide).
|
||
*/
|
||
private void setTileStyle(TextView tileTextView, int value) {
|
||
tileTextView.setText(value > 0 ? String.valueOf(value) : "");
|
||
tileTextView.setGravity(Gravity.CENTER);
|
||
tileTextView.setTypeface(null, android.graphics.Typeface.BOLD);
|
||
int backgroundColorId; int textColorId; int textSizeId;
|
||
switch (value) {
|
||
case 0: backgroundColorId = R.color.tile_empty; textColorId = android.R.color.transparent; textSizeId = R.dimen.text_size_tile_small; break;
|
||
case 2: backgroundColorId = R.color.tile_2; textColorId = R.color.text_tile_low; textSizeId = R.dimen.text_size_tile_small; break;
|
||
case 4: backgroundColorId = R.color.tile_4; textColorId = R.color.text_tile_low; textSizeId = R.dimen.text_size_tile_small; break;
|
||
case 8: backgroundColorId = R.color.tile_8; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_small; break;
|
||
case 16: backgroundColorId = R.color.tile_16; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_small; break;
|
||
case 32: backgroundColorId = R.color.tile_32; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_small; break;
|
||
case 64: backgroundColorId = R.color.tile_64; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_small; break;
|
||
case 128: backgroundColorId = R.color.tile_128; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_medium; break;
|
||
case 256: backgroundColorId = R.color.tile_256; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_medium; break;
|
||
case 512: backgroundColorId = R.color.tile_512; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_medium; break;
|
||
case 1024: backgroundColorId = R.color.tile_1024; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_large; break;
|
||
case 2048: backgroundColorId = R.color.tile_2048; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_large; break;
|
||
default: backgroundColorId = R.color.tile_super; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_large; break;
|
||
}
|
||
tileTextView.setBackgroundResource(R.drawable.tile_background);
|
||
tileTextView.getBackground().setTint(ContextCompat.getColor(this, backgroundColorId));
|
||
tileTextView.setTextColor(ContextCompat.getColor(this, textColorId));
|
||
tileTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(textSizeId));
|
||
}
|
||
|
||
|
||
// --- Gestion des Actions Utilisateur ---
|
||
|
||
/**
|
||
* Configure le listener pour détecter les swipes sur le plateau de jeu.
|
||
*/
|
||
@SuppressLint("ClickableViewAccessibility")
|
||
private void setupSwipeListener() {
|
||
boardGridLayout.setOnTouchListener(new OnSwipeTouchListener(this, new OnSwipeTouchListener.SwipeListener() {
|
||
@Override public void onSwipeTop() { handleSwipe(Direction.UP); }
|
||
@Override public void onSwipeBottom() { handleSwipe(Direction.DOWN); }
|
||
@Override public void onSwipeLeft() { handleSwipe(Direction.LEFT); }
|
||
@Override public void onSwipeRight() { handleSwipe(Direction.RIGHT); }
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Traite un swipe : met à jour la logique, synchronise l'affichage instantanément,
|
||
* puis lance des animations locales d'apparition/fusion sur les tuiles concernées.
|
||
* @param direction Direction du swipe.
|
||
*/
|
||
private void handleSwipe(Direction direction) {
|
||
// Bloque si jeu terminé
|
||
if (game == null || gameStats == null || currentGameState == GameFlowState.GAME_OVER) {
|
||
return;
|
||
}
|
||
// Note: On ne bloque plus sur 'isAnimating' pour cette approche
|
||
|
||
int scoreBefore = game.getCurrentScore();
|
||
int[][] boardBeforePush = game.getBoard(); // État avant le push
|
||
|
||
boolean boardChanged = false;
|
||
switch (direction) {
|
||
case UP: boardChanged = game.pushUp(); break;
|
||
case DOWN: boardChanged = game.pushDown(); break;
|
||
case LEFT: boardChanged = game.pushLeft(); break;
|
||
case RIGHT: boardChanged = game.pushRight(); break;
|
||
}
|
||
|
||
if (boardChanged) {
|
||
playSound(soundMoveId);
|
||
// Capture l'état APRÈS le push mais AVANT l'ajout de la nouvelle tuile
|
||
int[][] boardAfterPush = game.getBoard();
|
||
|
||
// Met à jour les stats générales
|
||
gameStats.recordMove();
|
||
int scoreAfter = game.getCurrentScore();
|
||
if (scoreAfter > scoreBefore) {
|
||
playSound(soundMergeId);
|
||
gameStats.recordMerge(1); // Simplifié
|
||
if (scoreAfter > game.getHighestScore()) {
|
||
game.setHighestScore(scoreAfter);
|
||
gameStats.setHighestScore(scoreAfter);
|
||
}
|
||
}
|
||
gameStats.updateHighestTile(game.getHighestTileValue());
|
||
// updateScores(); // Le score sera mis à jour par syncBoardView
|
||
|
||
// Ajoute la nouvelle tuile dans la logique du jeu
|
||
game.addNewTile();
|
||
int[][] boardAfterAdd = game.getBoard(); // État final logique
|
||
|
||
// *** Synchronise l'affichage avec l'état final logique ***
|
||
syncBoardView();
|
||
|
||
// *** Lance les animations locales sur les vues mises à jour ***
|
||
animateChanges(boardBeforePush, boardAfterPush, boardAfterAdd);
|
||
|
||
// La vérification de fin de partie est maintenant dans animateChanges->finalizeMove
|
||
// pour s'assurer qu'elle est faite APRÈS les animations.
|
||
|
||
} else {
|
||
// Mouvement invalide, on vérifie quand même si c'est la fin du jeu
|
||
if (game.isGameOver() && currentGameState != GameFlowState.GAME_OVER) {
|
||
currentGameState = GameFlowState.GAME_OVER;
|
||
long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
|
||
gameStats.recordLoss(); gameStats.endGame(timeTaken);
|
||
showGameOverDialog();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Identifie les tuiles qui ont fusionné ou sont apparues et lance des animations
|
||
* simples (scale/alpha) sur les vues correspondantes DÉJÀ positionnées par syncBoardView.
|
||
* @param boardBeforePush État avant le déplacement/fusion.
|
||
* @param boardAfterPush État après déplacement/fusion, avant ajout nouvelle tuile.
|
||
* @param boardAfterAdd État final après ajout nouvelle tuile.
|
||
*/
|
||
private void animateChanges(int[][] boardBeforePush, int[][] boardAfterPush, int[][] boardAfterAdd) {
|
||
List<Animator> animations = new ArrayList<>();
|
||
|
||
for (int r = 0; r < BOARD_SIZE; r++) {
|
||
for (int c = 0; c < BOARD_SIZE; c++) {
|
||
TextView currentView = tileViews[r][c]; // Vue à la position finale (après syncBoardView)
|
||
if (currentView == null) continue; // Pas de vue à animer ici
|
||
|
||
int valueAfterAdd = boardAfterAdd[r][c];
|
||
int valueAfterPush = boardAfterPush[r][c]; // Valeur avant l'ajout
|
||
int valueBeforePush = boardBeforePush[r][c]; // Valeur tout au début
|
||
|
||
// 1. Animation d'Apparition
|
||
// Si la case était vide après le push, mais a une valeur maintenant (c'est la nouvelle tuile)
|
||
if (valueAfterPush == 0 && valueAfterAdd > 0) {
|
||
//Log.d("AnimationDebug", "Animating APPEAR at ["+r+","+c+"]");
|
||
currentView.setScaleX(0.3f); currentView.setScaleY(0.3f); currentView.setAlpha(0f);
|
||
Animator appear = createAppearAnimation(currentView);
|
||
animations.add(appear);
|
||
}
|
||
// 2. Animation de Fusion
|
||
// Si la valeur a changé PENDANT le push (valeur après push > valeur avant push)
|
||
// ET que la case n'était pas vide avant (ce n'est pas un simple déplacement)
|
||
else if (valueAfterPush > valueBeforePush && valueBeforePush != 0) {
|
||
//Log.d("AnimationDebug", "Animating MERGE at ["+r+","+c+"]");
|
||
Animator merge = createMergeAnimation(currentView);
|
||
animations.add(merge);
|
||
}
|
||
// Note : les tuiles qui ont simplement bougé ne sont pas animées ici.
|
||
// Les tuiles qui ont disparu (fusionnées vers une autre case) sont gérées par syncBoardView qui les supprime.
|
||
}
|
||
}
|
||
|
||
if (!animations.isEmpty()) {
|
||
AnimatorSet animatorSet = new AnimatorSet();
|
||
animatorSet.playTogether(animations);
|
||
animatorSet.addListener(new AnimatorListenerAdapter() {
|
||
@Override
|
||
public void onAnimationEnd(Animator animation) {
|
||
// finalizeMove n'est plus responsable du déblocage, mais vérifie la fin
|
||
checkEndGameConditions();
|
||
}
|
||
});
|
||
animatorSet.start();
|
||
} else {
|
||
// Si aucune animation n'a été générée (ex: mouvement sans fusion ni nouvelle tuile possible)
|
||
checkEndGameConditions(); // Vérifie quand même la fin de partie
|
||
}
|
||
}
|
||
|
||
/** Crée une animation d'apparition (scale + alpha). */
|
||
private Animator createAppearAnimation(View view) {
|
||
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 0.3f, 1f);
|
||
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 0.3f, 1f);
|
||
ObjectAnimator alpha = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);
|
||
AnimatorSet set = new AnimatorSet();
|
||
set.playTogether(scaleX, scaleY, alpha);
|
||
set.setDuration(150); // Durée apparition
|
||
return set;
|
||
}
|
||
|
||
/** Crée une animation de 'pulse' pour une fusion. */
|
||
private Animator createMergeAnimation(View view) {
|
||
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 1.2f, 1f);
|
||
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 1.2f, 1f);
|
||
AnimatorSet set = new AnimatorSet();
|
||
set.playTogether(scaleX, scaleY);
|
||
set.setDuration(120); // Durée pulse fusion
|
||
return set;
|
||
}
|
||
|
||
/**
|
||
* Vérifie si le jeu est gagné ou perdu et affiche le dialogue approprié.
|
||
* Doit être appelé après la fin des animations potentielles.
|
||
*/
|
||
private void checkEndGameConditions() {
|
||
if (game == null || currentGameState == GameFlowState.GAME_OVER) return;
|
||
|
||
if (game.isGameWon() && currentGameState == GameFlowState.PLAYING) {
|
||
currentGameState = GameFlowState.WON_DIALOG_SHOWN;
|
||
long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
|
||
gameStats.recordWin(timeTaken);
|
||
showGameWonKeepPlayingDialog();
|
||
// La notif est déjà envoyée dans handleSwipe si on la veut immédiate
|
||
} else if (game.isGameOver()) {
|
||
currentGameState = GameFlowState.GAME_OVER;
|
||
long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
|
||
gameStats.recordLoss();
|
||
gameStats.endGame(timeTaken);
|
||
showGameOverDialog();
|
||
}
|
||
}
|
||
|
||
/** Énumération pour les directions de swipe. */
|
||
private enum Direction { UP, DOWN, LEFT, RIGHT }
|
||
|
||
// --- Dialogues ---
|
||
|
||
/**
|
||
* Affiche la boîte de dialogue demandant confirmation avant de redémarrer.
|
||
*/
|
||
private void showRestartConfirmationDialog() {
|
||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||
LayoutInflater inflater = getLayoutInflater();
|
||
View dialogView = inflater.inflate(R.layout.dialog_restart_confirm, null);
|
||
builder.setView(dialogView);
|
||
Button cancelButton = dialogView.findViewById(R.id.dialogCancelButton);
|
||
Button confirmButton = dialogView.findViewById(R.id.dialogConfirmButton);
|
||
final AlertDialog dialog = builder.create();
|
||
cancelButton.setOnClickListener(v -> dialog.dismiss());
|
||
confirmButton.setOnClickListener(v -> { dialog.dismiss(); startNewGame(); });
|
||
dialog.show();
|
||
}
|
||
|
||
/**
|
||
* Démarre une nouvelle partie logiquement et rafraîchit l'UI.
|
||
* Réinitialise les stats de la partie en cours via `gameStats.startGame()`.
|
||
* Ferme le panneau de statistiques s'il est ouvert.
|
||
*/
|
||
private void startNewGame() {
|
||
if (gameStats == null) gameStats = new GameStats(this); // Précaution si initialisation a échoué avant
|
||
gameStats.startGame(); // Réinitialise stats de partie (temps, mouvements, etc.)
|
||
game = new Game(); // Crée un nouveau jeu logique
|
||
game.setHighestScore(gameStats.getOverallHighScore()); // Applique le meilleur score global au nouveau jeu
|
||
currentGameState = GameFlowState.PLAYING; // Définit l'état à JOUER
|
||
|
||
// Ferme le panneau de statistiques s'il était ouvert
|
||
if (statisticsVisible) {
|
||
toggleStatistics(); // Utilise la méthode existante pour masquer proprement
|
||
}
|
||
|
||
syncBoardView();
|
||
|
||
updateUI(); // Met à jour l'affichage (plateau, scores)
|
||
}
|
||
|
||
|
||
/**
|
||
* Affiche la boîte de dialogue quand 2048 est atteint, en utilisant un layout personnalisé.
|
||
* Propose de continuer à jouer ou de commencer une nouvelle partie.
|
||
*/
|
||
private void showGameWonKeepPlayingDialog() {
|
||
playSound(soundWinId);
|
||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||
LayoutInflater inflater = getLayoutInflater();
|
||
View dialogView = inflater.inflate(R.layout.dialog_game_won, null);
|
||
builder.setView(dialogView);
|
||
builder.setCancelable(false);
|
||
|
||
Button keepPlayingButton = dialogView.findViewById(R.id.dialogKeepPlayingButton);
|
||
Button newGameButton = dialogView.findViewById(R.id.dialogNewGameButtonWon);
|
||
final AlertDialog dialog = builder.create();
|
||
|
||
keepPlayingButton.setOnClickListener(v -> {
|
||
// L'état est déjà WON_DIALOG_SHOWN, on ne fait rien de spécial, le jeu continue.
|
||
dialog.dismiss();
|
||
});
|
||
newGameButton.setOnClickListener(v -> {
|
||
dialog.dismiss();
|
||
startNewGame();
|
||
});
|
||
dialog.show();
|
||
}
|
||
|
||
/**
|
||
* Affiche la boîte de dialogue de fin de partie (plus de mouvements), en utilisant un layout personnalisé.
|
||
* Propose Nouvelle Partie ou Quitter.
|
||
*/
|
||
private void showGameOverDialog() {
|
||
playSound(soundGameOverId);
|
||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||
LayoutInflater inflater = getLayoutInflater();
|
||
View dialogView = inflater.inflate(R.layout.dialog_game_over, null);
|
||
builder.setView(dialogView);
|
||
builder.setCancelable(false);
|
||
|
||
TextView messageTextView = dialogView.findViewById(R.id.dialogMessageGameOver);
|
||
Button newGameButton = dialogView.findViewById(R.id.dialogNewGameButtonGameOver);
|
||
Button quitButton = dialogView.findViewById(R.id.dialogQuitButtonGameOver);
|
||
messageTextView.setText(getString(R.string.game_over_message, game.getCurrentScore()));
|
||
final AlertDialog dialog = builder.create();
|
||
|
||
newGameButton.setOnClickListener(v -> {
|
||
dialog.dismiss();
|
||
startNewGame();
|
||
});
|
||
quitButton.setOnClickListener(v -> {
|
||
dialog.dismiss();
|
||
finish(); // Ferme l'application
|
||
});
|
||
dialog.show();
|
||
}
|
||
|
||
// --- Menu Principal ---
|
||
|
||
/**
|
||
* Affiche la boîte de dialogue du menu principal en utilisant un layout personnalisé.
|
||
* Attache les listeners aux boutons pour déclencher les actions correspondantes.
|
||
*/
|
||
private void showMenu() {
|
||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||
LayoutInflater inflater = getLayoutInflater();
|
||
View dialogView = inflater.inflate(R.layout.dialog_main_menu, null); // Gonfle le layout personnalisé
|
||
builder.setView(dialogView);
|
||
builder.setCancelable(true);
|
||
|
||
// Récupère les boutons du layout personnalisé
|
||
Button howToPlayButton = dialogView.findViewById(R.id.menuButtonHowToPlay);
|
||
Button settingsButton = dialogView.findViewById(R.id.menuButtonSettings);
|
||
Button aboutButton = dialogView.findViewById(R.id.menuButtonAbout);
|
||
Button returnButton = dialogView.findViewById(R.id.menuButtonReturn);
|
||
|
||
final AlertDialog dialog = builder.create();
|
||
|
||
// Attache les listeners aux boutons
|
||
howToPlayButton.setOnClickListener(v -> {
|
||
dialog.dismiss(); // Ferme le menu
|
||
showHowToPlayDialog(); // Ouvre la dialogue "Comment Jouer"
|
||
});
|
||
|
||
settingsButton.setOnClickListener(v -> {
|
||
dialog.dismiss(); // Ferme le menu
|
||
showSettingsDialog(); // Ouvre la dialogue placeholder "Paramètres"
|
||
});
|
||
|
||
aboutButton.setOnClickListener(v -> {
|
||
dialog.dismiss(); // Ferme le menu
|
||
showAboutDialog(); // Ouvre la dialogue "À Propos"
|
||
});
|
||
|
||
returnButton.setOnClickListener(v -> {
|
||
dialog.dismiss(); // Ferme simplement le menu
|
||
});
|
||
|
||
dialog.show(); // Affiche la boîte de dialogue
|
||
}
|
||
|
||
/**
|
||
* Affiche une boîte de dialogue expliquant les règles du jeu,
|
||
* en utilisant un layout personnalisé pour une meilleure présentation.
|
||
*/
|
||
private void showHowToPlayDialog() {
|
||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||
LayoutInflater inflater = getLayoutInflater();
|
||
View dialogView = inflater.inflate(R.layout.dialog_how_to_play, null); // Gonfle le layout
|
||
builder.setView(dialogView);
|
||
builder.setCancelable(true); // Permet de fermer en cliquant à côté
|
||
|
||
// Récupère le bouton OK DANS la vue gonflée
|
||
Button okButton = dialogView.findViewById(R.id.dialogOkButtonHowToPlay);
|
||
|
||
final AlertDialog dialog = builder.create();
|
||
|
||
okButton.setOnClickListener(v -> dialog.dismiss()); // Ferme simplement
|
||
|
||
dialog.show();
|
||
}
|
||
|
||
/**
|
||
* Affiche la boîte de dialogue des paramètres en utilisant un layout personnalisé.
|
||
* Gère les interactions avec les différentes options.
|
||
*/
|
||
private void showSettingsDialog() {
|
||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||
LayoutInflater inflater = getLayoutInflater();
|
||
View dialogView = inflater.inflate(R.layout.dialog_settings, null);
|
||
builder.setView(dialogView).setCancelable(true);
|
||
|
||
// Vues
|
||
SwitchMaterial switchSound = dialogView.findViewById(R.id.switchSound);
|
||
SwitchMaterial switchNotifications = dialogView.findViewById(R.id.switchNotifications); // Activé
|
||
Button permissionsButton = dialogView.findViewById(R.id.buttonManagePermissions);
|
||
Button shareStatsButton = dialogView.findViewById(R.id.buttonShareStats);
|
||
Button resetStatsButton = dialogView.findViewById(R.id.buttonResetStats);
|
||
Button quitAppButton = dialogView.findViewById(R.id.buttonQuitApp);
|
||
Button closeButton = dialogView.findViewById(R.id.buttonCloseSettings);
|
||
// Ajout boutons de test (optionnel, pour débugger)
|
||
Button testNotifHS = new Button(this); // Crée programmatiquement
|
||
testNotifHS.setText(R.string.settings_test_notif_highscore);
|
||
Button testNotifInactiv = new Button(this);
|
||
testNotifInactiv.setText(R.string.settings_test_notif_inactivity);
|
||
// Ajouter ces boutons au layout 'dialogView' si nécessaire (ex: ((LinearLayout)dialogView).addView(...) )
|
||
|
||
final AlertDialog dialog = builder.create();
|
||
|
||
// Config Son (MAINTENANT ACTIF)
|
||
switchSound.setEnabled(true); // Activé
|
||
switchSound.setChecked(soundEnabled); // État actuel chargé
|
||
switchSound.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||
soundEnabled = isChecked; // Met à jour l'état
|
||
saveSoundPreference(isChecked); // Sauvegarde la préférence
|
||
Toast.makeText(this,
|
||
isChecked ? R.string.sound_enabled : R.string.sound_disabled,
|
||
Toast.LENGTH_SHORT).show();
|
||
});
|
||
|
||
// Config Notifications (Activé + Gestion Permission)
|
||
switchNotifications.setEnabled(true); // Activé
|
||
switchNotifications.setChecked(notificationsEnabled); // État actuel
|
||
switchNotifications.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||
if (isChecked) {
|
||
// L'utilisateur VEUT activer les notifications
|
||
requestNotificationPermission(); // Demande la permission si nécessaire
|
||
} else {
|
||
// L'utilisateur désactive les notifications
|
||
notificationsEnabled = false;
|
||
saveNotificationPreference(false);
|
||
Toast.makeText(this, R.string.notifications_disabled, Toast.LENGTH_SHORT).show();
|
||
// Ici, annuler les éventuelles notifications planifiées (WorkManager/AlarmManager)
|
||
}
|
||
});
|
||
|
||
// Listeners autres boutons (Permissions, Share, Reset, Quit, Close)
|
||
permissionsButton.setOnClickListener(v -> { openAppSettings(); dialog.dismiss(); });
|
||
shareStatsButton.setOnClickListener(v -> { shareStats(); dialog.dismiss(); });
|
||
resetStatsButton.setOnClickListener(v -> { dialog.dismiss(); showResetStatsConfirmationDialog(); });
|
||
quitAppButton.setOnClickListener(v -> { dialog.dismiss(); finishAffinity(); });
|
||
closeButton.setOnClickListener(v -> dialog.dismiss());
|
||
|
||
// Listeners boutons de test (si ajoutés)
|
||
testNotifHS.setOnClickListener(v -> { showHighScoreNotification(gameStats.getOverallHighScore()); });
|
||
testNotifInactiv.setOnClickListener(v -> { showInactivityNotification(); });
|
||
|
||
|
||
dialog.show();
|
||
}
|
||
|
||
/** Met à jour l'état du switch notification (utile si permission refusée). */
|
||
private void updateNotificationSwitchState(boolean isEnabled) {
|
||
// Si la vue des paramètres est actuellement affichée, met à jour le switch
|
||
View settingsDialogView = getLayoutInflater().inflate(R.layout.dialog_settings, null); // Attention, regonfler n'est pas idéal
|
||
// Mieux: Garder une référence à la vue ou au switch si la dialog est affichée.
|
||
// Pour la simplicité ici, on suppose qu'il faut rouvrir les paramètres pour voir le changement.
|
||
}
|
||
|
||
/** Sauvegarde la préférence d'activation des notifications. */
|
||
private void saveNotificationPreference(boolean enabled) {
|
||
if (preferences != null) {
|
||
preferences.edit().putBoolean("notifications_enabled", enabled).apply();
|
||
}
|
||
}
|
||
|
||
/** Charge la préférence d'activation des notifications. */
|
||
private void loadNotificationPreference() {
|
||
if (preferences != null) {
|
||
// Change le défaut de true à false
|
||
notificationsEnabled = preferences.getBoolean("notifications_enabled", false);
|
||
} else {
|
||
notificationsEnabled = false; // Assure une valeur par défaut si prefs est null
|
||
}
|
||
}
|
||
|
||
|
||
/** Ouvre les paramètres système de l'application. */
|
||
private void openAppSettings() {
|
||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||
Uri uri = Uri.fromParts("package", getPackageName(), null);
|
||
intent.setData(uri);
|
||
try {
|
||
startActivity(intent);
|
||
} catch (ActivityNotFoundException e) {
|
||
Toast.makeText(this, "Impossible d'ouvrir les paramètres.", Toast.LENGTH_LONG).show();
|
||
}
|
||
}
|
||
|
||
// --- Gestion des Permissions (Notifications) ---
|
||
|
||
/** Demande la permission POST_NOTIFICATIONS si nécessaire (Android 13+). */
|
||
private void requestNotificationPermission() {
|
||
// Vérifie si on est sur Android 13+
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||
// Vérifie si la permission n'est PAS déjà accordée
|
||
if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||
// Demande la permission
|
||
requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS);
|
||
// Le résultat sera géré dans le callback du requestPermissionLauncher
|
||
} else {
|
||
// La permission est déjà accordée, on peut activer directement
|
||
notificationsEnabled = true;
|
||
saveNotificationPreference(true);
|
||
Toast.makeText(this, R.string.notifications_enabled, Toast.LENGTH_SHORT).show();
|
||
// Planifier les notifications ici si ce n'est pas déjà fait
|
||
}
|
||
} else {
|
||
// Sur les versions antérieures à Android 13, pas besoin de demander la permission
|
||
notificationsEnabled = true;
|
||
saveNotificationPreference(true);
|
||
Toast.makeText(this, R.string.notifications_enabled, Toast.LENGTH_SHORT).show();
|
||
// Planifier les notifications ici
|
||
}
|
||
}
|
||
|
||
// --- Logique de Notification ---
|
||
|
||
/**
|
||
* Crée l'Intent qui sera lancé au clic sur une notification (ouvre MainActivity).
|
||
* @return PendingIntent configuré.
|
||
*/
|
||
private PendingIntent createNotificationTapIntent() {
|
||
Intent intent = new Intent(this, MainActivity.class);
|
||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); // Ouvre l'app ou la ramène devant
|
||
// FLAG_IMMUTABLE est requis pour Android 12+
|
||
int flags = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_UPDATE_CURRENT;
|
||
return PendingIntent.getActivity(this, 0, intent, flags);
|
||
}
|
||
|
||
/**
|
||
* Construit et affiche une notification simple.
|
||
* @param context Contexte.
|
||
* @param title Titre de la notification.
|
||
* @param message Corps du message de la notification.
|
||
* @param notificationId ID unique pour cette notification.
|
||
*/
|
||
private void showNotification(Context context, String title, String message, int notificationId) {
|
||
// Vérifie si les notifications sont activées et si la permission est accordée (pour Android 13+)
|
||
if (!notificationsEnabled || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||
ActivityCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED)) {
|
||
// Ne pas envoyer la notification si désactivé ou permission manquante
|
||
return;
|
||
}
|
||
|
||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||
.setSmallIcon(R.drawable.ic_stat_notification_2048) // Votre icône
|
||
.setContentTitle(title)
|
||
.setContentText(message)
|
||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||
.setContentIntent(createNotificationTapIntent()) // Action au clic
|
||
.setAutoCancel(true); // Ferme la notif après clic
|
||
|
||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||
// L'ID notificationId est utilisé pour mettre à jour une notif existante ou en afficher une nouvelle
|
||
notificationManager.notify(notificationId, builder.build());
|
||
}
|
||
|
||
/** Démarre le NotificationService s'il n'est pas déjà lancé. */
|
||
private void startNotificationService() {
|
||
Intent serviceIntent = new Intent(this, NotificationService.class);
|
||
// Utiliser startForegroundService pour API 26+ si le service doit faire qqch rapidement
|
||
// mais pour une tâche périodique simple startService suffit.
|
||
startService(serviceIntent);
|
||
}
|
||
|
||
/** Arrête le NotificationService. */
|
||
private void stopNotificationService() {
|
||
Intent serviceIntent = new Intent(this, NotificationService.class);
|
||
stopService(serviceIntent);
|
||
}
|
||
|
||
/** Affiche la notification d'accomplissement via le NotificationHelper. */
|
||
private void showAchievementNotification(int tileValue) {
|
||
// Vérifie l'état global avant d'envoyer (au cas où désactivé entre-temps)
|
||
if (!notificationsEnabled) return;
|
||
String title = getString(R.string.notification_title_achievement);
|
||
String message = getString(R.string.notification_text_achievement, tileValue);
|
||
NotificationHelper.showNotification(this, title, message, 1); // ID 1 pour achievement
|
||
}
|
||
|
||
/** Affiche la notification de rappel du meilleur score (pour test). */
|
||
private void showHighScoreNotification(int highScore) {
|
||
String title = getString(R.string.notification_title_highscore);
|
||
String message = getString(R.string.notification_text_highscore, highScore);
|
||
// Utiliser un ID spécifique (ex: 2)
|
||
showNotification(this, title, message, 2);
|
||
// NOTE: La planification réelle utiliserait WorkManager/AlarmManager
|
||
}
|
||
|
||
/** Affiche la notification de rappel d'inactivité (pour test). */
|
||
private void showInactivityNotification() {
|
||
String title = getString(R.string.notification_title_inactivity);
|
||
String message = getString(R.string.notification_text_inactivity);
|
||
// Utiliser un ID spécifique (ex: 3)
|
||
showNotification(this, title, message, 3);
|
||
// NOTE: La planification réelle utiliserait WorkManager/AlarmManager et suivi du temps
|
||
}
|
||
|
||
/**
|
||
* Crée et lance une Intent pour partager les statistiques du joueur.
|
||
*/
|
||
private void shareStats() {
|
||
if (gameStats == null) return;
|
||
|
||
// Construit le message à partager
|
||
String shareBody = getString(R.string.share_stats_body,
|
||
gameStats.getOverallHighScore(),
|
||
gameStats.getHighestTile(),
|
||
gameStats.getNumberOfTimesObjectiveReached(),
|
||
gameStats.getTotalGamesStarted(), // Ou totalGamesPlayed ?
|
||
GameStats.formatTime(gameStats.getTotalPlayTimeMs()),
|
||
gameStats.getTotalMoves()
|
||
);
|
||
|
||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||
shareIntent.setType("text/plain");
|
||
shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_stats_subject));
|
||
shareIntent.putExtra(Intent.EXTRA_TEXT, shareBody);
|
||
|
||
try {
|
||
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_stats_title)));
|
||
} catch (ActivityNotFoundException e) {
|
||
Toast.makeText(this, "Aucune application de partage disponible.", Toast.LENGTH_SHORT).show();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Affiche une boîte de dialogue pour confirmer la réinitialisation des statistiques.
|
||
*/
|
||
private void showResetStatsConfirmationDialog() {
|
||
new AlertDialog.Builder(this)
|
||
.setTitle(R.string.reset_stats_confirm_title)
|
||
.setMessage(R.string.reset_stats_confirm_message)
|
||
.setPositiveButton(R.string.confirm, (dialog, which) -> {
|
||
resetStatistics(); // Appelle la méthode de réinitialisation
|
||
dialog.dismiss();
|
||
})
|
||
.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss())
|
||
.setIcon(android.R.drawable.ic_dialog_alert) // Icône d'avertissement
|
||
.show();
|
||
}
|
||
|
||
/**
|
||
* Réinitialise toutes les statistiques via GameStats et sauvegarde les changements.
|
||
* Affiche une confirmation à l'utilisateur.
|
||
*/
|
||
private void resetStatistics() {
|
||
if (gameStats != null) {
|
||
gameStats.resetStats(); // Réinitialise les stats dans l'objet
|
||
gameStats.saveStats(); // Sauvegarde les stats réinitialisées
|
||
// Met aussi à jour le highScore de l'objet Game courant (si une partie est en cours)
|
||
if(game != null){
|
||
game.setHighestScore(gameStats.getOverallHighScore()); // Le HS est aussi reset dans GameStats
|
||
updateScores(); // Rafraichit l'affichage du HS si visible
|
||
}
|
||
Toast.makeText(this, R.string.stats_reset_confirmation, Toast.LENGTH_SHORT).show();
|
||
// Si les stats étaient visibles, on pourrait vouloir les rafraîchir
|
||
if (statisticsVisible && inflatedStatsView != null) {
|
||
updateStatisticsTextViews();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Affiche la boîte de dialogue "À Propos" en utilisant un layout personnalisé,
|
||
* incluant des informations sur l'application et un lien cliquable vers le site web.
|
||
*/
|
||
private void showAboutDialog() {
|
||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||
LayoutInflater inflater = getLayoutInflater();
|
||
View dialogView = inflater.inflate(R.layout.dialog_about, null); // Gonfle le layout
|
||
builder.setView(dialogView);
|
||
builder.setCancelable(true); // Permet de fermer
|
||
|
||
// Récupère les vues du layout
|
||
TextView websiteLinkTextView = dialogView.findViewById(R.id.websiteLinkTextView);
|
||
Button okButton = dialogView.findViewById(R.id.dialogOkButtonAbout);
|
||
|
||
final AlertDialog dialog = builder.create();
|
||
|
||
// Rend le lien cliquable pour ouvrir le navigateur
|
||
websiteLinkTextView.setOnClickListener(v -> {
|
||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.about_website_url)));
|
||
try {
|
||
startActivity(browserIntent);
|
||
} catch (ActivityNotFoundException e) {
|
||
// Gère le cas où aucun navigateur n'est installé
|
||
Toast.makeText(this, "Aucun navigateur web trouvé.", Toast.LENGTH_SHORT).show();
|
||
}
|
||
});
|
||
|
||
// Bouton OK pour fermer la dialogue
|
||
okButton.setOnClickListener(v -> dialog.dismiss());
|
||
|
||
dialog.show();
|
||
}
|
||
|
||
|
||
// --- Gestion Stats UI ---
|
||
|
||
/**
|
||
* Affiche ou masque le panneau de statistiques.
|
||
* Gonfle le layout via ViewStub si c'est la première fois.
|
||
*/
|
||
private void toggleStatistics() {
|
||
statisticsVisible = !statisticsVisible;
|
||
if (statisticsVisible) {
|
||
if (inflatedStatsView == null) { // Gonfle si pas encore fait
|
||
inflatedStatsView = statisticsViewStub.inflate();
|
||
// Attache listener au bouton Back une fois la vue gonflée
|
||
Button backButton = inflatedStatsView.findViewById(R.id.backButton);
|
||
backButton.setOnClickListener(v -> toggleStatistics()); // Recliquer sur Back re-appelle toggle
|
||
}
|
||
updateStatisticsTextViews(); // Remplit les champs avec les données actuelles
|
||
inflatedStatsView.setVisibility(View.VISIBLE); // Affiche
|
||
multiplayerButton.setVisibility(View.GONE); // Masque bouton multi
|
||
} else {
|
||
if (inflatedStatsView != null) { // Masque si la vue existe
|
||
inflatedStatsView.setVisibility(View.GONE);
|
||
}
|
||
multiplayerButton.setVisibility(View.VISIBLE); // Réaffiche bouton multi
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Remplit les TextViews du panneau de statistiques avec les données de l'objet GameStats.
|
||
* Doit être appelé seulement après que `inflatedStatsView` a été initialisé.
|
||
*/
|
||
private void updateStatisticsTextViews() {
|
||
if (inflatedStatsView == null || gameStats == null) return;
|
||
|
||
// Récupération des TextViews dans la vue gonflée
|
||
TextView highScoreStatsLabel = inflatedStatsView.findViewById(R.id.high_score_stats_label);
|
||
TextView totalGamesPlayedLabel = inflatedStatsView.findViewById(R.id.total_games_played_label);
|
||
TextView totalGamesStartedLabel = inflatedStatsView.findViewById(R.id.total_games_started_label);
|
||
TextView winPercentageLabel = inflatedStatsView.findViewById(R.id.win_percentage_label);
|
||
TextView totalPlayTimeLabel = inflatedStatsView.findViewById(R.id.total_play_time_label);
|
||
TextView totalMovesLabel = inflatedStatsView.findViewById(R.id.total_moves_label);
|
||
TextView currentMovesLabel = inflatedStatsView.findViewById(R.id.current_moves_label);
|
||
TextView currentGameTimeLabel = inflatedStatsView.findViewById(R.id.current_game_time_label);
|
||
TextView averageGameTimeLabel = inflatedStatsView.findViewById(R.id.average_game_time_label);
|
||
TextView bestWinningTimeLabel = inflatedStatsView.findViewById(R.id.best_winning_time_label);
|
||
TextView worstWinningTimeLabel = inflatedStatsView.findViewById(R.id.worst_winning_time_label);
|
||
TextView totalMergesLabel = inflatedStatsView.findViewById(R.id.total_merges_label);
|
||
TextView highestTileLabel = inflatedStatsView.findViewById(R.id.highest_tile_label);
|
||
TextView objectiveReachedLabel = inflatedStatsView.findViewById(R.id.number_of_time_objective_reached_label);
|
||
TextView perfectGameLabel = inflatedStatsView.findViewById(R.id.perfect_game_label);
|
||
TextView multiplayerGamesWonLabel = inflatedStatsView.findViewById(R.id.multiplayer_games_won_label);
|
||
TextView multiplayerGamesPlayedLabel = inflatedStatsView.findViewById(R.id.multiplayer_games_played_label);
|
||
TextView multiplayerWinRateLabel = inflatedStatsView.findViewById(R.id.multiplayer_win_rate_label);
|
||
TextView multiplayerBestWinningStreakLabel = inflatedStatsView.findViewById(R.id.multiplayer_best_winning_streak_label);
|
||
TextView multiplayerAverageScoreLabel = inflatedStatsView.findViewById(R.id.multiplayer_average_score_label);
|
||
TextView averageTimePerGameMultiLabel = inflatedStatsView.findViewById(R.id.average_time_per_game_label); // Potentiel ID dupliqué dans layout?
|
||
TextView totalMultiplayerLossesLabel = inflatedStatsView.findViewById(R.id.total_multiplayer_losses_label);
|
||
TextView multiplayerHighScoreLabel = inflatedStatsView.findViewById(R.id.multiplayer_high_score_label);
|
||
TextView mergesThisGameLabel = inflatedStatsView.findViewById(R.id.merges_this_game);
|
||
|
||
// MAJ textes avec getters de gameStats
|
||
highScoreStatsLabel.setText(getString(R.string.high_score_stats, gameStats.getOverallHighScore()));
|
||
totalGamesPlayedLabel.setText(getString(R.string.total_games_played, gameStats.getTotalGamesPlayed()));
|
||
totalGamesStartedLabel.setText(getString(R.string.total_games_started, gameStats.getTotalGamesStarted()));
|
||
totalMovesLabel.setText(getString(R.string.total_moves, gameStats.getTotalMoves()));
|
||
currentMovesLabel.setText(getString(R.string.current_moves, gameStats.getCurrentMoves()));
|
||
mergesThisGameLabel.setText(getString(R.string.merges_this_game_label, gameStats.getMergesThisGame()));
|
||
totalMergesLabel.setText(getString(R.string.total_merges, gameStats.getTotalMerges()));
|
||
highestTileLabel.setText(getString(R.string.highest_tile, gameStats.getHighestTile()));
|
||
objectiveReachedLabel.setText(getString(R.string.number_of_time_objective_reached, gameStats.getNumberOfTimesObjectiveReached()));
|
||
perfectGameLabel.setText(getString(R.string.perfect_games, gameStats.getPerfectGames()));
|
||
multiplayerGamesWonLabel.setText(getString(R.string.multiplayer_games_won, gameStats.getMultiplayerGamesWon()));
|
||
multiplayerGamesPlayedLabel.setText(getString(R.string.multiplayer_games_played, gameStats.getMultiplayerGamesPlayed()));
|
||
multiplayerBestWinningStreakLabel.setText(getString(R.string.multiplayer_best_winning_streak, gameStats.getMultiplayerBestWinningStreak()));
|
||
multiplayerAverageScoreLabel.setText(getString(R.string.multiplayer_average_score, gameStats.getMultiplayerAverageScore()));
|
||
totalMultiplayerLossesLabel.setText(getString(R.string.total_multiplayer_losses, gameStats.getTotalMultiplayerLosses()));
|
||
multiplayerHighScoreLabel.setText(getString(R.string.multiplayer_high_score, gameStats.getMultiplayerHighestScore()));
|
||
|
||
// Calculs Pourcentages
|
||
String winPercentage = (gameStats.getTotalGamesStarted() > 0) ? String.format("%.2f%%", ((double) gameStats.getNumberOfTimesObjectiveReached() / gameStats.getTotalGamesStarted()) * 100) : "N/A";
|
||
winPercentageLabel.setText(getString(R.string.win_percentage, winPercentage));
|
||
String multiplayerWinRate = (gameStats.getMultiplayerGamesPlayed() > 0) ? String.format("%.2f%%", ((double) gameStats.getMultiplayerGamesWon() / gameStats.getMultiplayerGamesPlayed()) * 100) : "N/A";
|
||
multiplayerWinRateLabel.setText(getString(R.string.multiplayer_win_rate, multiplayerWinRate));
|
||
|
||
// Calculs Temps
|
||
totalPlayTimeLabel.setText(getString(R.string.total_play_time, GameStats.formatTime(gameStats.getTotalPlayTimeMs())));
|
||
// Calcule le temps de la partie en cours seulement si elle n'est pas finie
|
||
long currentDurationMs = 0;
|
||
if (game != null && gameStats != null && currentGameState == GameFlowState.PLAYING && gameStats.getCurrentGameStartTimeMs() > 0) {
|
||
currentDurationMs = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
|
||
}
|
||
currentGameTimeLabel.setText(getString(R.string.current_game_time, GameStats.formatTime(currentDurationMs)));
|
||
|
||
averageGameTimeLabel.setText(getString(R.string.average_time_per_game, GameStats.formatTime(gameStats.getAverageGameTimeMs())));
|
||
averageTimePerGameMultiLabel.setText(getString(R.string.average_time_per_game_label, GameStats.formatTime(gameStats.getMultiplayerAverageTimeMs()))); // Assurez-vous que l'ID R.string.average_time_per_game_label est correct
|
||
bestWinningTimeLabel.setText(getString(R.string.best_winning_time, (gameStats.getBestWinningTimeMs() != Long.MAX_VALUE) ? GameStats.formatTime(gameStats.getBestWinningTimeMs()) : "N/A"));
|
||
worstWinningTimeLabel.setText(getString(R.string.worst_winning_time, (gameStats.getWorstWinningTimeMs() != 0) ? GameStats.formatTime(gameStats.getWorstWinningTimeMs()) : "N/A"));
|
||
}
|
||
|
||
|
||
// --- Placeholders Multi ---
|
||
|
||
/** Affiche un dialogue placeholder pour le multijoueur. */
|
||
private void showMultiplayerScreen() {
|
||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||
builder.setTitle("Multijoueur").setMessage("Fonctionnalité multijoueur à venir !").setPositiveButton("OK", null);
|
||
builder.create().show();
|
||
}
|
||
|
||
// --- Sauvegarde / Chargement ---
|
||
|
||
/** Sauvegarde l'état du jeu et le meilleur score via SharedPreferences. */
|
||
private void saveGame() {
|
||
SharedPreferences.Editor editor = preferences.edit();
|
||
if (game != null) {
|
||
editor.putString(GAME_STATE_KEY, game.toString()); // Sérialise Game (plateau + score courant)
|
||
// Le meilleur score est géré et sauvegardé par GameStats, mais on le sauve aussi ici pour la synchro au chargement
|
||
editor.putInt(HIGH_SCORE_KEY, game.getHighestScore());
|
||
} else {
|
||
editor.remove(GAME_STATE_KEY); // Optionnel: nettoyer si pas de jeu
|
||
}
|
||
editor.apply(); // Utilise apply() pour une sauvegarde asynchrone
|
||
}
|
||
|
||
/** Charge l'état du jeu depuis SharedPreferences et synchronise le meilleur score. */
|
||
private void loadGame() {
|
||
String gameStateString = preferences.getString(GAME_STATE_KEY, null);
|
||
// Charge le meilleur score depuis les préférences (sera aussi chargé par GameStats mais on l'utilise ici pour Game)
|
||
int savedHighScore = preferences.getInt(HIGH_SCORE_KEY, 0);
|
||
|
||
// Assure que GameStats charge son état (y compris le HS global)
|
||
if (gameStats == null) { gameStats = new GameStats(this); } // Précaution
|
||
gameStats.loadStats(); // Charge explicitement les stats (ce qui devrait inclure le HS global)
|
||
// S'assure que le HS chargé par gameStats est cohérent avec celui des prefs directes
|
||
if (savedHighScore > gameStats.getOverallHighScore()) {
|
||
gameStats.setHighestScore(savedHighScore); // Assure que GameStats a au moins le HS trouvé ici
|
||
} else {
|
||
savedHighScore = gameStats.getOverallHighScore(); // Utilise le HS de GameStats s'il est plus grand
|
||
}
|
||
|
||
|
||
Game loadedGame = null;
|
||
if (gameStateString != null) {
|
||
loadedGame = Game.deserialize(gameStateString);
|
||
}
|
||
|
||
if (loadedGame != null) {
|
||
game = loadedGame;
|
||
game.setHighestScore(savedHighScore); // Applique le HS synchronisé
|
||
// Détermine l'état basé sur le jeu chargé
|
||
if (game.isGameOver()) {
|
||
currentGameState = GameFlowState.GAME_OVER;
|
||
} else if (game.isGameWon()) {
|
||
// Si on charge une partie déjà gagnée, on considère qu'on a déjà vu la dialog
|
||
currentGameState = GameFlowState.WON_DIALOG_SHOWN;
|
||
} else {
|
||
currentGameState = GameFlowState.PLAYING;
|
||
// Le timer sera (re)démarré dans onResume si l'état est PLAYING
|
||
}
|
||
} else {
|
||
// Pas de sauvegarde valide ou erreur de désérialisation -> Commence une nouvelle partie implicitement
|
||
game = null; // Sera géré par l'appel à startNewGame dans initializeGameAndStats
|
||
}
|
||
}
|
||
|
||
} // Fin MainActivity// Fichier NotificationHelper.java
|
||
package legion.muyue.best2048;
|
||
|
||
import android.app.NotificationChannel;
|
||
import android.app.NotificationManager;
|
||
import android.app.PendingIntent;
|
||
import android.content.Context;
|
||
import android.content.Intent;
|
||
import android.os.Build;
|
||
import androidx.core.app.NotificationCompat;
|
||
import androidx.core.app.NotificationManagerCompat;
|
||
import androidx.core.content.ContextCompat; // Pour checkSelfPermission
|
||
|
||
/**
|
||
* Classe utilitaire pour simplifier la création et l'affichage des notifications
|
||
* et la gestion du canal de notification pour l'application Best 2048.
|
||
*/
|
||
public class NotificationHelper {
|
||
|
||
/** Identifiant unique du canal de notification pour cette application. */
|
||
public static final String CHANNEL_ID = "BEST_2048_CHANNEL"; // Doit correspondre à celui utilisé avant
|
||
|
||
/**
|
||
* Crée le canal de notification requis pour Android 8.0 (API 26) et supérieur.
|
||
* Cette méthode est idempotente (l'appeler plusieurs fois n'a pas d'effet négatif).
|
||
* Doit être appelée avant d'afficher la première notification sur API 26+.
|
||
*
|
||
* @param context Contexte applicatif.
|
||
*/
|
||
public static void createNotificationChannel(Context context) {
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||
CharSequence name = context.getString(R.string.notification_channel_name);
|
||
String description = context.getString(R.string.notification_channel_description);
|
||
int importance = NotificationManager.IMPORTANCE_DEFAULT; // Importance par défaut
|
||
|
||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
|
||
channel.setDescription(description);
|
||
// Enregistre le canal auprès du système.
|
||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||
if (notificationManager != null) {
|
||
notificationManager.createNotificationChannel(channel);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Construit et affiche une notification.
|
||
* Vérifie la permission POST_NOTIFICATIONS sur Android 13+ avant d'essayer d'afficher.
|
||
*
|
||
* @param context Contexte (peut être une Activity ou un Service).
|
||
* @param title Titre de la notification.
|
||
* @param message Contenu texte de la notification.
|
||
* @param notificationId ID unique pour cette notification (permet de la mettre à jour ou l'annuler).
|
||
*/
|
||
public static void showNotification(Context context, String title, String message, int notificationId) {
|
||
// Vérification de la permission pour Android 13+
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||
// Si la permission n'est pas accordée, ne pas tenter d'afficher la notification.
|
||
// L'application devrait idéalement gérer la demande de permission avant d'appeler cette méthode
|
||
// si elle sait que l'utilisateur a activé les notifications dans les paramètres.
|
||
System.err.println("Permission POST_NOTIFICATIONS manquante. Notification non affichée.");
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Intent pour ouvrir MainActivity au clic
|
||
Intent intent = new Intent(context, MainActivity.class);
|
||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||
int flags = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_UPDATE_CURRENT;
|
||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, flags);
|
||
|
||
// Construction de la notification via NotificationCompat pour la compatibilité
|
||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||
.setSmallIcon(R.drawable.ic_stat_notification_2048) // Votre icône de notification
|
||
.setContentTitle(title)
|
||
.setContentText(message)
|
||
.setPriority(NotificationCompat.PRIORITY_DEFAULT) // Priorité normale
|
||
.setContentIntent(pendingIntent) // Action au clic
|
||
.setAutoCancel(true); // Ferme la notification après le clic
|
||
|
||
// Affichage de la notification
|
||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||
try {
|
||
notificationManager.notify(notificationId, builder.build());
|
||
} catch (SecurityException e){
|
||
// Gérer l'exception de sécurité qui peut survenir même avec la vérification ci-dessus dans certains cas limites
|
||
System.err.println("Erreur de sécurité lors de l'affichage de la notification : " + e.getMessage());
|
||
}
|
||
}
|
||
}// Fichier NotificationService.java
|
||
package legion.muyue.best2048;
|
||
|
||
import android.app.Service;
|
||
import android.content.Intent;
|
||
import android.content.SharedPreferences;
|
||
import android.os.Handler;
|
||
import android.os.IBinder;
|
||
import android.os.Looper; // Important pour créer un Handler sur le Main Thread
|
||
import androidx.annotation.Nullable;
|
||
import java.util.concurrent.TimeUnit;
|
||
|
||
/**
|
||
* Service exécuté en arrière-plan pour envoyer des notifications périodiques
|
||
* (rappel de meilleur score, rappel d'inactivité).
|
||
* Utilise un Handler pour planifier les tâches répétitives.
|
||
* NOTE : Pour une robustesse accrue (garantie d'exécution même si l'app est tuée),
|
||
* WorkManager serait préférable en production.
|
||
*/
|
||
public class NotificationService extends Service {
|
||
|
||
private static final int NOTIFICATION_ID_HIGHSCORE = 2; // Doit être différent des autres notifs
|
||
private static final int NOTIFICATION_ID_INACTIVITY = 3;
|
||
// Intervalles (exemples : 1 jour pour HS, 3 jours pour inactivité)
|
||
private static final long HIGHSCORE_INTERVAL_MS = TimeUnit.DAYS.toMillis(1);
|
||
private static final long INACTIVITY_INTERVAL_MS = TimeUnit.DAYS.toMillis(3);
|
||
private static final long CHECK_INTERVAL_MS = TimeUnit.HOURS.toMillis(6); // Intervalle de vérification plus fréquent
|
||
|
||
private Handler handler;
|
||
private Runnable periodicTaskRunnable;
|
||
|
||
// Clés SharedPreferences (doivent correspondre à celles utilisées ailleurs)
|
||
private static final String PREFS_NAME = "Best2048_Prefs";
|
||
private static final String HIGH_SCORE_KEY = "high_score";
|
||
private static final String LAST_PLAYED_TIME_KEY = "last_played_time"; // Nouvelle clé
|
||
|
||
@Override
|
||
public void onCreate() {
|
||
super.onCreate();
|
||
// Utilise le Looper principal pour le Handler (simple, mais bloque si tâche longue)
|
||
// Pour des tâches plus lourdes, utiliser HandlerThread
|
||
handler = new Handler(Looper.getMainLooper());
|
||
// Pas besoin de créer le canal ici si MainActivity le fait déjà au démarrage
|
||
// NotificationHelper.createNotificationChannel(this);
|
||
|
||
periodicTaskRunnable = new Runnable() {
|
||
@Override
|
||
public void run() {
|
||
// Vérifie périodiquement s'il faut envoyer une notification
|
||
checkAndSendNotifications();
|
||
// Replanifie la tâche
|
||
handler.postDelayed(this, CHECK_INTERVAL_MS); // Vérifie toutes les X heures
|
||
}
|
||
};
|
||
}
|
||
|
||
@Override
|
||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||
// Lance la tâche périodique lors du démarrage du service
|
||
handler.removeCallbacks(periodicTaskRunnable); // Assure qu'il n'y a pas de doublon
|
||
handler.post(periodicTaskRunnable); // Lance immédiatement la première vérification
|
||
|
||
// START_STICKY : Le système essaiera de recréer le service s'il est tué.
|
||
return START_STICKY;
|
||
}
|
||
|
||
@Override
|
||
public void onDestroy() {
|
||
super.onDestroy();
|
||
// Arrête la planification des tâches lorsque le service est détruit
|
||
if (handler != null && periodicTaskRunnable != null) {
|
||
handler.removeCallbacks(periodicTaskRunnable);
|
||
}
|
||
}
|
||
|
||
@Nullable
|
||
@Override
|
||
public IBinder onBind(Intent intent) {
|
||
// Service non lié (Started Service)
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Vérifie les conditions et envoie les notifications périodiques si nécessaire.
|
||
*/
|
||
private void checkAndSendNotifications() {
|
||
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||
boolean notificationsEnabled = prefs.getBoolean("notifications_enabled", true); // Vérifie si activé
|
||
|
||
if (!notificationsEnabled) {
|
||
// Si désactivé dans les prefs, on arrête potentiellement le service?
|
||
// Ou juste ne rien envoyer. Pour l'instant, ne rien envoyer.
|
||
// stopSelf(); // Arrêterait le service
|
||
return;
|
||
}
|
||
|
||
// --- Notification High Score (Exemple: envoyer une fois par jour si non joué depuis ?) ---
|
||
// Logique simplifiée: on envoie juste le rappel basé sur un flag ou temps (pas implémenté ici)
|
||
// Pour une vraie app, il faudrait une logique pour ne pas spammer.
|
||
// Exemple: Envoyer si le dernier envoi date de plus de HIGHSCORE_INTERVAL_MS ?
|
||
int highScore = prefs.getInt(HIGH_SCORE_KEY, 0);
|
||
// Temporairement on l'envoie à chaque check pour test (à modifier!)
|
||
// if (shouldSendHighScoreNotification()) {
|
||
showHighScoreNotificationNow(highScore);
|
||
// }
|
||
|
||
|
||
// --- Notification d'Inactivité ---
|
||
long lastPlayedTime = prefs.getLong(LAST_PLAYED_TIME_KEY, 0);
|
||
if (lastPlayedTime > 0 && (System.currentTimeMillis() - lastPlayedTime > INACTIVITY_INTERVAL_MS)) {
|
||
// Si l'inactivité dépasse le seuil
|
||
showInactivityNotificationNow();
|
||
// Optionnel: Mettre à jour lastPlayedTime pour ne pas renvoyer immédiatement ?
|
||
// Ou attendre que l'utilisateur rejoue pour mettre à jour lastPlayedTime dans onPause.
|
||
}
|
||
}
|
||
|
||
/** Affiche la notification High Score */
|
||
private void showHighScoreNotificationNow(int highScore) {
|
||
String title = getString(R.string.notification_title_highscore);
|
||
String message = getString(R.string.notification_text_highscore, highScore);
|
||
NotificationHelper.showNotification(this, title, message, NOTIFICATION_ID_HIGHSCORE);
|
||
}
|
||
|
||
/** Affiche la notification d'Inactivité */
|
||
private void showInactivityNotificationNow() {
|
||
String title = getString(R.string.notification_title_inactivity);
|
||
String message = getString(R.string.notification_text_inactivity);
|
||
NotificationHelper.showNotification(this, title, message, NOTIFICATION_ID_INACTIVITY);
|
||
}
|
||
|
||
// Ajouter ici une logique plus fine si nécessaire pour savoir QUAND envoyer les notifs périodiques
|
||
// private boolean shouldSendHighScoreNotification() { ... }
|
||
|
||
}// Fichier OnSwipeTouchListener.java
|
||
/**
|
||
* Listener de vue personnalisé qui détecte les gestes de balayage (swipe)
|
||
* dans les quatre directions cardinales et notifie un listener externe.
|
||
* Utilise {@link GestureDetector} pour l'analyse des gestes.
|
||
*/
|
||
package legion.muyue.best2048;
|
||
|
||
import android.annotation.SuppressLint;
|
||
import android.content.Context;
|
||
import android.view.GestureDetector;
|
||
import android.view.MotionEvent;
|
||
import android.view.View;
|
||
import androidx.annotation.NonNull;
|
||
|
||
public class OnSwipeTouchListener implements View.OnTouchListener {
|
||
|
||
/** Détecteur de gestes standard d'Android. */
|
||
private final GestureDetector gestureDetector;
|
||
/** Listener externe à notifier lors de la détection d'un swipe. */
|
||
private final SwipeListener listener;
|
||
|
||
/**
|
||
* Interface à implémenter par les classes souhaitant réagir aux événements de swipe.
|
||
*/
|
||
public interface SwipeListener {
|
||
/** Appelée lorsqu'un swipe vers le haut est détecté. */
|
||
void onSwipeTop();
|
||
/** Appelée lorsqu'un swipe vers le bas est détecté. */
|
||
void onSwipeBottom();
|
||
/** Appelée lorsqu'un swipe vers la gauche est détecté. */
|
||
void onSwipeLeft();
|
||
/** Appelée lorsqu'un swipe vers la droite est détecté. */
|
||
void onSwipeRight();
|
||
}
|
||
|
||
/**
|
||
* Constructeur.
|
||
* @param context Contexte applicatif, nécessaire pour `GestureDetector`.
|
||
* @param listener Instance qui recevra les notifications de swipe. Ne doit pas être null.
|
||
*/
|
||
public OnSwipeTouchListener(Context context, @NonNull SwipeListener listener) {
|
||
this.gestureDetector = new GestureDetector(context, new GestureListener());
|
||
this.listener = listener;
|
||
}
|
||
|
||
/**
|
||
* Intercepte les événements tactiles sur la vue associée et les délègue
|
||
* au {@link GestureDetector} pour analyse.
|
||
* @param v La vue touchée.
|
||
* @param event L'événement tactile.
|
||
* @return true si le geste a été consommé par le détecteur, false sinon.
|
||
*/
|
||
@SuppressLint("ClickableViewAccessibility")
|
||
@Override
|
||
public boolean onTouch(View v, MotionEvent event) {
|
||
// Passe l'événement au GestureDetector. Si ce dernier le gère (ex: détecte un onFling),
|
||
// il retournera true, et l'événement ne sera pas propagé davantage.
|
||
return gestureDetector.onTouchEvent(event);
|
||
}
|
||
|
||
/**
|
||
* Classe interne implémentant l'écouteur de gestes pour détecter le 'fling' (balayage rapide).
|
||
*/
|
||
private final class GestureListener extends GestureDetector.SimpleOnGestureListener {
|
||
|
||
/** Distance minimale (en pixels) pour qu'un mouvement soit considéré comme un swipe. */
|
||
private static final int SWIPE_THRESHOLD = 100;
|
||
/** Vitesse minimale (en pixels/sec) pour qu'un mouvement soit considéré comme un swipe. */
|
||
private static final int SWIPE_VELOCITY_THRESHOLD = 100;
|
||
|
||
/**
|
||
* Toujours retourner true pour onDown garantit que les événements suivants
|
||
* (comme onFling) seront bien reçus par ce listener.
|
||
*/
|
||
@Override
|
||
public boolean onDown(@NonNull MotionEvent e) {
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Appelée quand un geste de 'fling' (balayage rapide) est détecté.
|
||
* Analyse la direction et la vitesse pour déterminer s'il s'agit d'un swipe valide
|
||
* et notifie le {@link SwipeListener} externe.
|
||
*/
|
||
@Override
|
||
public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
|
||
if (e1 == null) return false; // Point de départ est nécessaire
|
||
|
||
boolean result = false;
|
||
try {
|
||
float diffY = e2.getY() - e1.getY();
|
||
float diffX = e2.getX() - e1.getX();
|
||
|
||
// Priorité au mouvement le plus ample (horizontal ou vertical)
|
||
if (Math.abs(diffX) > Math.abs(diffY)) {
|
||
// Mouvement principalement horizontal
|
||
if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
|
||
if (diffX > 0) {
|
||
listener.onSwipeRight();
|
||
} else {
|
||
listener.onSwipeLeft();
|
||
}
|
||
result = true; // Geste horizontal traité
|
||
}
|
||
} else {
|
||
// Mouvement principalement vertical
|
||
if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
|
||
if (diffY > 0) {
|
||
listener.onSwipeBottom();
|
||
} else {
|
||
listener.onSwipeTop();
|
||
}
|
||
result = true; // Geste vertical traité
|
||
}
|
||
}
|
||
} catch (Exception exception) {
|
||
// En cas d'erreur inattendue, on logue discrètement.
|
||
System.err.println("Erreur dans OnSwipeTouchListener.onFling: " + exception.getMessage());
|
||
// Ne pas crasher l'application pour une erreur de détection de geste.
|
||
}
|
||
return result;
|
||
}
|
||
}
|
||
} // Fin OnSwipeTouchListener<?xml version="1.0" encoding="utf-8"?>
|
||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||
android:interpolator="@android:anim/accelerate_decelerate_interpolator">
|
||
|
||
<scale
|
||
android:duration="150"
|
||
android:fromXScale="1.0"
|
||
android:fromYScale="1.0"
|
||
android:pivotX="50%"
|
||
android:pivotY="50%"
|
||
android:toXScale="0.95"
|
||
android:toYScale="0.95" />
|
||
|
||
<alpha
|
||
android:duration="150"
|
||
android:fromAlpha="1.0"
|
||
android:toAlpha="0.8" />
|
||
</set><?xml version="1.0" encoding="utf-8"?>
|
||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||
android:interpolator="@android:anim/accelerate_decelerate_interpolator">
|
||
|
||
<scale
|
||
android:duration="150"
|
||
android:fromXScale="0.95"
|
||
android:fromYScale="0.95"
|
||
android:pivotX="50%"
|
||
android:pivotY="50%"
|
||
android:toXScale="1.0"
|
||
android:toYScale="1.0" />
|
||
|
||
<alpha
|
||
android:duration="150"
|
||
android:fromAlpha="0.8"
|
||
android:toAlpha="1.0" />
|
||
</set><?xml version="1.0" encoding="utf-8"?>
|
||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||
<item android:state_pressed="true">
|
||
<shape android:shape="rectangle">
|
||
<gradient
|
||
android:startColor="@color/multiplayer_button_pressed_start"
|
||
android:endColor="@color/multiplayer_button_pressed_end"
|
||
android:angle="270" />
|
||
<corners android:radius="30dp" /> <padding
|
||
android:left="16dp"
|
||
android:top="8dp"
|
||
android:right="16dp"
|
||
android:bottom="8dp" />
|
||
<stroke
|
||
android:width="2dp"
|
||
android:color="@color/multiplayer_button_stroke" />
|
||
</shape>
|
||
</item>
|
||
<item>
|
||
<shape android:shape="rectangle">
|
||
<gradient
|
||
android:startColor="@color/multiplayer_button_start"
|
||
android:endColor="@color/multiplayer_button_end"
|
||
android:angle="270" />
|
||
<corners android:radius="30dp" /> <padding
|
||
android:left="16dp"
|
||
android:top="8dp"
|
||
android:right="16dp"
|
||
android:bottom="8dp" />
|
||
<stroke
|
||
android:width="2dp"
|
||
android:color="@color/multiplayer_button_stroke" />
|
||
</shape>
|
||
</item>
|
||
</selector><?xml version="1.0" encoding="utf-8"?>
|
||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||
android:shape="rectangle">
|
||
<solid android:color="@color/white"/>
|
||
<corners android:radius="32dp"/>
|
||
</shape><vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||
android:width="24dp"
|
||
android:height="24dp"
|
||
android:viewportWidth="24.0"
|
||
android:viewportHeight="24.0"
|
||
android:tint="?attr/colorControlNormal"> <path
|
||
android:fillColor="@android:color/white"
|
||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/> </vector><?xml version="1.0" encoding="utf-8"?>
|
||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||
android:shape="rectangle">
|
||
|
||
<solid android:color="@color/tile_empty" />
|
||
<corners android:radius="@dimen/corner_radius" />
|
||
<padding android:left="@dimen/tile_margin"
|
||
android:top="@dimen/tile_margin"
|
||
android:right="@dimen/tile_margin"
|
||
android:bottom="@dimen/tile_margin" />
|
||
|
||
</shape><?xml version="1.0" encoding="utf-8"?>
|
||
<androidx.constraintlayout.widget.ConstraintLayout
|
||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||
xmlns:tools="http://schemas.android.com/tools"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="match_parent"
|
||
android:background="@color/background_color"
|
||
tools:context=".MainActivity">
|
||
|
||
<LinearLayout
|
||
android:id="@+id/northPanel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:orientation="horizontal"
|
||
android:layout_marginTop="32dp"
|
||
android:padding="@dimen/padding_general"
|
||
app:layout_constraintEnd_toEndOf="parent"
|
||
app:layout_constraintStart_toStartOf="parent"
|
||
app:layout_constraintTop_toTopOf="parent">
|
||
|
||
<androidx.constraintlayout.widget.ConstraintLayout
|
||
android:layout_width="0dp"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginEnd="@dimen/margin_between_elements"
|
||
android:layout_weight="1">
|
||
|
||
<TextView
|
||
android:id="@+id/gameLabel"
|
||
android:layout_width="0dp"
|
||
android:layout_height="0dp"
|
||
android:background="@color/game_label_background"
|
||
android:fontFamily="sans-serif-medium"
|
||
android:gravity="center"
|
||
android:padding="@dimen/padding_general"
|
||
android:text="@string/name_2048"
|
||
android:textColor="@color/white"
|
||
android:textSize="@dimen/text_size_game_label"
|
||
android:textStyle="bold"
|
||
app:layout_constraintBottom_toBottomOf="parent"
|
||
app:layout_constraintDimensionRatio="1:1"
|
||
app:layout_constraintEnd_toEndOf="parent"
|
||
app:layout_constraintStart_toStartOf="parent"
|
||
app:layout_constraintTop_toTopOf="parent" />
|
||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||
|
||
<LinearLayout
|
||
android:layout_width="0dp"
|
||
android:layout_height="match_parent"
|
||
android:layout_weight="1"
|
||
android:gravity="center"
|
||
android:orientation="vertical"
|
||
app:layout_constraintBottom_toBottomOf="@+id/gameLabel">
|
||
|
||
<TextView
|
||
android:id="@+id/scoreLabel"
|
||
style="@style/ScoreLabelStyle"
|
||
android:text="@string/score_placeholder" />
|
||
|
||
<TextView
|
||
android:id="@+id/highScoreLabel"
|
||
style="@style/ScoreLabelStyle"
|
||
android:layout_marginTop="@dimen/margin_between_elements"
|
||
android:text="@string/high_score_placeholder" />
|
||
|
||
</LinearLayout>
|
||
|
||
</LinearLayout>
|
||
|
||
<androidx.cardview.widget.CardView
|
||
android:id="@+id/gameBoardContainer"
|
||
android:layout_width="0dp"
|
||
android:layout_height="0dp"
|
||
android:layout_margin="@dimen/padding_general"
|
||
app:cardBackgroundColor="@android:color/transparent"
|
||
app:cardCornerRadius="@dimen/corner_radius"
|
||
app:cardElevation="0dp"
|
||
app:layout_constraintBottom_toTopOf="@+id/multiplayerButton"
|
||
app:layout_constraintDimensionRatio="1:1"
|
||
app:layout_constraintEnd_toEndOf="parent"
|
||
app:layout_constraintStart_toStartOf="parent"
|
||
app:layout_constraintTop_toBottomOf="@+id/northPanel">
|
||
|
||
<androidx.gridlayout.widget.GridLayout
|
||
android:id="@+id/gameBoard"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="match_parent"
|
||
android:background="@color/game_board_background"
|
||
android:padding="@dimen/padding_game_board"
|
||
app:columnCount="4"
|
||
app:rowCount="4" />
|
||
|
||
</androidx.cardview.widget.CardView>
|
||
|
||
<Button
|
||
android:id="@+id/multiplayerButton"
|
||
style="@style/LargeButtonStyle"
|
||
android:layout_width="match_parent" android:layout_height="wrap_content"
|
||
android:text="@string/multiplayer"
|
||
app:layout_constraintBottom_toTopOf="@+id/buttons_layout"
|
||
app:layout_constraintEnd_toEndOf="parent"
|
||
app:layout_constraintStart_toStartOf="parent"
|
||
app:layout_constraintTop_toBottomOf="@+id/gameBoardContainer" />
|
||
|
||
|
||
<include
|
||
android:id="@+id/buttons_layout"
|
||
layout="@layout/bottom_buttons_layout"
|
||
android:layout_width="0dp"
|
||
android:layout_marginBottom="32dp"
|
||
android:layout_height="wrap_content"
|
||
app:layout_constraintBottom_toBottomOf="parent"
|
||
app:layout_constraintEnd_toEndOf="parent"
|
||
app:layout_constraintStart_toStartOf="parent"
|
||
/>
|
||
|
||
<ViewStub
|
||
android:id="@+id/statsViewStub"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="match_parent"
|
||
android:inflatedId="@+id/statistics_layout"
|
||
android:layout="@layout/stats_layout" />
|
||
|
||
</androidx.constraintlayout.widget.ConstraintLayout><androidx.constraintlayout.widget.ConstraintLayout
|
||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content">
|
||
|
||
<Button
|
||
android:id="@+id/restartButton"
|
||
style="@style/ButtonStyle"
|
||
android:text="@string/restart"
|
||
app:layout_constraintHorizontal_chainStyle="spread"
|
||
app:layout_constraintStart_toStartOf="parent"
|
||
app:layout_constraintEnd_toStartOf="@+id/statsButton"
|
||
app:layout_constraintTop_toTopOf="parent"
|
||
app:layout_constraintBottom_toBottomOf="parent"
|
||
/>
|
||
|
||
<Button
|
||
android:id="@+id/statsButton"
|
||
style="@style/ButtonStyle"
|
||
android:text="@string/stats"
|
||
app:layout_constraintStart_toEndOf="@+id/restartButton"
|
||
app:layout_constraintEnd_toStartOf="@+id/menuButton"
|
||
app:layout_constraintTop_toTopOf="parent"
|
||
app:layout_constraintBottom_toBottomOf="parent"
|
||
|
||
/>
|
||
|
||
<Button
|
||
android:id="@+id/menuButton"
|
||
style="@style/ButtonStyle"
|
||
android:text="@string/menu"
|
||
app:layout_constraintStart_toEndOf="@+id/statsButton"
|
||
app:layout_constraintEnd_toEndOf="parent"
|
||
app:layout_constraintTop_toTopOf="parent"
|
||
app:layout_constraintBottom_toBottomOf="parent"
|
||
/>
|
||
|
||
</androidx.constraintlayout.widget.ConstraintLayout><?xml version="1.0" encoding="utf-8"?>
|
||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:orientation="vertical"
|
||
android:padding="24dp"
|
||
android:background="@drawable/dialog_background">
|
||
|
||
<TextView
|
||
android:id="@+id/dialogTitleAbout"
|
||
style="@style/SectionTitle" android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/about_title"
|
||
android:gravity="center"
|
||
android:layout_marginBottom="16dp"/>
|
||
|
||
<TextView
|
||
android:id="@+id/aboutMessageTextView"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/about_message"
|
||
android:textColor="@color/text_tile_low"
|
||
android:textSize="16sp"
|
||
android:fontFamily="@font/nunito_family"
|
||
android:gravity="center"
|
||
android:lineSpacingExtra="4dp"
|
||
android:layout_marginBottom="16dp"/>
|
||
|
||
<TextView
|
||
android:id="@+id/websiteLinkTextView"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/about_website_text"
|
||
android:textColor="@color/design_default_color_primary" android:textSize="16sp"
|
||
android:fontFamily="@font/nunito_family"
|
||
android:gravity="center"
|
||
android:clickable="true" android:focusable="true"
|
||
android:background="?android:attr/selectableItemBackground" android:paddingTop="8dp"
|
||
android:paddingBottom="8dp"
|
||
android:layout_marginBottom="16dp"/>
|
||
|
||
<Button
|
||
android:id="@+id/dialogOkButtonAbout"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginStart="0dp" android:layout_marginEnd="0dp"
|
||
android:layout_marginTop="8dp" android:layout_marginBottom="0dp"
|
||
android:text="@string/ok" />
|
||
|
||
</LinearLayout><?xml version="1.0" encoding="utf-8"?>
|
||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:orientation="vertical"
|
||
android:padding="24dp"
|
||
android:background="@drawable/dialog_background">
|
||
|
||
<TextView
|
||
android:id="@+id/dialogTitleGameOver"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/game_over_title"
|
||
android:textSize="20sp"
|
||
android:textStyle="bold"
|
||
android:textColor="@color/text_tile_low"
|
||
android:gravity="center"
|
||
android:layout_marginBottom="8dp"/>
|
||
|
||
<TextView
|
||
android:id="@+id/dialogMessageGameOver" android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/game_over_message" android:textSize="16sp"
|
||
android:textColor="@color/text_tile_low"
|
||
android:gravity="center"
|
||
android:layout_marginBottom="16dp"/>
|
||
|
||
<LinearLayout
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:orientation="horizontal">
|
||
|
||
<Button
|
||
android:id="@+id/dialogQuitButtonGameOver"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="0dp"
|
||
android:layout_height="wrap_content"
|
||
android:layout_weight="1"
|
||
android:layout_marginEnd="8dp"
|
||
android:text="@string/quit" />
|
||
|
||
<Button
|
||
android:id="@+id/dialogNewGameButtonGameOver"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="0dp"
|
||
android:layout_height="wrap_content"
|
||
android:layout_weight="1"
|
||
android:layout_marginStart="8dp"
|
||
android:text="@string/new_game" />
|
||
|
||
</LinearLayout>
|
||
|
||
</LinearLayout><?xml version="1.0" encoding="utf-8"?>
|
||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:orientation="vertical"
|
||
android:padding="24dp"
|
||
android:background="@drawable/dialog_background">
|
||
|
||
<TextView
|
||
android:id="@+id/dialogTitleWon"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/you_won_title"
|
||
android:textSize="20sp"
|
||
android:textStyle="bold"
|
||
android:textColor="@color/text_tile_low"
|
||
android:gravity="center"
|
||
android:layout_marginBottom="8dp"/>
|
||
|
||
<TextView
|
||
android:id="@+id/dialogMessageWon"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/you_won_message"
|
||
android:textSize="16sp"
|
||
android:textColor="@color/text_tile_low"
|
||
android:gravity="center"
|
||
android:layout_marginBottom="16dp"/>
|
||
|
||
<LinearLayout
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:orientation="horizontal">
|
||
|
||
<Button
|
||
android:id="@+id/dialogNewGameButtonWon"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="0dp"
|
||
android:layout_height="wrap_content"
|
||
android:layout_weight="1"
|
||
android:layout_marginEnd="8dp"
|
||
android:text="@string/new_game" />
|
||
|
||
<Button
|
||
android:id="@+id/dialogKeepPlayingButton"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="0dp"
|
||
android:layout_height="wrap_content"
|
||
android:layout_weight="1"
|
||
android:layout_marginStart="8dp"
|
||
android:text="@string/keep_playing" />
|
||
|
||
</LinearLayout>
|
||
|
||
</LinearLayout><?xml version="1.0" encoding="utf-8"?>
|
||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:orientation="vertical"
|
||
android:padding="24dp"
|
||
android:background="@drawable/dialog_background">
|
||
|
||
<TextView
|
||
android:id="@+id/dialogTitleHowToPlay"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/how_to_play_title"
|
||
style="@style/SectionTitle" android:gravity="center"
|
||
android:layout_marginBottom="16dp"/>
|
||
|
||
<ScrollView
|
||
android:layout_width="match_parent"
|
||
android:layout_height="0dp"
|
||
android:layout_weight="1"
|
||
android:layout_marginBottom="16dp">
|
||
|
||
<LinearLayout
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:orientation="vertical">
|
||
|
||
<TextView
|
||
android:id="@+id/howToPlayTextContent"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/how_to_play_instructions"
|
||
android:textColor="@color/text_tile_low"
|
||
android:textSize="16sp"
|
||
android:lineSpacingExtra="4dp"
|
||
android:fontFamily="@font/nunito_family"/>
|
||
|
||
</LinearLayout>
|
||
</ScrollView>
|
||
|
||
<Button
|
||
android:id="@+id/dialogOkButtonHowToPlay"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginStart="0dp" android:layout_marginEnd="0dp"
|
||
android:layout_marginTop="8dp" android:layout_marginBottom="0dp"
|
||
android:text="@string/ok" />
|
||
|
||
</LinearLayout><?xml version="1.0" encoding="utf-8"?>
|
||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:orientation="vertical"
|
||
android:padding="24dp"
|
||
android:background="@drawable/dialog_background">
|
||
|
||
<TextView
|
||
android:id="@+id/dialogTitleMenu"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/menu_title"
|
||
android:textSize="20sp"
|
||
android:textStyle="bold"
|
||
android:textColor="@color/text_tile_low"
|
||
android:gravity="center"
|
||
android:layout_marginBottom="16dp"/>
|
||
|
||
<Button
|
||
android:id="@+id/menuButtonHowToPlay"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginStart="0dp" android:layout_marginEnd="0dp"
|
||
android:layout_marginTop="4dp" android:layout_marginBottom="4dp"
|
||
android:text="@string/menu_option_how_to_play" />
|
||
|
||
<Button
|
||
android:id="@+id/menuButtonSettings"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginStart="0dp" android:layout_marginEnd="0dp"
|
||
android:layout_marginTop="4dp" android:layout_marginBottom="4dp"
|
||
android:text="@string/menu_option_settings" />
|
||
|
||
<Button
|
||
android:id="@+id/menuButtonAbout"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginStart="0dp" android:layout_marginEnd="0dp"
|
||
android:layout_marginTop="4dp" android:layout_marginBottom="4dp"
|
||
android:text="@string/menu_option_about" />
|
||
|
||
<View
|
||
android:layout_width="match_parent"
|
||
android:layout_height="1dp"
|
||
android:background="@color/game_board_background"
|
||
android:layout_marginTop="16dp"
|
||
android:layout_marginBottom="16dp"/>
|
||
|
||
<Button
|
||
android:id="@+id/menuButtonReturn"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginStart="0dp" android:layout_marginEnd="0dp"
|
||
android:layout_marginTop="4dp" android:layout_marginBottom="0dp"
|
||
android:text="@string/menu_option_return" />
|
||
|
||
</LinearLayout><?xml version="1.0" encoding="utf-8"?>
|
||
<androidx.constraintlayout.widget.ConstraintLayout
|
||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:padding="16dp"
|
||
android:background="@drawable/dialog_background">
|
||
|
||
<TextView
|
||
android:id="@+id/dialogTitle"
|
||
android:layout_width="0dp"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/restart_confirm_title"
|
||
android:textSize="20sp"
|
||
android:textStyle="bold"
|
||
android:textColor="@color/text_tile_low"
|
||
android:gravity="center"
|
||
app:layout_constraintTop_toTopOf="parent"
|
||
app:layout_constraintStart_toStartOf="parent"
|
||
app:layout_constraintEnd_toEndOf="parent" />
|
||
|
||
<TextView
|
||
android:id="@+id/dialogMessage"
|
||
android:layout_width="0dp"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/restart_confirm_message"
|
||
android:textSize="16sp"
|
||
android:textColor="@color/text_tile_low"
|
||
android:gravity="center"
|
||
android:layout_marginTop="8dp"
|
||
app:layout_constraintTop_toBottomOf="@+id/dialogTitle"
|
||
app:layout_constraintStart_toStartOf="parent"
|
||
app:layout_constraintEnd_toEndOf="parent" />
|
||
|
||
<LinearLayout
|
||
android:layout_width="0dp"
|
||
android:layout_height="wrap_content"
|
||
android:orientation="horizontal"
|
||
android:layout_marginTop="16dp"
|
||
app:layout_constraintTop_toBottomOf="@+id/dialogMessage"
|
||
app:layout_constraintStart_toStartOf="parent"
|
||
app:layout_constraintEnd_toEndOf="parent">
|
||
|
||
<Button
|
||
android:id="@+id/dialogCancelButton"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="0dp"
|
||
android:layout_height="wrap_content"
|
||
android:layout_weight="1"
|
||
android:layout_marginEnd="8dp"
|
||
android:text="@string/cancel" />
|
||
|
||
<Button
|
||
android:id="@+id/dialogConfirmButton"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="0dp"
|
||
android:layout_height="wrap_content"
|
||
android:layout_weight="1"
|
||
android:layout_marginStart="8dp"
|
||
android:text="@string/confirm" />
|
||
|
||
</LinearLayout>
|
||
|
||
</androidx.constraintlayout.widget.ConstraintLayout><?xml version="1.0" encoding="utf-8"?>
|
||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:orientation="vertical"
|
||
android:padding="24dp"
|
||
android:background="@drawable/dialog_background">
|
||
|
||
<TextView
|
||
android:id="@+id/dialogTitleSettings"
|
||
style="@style/SectionTitle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/settings_title"
|
||
android:gravity="center"
|
||
android:layout_marginBottom="16dp"/>
|
||
|
||
<LinearLayout
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:orientation="vertical"
|
||
android:layout_marginBottom="16dp">
|
||
|
||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||
android:id="@+id/switchSound"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/settings_sound_label"
|
||
android:textSize="16sp"
|
||
android:fontFamily="@font/nunito_family"
|
||
android:textColor="@color/text_tile_low"
|
||
android:enabled="true" /> <com.google.android.material.switchmaterial.SwitchMaterial
|
||
android:id="@+id/switchNotifications"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginTop="8dp"
|
||
android:text="@string/settings_notifications_label"
|
||
android:textSize="16sp"
|
||
android:fontFamily="@font/nunito_family"
|
||
android:textColor="@color/text_tile_low"
|
||
android:enabled="true" /> </LinearLayout>
|
||
|
||
<Button
|
||
android:id="@+id/buttonManagePermissions"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginStart="0dp" android:layout_marginEnd="0dp"
|
||
android:layout_marginTop="4dp" android:layout_marginBottom="4dp"
|
||
android:text="@string/settings_permissions_button" />
|
||
|
||
<Button
|
||
android:id="@+id/buttonShareStats"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginStart="0dp" android:layout_marginEnd="0dp"
|
||
android:layout_marginTop="4dp" android:layout_marginBottom="4dp"
|
||
android:text="@string/settings_share_stats_button" />
|
||
|
||
<Button
|
||
android:id="@+id/buttonResetStats"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginStart="0dp" android:layout_marginEnd="0dp"
|
||
android:layout_marginTop="4dp" android:layout_marginBottom="4dp"
|
||
android:backgroundTint="@android:color/holo_red_dark" android:text="@string/settings_reset_stats_button" />
|
||
|
||
<Button
|
||
android:id="@+id/buttonQuitApp"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginStart="0dp" android:layout_marginEnd="0dp"
|
||
android:layout_marginTop="4dp" android:layout_marginBottom="16dp"
|
||
android:text="@string/settings_quit_app_button" />
|
||
|
||
<Button
|
||
android:id="@+id/buttonCloseSettings"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginStart="0dp" android:layout_marginEnd="0dp"
|
||
android:layout_marginTop="8dp" android:layout_marginBottom="0dp"
|
||
android:text="@string/settings_close_button" />
|
||
|
||
</LinearLayout><?xml version="1.0" encoding="utf-8"?>
|
||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||
xmlns:tools="http://schemas.android.com/tools"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="match_parent"
|
||
android:background="@color/background_color"
|
||
tools:context=".MainActivity">
|
||
|
||
<TextView
|
||
android:id="@+id/statsTitle"
|
||
android:layout_width="wrap_content"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginTop="24dp"
|
||
android:text="@string/stats_title"
|
||
android:textSize="36sp"
|
||
android:textStyle="bold"
|
||
android:textColor="#333333"
|
||
app:layout_constraintEnd_toEndOf="parent"
|
||
app:layout_constraintStart_toStartOf="parent"
|
||
app:layout_constraintTop_toTopOf="parent" />
|
||
|
||
<ScrollView
|
||
android:layout_width="0dp"
|
||
android:layout_height="0dp"
|
||
android:layout_marginTop="16dp"
|
||
android:layout_marginBottom="16dp"
|
||
app:layout_constraintBottom_toTopOf="@+id/backButton"
|
||
app:layout_constraintEnd_toEndOf="parent"
|
||
app:layout_constraintStart_toStartOf="parent"
|
||
app:layout_constraintTop_toBottomOf="@+id/statsTitle"
|
||
android:fillViewport="true">
|
||
|
||
<LinearLayout
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:orientation="vertical"
|
||
android:padding="16dp">
|
||
|
||
<TextView
|
||
style="@style/SectionTitle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/general_section" />
|
||
|
||
<LinearLayout
|
||
style="@style/SectionContainer"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content">
|
||
|
||
<TextView
|
||
android:id="@+id/high_score_stats_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/high_score_stats" />
|
||
|
||
<TextView
|
||
android:id="@+id/total_games_played_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/total_games_played" />
|
||
|
||
<TextView
|
||
android:id="@+id/total_games_started_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/total_games_started" />
|
||
|
||
<TextView
|
||
android:id="@+id/win_percentage_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/win_percentage" />
|
||
|
||
<TextView
|
||
android:id="@+id/total_play_time_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/total_play_time" />
|
||
|
||
<TextView
|
||
android:id="@+id/total_moves_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/total_moves" />
|
||
</LinearLayout>
|
||
|
||
<TextView
|
||
style="@style/SectionTitle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/current_game_section" />
|
||
|
||
<LinearLayout
|
||
style="@style/SectionContainer"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content">
|
||
|
||
<TextView
|
||
android:id="@+id/current_moves_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/current_moves" />
|
||
|
||
<TextView
|
||
android:id="@+id/current_game_time_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/current_game_time" />
|
||
|
||
<TextView
|
||
android:id="@+id/merges_this_game"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/merges_this_game_label" />
|
||
</LinearLayout>
|
||
|
||
<TextView
|
||
style="@style/SectionTitle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/single_player_section" />
|
||
|
||
<LinearLayout
|
||
style="@style/SectionContainer"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content">
|
||
|
||
<TextView
|
||
android:id="@+id/average_game_time_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/average_time_per_game" />
|
||
|
||
<TextView
|
||
android:id="@+id/best_winning_time_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/best_winning_time" />
|
||
|
||
<TextView
|
||
android:id="@+id/worst_winning_time_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/worst_winning_time" />
|
||
|
||
<TextView
|
||
android:id="@+id/total_merges_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/total_merges" />
|
||
|
||
<TextView
|
||
android:id="@+id/highest_tile_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/highest_tile" />
|
||
|
||
<TextView
|
||
android:id="@+id/number_of_time_objective_reached_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/number_of_time_objective_reached" />
|
||
|
||
<TextView
|
||
android:id="@+id/perfect_game_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/perfect_games" />
|
||
</LinearLayout>
|
||
|
||
<TextView
|
||
style="@style/SectionTitle"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/multiplayer_section" />
|
||
|
||
<LinearLayout
|
||
style="@style/SectionContainer"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content">
|
||
|
||
<TextView
|
||
android:id="@+id/multiplayer_games_won_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/multiplayer_games_won" />
|
||
|
||
<TextView
|
||
android:id="@+id/multiplayer_games_played_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/multiplayer_games_played" />
|
||
|
||
<TextView
|
||
android:id="@+id/multiplayer_win_rate_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/multiplayer_win_rate" />
|
||
|
||
<TextView
|
||
android:id="@+id/multiplayer_best_winning_streak_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/multiplayer_best_winning_streak" />
|
||
|
||
<TextView
|
||
android:id="@+id/multiplayer_average_score_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/multiplayer_average_score" />
|
||
|
||
<TextView
|
||
android:id="@+id/average_time_per_game_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/average_time_per_game_label" />
|
||
|
||
<TextView
|
||
android:id="@+id/total_multiplayer_losses_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/total_multiplayer_losses" />
|
||
|
||
<TextView
|
||
android:id="@+id/multiplayer_high_score_label"
|
||
style="@style/StatLabel"
|
||
android:layout_width="match_parent"
|
||
android:layout_height="wrap_content"
|
||
android:text="@string/multiplayer_high_score" />
|
||
</LinearLayout>
|
||
|
||
</LinearLayout>
|
||
</ScrollView>
|
||
|
||
<Button
|
||
android:id="@+id/backButton"
|
||
style="@style/ButtonStyle"
|
||
android:layout_width="wrap_content"
|
||
android:layout_height="wrap_content"
|
||
android:layout_marginBottom="24dp"
|
||
android:text="@string/back_button_label"
|
||
app:layout_constraintBottom_toBottomOf="parent"
|
||
app:layout_constraintEnd_toEndOf="parent"
|
||
app:layout_constraintStart_toStartOf="parent" />
|
||
|
||
</androidx.constraintlayout.widget.ConstraintLayout><?xml version="1.0" encoding="utf-8"?>
|
||
<resources>
|
||
<color name="black">#FF000000</color>
|
||
<color name="white">#FFFFFFFF</color>
|
||
|
||
<color name="text_tile_low">#776e65</color>
|
||
<color name="text_tile_high">#f9f6f2</color>
|
||
|
||
<color name="background_color">#faf8ef</color>
|
||
<color name="game_board_background">#bbada0</color>
|
||
<color name="score_label_background">#f65e3b</color>
|
||
<color name="game_label_background">#edc22e</color>
|
||
<color name="button_background">#8f7a66</color>
|
||
<color name="button_text_color">#f9f6f2</color>
|
||
<color name="tile_empty">#cdc1b4</color>
|
||
<color name="tile_2">#eee4da</color>
|
||
<color name="tile_4">#ede0c8</color>
|
||
<color name="tile_8">#f2b179</color>
|
||
<color name="tile_16">#f59563</color>
|
||
<color name="tile_32">#f67c5f</color>
|
||
<color name="tile_64">#f65e3b</color>
|
||
<color name="tile_128">#edcf72</color>
|
||
<color name="tile_256">#edcc61</color>
|
||
<color name="tile_512">#edc850</color>
|
||
<color name="tile_1024">#edc53f</color>
|
||
<color name="tile_2048">#edc22e</color>
|
||
<color name="tile_super">#3c3a32</color>
|
||
<color name="multiplayer_button_start">#6200ee</color>
|
||
<color name="multiplayer_button_end">#3700b3</color>
|
||
<color name="multiplayer_button_pressed_start">#3700b3</color>
|
||
<color name="multiplayer_button_pressed_end">#03dac6</color>
|
||
<color name="multiplayer_button_stroke">#b0bec5</color>
|
||
|
||
<color name="stats_background">#875932</color>
|
||
|
||
</resources><resources>
|
||
<dimen name="padding_general">16dp</dimen>
|
||
<dimen name="margin_between_elements">8dp</dimen>
|
||
<dimen name="text_size_game_label">48sp</dimen>
|
||
<dimen name="text_size_score_label">18sp</dimen>
|
||
<dimen name="corner_radius">6dp</dimen>
|
||
<dimen name="padding_game_board">4dp</dimen>
|
||
<dimen name="tile_margin">4dp</dimen> <dimen name="text_size_tile_small">24sp</dimen>
|
||
<dimen name="text_size_tile_medium">20sp</dimen>
|
||
<dimen name="text_size_tile_large">16sp</dimen>
|
||
</resources><resources>
|
||
<string name="app_name">Best 2048</string>
|
||
<string name="name_2048">2048</string>
|
||
<string name="score">Score :</string>
|
||
<string name="score_placeholder">Score :\n%d</string>
|
||
<string name="high_score_placeholder">High Score :\n%d</string>
|
||
<string name="high_score">High Score :</string>
|
||
<string name="restart">Restart</string>
|
||
<string name="stats">Stats</string>
|
||
<string name="menu">Menu</string>
|
||
<string name="multiplayer">Multiplayer</string>
|
||
<string name="restart_confirm_title">Restart ?</string>
|
||
<string name="restart_confirm_message">Are you sure you want to restart the game ?</string>
|
||
<string name="cancel">Cancel</string>
|
||
<string name="confirm">Confirm</string>
|
||
<string name="high_score_stats">High Score: %d</string>
|
||
<string name="total_games_played">Total Games Played: %d</string>
|
||
<string name="total_games_started">Total Games Started: %d</string>
|
||
<string name="win_percentage">Win Percentage: %s</string>
|
||
<string name="total_play_time">Total Play Time: %s</string>
|
||
<string name="total_moves">Total Moves: %d</string>
|
||
<string name="current_moves">Current Moves: %d</string>
|
||
<string name="current_game_time">Current Game Time: %s</string>
|
||
<string name="merges_this_game_label">Merges: %d</string>
|
||
<string name="average_time_per_game">Average Game Time: %s</string>
|
||
<string name="best_winning_time">Best Winning Time: %s</string>
|
||
<string name="worst_winning_time">Worst Winning Time: %s</string>
|
||
<string name="total_merges">Total Merges: %d</string>
|
||
<string name="highest_tile">Highest Tile: %d</string>
|
||
<string name="number_of_time_objective_reached">Number of time objective reached: %d</string>
|
||
<string name="perfect_games">Perfect game : %d</string>
|
||
<string name="multiplayer_games_won">Multiplayer game won : %d</string>
|
||
<string name="multiplayer_games_played">Multiplayer game played : %d</string>
|
||
<string name="multiplayer_win_rate">Multiplayer win rate : %s</string>
|
||
<string name="multiplayer_best_winning_streak">Best winning streak : %d</string>
|
||
<string name="multiplayer_average_score">Multiplayer Average Score: %d</string>
|
||
<string name="average_time_per_game_label">Average time per game: %s</string>
|
||
<string name="total_multiplayer_losses">Total Multiplayer losses: %d</string>
|
||
<string name="multiplayer_high_score">Multiplayer High Score: %d</string>
|
||
<string name="stats_button_label">Stats</string>
|
||
<string name="stats_title">Statistics</string>
|
||
<string name="general_section">General</string>
|
||
<string name="current_game_section">Current Game</string>
|
||
<string name="single_player_section">Single Player</string>
|
||
<string name="multiplayer_section">Multiplayer</string>
|
||
<string name="back_button_label">Back</string>
|
||
<string name="you_won_title">You won!</string>
|
||
<string name="you_won_message">Congratulations, you\'ve reached 2048!</string>
|
||
<string name="keep_playing">Continue</string>
|
||
<string name="new_game">New Part</string>
|
||
<string name="game_over_title">Game over!</string>
|
||
<string name="game_over_message">No move possible.\nFinal score: %d</string>
|
||
<string name="quit">To leave</string>
|
||
<string name="menu_title">Main Menu</string>
|
||
<string name="menu_option_how_to_play">How to Play</string>
|
||
<string name="menu_option_settings">Settings</string>
|
||
<string name="menu_option_about">About</string>
|
||
<string name="menu_option_return">Back</string>
|
||
<string name="how_to_play_title">How to Play</string>
|
||
<string name="how_to_play_instructions">Swipe the screen (Up, Down, Left, Right) to move all the tiles.\n\nWhen two tiles with the same number touch, they merge into one!\n\nReach the 2048 tile to win.\n\nThe game is over if the board is full and no moves are possible.</string>
|
||
<string name="about_title">About Best 2048</string>
|
||
<string name="about_message">Version: 1.0 (University Project)\nDeveloped by: La Legion de Muyue\n(Leader: Muyue, Members: 2 others)\n\nBased on the popular game 2048.</string>
|
||
<string name="about_website_text">Website : legion-muyue.fr</string> <string name="about_website_url">https://legion-muyue.fr</string>
|
||
<string name="ok">OK</string>
|
||
<string name="settings_title">Settings</string>
|
||
<string name="settings_sound_label">Sound</string>
|
||
<string name="settings_notifications_label">Notifications</string>
|
||
<string name="settings_permissions_button">Manage Permissions</string>
|
||
<string name="settings_share_stats_button">Share my Statistics</string>
|
||
<string name="settings_reset_stats_button">Reset Statistics</string>
|
||
<string name="settings_quit_app_button">Quit Application</string>
|
||
<string name="settings_close_button">Close</string>
|
||
<string name="reset_stats_confirm_title">Reset Stats?</string>
|
||
<string name="reset_stats_confirm_message">Are you sure you want to erase all your saved statistics? This action is irreversible.</string>
|
||
<string name="share_stats_title">Share my stats via…</string>
|
||
<string name="share_stats_subject">My 2048 Statistics</string>
|
||
<string name="share_stats_body">Here are my stats on Best 2048:\n- Best Score: %d\n- Highest Tile: %d\n- Games Won: %d / %d\n- Total Time: %s\n- Total Moves: %d</string>
|
||
<string name="stats_reset_confirmation">Statistics reset.</string>
|
||
<string name="notification_channel_name">Game Updates</string>
|
||
<string name="notification_channel_description">Notifications related to the 2048 game</string>
|
||
<string name="notification_title_achievement">Congratulations!</string>
|
||
<string name="notification_text_achievement">You reached the %d tile!</string>
|
||
<string name="notification_title_highscore">New Challenge!</string>
|
||
<string name="notification_text_highscore">Your best score is %d. Can you do better?</string>
|
||
<string name="notification_title_inactivity">We miss you!</string>
|
||
<string name="notification_text_inactivity">How about a quick game of 2048 to relax?</string>
|
||
<string name="notifications_permission_required_title">Permission Required</string>
|
||
<string name="notifications_permission_required_message">To receive notifications (reminders, achievements), please allow the application to send notifications in the settings.</string>
|
||
<string name="go_to_settings">Go to Settings</string>
|
||
<string name="notifications_enabled">Notifications enabled.</string>
|
||
<string name="notifications_disabled">Notifications disabled.</string>
|
||
<string name="settings_test_notif_highscore">Test High Score Notif</string>
|
||
<string name="settings_test_notif_inactivity">Test Inactivity Notif</string>
|
||
<string name="sound_enabled">Sound effects enabled.</string>
|
||
<string name="sound_disabled">Sound effects disabled.</string>
|
||
</resources><resources>
|
||
<style name="Theme.Best2048" parent="Theme.AppCompat.Light.NoActionBar">
|
||
</style>
|
||
|
||
<style name="ScoreLabelStyle">
|
||
<item name="android:fontFamily">@font/nunito_family</item>
|
||
<item name="android:layout_width">match_parent</item>
|
||
<item name="android:layout_height">0dp</item>
|
||
<item name="android:layout_weight">1</item>
|
||
<item name="android:background">@color/score_label_background</item>
|
||
<item name="android:gravity">center</item>
|
||
<item name="android:padding">8dp</item>
|
||
<item name="android:textColor">@color/white</item>
|
||
<item name="android:textSize">@dimen/text_size_score_label</item>
|
||
</style>
|
||
|
||
<style name="ButtonStyle">
|
||
<item name="android:fontFamily">@font/nunito_family</item>
|
||
<item name="android:layout_width">0dp</item>
|
||
<item name="android:layout_height">wrap_content</item>
|
||
<item name="android:backgroundTint">@color/button_background</item>
|
||
<item name="android:textColor">@color/white</item>
|
||
<item name="android:layout_marginBottom">@dimen/padding_general</item>
|
||
<item name="android:layout_marginTop">@dimen/padding_general</item>
|
||
<item name="android:layout_marginStart">@dimen/padding_general</item>
|
||
<item name="android:layout_marginEnd">@dimen/padding_general</item>
|
||
<item name="android:buttonBarButtonStyle">?android:attr/buttonBarButtonStyle</item>
|
||
</style>
|
||
|
||
<style name="LargeButtonStyle">
|
||
<item name="android:fontFamily">@font/nunito_family</item>
|
||
<item name="android:layout_width">0dp</item>
|
||
<item name="android:layout_height">wrap_content</item>
|
||
<item name="android:layout_marginStart">@dimen/padding_general</item>
|
||
<item name="android:layout_marginEnd">@dimen/padding_general</item>
|
||
<item name="android:buttonBarButtonStyle">?android:attr/buttonBarButtonStyle</item>
|
||
<item name="android:layout_marginTop">@dimen/padding_general</item>
|
||
<item name="android:layout_marginBottom">@dimen/padding_general</item>
|
||
<item name="android:background">@drawable/button_multiplayer_background</item>
|
||
<item name="android:textColor">@color/white</item>
|
||
<item name="android:textAllCaps">true</item>
|
||
<item name="android:textStyle">bold</item>
|
||
<item name="android:textSize">20sp</item>
|
||
</style>
|
||
|
||
<style name="SectionTitle">
|
||
<item name="android:fontFamily">@font/nunito_family</item>
|
||
<item name="android:textSize">18sp</item>
|
||
<item name="android:textStyle">bold</item>
|
||
<item name="android:textColor">@color/text_tile_low</item> <item name="android:layout_marginBottom">4dp</item>
|
||
</style>
|
||
|
||
<style name="SectionContainer">
|
||
<item name="android:fontFamily">@font/nunito_family</item>
|
||
<item name="android:layout_width">match_parent</item>
|
||
<item name="android:layout_height">wrap_content</item>
|
||
<item name="android:orientation">vertical</item>
|
||
<item name="android:background">@drawable/dialog_background</item> <item name="android:padding">12dp</item>
|
||
<item name="android:layout_marginBottom">16dp</item>
|
||
</style>
|
||
|
||
<style name="StatLabel">
|
||
<item name="android:fontFamily">@font/nunito_family</item>
|
||
<item name="android:layout_width">match_parent</item>
|
||
<item name="android:layout_height">wrap_content</item>
|
||
<item name="android:textColor">@color/text_tile_low</item> <item name="android:textSize">16sp</item>
|
||
<item name="android:layout_marginBottom">4dp</item>
|
||
</style>
|
||
|
||
<style name="Theme.AppCompat.Light.NoActionBar.FullScreen" parent="Theme.AppCompat.Light.NoActionBar">
|
||
<item name="android:windowFullscreen">true</item>
|
||
<item name="android:windowContentOverlay">@null</item>
|
||
</style>
|
||
</resources> |