Feat: Ajout animations de base pour les tuiles
This commit is contained in:
parent
bf914f291d
commit
470649ad36
@ -40,6 +40,14 @@ import androidx.core.content.ContextCompat;
|
|||||||
import androidx.gridlayout.widget.GridLayout;
|
import androidx.gridlayout.widget.GridLayout;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.Toast;
|
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 java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
@ -68,6 +76,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private enum GameFlowState { PLAYING, WON_DIALOG_SHOWN, GAME_OVER }
|
private enum GameFlowState { PLAYING, WON_DIALOG_SHOWN, GAME_OVER }
|
||||||
private GameFlowState currentGameState = GameFlowState.PLAYING;
|
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 ---
|
// --- Preferences ---
|
||||||
private SharedPreferences preferences;
|
private SharedPreferences preferences;
|
||||||
private static final String PREFS_NAME = "Best2048_Prefs";
|
private static final String PREFS_NAME = "Best2048_Prefs";
|
||||||
@ -109,6 +120,74 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
if (notificationsEnabled) {
|
if (notificationsEnabled) {
|
||||||
startNotificationService();
|
startNotificationService();
|
||||||
}
|
}
|
||||||
|
syncBoardView();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronise COMPLETEMENT le GridLayout avec l'état actuel de 'game.board'.
|
||||||
|
* Crée/Met à jour/Supprime les TextViews nécessaires et les stocke dans tileViews.
|
||||||
|
* C'est la méthode qui assure que l'affichage correspond à la logique à un instant T.
|
||||||
|
*/
|
||||||
|
private void syncBoardView() {
|
||||||
|
if (game == null) return;
|
||||||
|
// Log.d("SyncDebug", "Syncing board view...");
|
||||||
|
|
||||||
|
// Pas besoin de removeAllViews si on gère correctement add/remove/update
|
||||||
|
for (int r = 0; r < BOARD_SIZE; r++) {
|
||||||
|
for (int c = 0; c < BOARD_SIZE; c++) {
|
||||||
|
int value = game.getCellValue(r, c);
|
||||||
|
TextView currentView = tileViews[r][c];
|
||||||
|
|
||||||
|
if (currentView == null && value > 0) {
|
||||||
|
// Une nouvelle tuile doit apparaître là où il n'y en avait pas
|
||||||
|
currentView = createTileTextView(value, r, c);
|
||||||
|
tileViews[r][c] = currentView;
|
||||||
|
boardGridLayout.addView(currentView);
|
||||||
|
// Log.d("SyncDebug", "Added view at ["+r+","+c+"]");
|
||||||
|
} else if (currentView != null && value == 0) {
|
||||||
|
// Une tuile doit disparaître
|
||||||
|
boardGridLayout.removeView(currentView);
|
||||||
|
tileViews[r][c] = null;
|
||||||
|
// Log.d("SyncDebug", "Removed view at ["+r+","+c+"]");
|
||||||
|
} else if (currentView != null && value > 0) {
|
||||||
|
// Une tuile existe déjà, on met juste à jour son style/texte
|
||||||
|
// Vérifie si la valeur a changé (fusion) pour potentielle animation future
|
||||||
|
// if (!currentView.getText().toString().equals(String.valueOf(value))) {
|
||||||
|
// Log.d("SyncDebug", "Updating view at ["+r+","+c+"] to "+value);
|
||||||
|
// }
|
||||||
|
setTileStyle(currentView, value); // Applique toujours le style correct
|
||||||
|
}
|
||||||
|
// Si currentView == null && value == 0 -> rien à faire
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Log.d("SyncDebug", "Board sync finished.");
|
||||||
|
updateScores(); // Assure que les scores sont aussi à jour
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée et configure une TextView pour une tuile. (Peut être simplifié)
|
||||||
|
* @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
|
||||||
|
|
||||||
|
// Les LayoutParams sont nécessaires pour GridLayout
|
||||||
|
GridLayout.LayoutParams params = new GridLayout.LayoutParams();
|
||||||
|
// Taille à 0dp pour que le poids (spec) fonctionne
|
||||||
|
params.width = 0;
|
||||||
|
params.height = 0;
|
||||||
|
// Position initiale basée sur row/col, poids pour remplir la cellule
|
||||||
|
params.rowSpec = GridLayout.spec(row, 1f);
|
||||||
|
params.columnSpec = GridLayout.spec(col, 1f);
|
||||||
|
int margin = (int) getResources().getDimension(R.dimen.tile_margin);
|
||||||
|
params.setMargins(margin, margin, margin, margin);
|
||||||
|
tileTextView.setLayoutParams(params);
|
||||||
|
|
||||||
|
return tileTextView;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -317,63 +396,169 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traite un geste de swipe de l'utilisateur sur le plateau de jeu.
|
* Traite un swipe : met à jour la logique, synchronise l'affichage instantanément,
|
||||||
* Met à jour le jeu, les statistiques et l'UI, et vérifie les conditions de fin de partie.
|
* puis lance des animations locales d'apparition/fusion sur les tuiles concernées.
|
||||||
* @param direction La direction du swipe détecté.
|
* @param direction Direction du swipe.
|
||||||
*/
|
*/
|
||||||
private void handleSwipe(Direction direction) {
|
private void handleSwipe(Direction direction) {
|
||||||
|
// Bloque si jeu terminé
|
||||||
if (game == null || gameStats == null || currentGameState == GameFlowState.GAME_OVER) {
|
if (game == null || gameStats == null || currentGameState == GameFlowState.GAME_OVER) {
|
||||||
return; // Ignore swipe si jeu terminé ou non initialisé
|
return;
|
||||||
}
|
}
|
||||||
|
// Note: On ne bloque plus sur 'isAnimating' pour cette approche
|
||||||
|
|
||||||
int scoreBefore = game.getCurrentScore();
|
int scoreBefore = game.getCurrentScore();
|
||||||
boolean boardChanged = false;
|
int[][] boardBeforePush = game.getBoard(); // État avant le push
|
||||||
|
|
||||||
// Tente d'effectuer le mouvement dans l'objet Game
|
boolean boardChanged = false;
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case UP: boardChanged = game.pushUp(); break;
|
case UP: boardChanged = game.pushUp(); break;
|
||||||
case DOWN: boardChanged = game.pushDown(); break;
|
case DOWN: boardChanged = game.pushDown(); break;
|
||||||
case LEFT: boardChanged = game.pushLeft(); break;
|
case LEFT: boardChanged = game.pushLeft(); break;
|
||||||
case RIGHT: boardChanged = game.pushRight(); break;
|
case RIGHT: boardChanged = game.pushRight(); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si le mouvement a modifié le plateau
|
|
||||||
if (boardChanged) {
|
if (boardChanged) {
|
||||||
|
// 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();
|
gameStats.recordMove();
|
||||||
int scoreAfter = game.getCurrentScore();
|
int scoreAfter = game.getCurrentScore();
|
||||||
int scoreDelta = scoreAfter - scoreBefore;
|
if (scoreAfter > scoreBefore) {
|
||||||
if (scoreDelta > 0) {
|
gameStats.recordMerge(1); // Simplifié
|
||||||
gameStats.recordMerge(1); // Simplification: compte comme 1 fusion si score augmente
|
|
||||||
if (scoreAfter > game.getHighestScore()) {
|
if (scoreAfter > game.getHighestScore()) {
|
||||||
game.setHighestScore(scoreAfter);
|
game.setHighestScore(scoreAfter);
|
||||||
gameStats.setHighestScore(scoreAfter); // Met à jour aussi dans GameStats pour sauvegarde
|
gameStats.setHighestScore(scoreAfter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gameStats.updateHighestTile(game.getHighestTileValue());
|
gameStats.updateHighestTile(game.getHighestTileValue());
|
||||||
game.addNewTile(); // Ajoute une nouvelle tuile
|
// updateScores(); // Le score sera mis à jour par syncBoardView
|
||||||
updateUI(); // Rafraîchit l'affichage
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifie l'état final après le mouvement (même si boardChanged est false)
|
// Ajoute la nouvelle tuile dans la logique du jeu
|
||||||
if (currentGameState != GameFlowState.GAME_OVER) {
|
game.addNewTile();
|
||||||
if (game.isGameWon() && currentGameState == GameFlowState.PLAYING) {
|
int[][] boardAfterAdd = game.getBoard(); // État final logique
|
||||||
currentGameState = GameFlowState.WON_DIALOG_SHOWN;
|
|
||||||
long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
|
// *** Synchronise l'affichage avec l'état final logique ***
|
||||||
gameStats.recordWin(timeTaken);
|
syncBoardView();
|
||||||
showAchievementNotification(2048);
|
|
||||||
showGameWonKeepPlayingDialog();
|
// *** Lance les animations locales sur les vues mises à jour ***
|
||||||
} else if (game.isGameOver()) {
|
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;
|
currentGameState = GameFlowState.GAME_OVER;
|
||||||
long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
|
long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
|
||||||
gameStats.recordLoss();
|
gameStats.recordLoss(); gameStats.endGame(timeTaken);
|
||||||
gameStats.endGame(timeTaken); // Finalise temps, etc.
|
|
||||||
showGameOverDialog();
|
showGameOverDialog();
|
||||||
// Met à jour l'UI pour afficher le score final si Game Over atteint sans mouvement (rare)
|
|
||||||
if (!boardChanged) updateUI();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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. */
|
/** Énumération pour les directions de swipe. */
|
||||||
private enum Direction { UP, DOWN, LEFT, RIGHT }
|
private enum Direction { UP, DOWN, LEFT, RIGHT }
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user