feat(match3): Verbesserung der Fall-Logik und Auffüllung leerer Felder

- Optimierung der Fall-Logik für Tiles, um sicherzustellen, dass leere Positionen korrekt gefüllt werden.
- Einführung einer neuen Methode zur Überprüfung und Auffüllung leerer gültiger Felder nach dem Fallen von Tiles.
- Anpassungen an der Animation und den Debug-Ausgaben zur besseren Nachverfolgbarkeit der Fall- und Auffüllprozesse.
- Verbesserung der Logik zur Handhabung von Tiles und deren Positionen im Spiel.
This commit is contained in:
Torsten Schulz (local)
2025-08-29 08:16:54 +02:00
parent 28833f0e0b
commit 5f99000f43
9 changed files with 655 additions and 137 deletions

View File

@@ -293,18 +293,18 @@ export default {
movesLeft: 15,
matchesMade: 0, // Neue: Zählt tatsächlich gemachte Matches
// Drag & drop
dragStartIndex: null,
dragStartX: null,
dragStartY: null,
// Drag & drop
dragStartIndex: null,
dragStartX: null,
dragStartY: null,
originalTilePosition: null,
isDragging: false,
isDragging: false,
draggedTileIndex: null, // Neuer: Index des aktuell gedraggten Tiles
adjacentTilesForHover: [], // Neue: Liste der benachbarten Tiles für Hover
currentlyAnimatingTile: null, // Neuer: Index des aktuell animierten Tiles
dragElement: null,
dragOffsetX: 0,
dragOffsetY: 0,
dragElement: null,
dragOffsetX: 0,
dragOffsetY: 0,
boundMouseMoveHandler: null,
boundTouchMoveHandler: null,
@@ -505,10 +505,10 @@ export default {
});
},
// Neue Hilfsmethode ohne Rekursion
loadLevelDataInternal(levelData) {
return apiClient.get(`/api/match3/levels/${levelData.id}`)
.then(response => {
// Neue Hilfsmethode ohne Rekursion
loadLevelDataInternal(levelData) {
return apiClient.get(`/api/match3/levels/${levelData.id}`)
.then(response => {
if (response.data && response.data.success && response.data.data) {
const freshLevelData = response.data.data;
@@ -652,14 +652,14 @@ export default {
})
.catch(error => {
// Fallback: Setze currentLevel basierend auf completedLevels
if (this.completedLevels > 0) {
this.currentLevel = this.completedLevels + 1;
} else {
this.currentLevel = 1;
}
if (this.completedLevels > 0) {
this.currentLevel = this.completedLevels + 1;
} else {
this.currentLevel = 1;
}
// Lade das Level basierend auf dem Fallback currentLevel
this.loadLevelData(this.currentLevel);
});
});
},
async initializeLevel() {
@@ -668,8 +668,8 @@ export default {
return;
}
// Safety check: ensure currentLevelData is loaded
if (!this.currentLevelData) {
// Safety check: ensure currentLevelData is loaded
if (!this.currentLevelData) {
// KEINE REKURSION: Lade Kampagnendaten nur einmal
this.loadCampaignData().then(async () => {
// Nach dem Laden der Kampagne, initialisiere das Level direkt
@@ -783,14 +783,14 @@ export default {
} else {
console.log('🔧 Keine initialen Matches gefunden, Level ist bereit');
}
// WICHTIG: Nach der Korrektur der initialen Matches das Brett erneut überprüfen
console.log('🔍 Überprüfe das Brett nach der Korrektur der initialen Matches...');
await this.checkBoardAfterSetup();
// WICHTIG: Setze das Spiel als aktiv, aber prüfe NICHT sofort die Level-Objekte
this.gameActive = true;
this.gameActive = true;
// KEINE Prüfung der Level-Objekte beim Start - diese werden nur nach Spielzügen geprüft
// WICHTIG: Reset Flag für Level-Initialisierung
@@ -1107,7 +1107,7 @@ export default {
const index = this.coordsToIndex(r, col);
if (index && board[index] && board[index].type === centerType && !this.isPowerUpTile(board[index].type)) {
verticalLength++;
} else {
} else {
break;
}
}
@@ -1118,7 +1118,7 @@ export default {
const index = this.coordsToIndex(row, c);
if (index && board[index] && board[index].type === centerType && !this.isPowerUpTile(board[index].type)) {
horizontalLength++;
} else {
} else {
break;
}
}
@@ -1408,10 +1408,10 @@ export default {
// Prüfe auf 3er-Match
if (row + 2 < this.boardHeight) {
const index1 = this.coordsToIndex(row, col);
const index2 = this.coordsToIndex(row + 1, col);
const index3 = this.coordsToIndex(row + 2, col);
const index1 = this.coordsToIndex(row, col);
const index2 = this.coordsToIndex(row + 1, col);
const index3 = this.coordsToIndex(row + 2, col);
// Prüfe ob alle drei Positionen bereits Teil eines höherwertigen Matches sind
if (!usedIndices.has(index1) && !usedIndices.has(index2) && !usedIndices.has(index3) &&
this.isValidMatch(index1, index2, index3, board)) {
@@ -1679,7 +1679,7 @@ export default {
document.removeEventListener('mousemove', this.boundMouseMoveHandler);
this.boundMouseMoveHandler = null;
}
if (this.boundTouchMoveHandler) {
if (this.boundTouchMoveHandler) {
document.removeEventListener('touchmove', this.boundTouchMoveHandler);
this.boundTouchMoveHandler = null;
}
@@ -1709,7 +1709,7 @@ export default {
if (this.isFalling) {
return;
}
// WICHTIG: Prüfe auf Regenbogen-Tile Tausch (vor dem Tausch)
// Prüfe beide Kombinationen der ursprünglichen Tiles
const rainbowSwapResult = await this.handleRainbowSwap(this.board[index1], this.board[index2]);
@@ -1758,11 +1758,11 @@ export default {
this.moves--;
this.movesLeft++;
// Wichtig: Auch ohne Matches Level-Objekte prüfen
// Wichtig: Auch ohne Matches Level-Objekte prüfen
// (falls der Spieler bereits alle Ziele erreicht hat)
// Nur prüfen wenn der Spieler bereits gespielt hat UND das Level nicht initialisiert wird
if (this.moves > 0 && !this.isInitializingLevel) {
this.checkLevelObjectives();
this.checkLevelObjectives();
}
}
},
@@ -1864,7 +1864,7 @@ export default {
// WICHTIG: Erhöhe den Match-Zähler für Level-Objekte
if (isPlayerMove && !this.isInitializingLevel) {
this.matchesMade += 1;
this.matchesMade += 1;
console.log(`🎯 Match-Zähler erhöht: ${this.matchesMade} Matches gemacht`);
}
@@ -1969,8 +1969,8 @@ export default {
console.log('🔧 Debug: Alle Power-ups nach dem Fall-Down:');
this.debugPowerUps();
// Fülle leere Positionen mit neuen Tiles auf
await this.fillEmptyPositions();
// Fülle leere gültige Felder mit neuen Tiles auf
await this.checkAndFillEmptyValidFields();
// Debug: Zeige alle Power-ups nach dem Füllen
console.log('🔧 Debug: Alle Power-ups nach dem Füllen:');
@@ -2003,7 +2003,7 @@ export default {
// WICHTIG: Prüfe Level-Objekte nach dem Verarbeiten der Matches
if (isPlayerMove && !this.isInitializingLevel) {
console.log('🎯 Prüfe Level-Objekte nach Match-Verarbeitung...');
this.checkLevelObjectives();
this.checkLevelObjectives();
}
}
@@ -2015,94 +2015,127 @@ export default {
let hasChanges = true;
let iteration = 0;
const maxIterations = 10; // Verhindert Endlosschleifen
// Wiederhole bis keine weiteren Änderungen mehr auftreten
while (hasChanges && iteration < this.boardHeight) {
hasChanges = false;
while (hasChanges && iteration < maxIterations) {
iteration++;
console.log(`🔧 Fall-Down Iteration ${iteration}...`);
console.log(`🔧 Fall-Down Iteration ${iteration}`);
hasChanges = false;
// Sammle alle Tiles, die fallen sollen
const tilesToFall = [];
// Gehe von unten nach oben durch jede Spalte
// 1. Von unten nach oben leere Felder finden und Tiles nach unten verschieben
for (let col = 0; col < this.boardWidth; col++) {
for (let row = this.boardHeight - 2; row >= 0; row--) { // Von vorletzter Zeile nach oben
const currentIndex = this.coordsToIndex(row, col);
const belowIndex = this.coordsToIndex(row + 1, col);
console.log(`🔧 Prüfe Spalte ${col}...`);
// Finde alle leeren Positionen in dieser Spalte von unten nach oben
for (let row = this.boardHeight - 1; row >= 0; row--) {
const index = this.coordsToIndex(row, col);
// Wenn aktuelles Tile existiert und das darunter leer ist
if (this.board[currentIndex] && !this.board[belowIndex] && !this.isPowerUpTile(this.board[currentIndex].type)) {
// Prüfe, ob das Zielfeld im board_layout gültig ist (nicht 'o')
const targetRow = row + 1;
const targetCol = col;
// Hole das board_layout für das aktuelle Level
// Wenn Position leer ist, prüfe ob sie im Layout gültig ist
if (!this.board[index]) {
if (this.currentLevelData && this.currentLevelData.boardLayout) {
const layout = this.currentLevelData.boardLayout;
const layoutRows = layout.split('\n');
// Prüfe, ob das Zielfeld im Layout gültig ist
if (targetRow < layoutRows.length && targetCol < layoutRows[targetRow].length) {
const targetChar = layoutRows[targetRow][targetCol];
// Nur fallen lassen, wenn das Zielfeld 'x' ist (gültig)
if (targetChar === 'x') {
tilesToFall.push({
fromIndex: currentIndex,
toIndex: belowIndex,
fromRow: row,
toRow: row + 1,
col: col
});
hasChanges = true;
} else {
console.log(`🔧 Tile kann nicht auf Position [${targetRow}, ${targetCol}] fallen - Layout zeigt '${targetChar}'`);
const layoutRows = this.currentLevelData.boardLayout.split('\n');
if (row < layoutRows.length && col < layoutRows[row].length) {
const targetChar = layoutRows[row][col];
if (targetChar !== 'x') {
console.log(`🔧 Position [${row}, ${col}] ist ungültig im Layout (${targetChar}) - überspringe`);
continue; // Überspringe ungültige Positionen
}
}
} else {
// Fallback: Wenn kein Layout vorhanden, normale Logik verwenden
tilesToFall.push({
fromIndex: currentIndex,
toIndex: belowIndex,
fromRow: row,
toRow: row + 1,
col: col
});
}
console.log(`🔧 Leere Position gefunden: [${row}, ${col}] -> Index ${index}`);
// Suche nach oben nach einem Tile
let tileFound = false;
for (let searchRow = row - 1; searchRow >= 0; searchRow--) {
const searchIndex = this.coordsToIndex(searchRow, col);
hasChanges = true;
if (this.board[searchIndex] && !this.isPowerUpTile(this.board[searchIndex].type)) {
console.log(`🔧 Tile ${this.board[searchIndex].type} gefunden an [${searchRow}, ${col}] -> verschiebe nach [${row}, ${col}]`);
// Verschiebe Tile nach unten
this.board[index] = this.board[searchIndex];
this.board[searchIndex] = null;
// Aktualisiere DOM
this.$forceUpdate();
// Warte kurz für Animation
await this.wait(500);
hasChanges = true;
tileFound = true;
break;
}
}
if (!tileFound) {
console.log(`🔧 Kein Tile über Position [${row}, ${col}] gefunden`);
}
}
}
}
// Führe Fall-Animation für alle Tiles aus
if (tilesToFall.length > 0) {
console.log(`🔧 ${tilesToFall.length} Tiles fallen in Iteration ${iteration}`);
// 2. Oberste Zeile nach fehlenden Feldern untersuchen und auffüllen
if (hasChanges) {
console.log('🔧 Fülle oberste Zeile auf...');
// Führe Fall-Animation aus, BEVOR die Tiles im Board verschoben werden
await this.animateTilesFalling(tilesToFall);
for (let col = 0; col < this.boardWidth; col++) {
const index = this.coordsToIndex(0, col);
// Wenn oberste Position leer ist, prüfe ob sie im Layout gültig ist
if (!this.board[index]) {
if (this.currentLevelData && this.currentLevelData.boardLayout) {
const layoutRows = this.currentLevelData.boardLayout.split('\n');
if (0 < layoutRows.length && col < layoutRows[0].length) {
const targetChar = layoutRows[0][col];
if (targetChar !== 'x') {
console.log(`🔧 Oberste Position [0, ${col}] ist ungültig im Layout (${targetChar}) - überspringe`);
continue;
}
}
}
// Erstelle neues Tile
const newTile = this.createRandomTile();
this.board[index] = newTile;
console.log(`🔧 Neues Tile ${newTile.type} an oberster Position [0, ${col}] hinzugefügt`);
hasChanges = true;
}
}
// Bewege Tiles im Board NACH der Animation
tilesToFall.forEach(fallData => {
this.board[fallData.toIndex] = this.board[fallData.fromIndex];
this.board[fallData.fromIndex] = null;
});
// Aktualisiere die Anzeige nach dem Fallen
// Aktualisiere DOM nach dem Auffüllen
this.$forceUpdate();
await this.wait(100);
}
// Kürzere Pause zwischen den Iterationen für bessere Performance
await this.wait(50);
console.log(`🔧 Iteration ${iteration} abgeschlossen - Änderungen: ${hasChanges}`);
}
console.log(`🔧 Fall-Down abgeschlossen nach ${iteration} Iterationen`);
if (iteration >= maxIterations) {
console.log('⚠️ Maximale Anzahl von Fall-Down Iterationen erreicht');
}
// Nach dem Fallen: Prüfe ob alle gültigen Felder ein Tile enthalten
await this.checkAndFillEmptyValidFields();
console.log('🔧 Fall-Down-Logik abgeschlossen');
},
// Hilfsfunktion: Zeige den aktuellen Board-Zustand in der Konsole
printBoardState() {
console.log('🔧 Board-Zustand:');
for (let row = 0; row < this.boardHeight; row++) {
let rowStr = '';
for (let col = 0; col < this.boardWidth; col++) {
const index = this.coordsToIndex(row, col);
const tile = this.board[index];
if (tile) {
rowStr += `[${tile.type}] `;
} else {
rowStr += '[ ] ';
}
}
console.log(` Zeile ${row}: ${rowStr}`);
}
},
// Fülle leere Positionen mit neuen Tiles auf
@@ -2458,34 +2491,28 @@ export default {
console.log(`🎬 Schrumpf-Animation abgeschlossen`);
},
// Animierte Fall-Animation für Tiles
// Animierte Fall-Animation für Tiles (einfache Animation nach dem Verschieben im Board)
async animateTilesFalling(tilesToFall) {
console.log(`🎬 Starte Fall-Animation für ${tilesToFall.length} Tiles...`);
// Sammle alle DOM-Elemente der fallenden Tiles (an ihren ursprünglichen Positionen)
// Sammle alle DOM-Elemente der fallenden Tiles (an ihren neuen Positionen)
const tileElements = tilesToFall.map(fallData => {
const element = document.querySelector(`[data-index="${fallData.fromIndex}"]`);
const element = document.querySelector(`[data-index="${fallData.toIndex}"]`);
if (element) {
// Berechne die Fall-Distanz (sollte immer 1 Zeile sein)
const fallDistance = fallData.toRow - fallData.fromRow;
if (fallDistance !== 1) {
console.warn(`⚠️ Unerwartete Fall-Distanz: ${fallDistance} Zeilen für Tile ${fallData.fromIndex}`);
}
// Berechne die Fall-Distanz
const fallDistance = fallData.fallDistance || (fallData.toRow - fallData.fromRow);
const tileHeight = 60; // Höhe eines Tiles in Pixeln
const fallPixels = fallDistance * tileHeight;
// Setze das Tile an seine ursprüngliche Position (oben)
element.style.transform = `translateY(0px)`;
element.style.transition = 'transform 0.3s ease-out';
// Setze das Tile an seine ursprüngliche Position (oben) mit transform
element.style.transform = `translateY(-${fallPixels}px)`;
element.style.transition = 'transform 0.4s ease-out';
// Füge CSS-Klasse für die Fall-Animation hinzu
element.classList.add('falling');
// Speichere die Fall-Pixel für später
element.dataset.fallPixels = fallPixels;
console.log(`🎬 Fall-Animation für Tile ${fallData.fromIndex}: ${fallDistance} Zeile(n) (${fallPixels}px)`);
console.log(`🎬 Fall-Animation für Tile ${fallData.toIndex}: ${fallDistance} Zeile(n) (${fallPixels}px)`);
}
return element;
}).filter(element => element !== null);
@@ -2500,17 +2527,16 @@ export default {
// Warte kurz, damit die transform-Eigenschaft gesetzt wird
await this.wait(50);
// Bewege alle Tiles nach unten (Fall-Animation)
// Bewege alle Tiles zu ihrer finalen Position (Fall-Animation)
tileElements.forEach(element => {
const fallPixels = parseInt(element.dataset.fallPixels);
element.style.transform = `translateY(${fallPixels}px)`;
element.style.transform = 'translateY(0px)';
});
// Spiele Fall-Sound ab
this.playSound('falling');
// Warte auf die Fall-Animation (0,3 Sekunden)
await this.wait(300);
// Warte auf die Fall-Animation (0,4 Sekunden)
await this.wait(400);
// Entferne die CSS-Klassen und transform-Eigenschaften
tileElements.forEach(element => {
@@ -2749,8 +2775,8 @@ export default {
// Führe Fall-Down-Logik aus
await this.fallTilesDown();
// Fülle leere Positionen mit neuen Tiles auf
await this.fillEmptyPositions();
// Fülle leere gültige Felder mit neuen Tiles auf
await this.checkAndFillEmptyValidFields();
console.log('🔧 Fall-Prozess abgeschlossen');
},
@@ -2916,8 +2942,8 @@ export default {
// Führe Fall-Down-Logik aus
await this.fallTilesDown();
// Fülle leere Positionen mit neuen Tiles auf
await this.fillEmptyPositions();
// Fülle leere gültige Felder mit neuen Tiles auf
await this.checkAndFillEmptyValidFields();
// Erhöhe den Zug-Zähler
this.countPowerUpMove();
@@ -2961,8 +2987,8 @@ export default {
// Führe Fall-Down-Logik aus
await this.fallTilesDown();
// Fülle leere Positionen mit neuen Tiles auf
await this.fillEmptyPositions();
// Fülle leere gültige Felder mit neuen Tiles auf
await this.checkAndFillEmptyValidFields();
// Erhöhe den Zug-Zähler
this.countPowerUpMove();
@@ -3026,8 +3052,8 @@ export default {
// Führe Fall-Down-Logik aus
await this.fallTilesDown();
// Fülle leere Positionen mit neuen Tiles auf
await this.fillEmptyPositions();
// Fülle leere gültige Felder mit neuen Tiles auf
await this.checkAndFillEmptyValidFields();
// Erhöhe den Zug-Zähler
this.countPowerUpMove();
@@ -3090,8 +3116,8 @@ export default {
// Führe Fall-Down-Logik aus
await this.fallTilesDown();
// Fülle leere Positionen mit neuen Tiles auf
await this.fillEmptyPositions();
// Fülle leere gültige Felder mit neuen Tiles auf
await this.checkAndFillEmptyValidFields();
// Erhöhe den Zug-Zähler
this.countPowerUpMove();
@@ -3643,7 +3669,7 @@ export default {
draggedElement.style.transition = 'all 0.3s ease-out';
// Warte kurz und setze dann alle Styles komplett zurück
setTimeout(() => {
setTimeout(() => {
draggedElement.style.transition = '';
}, 300);
}
@@ -4781,8 +4807,8 @@ export default {
// Führe Fall-Down-Logik aus
await this.fallTilesDown();
// Fülle leere Positionen mit neuen Tiles auf
await this.fillEmptyPositions();
// Fülle leere gültige Felder mit neuen Tiles auf
await this.checkAndFillEmptyValidFields();
// Erhöhe den Zug-Zähler
this.countPowerUpMove();
@@ -4918,8 +4944,8 @@ export default {
// Führe Fall-Down-Logik aus
await this.fallTilesDown();
// Fülle leere Positionen mit neuen Tiles auf
await this.fillEmptyPositions();
// Fülle leere gültige Felder mit neuen Tiles auf
await this.checkAndFillEmptyValidFields();
// WICHTIG: Zug-Zähler wird bereits in handleRainbowSwap erhöht, nicht hier nochmal!
// this.countPowerUpMove(); // ENTFERNT - wird bereits in handleRainbowSwap aufgerufen