Files
yourpart3/frontend/src/views/minigames/Match3Game.vue
Torsten Schulz (local) 7b07b07bec feat(match3): Optimierung der L-Form-Match-Erkennung und Anpassung der Tile-Darstellung
- Überarbeitung der Logik zur Erkennung von L-Form-Matches, um die Überprüfung der vertikalen und horizontalen Linien zu verbessern.
- Anpassung der Darstellung von Tiles, um Power-Up-Tiles korrekt zu behandeln und die Benutzeroberfläche zu vereinheitlichen.
- Erweiterung der Debug-Ausgaben zur besseren Nachverfolgbarkeit von L-Form-Matches und deren Längen.
2025-08-27 13:15:36 +02:00

6013 lines
209 KiB
Vue

<template>
<div class="contenthidden">
<div class="contentscroll">
<!-- Spiel-Titel -->
<div class="game-title">
<h1>{{ $t('minigames.match3.title') }}</h1>
<p>{{ $t('minigames.match3.campaignDescription') }}</p>
</div>
<!-- Kampagnen-Status -->
<div class="game-layout">
<div class="stats-section">
<div class="stats-card">
<div class="stats-header">
<div class="stats-header-content">
<h3 class="stats-title">{{ $t('minigames.match3.gameStats') }}</h3>
<button class="toggle-button" @click="toggleStats">
<span class="toggle-icon">{{ toggleIcon }}</span>
</button>
</div>
</div>
<div class="stats-list" v-if="isStatsExpanded">
<div class="stat-row">
<span class="stat-value score-value">{{ score }}</span>
<span class="stat-label">{{ $t('minigames.match3.score') }}</span>
</div>
<div class="stat-row">
<span class="stat-value moves-value">{{ moves }}</span>
<span class="stat-label">{{ $t('minigames.match3.moves') }}</span>
</div>
<div class="stat-row">
<span class="stat-value level-value">{{ currentLevel }}</span>
<span class="stat-label">{{ $t('minigames.match3.currentLevel') }}</span>
</div>
<div class="stat-row">
<span class="stat-value stars-value">{{ stars }}</span>
<span class="stat-label">{{ $t('minigames.match3.stars') }}</span>
</div>
</div>
</div>
</div>
<div class="game-content">
<!-- Verbleibende Züge -->
<div class="moves-left-display">
<span class="moves-left-label">{{ $t('minigames.match3.movesLeft') }}:</span>
<span class="moves-left-value">{{ safeMovesLeft }}</span>
</div>
<!-- Level-Info -->
<div class="level-info-card" v-if="currentLevelData">
<div class="level-header">
<div class="level-header-content">
<h3 class="level-title">
{{ $t('minigames.match3.level') }} {{ currentLevel }}: {{ currentLevelData.name }}
</h3>
<button class="toggle-button" @click="toggleLevelDescription">
<span class="toggle-icon">{{ levelDescriptionExpanded ? '▼' : '▶' }}</span>
</button>
</div>
</div>
<div class="level-content" v-show="levelDescriptionExpanded">
<p>{{ currentLevelData.description }}</p>
<div class="level-objectives">
<div v-for="(objective, index) in currentLevelData.objectives" :key="index" class="objective-item">
<span class="objective-icon">{{ objective.completed ? '✓' : '○' }}</span>
<span :class="{ 'completed': objective.completed }">
{{ objective.description }}
</span>
<span class="objective-progress">
({{ getObjectiveProgress(objective) }})
</span>
</div>
</div>
</div>
</div>
<!-- Game Board -->
<div class="game-board-container">
<div class="game-board"
v-if="board.length > 0 && boardWidth > 0 && boardHeight > 0 && gameActive && currentLevelData"
:style="{
'grid-template-columns': `repeat(${boardWidth}, 1fr)`,
'grid-template-rows': `repeat(${boardHeight}, 1fr)`
}">
<template v-for="(tile, index) in board" :key="`tile-${index}`">
<div v-if="isValidPosition(Math.floor(index / boardWidth), index % boardWidth)"
:class="['game-tile', {
'empty': !tile,
'dragging': draggedTileIndex === index,
'drag-hover': isDragging && adjacentTilesForHover.includes(index),
'rocket-horizontal': tile && tile.type === 'rocket-horizontal',
'rocket-vertical': tile && tile.type === 'rocket-vertical',
'bomb': tile && tile.type === 'bomb'
}]"
:data-index="index"
:data-type="tile ? tile.type : null"
@mousedown="onTileMouseDown($event, index)"
@mouseup="onTileMouseUp($event, index)"
@mousemove="onTileMouseMove($event)"
@mouseenter="onTileMouseEnter($event, index)"
@mouseleave="onTileMouseLeave($event, index)"
@touchstart="onTileMouseDown($event, index)"
@touchend="onTileMouseUp($event, index)"
@dblclick="handleDoubleClick(index, $event)">
<span v-if="tile" class="tile-symbol">{{ getTileSymbol(tile.type) }}</span>
</div>
</template>
</div>
<!-- Loading State -->
<div v-else class="game-board-loading">
<p>Lade Spielbrett...</p>
<div class="debug-info">
<p>Debug-Info:</p>
<p>Board-Länge: {{ board.length }}</p>
<p>Board-Breite: {{ boardWidth }}</p>
<p>Board-Höhe: {{ boardHeight }}</p>
<p>Game Active: {{ gameActive }}</p>
<p>Current Level Data: {{ currentLevelData ? 'Ja' : 'Nein' }}</p>
<p>Board Layout: {{ boardLayout ? 'Ja' : 'Nein' }}</p>
</div>
</div>
<!-- Power-Up Animationen (relativ zum Game-Board) -->
<div v-if="showExplosion" class="power-up-animation" :style="{ left: explosionPosition.x + 'px', top: explosionPosition.y + 'px' }">
<div class="explosion-effect"></div>
</div>
<div v-if="showRocketFlight" class="power-up-animation" :style="{ left: rocketStartPos.x + 'px', top: rocketStartPos.y + 'px' }">
<div class="rocket-flight" :style="{ '--dx': (rocketEndPos.x - rocketStartPos.x) + 'px', '--dy': (rocketEndPos.y - rocketStartPos.y) + 'px' }"></div>
</div>
<div v-if="showRainbowEffect" class="power-up-animation" :style="{ left: rainbowCenter.x + 'px', top: rainbowCenter.y + 'px' }">
<div class="rainbow-effect"></div>
</div>
<div v-if="showBombEffect" class="power-up-animation" :style="{ left: bombCenter.x + 'px', top: bombCenter.y + 'px' }">
<div class="bomb-effect"></div>
</div>
<div v-if="showRocketEffect" class="power-up-animation" :style="{ left: rocketCenter.x + 'px', top: rocketCenter.y + 'px' }">
<div class="rocket-explosion">🚀💥</div>
</div>
<!-- Raketen-Flug-Animation -->
<div v-if="rocketTarget.x > 0 && rocketTarget.y > 0" class="rocket-flight-animation" :style="{ left: rocketTarget.x + 'px', top: rocketTarget.y + 'px' }">
<div class="rocket-flight">🚀</div>
</div>
<!-- Fliegende Rakete -->
<div v-if="showFlyingRocket" class="flying-rocket" :style="{ left: rocketCenter.x + 'px', top: rocketCenter.y + 'px' }">
<div class="rocket-icon">🚀</div>
</div>
</div>
<!-- Spiel-Kontrollen -->
<div class="game-controls">
<button class="btn btn-primary" @click="restartLevel">
{{ $t('minigames.match3.restartLevel') }}
</button>
<button class="btn btn-secondary" @click="pauseGame">
{{ $t('minigames.match3.pause') }}
</button>
</div>
</div>
</div>
<!-- Level-Abschluss Dialog -->
<v-dialog v-model="showLevelComplete" max-width="500">
<v-card>
<v-card-title class="text-h5 text-center text-success">
🎉 {{ $t('minigames.match3.levelComplete') }}! 🎉
</v-card-title>
<v-card-text class="text-center">
<div class="level-complete-stats">
<div class="stat-item">
<span class="stat-label">{{ $t('minigames.match3.levelScore') }}:</span>
<span class="stat-value">{{ levelScore }}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ $t('minigames.match3.movesUsed') }}:</span>
<span class="stat-value">{{ moves }}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ $t('minigames.match3.starsEarned') }}:</span>
<span class="stat-value">{{ levelStars }}</span>
</div>
</div>
</v-card-text>
<v-card-actions class="justify-center">
<v-btn class="custom-btn primary-btn" @click="nextLevel">
{{ $t('minigames.match3.nextLevel') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Kampagnen-Abschluss Dialog -->
<v-dialog v-model="showCampaignComplete" max-width="600">
<v-card>
<v-card-title class="text-h5 text-center text-success">
🏆 {{ $t('minigames.match3.campaignComplete') }}! 🏆
</v-card-title>
<v-card-text class="text-center">
<div class="campaign-complete-stats">
<div class="stat-item">
<span class="stat-label">{{ $t('minigames.match3.totalScore') }}:</span>
<span class="stat-value">{{ score }}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ $t('minigames.match3.totalStars') }}:</span>
<span class="stat-value">{{ stars }}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ $t('minigames.match3.levelsCompleted') }}:</span>
<span class="stat-value">{{ completedLevels }}</span>
</div>
</div>
</v-card-text>
<v-card-actions class="justify-center">
<v-btn class="custom-btn primary-btn" @click="restartCampaign">
{{ $t('minigames.match3.restartCampaign') }}
</v-btn>
<v-btn class="custom-btn secondary-btn" @click="$router.push('/')">
{{ $t('minigames.backToGames') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Pause Dialog -->
<v-dialog v-model="showPause" max-width="400">
<v-card>
<v-card-title class="text-h5 text-center">
{{ $t('minigames.match3.paused') }}
</v-card-title>
<v-card-actions class="justify-center">
<v-btn class="custom-btn primary-btn" @click="resumeGame">
{{ $t('minigames.match3.resume') }}
</v-btn>
<v-btn class="custom-btn secondary-btn" @click="restartLevel">
{{ $t('minigames.match3.restartLevel') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Power-Up Animationen -->
</div>
</div>
</template>
<script>
import apiClient from '../../utils/axios.js';
export default {
name: 'Match3Game',
data() {
const initialData = {
// Game state
gameActive: false,
isPaused: false,
showPause: false,
showLevelComplete: false,
showCampaignComplete: false,
showGameOver: false,
// Campaign data
campaignData: null,
currentLevel: 1, // Standardwert, wird später überschrieben
completedLevels: 0,
currentLevelData: null,
// Game board
board: [],
boardLayout: [], // Neue: Array der Level-Form
boardWidth: 6, // Standardwert für 6x6
boardHeight: 6, // Standardwert für 6x6
tileTypes: ['gem', 'star', 'heart'], // Normale Tiles (ohne Power-Ups)
powerUpTypes: ['rocket', 'bomb', 'rainbow'], // Power-Up Tiles (werden nur durch Matches erstellt)
// Game progress
score: 0,
levelScore: 0,
stars: 0,
levelStars: 0,
moves: 0,
movesLeft: 15,
matchesMade: 0, // Neue: Zählt tatsächlich gemachte Matches
// Drag & drop
dragStartIndex: null,
dragStartX: null,
dragStartY: null,
originalTilePosition: null,
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,
boundMouseMoveHandler: null,
boundTouchMoveHandler: null,
// Fall animations
isFalling: false,
fallingTiles: [],
newTiles: [],
// Match tracking
matchedTiles: [],
cascadeRound: 0, // Neue: Zählt Kaskaden-Runden
// UI state
statsExpanded: false,
levelDescriptionExpanded: true,
// SICHERHEIT: Verhindere zu viele API-Aufrufe
isLoadingData: false,
lastApiCall: 0,
apiCallCooldown: 1000, // 1 Sekunde Cooldown zwischen API-Aufrufen
// WICHTIG: Verhindere mehrfache Level-Initialisierung
isInitializingLevel: false,
// Animation states
showExplosion: false,
explosionPosition: { x: 0, y: 0 },
showRocketFlight: false,
rocketStartPos: { x: 0, y: 0 },
rocketEndPos: { x: 0, y: 0 },
showRainbowEffect: false,
rainbowCenter: { x: 0, y: 0 },
showBombEffect: false,
bombCenter: { x: 0, y: 0 },
showRocketEffect: false,
rocketCenter: { x: 0, y: 0 },
rocketTarget: { x: -1, y: -1 },
showFlyingRocket: false,
// Sound-Effekte
sounds: {
move: null,
bomb: null,
rocket: null,
rainbow: null
}
};
return initialData;
},
mounted() {
// Initialisiere Sound-Effekte
this.initializeSounds();
// Lade die Kampagnendaten
this.loadCampaignData();
// Füge globale Event-Listener hinzu
document.addEventListener('mousemove', this.onGlobalMouseMove);
document.addEventListener('mouseup', this.onGlobalMouseUp);
},
beforeUnmount() {
// Entferne globale Event-Listener
document.removeEventListener('mousemove', this.onGlobalMouseMove);
document.removeEventListener('mouseup', this.onGlobalMouseUp);
},
methods: {
// Initialisiere Sound-Effekte
initializeSounds() {
try {
// Verwende den korrekten Pfad für Vite public Ordner
this.sounds.move = new Audio('/sounds/match3/move.wav');
this.sounds.bomb = new Audio('/sounds/match3/bomb.wav');
this.sounds.rocket = new Audio('/sounds/match3/roket.wav'); // Beachte: Datei heißt "roket.wav"
this.sounds.rainbow = new Audio('/sounds/match3/rainbow.wav');
this.sounds.falling = new Audio('/sounds/match3/falling.wav');
// Setze Lautstärke auf 50%
this.sounds.move.volume = 0.5;
this.sounds.bomb.volume = 0.5;
this.sounds.rocket.volume = 0.5;
this.sounds.rainbow.volume = 0.5;
this.sounds.falling.volume = 0.5;
// Warte bis alle Sounds geladen sind
Promise.all([
this.sounds.move.load(),
this.sounds.bomb.load(),
this.sounds.rocket.load(),
this.sounds.rainbow.load(),
this.sounds.falling.load()
]).then(() => {
console.log('🔊 Sounds erfolgreich geladen:', {
move: this.sounds.move.readyState,
bomb: this.sounds.bomb.readyState,
rocket: this.sounds.rocket.readyState,
rainbow: this.sounds.rainbow.readyState,
falling: this.sounds.falling.readyState
});
}).catch(error => {
console.warn('⚠️ Fehler beim Laden der Sounds:', error);
});
} catch (error) {
console.warn('⚠️ Sounds konnten nicht geladen werden:', error);
}
},
// Hilfsmethode: Spiele Sound ab
playSound(soundType) {
try {
if (this.sounds[soundType]) {
// Setze Audio auf Anfang zurück
this.sounds[soundType].currentTime = 0;
// Spiele Sound ab
this.sounds[soundType].play().catch(error => {
console.warn(`⚠️ Sound ${soundType} konnte nicht abgespielt werden:`, error);
});
}
} catch (error) {
console.warn(`⚠️ Fehler beim Abspielen von Sound ${soundType}:`, error);
}
},
loadCampaignData() {
// SICHERHEIT: Verhindere zu viele API-Aufrufe
const now = Date.now();
if (this.isLoadingData || (now - this.lastApiCall) < this.apiCallCooldown) {
return Promise.resolve();
}
this.isLoadingData = true;
this.lastApiCall = now;
// Lade Kampagnen-Daten
return apiClient.get('/api/match3/campaigns/1')
.then(response => {
// Korrekte Datenstruktur extrahieren: response.data.data
if (response.data.success && response.data.data) {
this.campaignData = response.data.data;
// WICHTIG: Warte bis das Level vollständig initialisiert ist
this.$nextTick(() => {
// Lade den Benutzer-Fortschritt nach den Kampagnendaten
this.loadUserProgressAndInitialize();
});
} else {
throw new Error('Ungültige API-Response-Struktur');
}
})
.catch(error => {
this.handleLoadError('Kampagnendaten konnten nicht geladen werden', error);
})
.finally(() => {
this.isLoadingData = false;
});
},
loadLevelData(levelOrder) {
// Finde zuerst die Level-ID basierend auf der Reihenfolge
if (!this.campaignData || !this.campaignData.levels) {
// KEINE REKURSION: Lade Kampagne nur einmal
return this.loadCampaignData().then(() => {
// Nach dem Laden der Kampagne, versuche das Level zu finden
const levelData = this.campaignData.levels.find(l => l.order === levelOrder);
if (levelData) {
return this.loadLevelDataInternal(levelData).then(() => {
// WICHTIG: Nach dem Laden der Level-Daten, initialisiere das Level
this.initializeLevel();
});
} else {
throw new Error(`Level ${levelOrder} nicht in Kampagnendaten gefunden`);
}
});
}
const levelData = this.campaignData.levels.find(l => l.order === levelOrder);
if (!levelData) {
// KEINE REKURSION: Lade Kampagne nur einmal
return this.loadCampaignData().then(() => {
const retryLevelData = this.campaignData.levels.find(l => l.order === levelOrder);
if (!retryLevelData) {
throw new Error(`Level ${levelOrder} nicht gefunden`);
}
// Verwende die gefundenen Daten direkt, ohne Rekursion
return this.loadLevelDataInternal(retryLevelData).then(() => {
// WICHTIG: Nach dem Laden der Level-Daten, initialisiere das Level
this.initializeLevel();
});
});
}
// Verwende die gefundenen Daten direkt
return this.loadLevelDataInternal(levelData).then(() => {
// WICHTIG: Nach dem Laden der Level-Daten, initialisiere das Level
this.initializeLevel();
});
},
// 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;
// Aktualisiere die aktuellen Level-Daten
this.currentLevelData = freshLevelData;
this.boardWidth = freshLevelData.boardWidth;
this.boardHeight = freshLevelData.boardHeight;
// Neue Tile-Typen aus der levelTileTypes-Structure extrahieren
if (freshLevelData.levelTileTypes && freshLevelData.levelTileTypes.length > 0) {
this.tileTypes = freshLevelData.levelTileTypes
.filter(ltt => ltt.isActive && ltt.tileType && ltt.tileType.isActive)
.map(ltt => ltt.tileType.name);
} else {
// Fallback: Verwende die alte tileTypes-Array
this.tileTypes = freshLevelData.tileTypes || ['gem', 'star', 'heart'];
}
// WICHTIG: Alle Level-Objekte zurücksetzen (nicht als abgeschlossen markieren)
if (freshLevelData.objectives) {
freshLevelData.objectives.forEach(objective => {
objective.completed = false;
});
}
this.boardLayout = this.parseBoardLayout(freshLevelData.boardLayout);
this.movesLeft = freshLevelData.moveLimit;
return Promise.resolve();
} else {
throw new Error('Ungültige API-Response für Level-Daten');
}
})
.catch(error => {
// Keine Fallback-Daten mehr - wirf einen Fehler
throw new Error('Level-Daten konnten nicht vom Backend geladen werden');
});
},
loadUserProgress() {
// Prüfe ob Benutzer eingeloggt ist
if (!this.$store.getters.isLoggedIn || !this.$store.getters.user) {
// Fallback: Setze currentLevel basierend auf completedLevels
if (this.completedLevels > 0) {
this.currentLevel = this.completedLevels + 1;
} else {
this.currentLevel = 1;
}
return;
}
// Lade Benutzer-Fortschritt
apiClient.get('/api/match3/campaigns/1/progress')
.then(response => {
if (response.data.success) {
const progress = response.data.data;
this.score = progress.totalScore;
this.stars = progress.totalStars;
this.completedLevels = progress.levelsCompleted;
// WICHTIG: Setze das aktuelle Level korrekt
// Wenn Level 2 abgeschlossen ist, sollte das aktuelle Level 3 sein
// Verwende NICHT progress.currentLevel, da das falsch sein könnte
if (progress.levelsCompleted > 0) {
this.currentLevel = progress.levelsCompleted + 1;
} else {
this.currentLevel = 1;
}
// Sicherheitscheck: Stelle sicher, dass das Level nicht höher als verfügbar ist
if (this.campaignData && this.campaignData.levels) {
const maxLevel = Math.max(...this.campaignData.levels.map(l => l.order));
if (this.currentLevel > maxLevel) {
this.currentLevel = maxLevel;
}
}
this.movesLeft = progress.moveLimit - progress.movesUsed; // Verbleibende Züge berechnen
}
})
.catch(error => {
// Fallback: Setze currentLevel basierend auf completedLevels
if (this.completedLevels > 0) {
this.currentLevel = this.completedLevels + 1;
} else {
this.currentLevel = 1;
}
// Fallback für movesLeft
if (this.currentLevelData) {
this.movesLeft = this.currentLevelData.moveLimit - this.moves;
}
});
},
// Neue Methode: Lade Fortschritt und initialisiere dann das Level
loadUserProgressAndInitialize() {
// Prüfe ob Benutzer eingeloggt ist
if (!this.$store.getters.isLoggedIn || !this.$store.getters.user) {
// Fallback: Setze currentLevel basierend auf completedLevels
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);
return;
}
// Lade Benutzer-Fortschritt
apiClient.get('/api/match3/campaigns/1/progress')
.then(response => {
if (response.data.success) {
const progress = response.data.data;
this.score = progress.totalScore || 0;
this.stars = progress.totalStars || 0;
this.completedLevels = progress.levelsCompleted || 0;
// WICHTIG: Setze das aktuelle Level korrekt
// Wenn Level 2 abgeschlossen ist, sollte das aktuelle Level 3 sein
// Verwende NICHT progress.currentLevel, da das falsch sein könnte
if (progress.levelsCompleted > 0) {
this.currentLevel = progress.levelsCompleted + 1;
} else {
this.currentLevel = 1;
}
// Sicherheitscheck: Stelle sicher, dass das Level nicht höher als verfügbar ist
if (this.campaignData && this.campaignData.levels) {
const maxLevel = Math.max(...this.campaignData.levels.map(l => l.order));
if (this.currentLevel > maxLevel) {
this.currentLevel = maxLevel;
}
}
// Jetzt lade das richtige Level basierend auf dem geladenen Fortschritt
this.loadLevelData(this.currentLevel);
}
})
.catch(error => {
// Fallback: Setze currentLevel basierend auf completedLevels
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);
});
},
initializeLevel() {
// WICHTIG: Verhindere mehrfache Level-Initialisierung
if (this.isInitializingLevel) {
return;
}
// Safety check: ensure currentLevelData is loaded
if (!this.currentLevelData) {
// KEINE REKURSION: Lade Kampagnendaten nur einmal
this.loadCampaignData().then(() => {
// Nach dem Laden der Kampagne, initialisiere das Level direkt
if (this.currentLevelData) {
this.initializeLevelInternal();
} else {
// Level-Daten konnten nicht geladen werden
}
});
return;
}
// WICHTIG: Setze Flag für Level-Initialisierung
this.isInitializingLevel = true;
// Verwende die bereits geladenen Level-Daten direkt
this.initializeLevelInternal();
},
// Neue Hilfsmethode ohne Rekursion
initializeLevelInternal() {
const levelData = this.currentLevelData;
// Neue Level-Felder verwenden
this.boardWidth = levelData.boardWidth;
this.boardHeight = levelData.boardHeight;
// Neue Tile-Typen aus der levelTileTypes-Structure extrahieren
if (levelData.levelTileTypes && levelData.levelTileTypes.length > 0) {
this.tileTypes = levelData.levelTileTypes
.filter(ltt => ltt.isActive && ltt.tileType && ltt.tileType.isActive)
.map(ltt => ltt.tileType.name);
} else {
// Fallback: Verwende die alte tileTypes-Array
this.tileTypes = levelData.tileTypes || ['gem', 'star', 'heart'];
}
// Board-Layout parsen
if (!levelData.boardLayout) {
this.boardLayout = [];
} else {
// Teile den String in Zeilen auf
const rows = levelData.boardLayout.split('\n').filter(row => row.trim().length > 0);
const layout = [];
for (let row = 0; row < rows.length; row++) {
const rowString = rows[row];
const rowArray = [];
for (let col = 0; col < rowString.length; col++) {
const char = rowString[col];
// Backend-Format: 'x' = Feld (tile), 'o' = kein Feld, andere = spezifischer Tile-Typ
// Fallback-Format: 't' = Feld (tile), 'f' = kein Feld
if (char === 'o' || char === 'f') {
rowArray.push({ type: 'empty', char: char });
} else if (char === 'x' || char === 't') {
rowArray.push({ type: 'tile', char: char });
} else {
// Spezifischer Tile-Typ
rowArray.push({ type: 'specific', char: char });
}
}
layout.push(rowArray);
}
this.boardLayout = layout;
}
// WICHTIG: Alle Spiel-Statistiken zurücksetzen
this.moves = 0;
this.levelScore = 0;
this.levelStars = 0; // WICHTIG: Level-Sterne müssen auf 0 gesetzt werden
this.matchesMade = 0; // Reset Match-Zähler
this.cascadeRound = 0; // Reset Kaskaden-Zähler
this.movesLeft = levelData.moveLimit; // Initialisiere verbleibende Züge
// Zusätzliche Sicherheit: Stelle sicher, dass levelStars wirklich 0 ist
if (this.levelStars !== 0) {
console.warn('LevelStars wurde nicht korrekt zurückgesetzt, setze auf 0');
this.levelStars = 0;
}
// WICHTIG: Alle Arrays zurücksetzen
this.matchedTiles = [];
this.fallingTiles = [];
this.newTiles = [];
this.isFalling = false;
// WICHTIG: Alle Level-Objekte zurücksetzen (nicht als abgeschlossen markieren)
if (levelData.objectives) {
levelData.objectives.forEach(objective => {
objective.completed = false;
});
}
// Generiere Board basierend auf dem Layout
this.board = this.generateBoardFromLayout();
// Zusätzliche Prüfung: Stelle sicher, dass keine initialen Matches vorhanden sind
// WICHTIG: Reduziere Debug-Ausgaben für bessere Performance
let attempts = 0;
const maxAttempts = 10; // Verhindere Endlosschleifen
// Prüfe und korrigiere initiale Matches
console.log('🔧 Prüfe auf initiale Matches...');
const initialMatches = this.findMatchesOnBoard(this.board, true);
if (initialMatches.length > 0) {
console.log(`🔧 ${initialMatches.length} initiale Matches gefunden, starte Korrektur...`);
this.fixInitialMatches();
} else {
console.log('🔧 Keine initialen Matches gefunden, Level ist bereit');
}
// WICHTIG: Setze das Spiel als aktiv, aber prüfe NICHT sofort die Level-Objekte
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
this.isInitializingLevel = false;
},
// Neue Methode: Räume initiale Matches auf (ohne Punkte/Level-Objekte)
cleanupInitialMatches(matches) {
console.log('🔧 cleanupInitialMatches - Starte Aufräumen');
if (!matches || matches.length === 0) {
console.log('🔧 Keine Matches zum Aufräumen gefunden');
return;
}
// Sammle alle betroffenen Indizes
const matchedIndices = new Set();
matches.forEach(match => {
match.forEach(index => matchedIndices.add(index));
});
console.log('🔧 Zu entfernende Indizes:', Array.from(matchedIndices));
// Entferne alle gematchten Tiles (außer Regenbögen)
matchedIndices.forEach(index => {
if (this.board[index] && this.board[index].type !== 'rainbow') {
console.log(`🔧 Entferne Tile an Position ${index}:`, this.board[index]);
this.board[index] = null;
}
});
console.log('🔧 Alle gematchten Tiles entfernt, fülle leere Felder');
// Fülle leere Felder direkt mit neuen Tiles (nur beim Level-Start)
this.fillEmptySpacesWithNewTiles();
},
// Hilfsmethode: Entferne Tile sicher (schützt Regenbögen)
safeRemoveTile(index) {
if (this.board[index] && this.board[index].type !== 'rainbow') {
this.board[index] = null;
return true;
}
return false;
},
// Neue Methode: Fülle leere Felder direkt mit neuen Tiles (für Level-Start)
fillEmptySpacesWithNewTiles() {
console.log('🔧 fillEmptySpacesWithNewTiles - Starte Auffüllen');
let tilesAdded = 0;
// Gehe durch jede Spalte
for (let col = 0; col < this.boardWidth; col++) {
// Gehe von oben nach unten durch die Spalte
for (let row = 0; row < this.boardHeight; row++) {
if (this.isValidPosition(row, col)) {
const index = this.coordsToIndex(row, col);
if (index !== null && !this.board[index]) {
// WICHTIG: Prüfe ob es sich um ein freies Feld (o) handelt
if (this.boardLayout[row] && this.boardLayout[row][col] &&
this.boardLayout[row][col].type === 'empty') {
// Freies Feld - überspringe es
console.log(`🔧 Überspringe freies Feld an [${row},${col}]`);
continue;
}
// Erstelle ein neues Tile
const tileType = this.tileTypes[Math.floor(Math.random() * this.tileTypes.length)];
this.board[index] = {
type: tileType,
id: Date.now() + row * this.boardWidth + col + Math.random(),
row: row,
col: col
};
tilesAdded++;
console.log(`🔧 Neues Tile ${tileType} an Position [${row},${col}] hinzugefügt`);
}
}
}
}
console.log(`🔧 Insgesamt ${tilesAdded} neue Tiles hinzugefügt`);
},
startDrag(index, event) {
if (!this.gameActive || this.isDragging) {
return;
}
// Verhindere Drag während der Fall-Animation
if (this.isFalling) {
return;
}
this.dragStartIndex = index;
this.dragStartX = event.clientX || event.touches?.[0]?.clientX;
this.dragStartY = event.clientY || event.touches?.[0]?.clientY;
this.isDragging = true;
// Drag-Effekt starten
this.startDragEffect(event, index);
// Verhindere Standard-Browser-Verhalten
event.preventDefault();
event.stopPropagation();
// Registriere Document-Level Event-Handler für bessere Kontrolle
document.addEventListener('mouseup', this.handleDocumentMouseUp, true);
document.addEventListener('touchend', this.handleDocumentTouchEnd, true);
},
endDrag(event) {
if (!this.isDragging || this.dragStartIndex === null) {
return;
}
// Verhindere Drag während der Fall-Animation
if (this.isFalling) {
this.endDragEffect();
this.dragStartIndex = null;
this.dragStartX = null;
this.dragStartY = null;
this.isDragging = false;
return;
}
// Drag-Effekt beenden
this.endDragEffect();
const endX = event.clientX || event.changedTouches?.[0]?.clientX;
const endY = event.clientY || event.changedTouches?.[0]?.clientY;
if (endX && endY) {
// Finde das Tile, über dem der Drag endet
const targetIndex = this.findTileAtPosition(endX, endY);
if (targetIndex !== null && targetIndex !== this.dragStartIndex) {
// Prüfe ob die Tiles benachbart sind
if (this.areTilesAdjacent(this.dragStartIndex, targetIndex)) {
this.swapTiles(this.dragStartIndex, targetIndex);
}
}
}
// Reset drag state
this.dragStartIndex = null;
this.dragStartX = null;
this.dragStartY = null;
this.isDragging = false;
},
// Neue Methode: Drag abbrechen (z.B. bei mouseleave)
cancelDrag() {
if (!this.isDragging || this.dragStartIndex === null) {
return;
}
// Drag-Effekt beenden ohne Swap
this.endDragEffect();
// Reset drag state
this.dragStartIndex = null;
this.dragStartX = null;
this.dragStartY = null;
this.isDragging = false;
},
// Document-Level Event-Handler für bessere Kontrolle
handleDocumentMouseUp(event) {
if (this.isDragging) {
this.endDrag(event);
}
},
handleDocumentTouchEnd(event) {
if (this.isDragging) {
this.endDrag(event);
}
},
// Neue Methode: Finde das Tile an einer bestimmten Bildschirm-Position
findTileAtPosition(clientX, clientY) {
// Durchsuche alle Tiles und finde das, das an der Position liegt
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && !this.board[i].isSpecial) {
const tileElement = document.querySelector(`[data-index="${i}"]`);
if (tileElement) {
const rect = tileElement.getBoundingClientRect();
if (clientX >= rect.left && clientX <= rect.right &&
clientY >= rect.top && clientY <= rect.bottom) {
return i;
}
}
}
}
return null;
},
// Neue Methode: Prüfe ob zwei Tiles benachbart sind
areTilesAdjacent(index1, index2) {
if (index1 === null || index2 === null) return false;
const { row: row1, col: col1 } = this.indexToCoords(index1);
const { row: row2, col: col2 } = this.indexToCoords(index2);
// Tiles sind benachbart wenn sie sich in der gleichen Zeile oder Spalte befinden
// und nur 1 Position voneinander entfernt sind
const rowDiff = Math.abs(row1 - row2);
const colDiff = Math.abs(col1 - col2);
return (rowDiff === 1 && colDiff === 0) || (rowDiff === 0 && colDiff === 1);
},
getAdjacentIndex(index, direction) {
if (index === null) return null;
const { row, col } = this.indexToCoords(index);
let targetRow = row;
let targetCol = col;
switch (direction) {
case 'up':
targetRow = row - 1;
break;
case 'down':
targetRow = row + 1;
break;
case 'left':
targetCol = col - 1;
break;
case 'right':
targetCol = col + 1;
break;
default:
return null;
}
// Prüfe ob die Zielposition im Layout gültig ist
if (!this.isValidPosition(targetRow, targetCol)) {
return null;
}
return this.coordsToIndex(targetRow, targetCol);
},
findMatchesOnBoard(board, showDebug = true) {
const matches = [];
// L-Form Matches finden (alle 4 Richtungen) - PRIORITÄT vor regulären Matches
for (let row = 0; row < this.boardHeight; row++) {
for (let col = 0; col < this.boardWidth; col++) {
// Prüfe ob an dieser Position ein Tile existiert
const centerIndex = this.coordsToIndex(row, col);
if (!centerIndex || !board[centerIndex]) continue;
const centerType = board[centerIndex].type;
if (this.isPowerUpTile(centerType)) continue; // Power-ups können nicht Teil von L-Formen sein
// L-Form nach rechts unten (┌)
if (row + 2 < this.boardHeight && col + 2 < this.boardWidth) {
// Prüfe vertikale Linie nach unten
let verticalLength = 1;
for (let r = row + 1; r < this.boardHeight; r++) {
const index = this.coordsToIndex(r, col);
if (index && board[index] && board[index].type === centerType && !this.isPowerUpTile(board[index].type)) {
verticalLength++;
} else {
break;
}
}
// Prüfe horizontale Linie nach rechts
let horizontalLength = 1;
for (let c = col + 1; c < this.boardWidth; c++) {
const index = this.coordsToIndex(row, c);
if (index && board[index] && board[index].type === centerType && !this.isPowerUpTile(board[index].type)) {
horizontalLength++;
} else {
break;
}
}
// L-Form gefunden wenn beide Linien mindestens 3 Tiles lang sind
if (verticalLength >= 3 && horizontalLength >= 3) {
const lShapeIndices = [];
// Sammle alle Indizes der vertikalen Linie
for (let r = row; r < row + verticalLength; r++) {
const index = this.coordsToIndex(r, col);
if (index) lShapeIndices.push(index);
}
// Sammle alle Indizes der horizontalen Linie (ohne den Eckpunkt zu duplizieren)
for (let c = col + 1; c < col + horizontalLength; c++) {
const index = this.coordsToIndex(row, c);
if (index) lShapeIndices.push(index);
}
matches.push({
type: 'l-shape',
indices: lShapeIndices,
corner: centerIndex,
direction: 'bottom-right',
verticalLength,
horizontalLength
});
if (showDebug) console.log(`🔍 L-Form Match (┌): [${row},${col}] mit ${verticalLength}er vertikal und ${horizontalLength}er horizontal`);
}
}
// L-Form nach links unten (┐)
if (row + 2 < this.boardHeight && col >= 2) {
// Prüfe vertikale Linie nach unten
let verticalLength = 1;
for (let r = row + 1; r < this.boardHeight; r++) {
const index = this.coordsToIndex(r, col);
if (index && board[index] && board[index].type === centerType && !this.isPowerUpTile(board[index].type)) {
verticalLength++;
} else {
break;
}
}
// Prüfe horizontale Linie nach links
let horizontalLength = 1;
for (let c = col - 1; c >= 0; c--) {
const index = this.coordsToIndex(row, c);
if (index && board[index] && board[index].type === centerType && !this.isPowerUpTile(board[index].type)) {
horizontalLength++;
} else {
break;
}
}
// L-Form gefunden wenn beide Linien mindestens 3 Tiles lang sind
if (verticalLength >= 3 && horizontalLength >= 3) {
const lShapeIndices = [];
// Sammle alle Indizes der vertikalen Linie
for (let r = row; r < row + verticalLength; r++) {
const index = this.coordsToIndex(r, col);
if (index) lShapeIndices.push(index);
}
// Sammle alle Indizes der horizontalen Linie (ohne den Eckpunkt zu duplizieren)
for (let c = col - 1; c >= col - horizontalLength; c--) {
const index = this.coordsToIndex(row, c);
if (index) lShapeIndices.push(index);
}
matches.push({
type: 'l-shape',
indices: lShapeIndices,
corner: centerIndex,
direction: 'bottom-left',
verticalLength,
horizontalLength
});
if (showDebug) console.log(`🔍 L-Form Match (┐): [${row},${col}] mit ${verticalLength}er vertikal und ${horizontalLength}er horizontal`);
}
}
// L-Form nach rechts oben (└)
if (row >= 2 && col + 2 < this.boardWidth) {
// Prüfe vertikale Linie nach oben
let verticalLength = 1;
for (let r = row - 1; r >= 0; r--) {
const index = this.coordsToIndex(r, col);
if (index && board[index] && board[index].type === centerType && !this.isPowerUpTile(board[index].type)) {
verticalLength++;
} else {
break;
}
}
// Prüfe horizontale Linie nach rechts
let horizontalLength = 1;
for (let c = col + 1; c < this.boardWidth; c++) {
const index = this.coordsToIndex(row, c);
if (index && board[index] && board[index].type === centerType && !this.isPowerUpTile(board[index].type)) {
horizontalLength++;
} else {
break;
}
}
// L-Form gefunden wenn beide Linien mindestens 3 Tiles lang sind
if (verticalLength >= 3 && horizontalLength >= 3) {
const lShapeIndices = [];
// Sammle alle Indizes der vertikalen Linie
for (let r = row; r >= row - verticalLength; r--) {
const index = this.coordsToIndex(r, col);
if (index) lShapeIndices.push(index);
}
// Sammle alle Indizes der horizontalen Linie (ohne den Eckpunkt zu duplizieren)
for (let c = col + 1; c < col + horizontalLength; c++) {
const index = this.coordsToIndex(row, c);
if (index) lShapeIndices.push(index);
}
matches.push({
type: 'l-shape',
indices: lShapeIndices,
corner: centerIndex,
direction: 'top-right',
verticalLength,
horizontalLength
});
if (showDebug) console.log(`🔍 L-Form Match (└): [${row},${col}] mit ${verticalLength}er vertikal und ${horizontalLength}er horizontal`);
}
}
// L-Form nach links oben (┘)
if (row >= 2 && col >= 2) {
// Prüfe vertikale Linie nach oben
let verticalLength = 1;
for (let r = row - 1; r >= 0; r--) {
const index = this.coordsToIndex(r, col);
if (index && board[index] && board[index].type === centerType && !this.isPowerUpTile(board[index].type)) {
verticalLength++;
} else {
break;
}
}
// Prüfe horizontale Linie nach links
let horizontalLength = 1;
for (let c = col - 1; c >= 0; c--) {
const index = this.coordsToIndex(row, c);
if (index && board[index] && board[index].type === centerType && !this.isPowerUpTile(board[index].type)) {
horizontalLength++;
} else {
break;
}
}
// L-Form gefunden wenn beide Linien mindestens 3 Tiles lang sind
if (verticalLength >= 3 && horizontalLength >= 3) {
const lShapeIndices = [];
// Sammle alle Indizes der vertikalen Linie
for (let r = row; r >= row - verticalLength; r--) {
const index = this.coordsToIndex(r, col);
if (index) lShapeIndices.push(index);
}
// Sammle alle Indizes der horizontalen Linie (ohne den Eckpunkt zu duplizieren)
for (let c = col - 1; c >= col - horizontalLength; c--) {
const index = this.coordsToIndex(row, c);
if (index) lShapeIndices.push(index);
}
matches.push({
type: 'l-shape',
indices: lShapeIndices,
corner: centerIndex,
direction: 'top-left',
verticalLength,
horizontalLength
});
if (showDebug) console.log(`🔍 L-Form Match (┘): [${row},${col}] mit ${verticalLength}er vertikal und ${horizontalLength}er horizontal`);
}
}
}
}
// Sammle alle Indizes, die bereits Teil einer L-Form sind
const lShapeIndices = new Set();
matches.forEach(match => {
if (match.type === 'l-shape') {
match.indices.forEach(index => lShapeIndices.add(index));
}
});
// Horizontale Matches finden (3er, 4er, 5er) - NACH L-Form Matches
for (let row = 0; row < this.boardHeight; row++) {
for (let col = 0; col < this.boardWidth - 2; col++) {
// Prüfe ob diese Position bereits Teil einer L-Form ist
const currentIndex = this.coordsToIndex(row, col);
if (lShapeIndices.has(currentIndex)) {
if (showDebug) console.log(`🔍 Überspringe Position [${row},${col}] - bereits Teil einer L-Form`);
continue;
}
// Prüfe auf 3er-Match
if (col + 2 < this.boardWidth) {
const index1 = this.coordsToIndex(row, col);
const index2 = this.coordsToIndex(row, col + 1);
const index3 = this.coordsToIndex(row, col + 2);
// Prüfe ob alle drei Positionen bereits Teil einer L-Form sind
if (!lShapeIndices.has(index1) && !lShapeIndices.has(index2) && !lShapeIndices.has(index3) &&
this.isValidMatch(index1, index2, index3, board)) {
matches.push([index1, index2, index3]);
if (showDebug) console.log(`🔍 3er-Match horizontal: [${row},${col}] bis [${row},${col+2}]`);
}
}
// Prüfe auf 4er-Match
if (col + 3 < this.boardWidth) {
const index1 = this.coordsToIndex(row, col);
const index2 = this.coordsToIndex(row, col + 1);
const index3 = this.coordsToIndex(row, col + 2);
const index4 = this.coordsToIndex(row, col + 3);
// Prüfe ob alle vier Positionen bereits Teil einer L-Form sind
if (!lShapeIndices.has(index1) && !lShapeIndices.has(index2) && !lShapeIndices.has(index3) && !lShapeIndices.has(index4) &&
this.isValidMatch(index1, index2, index3, board) &&
this.isValidMatch(index2, index3, index4, board)) {
matches.push([index1, index2, index3, index4]);
if (showDebug) console.log(`🔍 4er-Match horizontal: [${row},${col}] bis [${row},${col+3}]`);
}
}
// Prüfe auf 5er-Match
if (col + 4 < this.boardWidth) {
const index1 = this.coordsToIndex(row, col);
const index2 = this.coordsToIndex(row, col + 1);
const index3 = this.coordsToIndex(row, col + 2);
const index4 = this.coordsToIndex(row, col + 3);
const index5 = this.coordsToIndex(row, col + 4);
// Prüfe ob alle fünf Positionen bereits Teil einer L-Form sind
if (!lShapeIndices.has(index1) && !lShapeIndices.has(index2) && !lShapeIndices.has(index3) && !lShapeIndices.has(index4) && !lShapeIndices.has(index5) &&
this.isValidMatch(index1, index2, index3, board) &&
this.isValidMatch(index2, index3, index4, board) &&
this.isValidMatch(index3, index4, index5, board)) {
matches.push([index1, index2, index3, index4, index5]);
if (showDebug) console.log(`🔍 5er-Match horizontal: [${row},${col}] bis [${row},${col+4}]`);
}
}
}
}
// Vertikale Matches finden (3er, 4er, 5er) - NACH L-Form Matches
for (let row = 0; row < this.boardHeight - 2; row++) {
for (let col = 0; col < this.boardWidth; col++) {
// Prüfe ob diese Position bereits Teil einer L-Form ist
const currentIndex = this.coordsToIndex(row, col);
if (lShapeIndices.has(currentIndex)) {
if (showDebug) console.log(`🔍 Überspringe Position [${row},${col}] - bereits Teil einer L-Form`);
continue;
}
// 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);
// Prüfe ob alle drei Positionen bereits Teil einer L-Form sind
if (!lShapeIndices.has(index1) && !lShapeIndices.has(index2) && !lShapeIndices.has(index3) &&
this.isValidMatch(index1, index2, index3, board)) {
matches.push([index1, index2, index3]);
if (showDebug) console.log(`🔍 3er-Match vertikal: [${row},${col}] bis [${row+2},${col}]`);
}
}
// Prüfe auf 4er-Match
if (row + 3 < this.boardHeight) {
const index1 = this.coordsToIndex(row, col);
const index2 = this.coordsToIndex(row + 1, col);
const index3 = this.coordsToIndex(row + 2, col);
const index4 = this.coordsToIndex(row + 3, col);
// Prüfe ob alle vier Positionen bereits Teil einer L-Form sind
if (!lShapeIndices.has(index1) && !lShapeIndices.has(index2) && !lShapeIndices.has(index3) && !lShapeIndices.has(index4) &&
this.isValidMatch(index1, index2, index3, board) &&
this.isValidMatch(index2, index3, index4, board)) {
matches.push([index1, index2, index3, index4]);
if (showDebug) console.log(`🔍 4er-Match vertikal: [${row},${col}] bis [${row+3},${col}]`);
}
}
// Prüfe auf 5er-Match
if (row + 4 < this.boardHeight) {
const index1 = this.coordsToIndex(row, col);
const index2 = this.coordsToIndex(row + 1, col);
const index3 = this.coordsToIndex(row + 2, col);
const index4 = this.coordsToIndex(row + 3, col);
const index5 = this.coordsToIndex(row + 4, col);
// Prüfe ob alle fünf Positionen bereits Teil einer L-Form sind
if (!lShapeIndices.has(index1) && !lShapeIndices.has(index2) && !lShapeIndices.has(index3) && !lShapeIndices.has(index4) && !lShapeIndices.has(index5) &&
this.isValidMatch(index1, index2, index3, board) &&
this.isValidMatch(index2, index3, index4, board) &&
this.isValidMatch(index3, index4, index5, board)) {
matches.push([index1, index2, index3, index4, index5]);
if (showDebug) console.log(`🔍 5er-Match vertikal: [${row},${col}] bis [${row+4},${col}]`);
}
}
}
}
if (showDebug) {
console.log(`🔍 Gefundene Matches: ${matches.length}`, matches);
}
return matches;
},
// Hilfsmethode: Prüfe ob ein Tile ein Raketen-Power-up ist
isRocketTile(tileType) {
return tileType === 'rocket' || tileType === 'rocket-horizontal' || tileType === 'rocket-vertical';
},
// Hilfsmethode: Prüfe ob ein Tile ein Power-up ist
isPowerUpTile(tileType) {
return this.isRocketTile(tileType) || tileType === 'bomb' || tileType === 'rocket-horizontal' || tileType === 'rocket-vertical';
},
// Hilfsmethode: Debug-Ausgabe für Power-ups
debugPowerUps() {
console.log('🔍 Debug: Alle Power-ups auf dem Board:');
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && this.isPowerUpTile(this.board[i].type)) {
console.log(`🔍 Power-up ${this.board[i].type} an Position ${i}`);
}
}
},
// Hilfsmethode: Prüfe ob 3 Tiles einen gültigen Match bilden
isValidMatch(index1, index2, index3, board) {
// Prüfe ob alle Indizes gültig sind
if (index1 === null || index2 === null || index3 === null) {
return false;
}
// Prüfe ob alle Positionen gültig sind
const pos1 = this.indexToCoords(index1);
const pos2 = this.indexToCoords(index2);
const pos3 = this.indexToCoords(index3);
if (!this.isValidPosition(pos1.row, pos1.col) ||
!this.isValidPosition(pos2.row, pos2.col) ||
!this.isValidPosition(pos3.row, pos3.col)) {
return false;
}
// Prüfe ob alle Tiles existieren
if (!board[index1] || !board[index2] || !board[index3]) {
return false;
}
// Prüfe ob alle Tiles den gleichen Typ haben
const type1 = board[index1].type;
const type2 = board[index2].type;
const type3 = board[index3].type;
// Power-ups können nicht gematcht werden
if (this.isPowerUpTile(type1) || this.isPowerUpTile(type2) || this.isPowerUpTile(type3)) {
return false;
}
return type1 === type2 && type2 === type3;
},
// Hilfsmethode: Prüfe ob 3 Tiles einen gültigen Match bilden (für L-Form)
isValidMatchForLShape(index1, index2, index3, board) {
// Prüfe ob alle Indizes gültig sind
if (index1 === null || index2 === null || index3 === null) {
return false;
}
// Prüfe ob alle Positionen gültig sind
const pos1 = this.indexToCoords(index1);
const pos2 = this.indexToCoords(index2);
const pos3 = this.indexToCoords(index3);
if (!this.isValidPosition(pos1.row, pos1.col) ||
!this.isValidPosition(pos2.row, pos2.col) ||
!this.isValidPosition(pos3.row, pos3.col)) {
return false;
}
// Prüfe ob alle Tiles existieren
if (!board[index1] || !board[index2] || !board[index3]) {
return false;
}
// Prüfe ob alle Tiles den gleichen Typ haben
const type1 = board[index1].type;
const type2 = board[index2].type;
const type3 = board[index3].type;
// Power-ups können nicht gematcht werden
if (this.isPowerUpTile(type1) || this.isPowerUpTile(type2) || this.isPowerUpTile(type3)) {
return false;
}
return type1 === type2 && type2 === type3;
},
fixInitialMatches() {
console.log('🔧 fixInitialMatches - Starte Korrektur initialer Matches');
let attempts = 0;
const maxAttempts = 100; // Erhöht für komplexere Matches
do {
// Finde alle initialen Matches
const initialMatches = this.findMatchesOnBoard(this.board, false);
if (initialMatches.length === 0) {
console.log('🔧 Keine Matches mehr gefunden, Level ist bereit');
break;
}
console.log(`🔧 Versuch ${attempts + 1}: ${initialMatches.length} Matches gefunden`);
// Wähle einen zufälligen Match
const randomMatch = initialMatches[Math.floor(Math.random() * initialMatches.length)];
const randomTileIndex = randomMatch[Math.floor(Math.random() * randomMatch.length)];
const currentType = this.board[randomTileIndex].type;
// Wähle einen anderen Tile-Typ
const availableTypes = this.tileTypes.filter(type => type !== currentType);
const newType = availableTypes[Math.floor(Math.random() * availableTypes.length)];
// Ändere den Tile-Typ
this.board[randomTileIndex].type = newType;
console.log(`🔧 Änderte Tile an Position ${randomTileIndex} von ${currentType} zu ${newType}`);
attempts++;
} while (attempts < maxAttempts);
if (attempts >= maxAttempts) {
console.warn('⚠️ Konnte initiale Matches nicht vollständig korrigieren nach', maxAttempts, 'Versuchen');
} else {
console.log(`🔧 Initiale Matches in ${attempts} Versuchen korrigiert`);
}
},
startDragEffect(event, index) {
// Erstelle ein visuelles Drag-Element
this.dragElement = document.createElement('div');
this.dragElement.className = 'drag-tile';
this.dragElement.innerHTML = `
<div class="tile-content">
<span class="tile-icon">${this.getTileSymbol(this.board[index].type)}</span>
</div>
`;
// Position des ursprünglichen Tiles ermitteln
const originalTile = event.target;
const rect = originalTile.getBoundingClientRect();
// Drag-Element positionieren
this.dragElement.style.position = 'fixed';
this.dragElement.style.left = rect.left + 'px';
this.dragElement.style.top = rect.top + 'px';
this.dragElement.style.width = rect.width + 'px';
this.dragElement.style.height = rect.height + 'px';
this.dragElement.style.zIndex = '1000';
this.dragElement.style.pointerEvents = 'none';
// Offset für präzise Positionierung berechnen
this.dragOffsetX = event.clientX - rect.left;
this.dragOffsetY = event.clientY - rect.top;
// Drag-Element zum DOM hinzufügen
document.body.appendChild(this.dragElement);
// Maus/Touch-Bewegung verfolgen (mit korrektem this-Binding)
this.boundMouseMoveHandler = this.updateDragEffect.bind(this);
this.boundTouchMoveHandler = this.updateDragEffect.bind(this);
document.addEventListener('mousemove', this.boundMouseMoveHandler);
document.addEventListener('touchmove', this.boundTouchMoveHandler);
// Ursprüngliches Tile ausblenden
originalTile.style.opacity = '0.3';
},
updateDragEffect(event) {
if (!this.dragElement || !this.isDragging) return;
const clientX = event.clientX || event.touches?.[0]?.clientX;
const clientY = event.clientY || event.touches?.[0]?.clientY;
if (clientX && clientY) {
// Drag-Element mit Maus/Touch bewegen
this.dragElement.style.left = (clientX - this.dragOffsetX) + 'px';
this.dragElement.style.top = (clientY - this.dragOffsetY) + 'px';
}
},
endDragEffect() {
// Event-Listener entfernen
if (this.boundMouseMoveHandler) {
document.removeEventListener('mousemove', this.boundMouseMoveHandler);
this.boundMouseMoveHandler = null;
}
if (this.boundTouchMoveHandler) {
document.removeEventListener('touchmove', this.boundTouchMoveHandler);
this.boundTouchMoveHandler = null;
}
// Document-Level Event-Handler entfernen
document.removeEventListener('mouseup', this.handleDocumentMouseUp, true);
document.removeEventListener('touchend', this.handleDocumentTouchEnd, true);
// Drag-Element entfernen
if (this.dragElement) {
document.body.removeChild(this.dragElement);
this.dragElement = null;
}
// Ursprüngliches Tile wieder einblenden
if (this.dragStartIndex !== null) {
const originalTile = document.querySelector(`[data-index="${this.dragStartIndex}"]`);
if (originalTile) {
originalTile.style.opacity = '1';
}
}
},
async swapTiles(index1, index2) {
// Verhindere Swaps während der Fall-Animation
if (this.isFalling) {
return;
}
// Tausche die Tiles
const temp = this.board[index1];
this.board[index1] = this.board[index2];
this.board[index2] = temp;
// WICHTIG: Prüfe auf Regenbogen-Tile Tausch (vor normalen Matches)
// Prüfe beide Kombinationen: temp + board[index1] und board[index1] + temp
if (this.handleRainbowSwap(temp, this.board[index1]) ||
this.handleRainbowSwap(this.board[index1], temp)) {
// Regenbogen-Tausch erfolgreich - Zug-Zähler wird bereits in handleRainbowSwap erhöht
return; // Beende hier, da Regenbogen-Tausch bereits verarbeitet wurde
}
// Spiele Move-Sound
this.playSound('move');
// Erhöhe den Zug-Zähler nur für normale Swaps
this.moves++;
this.movesLeft--;
// Prüfe auf normale Matches nach dem Swap
const matches = this.findMatchesOnBoard(this.board, false); // Reduziere Debug-Ausgaben
if (matches.length > 0) {
await this.handleMatches(matches, true); // true = Spieler-Zug
} else {
// Keine Matches - tausche zurück
const tempBack = this.board[index1];
this.board[index1] = this.board[index2];
this.board[index2] = tempBack;
// Zug rückgängig machen
this.moves--;
this.movesLeft++;
// 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();
}
}
},
// Verarbeite gefundene Matches
async handleMatches(matches, isPlayerMove = false) {
if (matches.length === 0) {
console.log('🔧 Keine Matches zum Verarbeiten');
return;
}
console.log(`🔧 Verarbeite ${matches.length} Matches...`);
// Debug: Zeige alle gefundenen Matches
console.log('🔧 Debug: Alle gefundenen Matches:');
matches.forEach((match, index) => {
if (match.type === 'l-shape') {
console.log(` Match ${index + 1}: L-Form ${match.direction} an Ecke ${match.corner}`);
} else if (Array.isArray(match)) {
console.log(` Match ${index + 1}: ${match.length}er-Match [${match.join(', ')}]`);
}
});
// Sammle alle zu entfernenden Tile-Indizes (aus 3er-Matches UND 4er-Matches)
const tilesToRemove = new Set();
// Sammle alle Power-up-Positionen, die NICHT entfernt werden dürfen
const powerUpPositions = new Set();
// Verarbeite zuerst L-Form-Matches (Bomben)
matches.forEach((match, matchIndex) => {
if (match.type === 'l-shape') {
// L-Form Match: Erstelle Bombe an der Ecke
console.log(`🔧 L-Form Match ${matchIndex + 1}: ${match.direction} an Ecke ${match.corner}`);
// Alle Tiles der L-Form entfernen (außer der Ecke, wo die Bombe ist)
match.indices.forEach(tileIndex => {
if (tileIndex !== match.corner) {
tilesToRemove.add(tileIndex);
}
});
// Bombe wird später in createPowerUpsForMatches erstellt
console.log(`💣 Bombe wird später an Position ${match.corner} erstellt`);
}
});
// Verarbeite dann normale Matches (3er, 4er, 5er)
matches.forEach((match, matchIndex) => {
if (Array.isArray(match) && match.length >= 3) {
// Normale Matches (3er, 4er, 5er)
console.log(`🔧 ${match.length}er-Match ${matchIndex + 1}: [${match.join(', ')}]`);
match.forEach(tileIndex => {
// WICHTIG: Power-ups dürfen NIE entfernt werden
if (this.board[tileIndex] && this.isPowerUpTile(this.board[tileIndex].type)) {
console.log(`🔧 Power-up ${this.board[tileIndex].type} an Position ${tileIndex} wird NICHT entfernt (Typ-Prüfung)`);
return; // Überspringe dieses Tile
}
// Nur normale Tiles zur Entfernung hinzufügen
console.log(`🔧 Füge Tile ${this.board[tileIndex].type} an Position ${tileIndex} zur Entfernung hinzu`);
tilesToRemove.add(tileIndex);
});
}
});
// Debug: Zeige alle Tiles, die zur Entfernung hinzugefügt wurden
console.log('🔧 Debug: Alle Tiles, die zur Entfernung hinzugefügt wurden:');
tilesToRemove.forEach(index => {
if (this.board[index]) {
console.log(` - Position ${index}: ${this.board[index].type}`);
}
});
if (tilesToRemove.size === 0) {
console.log('🔧 Keine Tiles zum Entfernen gefunden');
return;
}
console.log(`🔧 ${tilesToRemove.size} Tiles zum Entfernen gefunden`);
// Debug: Zeige alle Tiles, die entfernt werden sollen
console.log('🔧 Debug: Tiles die entfernt werden sollen:');
tilesToRemove.forEach(index => {
if (this.board[index]) {
console.log(` - Position ${index}: ${this.board[index].type}`);
}
});
// Starte Schrumpf-Animation für alle zu entfernenden Tiles
console.log(`🔧 Starte Schrumpf-Animation für ${tilesToRemove.size} Tiles...`);
await this.animateTileRemoval(Array.from(tilesToRemove));
// Entferne alle gematchten Tiles nach der Animation
tilesToRemove.forEach(index => {
if (this.board[index]) {
console.log(`🔧 Entferne Tile ${this.board[index].type} an Position ${index}`);
this.board[index] = null;
}
});
// Debug: Zeige alle Power-ups nach der Tile-Entfernung
console.log('🔧 Debug: Alle Power-ups nach Tile-Entfernung:');
this.debugPowerUps();
// Aktualisiere die Anzeige
this.$forceUpdate();
console.log(`🔧 ${tilesToRemove.size} einzigartige Tiles aus 3er-Matches entfernt`);
// JETZT erst Power-ups erstellen, nachdem die Tiles entfernt wurden
console.log('🔧 Erstelle Power-ups nach der Tile-Entfernung...');
const powerUpsCreated = await this.createPowerUpsForMatches(matches);
// Debug: Zeige alle Power-ups nach der Erstellung
console.log('🔧 Debug: Alle Power-ups nach createPowerUpsForMatches:');
this.debugPowerUps();
// Debug: Zeige alle Power-ups im Board nach der Erstellung
console.log('🔧 Debug: Board nach Power-up-Erstellung:');
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && this.isPowerUpTile(this.board[i].type)) {
console.log(`🔧 Position ${i}: Power-up ${this.board[i].type}`);
console.log(`🔧 Board[${i}] = ${JSON.stringify(this.board[i])}`);
}
}
// Debug: Zeige alle Power-ups im Template
console.log('🔧 Debug: Power-ups im Template:');
for (let i = 0; i < this.board.length; i++) {
if (this.board[i]) {
console.log(`🔧 Position ${i}: Tile ${this.board[i].type}, isPowerUpTile: ${this.isPowerUpTile(this.board[i].type)}, isRocketTile: ${this.isRocketTile(this.board[i].type)}`);
if (this.isPowerUpTile(this.board[i].type)) {
console.log(`🔧 ✅ Power-up erkannt: ${this.board[i].type} an Position ${i}`);
}
}
}
// Debug: Zeige alle Power-ups vor dem Fall-Down
console.log('🔧 Debug: Alle Power-ups vor dem Fall-Down:');
this.debugPowerUps();
// Debug: Zeige alle Power-ups nach dem Fall-Down
console.log('🔧 Debug: Alle Power-ups nach dem Fall-Down:');
this.debugPowerUps();
// Führe Fall-Down-Logik aus
await this.fallTilesDown();
// Debug: Zeige alle Power-ups nach dem Fall-Down
console.log('🔧 Debug: Alle Power-ups nach dem Fall-Down:');
this.debugPowerUps();
// Fülle leere Positionen mit neuen Tiles auf
await this.fillEmptyPositions();
// Debug: Zeige alle Power-ups nach dem Füllen
console.log('🔧 Debug: Alle Power-ups nach dem Füllen:');
this.debugPowerUps();
// Prüfe ob Power-ups erstellt wurden - wenn ja, keine Cascade-Matches prüfen
// Verwende den Rückgabewert von createPowerUpsForMatches
if (!powerUpsCreated) {
// Nur Cascade-Matches prüfen, wenn keine Power-ups erstellt wurden
await this.checkForCascadeMatches();
} else {
console.log(`🔧 ${powerUpsCreated} Power-ups erstellt - überspringe Cascade-Match-Prüfung`);
// Debug: Zeige alle Power-ups nach der Verarbeitung
console.log('🔧 Debug: Alle Power-ups nach Power-up-Verarbeitung:');
this.debugPowerUps();
// Debug: Zeige alle Power-ups im Template nach der Verarbeitung
console.log('🔧 Debug: Power-ups im Template nach Verarbeitung:');
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && this.isPowerUpTile(this.board[i].type)) {
console.log(`🔧 ✅ Power-up im Template: ${this.board[i].type} an Position ${i}`);
}
}
}
},
// Fall-Down-Logik: Bewege Tiles nach unten in leere Positionen
async fallTilesDown() {
console.log('🔧 Starte Fall-Down-Logik...');
let hasChanges = true;
let iteration = 0;
// Wiederhole bis keine weiteren Änderungen mehr auftreten
while (hasChanges && iteration < this.boardHeight) {
hasChanges = false;
iteration++;
console.log(`🔧 Fall-Down Iteration ${iteration}`);
// Sammle alle Tiles, die fallen sollen
const tilesToFall = [];
// Gehe von unten nach oben durch jede Spalte
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);
// 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
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}'`);
}
}
} else {
// Fallback: Wenn kein Layout vorhanden, normale Logik verwenden
tilesToFall.push({
fromIndex: currentIndex,
toIndex: belowIndex,
fromRow: row,
toRow: row + 1,
col: col
});
hasChanges = true;
}
}
}
}
// Führe Fall-Animation für alle Tiles aus
if (tilesToFall.length > 0) {
console.log(`🔧 ${tilesToFall.length} Tiles fallen in Iteration ${iteration}`);
// Führe Fall-Animation aus, BEVOR die Tiles im Board verschoben werden
await this.animateTilesFalling(tilesToFall);
// 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
this.$forceUpdate();
}
// Kurze Pause zwischen den Iterationen
await this.wait(100);
}
console.log(`🔧 Fall-Down abgeschlossen nach ${iteration} Iterationen`);
// Nach dem Fallen: Prüfe ob alle gültigen Felder ein Tile enthalten
await this.checkAndFillEmptyValidFields();
},
// Fülle leere Positionen mit neuen Tiles auf
async fillEmptyPositions() {
console.log('🔧 Fülle leere Positionen mit neuen Tiles auf...');
let newTilesAdded = 0;
const newTilePositions = [];
// Fülle nur die obersten leeren Zeilen mit neuen Tiles
for (let col = 0; col < this.boardWidth; col++) {
// Finde die oberste leere Position in dieser Spalte
let topEmptyRow = -1;
for (let row = 0; row < this.boardHeight; row++) {
const index = this.coordsToIndex(row, col);
if (!this.board[index]) {
topEmptyRow = row;
break;
}
}
// Wenn eine leere Position gefunden wurde, prüfe ob sie im Layout gültig ist
if (topEmptyRow !== -1) {
// Prüfe, ob das Feld im board_layout gültig ist (nicht 'o')
if (this.currentLevelData && this.currentLevelData.boardLayout) {
const layout = this.currentLevelData.boardLayout;
const layoutRows = layout.split('\n');
// Prüfe, ob das Feld im Layout gültig ist
if (topEmptyRow < layoutRows.length && col < layoutRows[topEmptyRow].length) {
const targetChar = layoutRows[topEmptyRow][col];
// Nur neues Tile hinzufügen, wenn das Feld 'x' ist (gültig)
if (targetChar === 'x') {
const index = this.coordsToIndex(topEmptyRow, col);
const newTile = this.createRandomTile();
this.board[index] = newTile;
newTilesAdded++;
newTilePositions.push({ index, row: topEmptyRow, col, type: newTile.type });
console.log(`🔧 Neues Tile ${newTile.type} an Position [${topEmptyRow}, ${col}] hinzugefügt`);
} else {
console.log(`🔧 Position [${topEmptyRow}, ${col}] wird nicht gefüllt - Layout zeigt '${targetChar}'`);
}
}
} else {
// Fallback: Wenn kein Layout vorhanden, normale Logik verwenden
const index = this.coordsToIndex(topEmptyRow, col);
const newTile = this.createRandomTile();
this.board[index] = newTile;
newTilesAdded++;
newTilePositions.push({ index, row: topEmptyRow, col, type: newTile.type });
console.log(`🔧 Neues Tile ${newTile.type} an Position [${topEmptyRow}, ${col}] hinzugefügt`);
}
}
}
// Aktualisiere die Anzeige
this.$forceUpdate();
// Führe Animation für neue Tiles aus
if (newTilePositions.length > 0) {
await this.animateNewTilesAppearing(newTilePositions);
}
console.log(`🔧 ${newTilesAdded} neue Tiles hinzugefügt`);
},
// Erstelle Power-ups für 4er-Matches und L-Form-Matches
async createPowerUpsForMatches(matches) {
console.log('🔧 Prüfe auf Power-up-Erstellung...');
let powerUpsCreated = 0;
matches.forEach(match => {
// Prüfe auf L-Form-Matches (Bomben)
if (match.type === 'l-shape') {
console.log(`🔧 L-Form-Match gefunden: ${match.direction} an Ecke ${match.corner} - erstelle Bombe`);
// Bombe an der Ecke erstellen
this.board[match.corner] = { type: 'bomb' };
console.log(`💣 Bombe an Position ${match.corner} erstellt`);
console.log(`🔧 Board[${match.corner}] = ${JSON.stringify(this.board[match.corner])}`);
powerUpsCreated++;
}
// Prüfe auf normale 4er-Matches (Arrays)
else if (Array.isArray(match) && match.length === 4) {
console.log(`🔧 4er-Match gefunden: ${match.join(', ')} - erstelle Rakete`);
// Rakete erscheint an der Position des zweiten Tiles
const rocketIndex = match[1];
// Erstelle Rakete basierend auf der Richtung des Matches
const rocketType = this.determineRocketType(match);
this.board[rocketIndex] = { type: rocketType };
console.log(`🚀 Rakete ${rocketType} an Position ${rocketIndex} erstellt`);
console.log(`🔧 Board[${rocketIndex}] = ${JSON.stringify(this.board[rocketIndex])}`);
powerUpsCreated++;
}
});
// Aktualisiere die Anzeige nach der Power-up-Erstellung
if (powerUpsCreated > 0) {
this.$forceUpdate();
console.log(`🔧 ${powerUpsCreated} Power-ups erstellt und Board aktualisiert`);
}
return powerUpsCreated;
},
// Bestimme den Raketen-Typ basierend auf der Match-Richtung
determineRocketType(match) {
// Prüfe ob der Match horizontal oder vertikal ist
const firstPos = this.indexToCoords(match[0]);
const secondPos = this.indexToCoords(match[1]);
if (firstPos.row === secondPos.row) {
// Horizontaler Match - horizontale Rakete
return 'rocket-horizontal';
} else {
// Vertikaler Match - vertikale Rakete
return 'rocket-vertical';
}
},
// Prüfe und fülle alle leeren gültigen Felder nach dem Fallen
async checkAndFillEmptyValidFields() {
console.log('🔧 Prüfe alle leeren gültigen Felder...');
let hasEmptyValidFields = false;
const emptyValidFields = [];
// Gehe durch alle Positionen im Board
for (let row = 0; row < this.boardHeight; row++) {
for (let col = 0; col < this.boardWidth; col++) {
const index = this.coordsToIndex(row, col);
// 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 Feld im Layout gültig ist (nicht 'o')
if (row < layoutRows.length && col < layoutRows[row].length) {
const targetChar = layoutRows[row][col];
if (targetChar === 'x') {
// Gültiges Feld ist leer - muss gefüllt werden
emptyValidFields.push({ index, row, col });
hasEmptyValidFields = true;
console.log(`🔧 Gültiges Feld [${row}, ${col}] ist leer und muss gefüllt werden`);
}
}
}
} else if (this.board[index] && this.isPowerUpTile(this.board[index].type)) {
// Position enthält bereits ein Power-up - nicht überschreiben
console.log(`🔧 Position [${row}, ${col}] enthält bereits Power-up ${this.board[index].type} - wird nicht überschrieben`);
}
}
}
if (hasEmptyValidFields) {
console.log(`🔧 ${emptyValidFields.length} leere gültige Felder gefunden - starte erneuten Fall-Prozess`);
// Fülle alle leeren gültigen Felder mit neuen Tiles
for (const field of emptyValidFields) {
const newTile = this.createRandomTile();
this.board[field.index] = newTile;
console.log(`🔧 Neues Tile ${newTile.type} an Position [${field.row}, ${field.col}] hinzugefügt`);
}
// Aktualisiere die Anzeige
this.$forceUpdate();
// Führe Animation für neue Tiles aus
await this.animateNewTilesAppearing(emptyValidFields.map(field => ({
index: field.index,
row: field.row,
col: field.col,
type: this.board[field.index].type
})));
console.log(`🔧 Alle leeren gültigen Felder gefüllt`);
} else {
console.log('🔧 Alle gültigen Felder enthalten Tiles - Board ist vollständig');
}
},
// Animierte Entfernung von Tiles mit Schrumpf-Effekt
async animateTileRemoval(tileIndices) {
console.log(`🎬 Starte Schrumpf-Animation für ${tileIndices.length} Tiles...`);
// Sammle alle DOM-Elemente der zu entfernenden Tiles
const tileElements = tileIndices.map(index => {
const element = document.querySelector(`[data-index="${index}"]`);
if (element) {
// Füge CSS-Klasse für die Animation hinzu
element.classList.add('removing');
console.log(`🎬 CSS-Klasse 'removing' zu Tile ${index} hinzugefügt`);
}
return element;
}).filter(element => element !== null);
if (tileElements.length === 0) {
console.warn(`⚠️ Keine DOM-Elemente für Animation gefunden`);
return;
}
console.log(`🎬 ${tileElements.length} DOM-Elemente für Animation vorbereitet`);
// Warte auf die Animation (0,75 Sekunden)
await this.wait(750);
// Entferne die CSS-Klassen
tileElements.forEach(element => {
element.classList.remove('removing');
});
console.log(`🎬 Schrumpf-Animation abgeschlossen`);
},
// Animierte Fall-Animation für Tiles
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)
const tileElements = tilesToFall.map(fallData => {
const element = document.querySelector(`[data-index="${fallData.fromIndex}"]`);
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}`);
}
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.5s 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)`);
}
return element;
}).filter(element => element !== null);
if (tileElements.length === 0) {
console.warn(`⚠️ Keine DOM-Elemente für Fall-Animation gefunden`);
return;
}
console.log(`🎬 ${tileElements.length} DOM-Elemente für Fall-Animation vorbereitet`);
// Warte kurz, damit die transform-Eigenschaft gesetzt wird
await this.wait(50);
// Bewege alle Tiles nach unten (Fall-Animation)
tileElements.forEach(element => {
const fallPixels = parseInt(element.dataset.fallPixels);
element.style.transform = `translateY(${fallPixels}px)`;
});
// Spiele Fall-Sound ab
this.playSound('falling');
// Warte auf die Fall-Animation (0,5 Sekunden)
await this.wait(500);
// Entferne die CSS-Klassen und transform-Eigenschaften
tileElements.forEach(element => {
element.classList.remove('falling');
element.style.removeProperty('transform');
element.style.removeProperty('transition');
});
console.log(`🎬 Fall-Animation abgeschlossen`);
},
// Animierte Erscheinung neuer Tiles
async animateNewTilesAppearing(newTilePositions) {
console.log(`🎬 Starte Erscheinungs-Animation für ${newTilePositions.length} neue Tiles...`);
// Sammle alle DOM-Elemente der neuen Tiles
const tileElements = newTilePositions.map(tileData => {
const element = document.querySelector(`[data-index="${tileData.index}"]`);
if (element) {
// Füge CSS-Klasse für die Erscheinungs-Animation hinzu
element.classList.add('new-tile');
console.log(`🎬 CSS-Klasse 'new-tile' zu Tile ${tileData.index} hinzugefügt`);
}
return element;
}).filter(element => element !== null);
if (tileElements.length === 0) {
console.warn(`⚠️ Keine DOM-Elemente für Erscheinungs-Animation gefunden`);
return;
}
console.log(`🎬 ${tileElements.length} DOM-Elemente für Erscheinungs-Animation vorbereitet`);
// Warte auf die Erscheinungs-Animation (0,5 Sekunden)
await this.wait(500);
// Entferne die CSS-Klassen
tileElements.forEach(element => {
element.classList.remove('new-tile');
});
console.log(`🎬 Erscheinungs-Animation abgeschlossen`);
},
// Erstelle ein zufälliges Tile
createRandomTile() {
// Verwende die verfügbaren Tile-Typen aus dem aktuellen Level
if (this.currentLevelData && this.currentLevelData.tileTypes && this.currentLevelData.tileTypes.length > 0) {
const randomType = this.currentLevelData.tileTypes[
Math.floor(Math.random() * this.currentLevelData.tileTypes.length)
];
return { type: randomType };
} else {
// Fallback: Verwende Standard-Tile-Typen
const defaultTypes = ['red', 'blue', 'green', 'yellow', 'purple'];
const randomType = defaultTypes[Math.floor(Math.random() * defaultTypes.length)];
return { type: randomType };
}
},
checkLevelObjectives() {
// WICHTIG: Zusätzliche Sicherheit - prüfe ob das Level gerade initialisiert wird
if (this.isInitializingLevel) {
return;
}
if (!this.currentLevelData || !this.currentLevelData.objectives) {
return;
}
// WICHTIG: Prüfe nur wenn der Spieler bereits gespielt hat
if (this.moves === 0) {
return;
}
// WICHTIG: Prüfe ob es überhaupt Level-Objekte gibt
if (this.currentLevelData.objectives.length === 0) {
return;
}
let allObjectivesCompleted = true;
this.currentLevelData.objectives.forEach((objective, index) => {
let isCompleted = false;
switch (objective.type) {
case 'score':
isCompleted = this.checkObjectiveCompletion(this.levelScore, objective.target, objective.operator);
break;
case 'matches':
isCompleted = this.checkObjectiveCompletion(this.matchesMade, objective.target, objective.operator);
break;
case 'moves':
isCompleted = this.checkObjectiveCompletion(this.moves, objective.target, objective.operator);
break;
default:
isCompleted = false;
}
// Setze completed-Status
objective.completed = isCompleted;
if (!isCompleted) {
allObjectivesCompleted = false;
}
});
// Prüfe ob alle Objekte erreicht wurden
if (allObjectivesCompleted) {
this.completeLevel();
}
},
completeLevel() {
// Level als abgeschlossen markieren
this.gameActive = false;
// Berechne Sterne basierend auf Performance
this.calculateStars();
// Speichere den Fortschritt
this.saveProgressToBackend();
// Zeige Level-Abschluss Dialog
this.showLevelComplete = true;
},
// Hilfsmethode für Objective-Prüfung mit Operatoren
checkObjectiveCompletion(currentValue, targetValue, operator) {
switch (operator) {
case '>=':
return currentValue >= targetValue;
case '<=':
return currentValue <= targetValue;
case '=':
return currentValue === targetValue;
case '>':
return currentValue > targetValue;
case '<':
return currentValue < targetValue;
default:
return currentValue >= targetValue; // Fallback
}
},
// Zentrale Fehlerbehandlung für Lade-Fehler
handleLoadError(message, error) {
// Setze Spiel-Status auf Fehler
this.gameActive = false;
this.isLoadingData = false;
// Verwende den bestehenden ErrorDialog aus der App.vue
if (this.$refs.errorDialog) {
const fullMessage = `${message}\n\nFehler-Details: ${error.message || 'Unbekannter Fehler'}\n\nDas Spiel konnte nicht geladen werden. Überprüfe deine Internetverbindung und versuche es erneut.`;
this.$refs.errorDialog.open(fullMessage);
} else {
// Fallback: Zeige einfache Alert-Meldung
alert(`${message}\n\nFehler: ${error.message || 'Unbekannter Fehler'}`);
}
},
calculateStars() {
// Einfache Sterne-Berechnung basierend auf verbleibenden Zügen
const movesUsed = this.currentLevelData.moveLimit - this.movesLeft;
const maxMoves = this.currentLevelData.moveLimit;
if (movesUsed <= maxMoves * 0.5) {
this.levelStars = 3; // 3 Sterne
} else if (movesUsed <= maxMoves * 0.8) {
this.levelStars = 2; // 2 Sterne
} else {
this.levelStars = 1; // 1 Stern
}
this.stars += this.levelStars;
},
startFallAnimation(matchedIndices = []) {
this.isFalling = true;
// Sicherheitscheck: Stelle sicher, dass matchedIndices ein Array ist
if (!Array.isArray(matchedIndices)) {
matchedIndices = [];
}
// Animiere das Verschwinden der gematchten Tiles
this.animateTileDisappearance(matchedIndices);
// Nach der Verschwinden-Animation: Tiles entfernen und Fall-Animation starten
setTimeout(() => {
// Setze gematchte Tiles auf null (außer Regenbögen)
matchedIndices.forEach(index => {
if (this.board[index] && this.board[index].type !== 'rainbow') {
this.safeRemoveTile(index);
}
});
// Starte Fall-Animation
this.processFalling();
}, 300);
},
animateTileDisappearance(matchedIndices = []) {
// Sicherheitscheck: Stelle sicher, dass matchedIndices ein Array ist
if (!Array.isArray(matchedIndices)) {
matchedIndices = [];
}
matchedIndices.forEach(index => {
const tileElement = document.querySelector(`[data-index="${index}"]`);
if (tileElement) {
tileElement.classList.add('disappearing');
}
});
// Entferne die disappearing-Klasse nach der Animation
setTimeout(() => {
matchedIndices.forEach(index => {
const tileElement = document.querySelector(`[data-index="${index}"]`);
if (tileElement) {
tileElement.classList.remove('disappearing');
tileElement.style.transform = '';
tileElement.style.opacity = '';
tileElement.style.zIndex = '';
}
});
}, 300);
},
// Fall-Prozess: Tiles fallen lassen und neue hinzufügen
async processFalling() {
console.log('🔧 Starte Fall-Prozess...');
// Führe Fall-Down-Logik aus
await this.fallTilesDown();
// Fülle leere Positionen mit neuen Tiles auf
await this.fillEmptyPositions();
console.log('🔧 Fall-Prozess abgeschlossen');
},
// Prüfe auf Cascade-Matches nach dem Fallen
async checkForCascadeMatches() {
console.log('🔧 Prüfe auf Cascade-Matches...');
// Warte kurz, damit alle Animationen abgeschlossen sind
await this.wait(200);
// Prüfe ob neue Matches entstanden sind
const newMatches = this.findMatchesOnBoard(this.board, false);
// Filtere Power-up-Matches heraus (diese sollen nicht als Cascade-Matches behandelt werden)
const filteredMatches = newMatches.filter(match => {
if (match.type === 'l-shape') {
console.log('🔧 L-Form Match in Cascade gefunden - überspringe');
return false;
}
if (Array.isArray(match) && match.length === 4) {
console.log('🔧 4er-Match in Cascade gefunden - überspringe');
return false;
}
return true;
});
if (filteredMatches.length > 0) {
console.log(`🔧 ${filteredMatches.length} neue Cascade-Matches gefunden`);
await this.handleMatches(filteredMatches, false);
} else {
console.log('🔧 Keine neuen Cascade-Matches gefunden');
}
},
// Führe einen Move durch (Tausch zweier Tiles)
async performMove(fromIndex, toIndex) {
console.log(`🔧 Führe Move durch: ${fromIndex}${toIndex}`);
// Prüfe ob beide Positionen gültig sind
if (!this.board[fromIndex] || !this.board[toIndex]) {
console.log('⚠️ Ungültige Positionen für Move');
return false;
}
// Prüfe ob die Tiles benachbart sind
if (!this.areTilesAdjacent(fromIndex, toIndex)) {
console.log('⚠️ Tiles sind nicht benachbart');
return false;
}
// Prüfe ob ein Tile eine Rakete ist
if (this.isRocketTile(this.board[fromIndex].type) || this.isRocketTile(this.board[toIndex].type)) {
console.log('🚀 Rakete wird aktiviert!');
return await this.activateRocket(fromIndex, toIndex);
}
// Prüfe ob das Ziel ein leeres Feld ist
if (!this.board[toIndex]) {
console.log('⚠️ Verschieben auf leeres Feld nicht erlaubt');
return false;
}
// Führe den Tausch durch
const tempTile = { ...this.board[fromIndex] };
this.board[fromIndex] = { ...this.board[toIndex] };
this.board[toIndex] = tempTile;
// Aktualisiere die Anzeige
this.$forceUpdate();
// Warte auf Animation (0,75 Sekunden)
await this.wait(750);
// Prüfe ob der Move einen gültigen Match erzeugt hat
const matches = this.findMatchesOnBoard(this.board, false);
const hasValidMatch = matches.some(match =>
(Array.isArray(match) && match.length >= 3) ||
match.type === 'l-shape'
);
if (hasValidMatch) {
console.log(`✅ Move erfolgreich - ${matches.length} Match(es) gefunden!`);
await this.handleMatches(matches, true);
return true;
} else {
console.log('❌ Move nicht erfolgreich - Kein 3er-Match');
// Tausche die Tiles zurück
this.board[fromIndex] = { ...this.board[toIndex] };
this.board[toIndex] = tempTile;
this.$forceUpdate();
return false;
}
},
// Aktiviere eine Rakete (durch Verschieben oder Doppelklick)
async activateRocket(rocketIndex, targetIndex) {
console.log(`🚀 Aktiviere Rakete an Position ${rocketIndex}`);
// Bestimme das Zentrum der Raketen-Explosion
let explosionCenter;
let tilesToRemove = new Set();
if (this.isRocketTile(this.board[rocketIndex].type)) {
// Rakete wird auf ein Nachbarfeld verschoben
explosionCenter = targetIndex;
tilesToRemove = this.getRocketExplosionTiles(targetIndex);
} else {
// Ein Tile wird auf die Rakete verschoben
explosionCenter = rocketIndex;
tilesToRemove = this.getRocketExplosionTiles(rocketIndex);
}
// Entferne die Tiles um das Explosionszentrum
if (tilesToRemove.size > 0) {
console.log(`🚀 Entferne ${tilesToRemove.size} Tiles um Position ${explosionCenter}`);
// Starte Schrumpf-Animation für alle zu entfernenden Tiles
await this.animateTileRemoval(Array.from(tilesToRemove));
// Entferne alle Tiles nach der Animation
tilesToRemove.forEach(index => {
if (this.board[index]) {
console.log(`🚀 Entferne Tile ${this.board[index].type} an Position ${index}`);
this.board[index] = null;
}
});
// Entferne auch die Rakete selbst
if (this.isRocketTile(this.board[rocketIndex].type)) {
console.log(`🚀 Entferne Rakete an Position ${rocketIndex}`);
this.board[rocketIndex] = null;
}
// Aktualisiere die Anzeige
this.$forceUpdate();
// Zeige Raketen-Explosions-Animation
await this.showRocketExplosionAnimation(explosionCenter);
// Führe Fall-Down-Logik aus
await this.fallTilesDown();
// Fülle leere Positionen mit neuen Tiles auf
await this.fillEmptyPositions();
// Erhöhe den Zug-Zähler
this.moves++;
this.movesLeft--;
return true;
}
return false;
},
// Hole alle Tiles, die von der Raketen-Explosion betroffen sind
getRocketExplosionTiles(centerIndex) {
const tilesToRemove = new Set();
const centerPos = this.indexToCoords(centerIndex);
// Prüfe alle 4 Richtungen: oben, unten, links, rechts
const directions = [
{ row: -1, col: 0 }, // oben
{ row: 1, col: 0 }, // unten
{ row: 0, col: -1 }, // links
{ row: 0, col: 1 } // rechts
];
directions.forEach(dir => {
const newRow = centerPos.row + dir.row;
const newCol = centerPos.col + dir.col;
if (this.isValidPosition(newRow, newCol)) {
const newIndex = this.coordsToIndex(newRow, newCol);
if (newIndex !== null && this.board[newIndex] && !this.isRocketTile(this.board[newIndex].type)) {
tilesToRemove.add(newIndex);
}
}
});
return tilesToRemove;
},
// Zeige Raketen-Explosions-Animation
async showRocketExplosionAnimation(centerIndex) {
const centerPos = this.indexToCoords(centerIndex);
const tileElement = document.querySelector(`[data-index="${centerIndex}"]`);
if (tileElement) {
const rect = tileElement.getBoundingClientRect();
this.rocketCenter = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
}
// Zeige Explosions-Animation
this.showRocketEffect = true;
this.playSound('rocket');
// Warte auf Animation
await this.wait(1000);
// Verstecke Animation
this.showRocketEffect = false;
// Rakete fliegt zu einem zufälligen belegten Feld
await this.rocketFlightToRandomTile();
},
// Rakete fliegt zu einem zufälligen belegten Feld
async rocketFlightToRandomTile() {
// Finde alle belegten Felder
const occupiedTiles = [];
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && !this.isRocketTile(this.board[i].type)) {
occupiedTiles.push(i);
}
}
if (occupiedTiles.length > 0) {
// Wähle ein zufälliges belegtes Feld
const randomTargetIndex = occupiedTiles[Math.floor(Math.random() * occupiedTiles.length)];
const targetPos = this.indexToCoords(randomTargetIndex);
console.log(`🚀 Rakete fliegt zu Position [${targetPos.row}, ${targetPos.col}]`);
// Zeige Flug-Animation
await this.showRocketFlightAnimation(randomTargetIndex);
// Entferne das Ziel-Tile nach der Animation
this.board[randomTargetIndex] = null;
console.log(`🚀 Ziel-Tile an Position ${randomTargetIndex} entfernt`);
// Aktualisiere die Anzeige
this.$forceUpdate();
}
},
// Zeige Raketen-Flug-Animation
async showRocketFlightAnimation(targetIndex) {
const targetPos = this.indexToCoords(targetIndex);
const targetElement = document.querySelector(`[data-index="${targetIndex}"]`);
if (targetElement) {
const rect = targetElement.getBoundingClientRect();
this.rocketTarget = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
}
// Zeige Flug-Animation
console.log(`🚀 Rakete fliegt zu Ziel...`);
// Zeige die fliegende Rakete
this.showFlyingRocket = true;
// Berechne die Flug-Distanz
const flyX = this.rocketTarget.x - this.rocketCenter.x;
const flyY = this.rocketTarget.y - this.rocketCenter.y;
// Setze CSS-Variablen für die Flug-Animation
const flyingRocketElement = document.querySelector('.flying-rocket');
if (flyingRocketElement) {
flyingRocketElement.style.setProperty('--fly-x', `${flyX}px`);
flyingRocketElement.style.setProperty('--fly-y', `${flyY}px`);
}
// Warte auf Flug-Animation (1 Sekunde)
await this.wait(1000);
// Verstecke die fliegende Rakete
this.showFlyingRocket = false;
console.log(`🚀 Rakete erreicht Ziel und zerstört es`);
// Aktualisiere die Anzeige
this.$forceUpdate();
},
// Hilfsmethode: Warte für eine bestimmte Zeit
wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
// Hilfsmethode: Prüfe ob zwei Tiles benachbart sind
areTilesAdjacent(index1, index2) {
const pos1 = this.indexToCoords(index1);
const pos2 = this.indexToCoords(index2);
// Prüfe horizontale Nachbarschaft
if (pos1.row === pos2.row && Math.abs(pos1.col - pos2.col) === 1) {
return true;
}
// Prüfe vertikale Nachbarschaft
if (pos1.col === pos2.col && Math.abs(pos1.row - pos2.row) === 1) {
return true;
}
return false;
},
// Markiere alle benachbarten Tiles als verfügbar für den Move
highlightAdjacentTiles(tileIndex) {
// Entferne zuerst alle bestehenden Hervorhebungen
this.clearAllHighlights();
// Finde alle benachbarten Tiles
const adjacentTiles = [];
const currentPos = this.indexToCoords(tileIndex);
console.log(`🔧 Position: [${currentPos.row}, ${currentPos.col}]`);
// Prüfe alle 4 Richtungen: oben, unten, links, rechts
const directions = [
{ row: -1, col: 0 }, // oben
{ row: 1, col: 0 }, // unten
{ row: 0, col: -1 }, // links
{ row: 0, col: 1 } // rechts
];
directions.forEach(dir => {
const newRow = currentPos.row + dir.row;
const newCol = currentPos.col + dir.col;
if (this.isValidPosition(newRow, newCol)) {
const newIndex = this.coordsToIndex(newRow, newCol);
if (newIndex !== null && this.board[newIndex]) {
adjacentTiles.push(newIndex);
console.log(`🔧 Benachbart: [${newRow}, ${newCol}] -> Index ${newIndex}`);
}
}
});
console.log(`🔧 ${adjacentTiles.length} benachbarte Tiles gefunden`);
// Markiere alle benachbarten Tiles
adjacentTiles.forEach(index => {
const tileElement = document.querySelector(`[data-index="${index}"]`);
if (tileElement) {
tileElement.classList.add('adjacent-available');
console.log(`🔧 CSS-Klasse zu Tile ${index} hinzugefügt`);
// Test: Überprüfe ob die Klasse tatsächlich angewendet wurde
setTimeout(() => {
if (tileElement.classList.contains('adjacent-available')) {
console.log(`✅ CSS-Klasse 'adjacent-available' ist auf Tile ${index} aktiv`);
} else {
console.warn(`❌ CSS-Klasse 'adjacent-available' ist NICHT auf Tile ${index} aktiv`);
}
}, 100);
} else {
console.warn(`⚠️ DOM-Element für Tile ${index} nicht gefunden`);
}
});
// Speichere die benachbarten Tiles für Hover-Events
this.adjacentTilesForHover = adjacentTiles;
},
// Entferne alle Hervorhebungen und CSS-Klassen
clearAllHighlights() {
// Entferne dragging-Klasse vom gedraggten Tile
if (this.draggedTileIndex !== null) {
const draggedElement = document.querySelector(`[data-index="${this.draggedTileIndex}"]`);
if (draggedElement) {
draggedElement.classList.remove('dragging');
}
}
// Entferne alle adjacent-available Klassen
const adjacentElements = document.querySelectorAll('.adjacent-available');
adjacentElements.forEach(element => {
element.classList.remove('adjacent-available');
});
// Entferne alle drag-hover Klassen
const hoverElements = document.querySelectorAll('.drag-hover');
hoverElements.forEach(element => {
element.classList.remove('drag-hover');
});
// Leere die Liste der benachbarten Tiles
this.adjacentTilesForHover = [];
},
// Drag & Drop Event-Handler
onTileMouseDown(event, tileIndex) {
console.log(`🔧 onTileMouseDown aufgerufen für Tile ${tileIndex}`);
// Verhindere Standard-Verhalten
event.preventDefault();
event.stopPropagation();
// Prüfe ob das Tile existiert
if (!this.board[tileIndex]) {
console.warn(`⚠️ Tile ${tileIndex} existiert nicht im Board`);
return;
}
// Prüfe ob bereits ein Drag läuft
if (this.isDragging) {
console.log(`🔧 Drag läuft bereits, ignoriere neuen Start`);
return;
}
console.log(`🔧 Starte Drag für Tile ${tileIndex}`);
// Setze Drag-Status
this.draggedTileIndex = tileIndex;
this.isDragging = true;
// Speichere Start-Position für Drag-Offset
this.dragStartX = event.clientX;
this.dragStartY = event.clientY;
// WICHTIG: Speichere die ursprüngliche Position des gedraggten Tiles
const tileElement = event.target.closest('.game-tile');
if (tileElement) {
const rect = tileElement.getBoundingClientRect();
this.originalTilePosition = {
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height
};
console.log(`🔧 Ursprüngliche Tile-Position gespeichert: [${rect.left}, ${rect.top}] mit Größe [${rect.width}x${rect.height}]`);
// Füge CSS-Klassen für visuelles Feedback hinzu
tileElement.classList.add('dragging');
console.log(`🔧 'dragging' Klasse zu Tile ${tileIndex} hinzugefügt`);
} else {
console.warn(`⚠️ Konnte .game-tile Element für Tile ${tileIndex} nicht finden`);
}
console.log(`🔧 Drag-Start-Position: dragStartX=${this.dragStartX}, dragStartY=${this.dragStartY}`);
// Markiere alle benachbarten Tiles als verfügbar für den Move
this.highlightAdjacentTiles(tileIndex);
// WICHTIG: Entferne alle falschen drag-hover Klassen von anderen Tiles
this.removeFalseDragHoverClasses();
console.log(`🔧 Drag-Status: isDragging=${this.isDragging}, draggedTileIndex=${this.draggedTileIndex}`);
// DEBUG: Zeige alle Tiles mit CSS-Klassen nach dem Drag-Start
this.debugAllTileClasses();
},
// Entferne alle falschen drag-hover Klassen von nicht-benachbarten Tiles
removeFalseDragHoverClasses() {
if (!this.adjacentTilesForHover || this.adjacentTilesForHover.length === 0) {
return;
}
console.log(`🔧 Entferne falsche drag-hover Klassen...`);
// Finde alle Tiles mit drag-hover Klasse
const allHoverElements = document.querySelectorAll('.drag-hover');
allHoverElements.forEach(element => {
const dataIndex = element.getAttribute('data-index');
if (dataIndex !== null) {
const tileIndex = parseInt(dataIndex);
// Prüfe ob das Tile benachbart ist
if (!this.adjacentTilesForHover.includes(tileIndex)) {
element.classList.remove('drag-hover');
console.log(`🔧 Entferne falsche drag-hover Klasse von Tile ${tileIndex}`);
}
}
});
},
// Neue Methode: Tile-Moving mit Animation
onTileMouseMove(event) {
if (!this.isDragging || this.draggedTileIndex === null) {
return;
}
console.log(`🔧 onTileMouseMove: clientX=${event.clientX}, clientY=${event.clientY}`);
// Berechne Drag-Offset
const deltaX = event.clientX - this.dragStartX;
const deltaY = event.clientY - this.dragStartY;
console.log(`🔧 Drag-Offset: deltaX=${deltaX}px, deltaY=${deltaY}px`);
// Bewege das gedraggte Tile
const draggedElement = document.querySelector(`[data-index="${this.draggedTileIndex}"]`);
if (draggedElement) {
console.log(`🔧 Bewege Tile ${this.draggedTileIndex} um ${deltaX}px, ${deltaY}px`);
// Setze alle Styles direkt
draggedElement.style.position = 'relative';
draggedElement.style.left = `${deltaX}px`;
draggedElement.style.top = `${deltaY}px`;
draggedElement.style.zIndex = '1000';
draggedElement.style.pointerEvents = 'none';
console.log(`🔧 Styles gesetzt: position=${draggedElement.style.position}, left=${draggedElement.style.left}, top=${draggedElement.style.top}`);
} else {
console.warn(`⚠️ Konnte gedraggtes Tile ${this.draggedTileIndex} nicht finden`);
}
// Prüfe Überlappung mit benachbarten Tiles
this.checkTileOverlap(deltaX, deltaY);
},
// Prüfe Überlappung und starte Animation
checkTileOverlap(deltaX, deltaY) {
const draggedElement = document.querySelector(`[data-index="${this.draggedTileIndex}"]`);
if (!draggedElement) return;
const draggedRect = draggedElement.getBoundingClientRect();
// WICHTIG: Prüfe zuerst die Überlappung mit der ursprünglichen Position des gedraggten Tiles
if (this.originalTilePosition) {
// Berechne die ursprüngliche Position des gedraggten Tiles
const originalRect = {
left: this.originalTilePosition.left,
top: this.originalTilePosition.top,
right: this.originalTilePosition.left + draggedRect.width,
bottom: this.originalTilePosition.top + draggedRect.height
};
// Berechne Überlappung mit der ursprünglichen Position
const overlapX = Math.min(draggedRect.right, originalRect.right) - Math.max(draggedRect.left, originalRect.left);
const overlapY = Math.min(draggedRect.bottom, originalRect.bottom) - Math.max(draggedRect.top, originalRect.top);
if (overlapX > 0 && overlapY > 0) {
const overlapArea = overlapX * overlapY;
const draggedArea = draggedRect.width * draggedRect.height;
const overlapWithOriginal = (overlapArea / draggedArea) * 100;
console.log(`🔧 Überlappung mit ursprünglicher Position: ${overlapWithOriginal.toFixed(1)}%`);
// Animation stoppen nur wenn das gedraggte Tile fast komplett zurück zu seiner ursprünglichen Position ist
// UND eine Animation läuft
if (overlapWithOriginal > 98 && this.currentlyAnimatingTile !== null) {
console.log(`🔧 Gedraggtes Tile ist fast zurück zu seiner ursprünglichen Position (${overlapWithOriginal.toFixed(1)}%), stoppe Animation für Tile ${this.currentlyAnimatingTile}`);
this.stopMovePreviewAnimation(this.currentlyAnimatingTile);
this.currentlyAnimatingTile = null;
return; // Keine weiteren Animationen starten
}
}
}
// Sammle alle Überlappungen mit benachbarten Tiles über 10%
const overlappingTiles = [];
this.adjacentTilesForHover.forEach(adjacentIndex => {
const adjacentElement = document.querySelector(`[data-index="${adjacentIndex}"]`);
if (!adjacentElement) return;
const adjacentRect = adjacentElement.getBoundingClientRect();
// Berechne Überlappung
const overlapX = Math.min(draggedRect.right, adjacentRect.right) - Math.max(draggedRect.left, adjacentRect.left);
const overlapY = Math.min(draggedRect.bottom, adjacentRect.bottom) - Math.max(draggedRect.top, adjacentRect.top);
if (overlapX > 0 && overlapY > 0) {
const overlapArea = overlapX * overlapY;
const draggedArea = draggedRect.width * draggedRect.height;
const overlapPercentage = (overlapArea / draggedArea) * 100;
console.log(`🔧 Überlappung mit Tile ${adjacentIndex}: ${overlapPercentage.toFixed(1)}%`);
if (overlapPercentage > 10) {
overlappingTiles.push({
index: adjacentIndex,
percentage: overlapPercentage
});
}
}
});
// Sortiere nach Überlappungs-Prozentsatz (höchste zuerst)
overlappingTiles.sort((a, b) => b.percentage - a.percentage);
if (overlappingTiles.length > 0) {
const bestTile = overlappingTiles[0];
console.log(`🔧 Beste Überlappung: Tile ${bestTile.index} mit ${bestTile.percentage.toFixed(1)}%`);
// Wenn ein anderes Tile animiert ist, stoppe es
if (this.currentlyAnimatingTile !== null && this.currentlyAnimatingTile !== bestTile.index) {
console.log(`🔧 Stoppe vorheriges animiertes Tile ${this.currentlyAnimatingTile} für bessere Überlappung`);
this.stopMovePreviewAnimation(this.currentlyAnimatingTile);
}
// Starte Animation für das beste Tile
if (this.currentlyAnimatingTile === null) {
this.currentlyAnimatingTile = bestTile.index;
this.startMovePreviewAnimation(bestTile.index);
}
} else {
// Keine Überlappung über 10% - stoppe alle Animationen
if (this.currentlyAnimatingTile !== null) {
console.log(`🔧 Keine Überlappung über 10%, stoppe Animation für Tile ${this.currentlyAnimatingTile}`);
this.stopMovePreviewAnimation(this.currentlyAnimatingTile);
this.currentlyAnimatingTile = null;
}
}
},
// Starte Move-Preview Animation
startMovePreviewAnimation(adjacentIndex) {
const adjacentElement = document.querySelector(`[data-index="${adjacentIndex}"]`);
if (!adjacentElement) return;
// WICHTIG: Verwende die gespeicherte ursprüngliche Position des gedraggten Tiles
if (!this.originalTilePosition) {
console.warn(`⚠️ Keine ursprüngliche Tile-Position gespeichert`);
return;
}
const adjacentRect = adjacentElement.getBoundingClientRect();
// Berechne den Offset zur ursprünglichen Position des gedraggten Tiles
const deltaX = this.originalTilePosition.left - adjacentRect.left;
const deltaY = this.originalTilePosition.top - adjacentRect.top;
console.log(`🎬 Starte Animation für Tile ${adjacentIndex}:`);
console.log(` - Ursprüngliche Position gedraggtes Tile: [${this.originalTilePosition.left}, ${this.originalTilePosition.top}]`);
console.log(` - Aktuelle Position benachbartes Tile: [${adjacentRect.left}, ${adjacentRect.top}]`);
console.log(` - Delta zur ursprünglichen Position: [${deltaX}px, ${deltaY}px]`);
// Verwende transform direkt mit !important über setProperty
adjacentElement.style.setProperty('transform', `translate(${deltaX}px, ${deltaY}px)`, 'important');
adjacentElement.style.zIndex = '999';
adjacentElement.style.transition = 'all 0.3s ease-out';
// Füge eine CSS-Klasse hinzu um den transform zu priorisieren
adjacentElement.classList.add('preview-animating');
console.log(`🎬 Styles gesetzt für Tile ${adjacentIndex}: transform=${adjacentElement.style.transform}`);
},
// Stoppe Move-Preview Animation
stopMovePreviewAnimation(adjacentIndex) {
const adjacentElement = document.querySelector(`[data-index="${adjacentIndex}"]`);
if (!adjacentElement) return;
console.log(`⏹️ Stoppe Animation für Tile ${adjacentIndex}`);
// Bewege das Tile zurück zur ursprünglichen Position
adjacentElement.style.transition = 'all 0.3s ease-out';
adjacentElement.style.setProperty('transform', 'translate(0px, 0px)', 'important');
adjacentElement.style.zIndex = 'auto';
// Warte kurz und setze dann alle Styles komplett zurück
setTimeout(() => {
adjacentElement.style.removeProperty('transform');
adjacentElement.style.transition = '';
adjacentElement.classList.remove('preview-animating');
}, 300);
console.log(`⏹️ Animation gestoppt für Tile ${adjacentIndex}`);
},
// Hilfsmethode: Prüfe ob ein Tile gerade animiert wird
isTileAnimating(tileIndex) {
// Verwende den gespeicherten Index statt der DOM-Styles
return this.currentlyAnimatingTile === tileIndex;
},
// Setze alle Tile-Animationen zurück
resetAllTileAnimations() {
// Setze das gedraggte Tile zurück
const draggedElement = document.querySelector(`[data-index="${this.draggedTileIndex}"]`);
if (draggedElement) {
console.log(`🔄 Setze gedraggtes Tile ${this.draggedTileIndex} zurück`);
// Setze alle Styles zurück
draggedElement.style.position = '';
draggedElement.style.left = '';
draggedElement.style.top = '';
draggedElement.style.zIndex = '';
draggedElement.style.pointerEvents = '';
draggedElement.style.transition = 'all 0.3s ease-out';
// Warte kurz und setze dann alle Styles komplett zurück
setTimeout(() => {
draggedElement.style.transition = '';
}, 300);
}
// Setze alle benachbarten Tiles zurück
this.adjacentTilesForHover.forEach(adjacentIndex => {
this.stopMovePreviewAnimation(adjacentIndex);
});
// Zusätzlich: Setze alle Tiles zurück, die möglicherweise noch transform haben
document.querySelectorAll('.game-tile').forEach(tileElement => {
if (tileElement.style.transform && tileElement.style.transform !== '') {
console.log(`🔄 Setze transform für Tile zurück: ${tileElement.dataset.index}`);
tileElement.style.removeProperty('transform');
tileElement.style.transition = '';
tileElement.style.zIndex = '';
tileElement.classList.remove('preview-animating');
}
});
// Setze den aktuell animierten Tile zurück
this.currentlyAnimatingTile = null;
console.log(`🔄 Alle Tile-Animationen zurückgesetzt`);
},
// Globaler Mouse-Move Handler für kontinuierliches Dragging
onGlobalMouseMove(event) {
if (this.isDragging) {
this.onTileMouseMove(event);
}
},
// Globaler Mouse-Up Handler für Drag & Drop
onGlobalMouseUp(event) {
if (this.isDragging) {
console.log(`🔧 Globaler MouseUp während Drag - beende Drag`);
this.endDrag();
}
},
onTileMouseUp(event, tileIndex) {
console.log(`🔧 onTileMouseUp aufgerufen für Tile ${tileIndex}`);
// Verhindere Standard-Verhalten
event.preventDefault();
event.stopPropagation();
// Prüfe ob ein Drag läuft
if (!this.isDragging || this.draggedTileIndex === null) {
console.log(`🔧 Kein aktiver Drag, ignoriere MouseUp`);
return;
}
console.log(`🔧 Beende Drag: draggedTileIndex=${this.draggedTileIndex}, targetTile=${tileIndex}`);
// WICHTIG: Prüfe ob ein Tile tatsächlich animiert wurde
let shouldPerformMove = false;
let targetTileIndex = null;
if (this.draggedTileIndex !== tileIndex) {
// Verschiedene Tiles - prüfe ob das Ziel-Tile animiert wurde
if (this.currentlyAnimatingTile === tileIndex) {
// Das Ziel-Tile ist animiert - führe Move durch
shouldPerformMove = true;
targetTileIndex = tileIndex;
console.log(`🔧 Ziel-Tile ${tileIndex} ist animiert - führe Move durch`);
} else {
// Das Ziel-Tile ist nicht animiert - kein Move
console.log(`🔧 Ziel-Tile ${tileIndex} ist nicht animiert - kein Move`);
}
} else {
console.log(`🔧 Gleiches Tile, kein Move erforderlich`);
}
// Setze alle Animationen zurück
this.resetAllTileAnimations();
// Führe den Move durch, wenn erlaubt
if (shouldPerformMove && targetTileIndex !== null) {
console.log(`🔧 Führe Move durch: ${this.draggedTileIndex}${targetTileIndex}`);
this.performMove(this.draggedTileIndex, targetTileIndex);
}
// Cleanup
this.isDragging = false;
this.draggedTileIndex = null;
this.dragStartX = null;
this.dragStartY = null;
this.originalTilePosition = null;
this.currentlyAnimatingTile = null;
// Entferne alle CSS-Klassen und Hervorhebungen
this.clearAllHighlights();
console.log(`🔧 Drag beendet, Status zurückgesetzt`);
},
// Beende den Drag korrekt
endDrag() {
console.log(`🔧 endDrag aufgerufen`);
if (!this.isDragging) {
console.log(`🔧 Kein aktiver Drag, ignoriere endDrag`);
return;
}
// Prüfe ob ein Tile tatsächlich animiert wurde UND ob es sich um ein anderes Tile handelt
let shouldPerformMove = false;
let targetTileIndex = null;
if (this.currentlyAnimatingTile !== null && this.currentlyAnimatingTile !== this.draggedTileIndex) {
// Ein anderes Tile ist animiert - führe Move durch
shouldPerformMove = true;
targetTileIndex = this.currentlyAnimatingTile;
console.log(`🔧 Anderes Tile ${targetTileIndex} ist animiert - führe Move durch`);
} else if (this.currentlyAnimatingTile === this.draggedTileIndex) {
// Das gedraggte Tile ist auf sich selbst animiert - kein Move
console.log(`🔧 Gedraggtes Tile ist auf sich selbst animiert - kein Move`);
} else {
console.log(`🔧 Kein Tile animiert - kein Move`);
}
// Zusätzliche Prüfung: Wenn das gedraggte Tile fast an seiner ursprünglichen Position ist, kein Move
if (shouldPerformMove && this.originalTilePosition) {
const draggedElement = document.querySelector(`[data-index="${this.draggedTileIndex}"]`);
if (draggedElement) {
const currentRect = draggedElement.getBoundingClientRect();
const deltaX = Math.abs(currentRect.left - this.originalTilePosition.left);
const deltaY = Math.abs(currentRect.top - this.originalTilePosition.top);
// Wenn das gedraggte Tile weniger als 20px von seiner ursprünglichen Position entfernt ist, kein Move
if (deltaX < 20 && deltaY < 20) {
console.log(`🔧 Gedraggtes Tile ist zu nah an seiner ursprünglichen Position (${deltaX}px, ${deltaY}px) - kein Move`);
shouldPerformMove = false;
targetTileIndex = null;
}
}
}
// Setze alle Animationen zurück
this.resetAllTileAnimations();
// Führe den Move durch, wenn erlaubt
if (shouldPerformMove && targetTileIndex !== null) {
console.log(`🔧 Führe Move durch: ${this.draggedTileIndex}${targetTileIndex}`);
this.performMove(this.draggedTileIndex, targetTileIndex);
}
// Cleanup
this.isDragging = false;
this.draggedTileIndex = null;
this.dragStartX = null;
this.dragStartY = null;
this.originalTilePosition = null;
this.currentlyAnimatingTile = null;
// Entferne alle CSS-Klassen und Hervorhebungen
this.clearAllHighlights();
console.log(`🔧 Drag beendet, Status zurückgesetzt`);
},
// Debug-Methode: Zeige aktuellen Drag-Status
debugDragStatus() {
console.log('🔍 Aktueller Drag-Status:');
console.log(` - isDragging: ${this.isDragging}`);
console.log(` - draggedTileIndex: ${this.draggedTileIndex}`);
console.log(` - adjacentTilesForHover: [${this.adjacentTilesForHover.join(', ')}]`);
// Zeige alle aktiven CSS-Klassen
const draggingElements = document.querySelectorAll('.dragging');
const adjacentElements = document.querySelectorAll('.adjacent-available');
const hoverElements = document.querySelectorAll('.drag-hover');
console.log(` - .dragging Elemente: ${draggingElements.length}`);
console.log(` - .adjacent-available Elemente: ${adjacentElements.length}`);
console.log(` - .drag-hover Elemente: ${hoverElements.length}`);
},
// DEBUG: Zeige alle Tiles mit CSS-Klassen nach dem Drag-Start
debugAllTileClasses() {
console.log('🔍 DEBUG: Alle Tiles mit CSS-Klassen nach Drag-Start:');
// Durchsuche alle Tiles im Board
for (let i = 0; i < this.board.length; i++) {
if (this.board[i]) { // Nur Tiles die existieren
const tileElement = document.querySelector(`[data-index="${i}"]`);
if (tileElement) {
const hasDragging = tileElement.classList.contains('dragging');
const hasAdjacent = tileElement.classList.contains('adjacent-available');
const hasHover = tileElement.classList.contains('drag-hover');
if (hasDragging || hasAdjacent || hasHover) {
console.log(` Tile ${i}: .dragging=${hasDragging}, .adjacent-available=${hasAdjacent}, .drag-hover=${hasHover}`);
}
}
}
}
// Zeige auch alle Tiles mit .adjacent-available Klasse
const adjacentElements = document.querySelectorAll('.adjacent-available');
if (adjacentElements.length > 0) {
console.log(`🔍 Tiles mit .adjacent-available Klasse:`);
adjacentElements.forEach(element => {
const index = element.getAttribute('data-index');
console.log(` - Tile ${index}`);
});
} else {
console.log(`❌ Keine Tiles mit .adjacent-available Klasse gefunden!`);
}
},
onTileMouseEnter(event, tileIndex) {
// Debug: Zeige alle Event-Parameter
console.log(`🔧 onTileMouseEnter: Tile ${tileIndex}, isDragging: ${this.isDragging}, draggedTileIndex: ${this.draggedTileIndex}`);
if (!this.isDragging || this.draggedTileIndex === null) {
console.log(`🔧 Kein aktiver Drag, ignoriere MouseEnter`);
return;
}
// Prüfe ob das Tile in der Liste der benachbarten Tiles ist
if (this.adjacentTilesForHover && this.adjacentTilesForHover.includes(tileIndex)) {
// Füge Hover-Effekt nur für benachbarte Tiles hinzu
const tileElement = event.target.closest('.game-tile');
if (tileElement) {
tileElement.classList.add('drag-hover');
console.log(`✅ Hover über benachbartem Tile ${tileIndex} - drag-hover Klasse hinzugefügt`);
}
} else {
console.log(`❌ Hover über NICHT-benachbartem Tile ${tileIndex} - wird ignoriert`);
// WICHTIG: Entferne drag-hover Klasse falls sie fälschlicherweise gesetzt wurde
const tileElement = event.target.closest('.game-tile');
if (tileElement && tileElement.classList.contains('drag-hover')) {
tileElement.classList.remove('drag-hover');
console.log(`🔧 Entferne fälschliche drag-hover Klasse von Tile ${tileIndex}`);
}
}
},
onTileMouseLeave(event, tileIndex) {
// Entferne Hover-Effekt
const tileElement = event.target.closest('.game-tile');
if (tileElement) {
tileElement.classList.remove('drag-hover');
}
},
checkObjectives() {
if (!this.currentLevelData) return;
// Punkte-Objective
const scoreObjective = this.currentLevelData.objectives.find(o => o.description.includes('Punkte'));
if (scoreObjective && !scoreObjective.completed) {
scoreObjective.completed = this.levelScore >= scoreObjective.target;
}
// Matches-Objective
const matchesObjective = this.currentLevelData.objectives.find(o => o.description.includes('Matches'));
if (matchesObjective && !matchesObjective.completed) {
// TODO: Match-Zähler implementieren
matchesObjective.completed = this.moves >= matchesObjective.target;
}
// Züge-Objective
const movesObjective = this.currentLevelData.objectives.find(o => o.description.includes('Züge'));
if (movesObjective && !movesObjective.completed) {
movesObjective.completed = this.moves <= movesObjective.target;
}
},
checkLevelComplete() {
if (!this.currentLevelData) return;
const allObjectivesCompleted = this.currentLevelData.objectives.every(o => o.completed);
if (allObjectivesCompleted) {
this.calculateLevelStars();
this.gameActive = false;
this.showLevelComplete = true;
// Speichere Fortschritt im Backend
this.saveProgressToBackend();
}
},
// Generiere einen sicheren Hash für Anti-Cheat-Schutz
generateProgressHash(progressData) {
// Erstelle einen String mit allen relevanten Daten
const dataString = `${progressData.levelId}|${progressData.score}|${progressData.moves}|${progressData.stars}|${progressData.isCompleted}|${this.currentLevelData.boardLayout}|${this.currentLevelData.moveLimit}`;
// Verwende eine einfache Hash-Funktion (in Produktion sollte ein stärkerer Hash verwendet werden)
let hash = 0;
for (let i = 0; i < dataString.length; i++) {
const char = dataString.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Konvertiere zu 32-bit Integer
}
// Füge einen Salt hinzu (in Produktion sollte dieser geheim gehalten werden)
const salt = 'YourPart3_Match3_Security_2024';
const saltedString = `${dataString}|${salt}`;
let saltedHash = 0;
for (let i = 0; i < saltedString.length; i++) {
const char = saltedString.charCodeAt(i);
saltedHash = ((saltedHash << 5) - hash) + char;
saltedHash = saltedHash & saltedHash;
}
return Math.abs(saltedHash).toString(16);
},
saveProgressToBackend() {
// Prüfe ob Benutzer eingeloggt ist
if (!this.$store.getters.isLoggedIn || !this.$store.getters.user) {
return; // Kein Fortschritt speichern wenn nicht eingeloggt
}
// WICHTIG: Speichere den Fortschritt nur wenn das Level abgeschlossen ist
// Ein Level ist nur abgeschlossen, wenn es mindestens 1 Stern gibt UND der Score > 0 ist
if (this.levelStars === 0 || this.levelScore === 0) {
return; // Kein Fortschritt speichern wenn das Level nicht abgeschlossen ist
}
// Zusätzliche Sicherheit: Prüfe ob das Level wirklich abgeschlossen ist
if (!this.gameActive || this.showLevelComplete) {
} else {
console.warn('WARNUNG: Versuche Fortschritt zu speichern, obwohl Level noch aktiv ist!');
return; // Kein Fortschritt speichern wenn das Level noch aktiv ist
}
const progressData = {
levelId: this.currentLevel,
score: this.levelScore,
moves: this.moves,
time: 0, // TODO: Zeit implementieren
stars: this.levelStars,
isCompleted: true,
totalScore: this.score,
totalStars: this.stars
// WICHTIG: levelsCompleted wird vom Backend berechnet, nicht vom Frontend gesendet
};
// Generiere Anti-Cheat-Hash
const securityHash = this.generateProgressHash(progressData);
// Füge Hash zu den Daten hinzu
const secureProgressData = {
...progressData,
securityHash: securityHash,
timestamp: Date.now()
};
apiClient.post(`/api/match3/campaigns/1/levels/${this.currentLevel}/progress`, secureProgressData)
.then(response => {
if (response.data.success) {
// Fortschritt erfolgreich gespeichert
}
})
.catch(error => {
console.error('Fehler beim Speichern des Fortschritts:', error);
});
},
calculateLevelStars() {
let starCount = 0;
// 1 Stern für Level-Abschluss
starCount++;
// 1 Stern für gute Punktzahl
if (this.levelScore >= this.currentLevelData.objectives[0].target * 1.5) {
starCount++;
}
// 1 Stern für effiziente Züge
const movesObjective = this.currentLevelData.objectives.find(o => o.description.includes('Züge'));
if (movesObjective && this.moves <= movesObjective.target * 0.8) {
starCount++;
}
this.levelStars = starCount;
this.stars += starCount;
},
async nextLevel() {
this.showLevelComplete = false;
// WICHTIG: completedLevels wird vom Backend berechnet, nicht hier erhöht
this.currentLevel++;
// Prüfe ob das nächste Level existiert
if (this.campaignData && this.campaignData.levels) {
const nextLevelExists = this.campaignData.levels.some(l => l.order === this.currentLevel);
if (!nextLevelExists) {
this.showCampaignComplete = true;
return;
}
}
// Speichere den Fortschritt nur wenn das Level abgeschlossen ist
if (this.levelStars > 0) {
this.saveProgressToBackend();
}
// Alle Level-spezifischen Eigenschaften zurücksetzen
this.levelScore = 0;
this.levelStars = 0;
this.moves = 0;
this.matchesMade = 0;
this.cascadeRound = 0;
// WICHTIG: Alle Arrays zurücksetzen
this.matchedTiles = [];
this.fallingTiles = [];
this.newTiles = [];
this.isFalling = false;
// WICHTIG: Lade nur die spezifischen Level-Daten
try {
await this.loadLevelData(this.currentLevel);
// WICHTIG: Warte bis das Level vollständig initialisiert ist
await this.$nextTick();
// initializeLevel wird bereits in loadLevelData aufgerufen
} catch (error) {
console.error('❌ Fehler beim Neustarten des Levels:', error);
this.handleLoadError('Level konnte nicht neu gestartet werden', error);
}
},
async restartLevel() {
this.showLevelComplete = false;
this.showPause = false;
// Alle Level-spezifischen Eigenschaften zurücksetzen
this.levelScore = 0;
this.levelStars = 0;
this.moves = 0;
this.matchesMade = 0;
this.cascadeRound = 0;
// WICHTIG: Alle Arrays zurücksetzen
this.matchedTiles = [];
this.fallingTiles = [];
this.newTiles = [];
this.isFalling = false;
// WICHTIG: Lade nur die spezifischen Level-Daten
try {
await this.loadLevelData(this.currentLevel);
// WICHTIG: Warte bis das Level vollständig initialisiert ist
await this.$nextTick();
// initializeLevel wird bereits in loadLevelData aufgerufen
} catch (error) {
console.error('❌ Fehler beim Neustarten des Levels:', error);
this.handleLoadError('Level konnte nicht neu gestartet werden', error);
}
// Zusätzliche Prüfung: Stelle sicher, dass das neue Board keine initialen Matches hat
setTimeout(() => {
if (this.board && this.board.length > 0) {
const initialMatches = this.findMatchesOnBoard(this.board, false); // Reduziere Debug-Ausgaben
if (initialMatches.length > 0) {
this.fixInitialMatches();
}
}
}, 100);
},
async restartCampaign() {
this.showCampaignComplete = false;
// Speichere den aktuellen Fortschritt nur wenn das Level abgeschlossen ist
if (this.levelStars > 0) {
this.saveProgressToBackend();
}
this.currentLevel = 1;
this.score = 0;
this.stars = 0;
this.completedLevels = 0;
// WICHTIG: Alle Arrays zurücksetzen
this.matchedTiles = [];
this.fallingTiles = [];
this.newTiles = [];
this.isFalling = false;
this.cascadeRound = 0;
// WICHTIG: Lade Kampagnendaten für Kampagnen-Restart
try {
await this.loadCampaignData();
// WICHTIG: Warte bis das Level vollständig initialisiert ist
await this.$nextTick();
// initializeLevel wird bereits in loadCampaignData aufgerufen
} catch (error) {
console.error('❌ Fehler beim Neustarten der Kampagne:', error);
this.handleLoadError('Kampagne konnte nicht neu gestartet werden', error);
}
// Zusätzliche Prüfung: Stelle sicher, dass das neue Board keine initialen Matches hat
setTimeout(() => {
if (this.board && this.board.length > 0) {
const initialMatches = this.findMatchesOnBoard(this.board, false); // Reduziere Debug-Ausgaben
if (initialMatches.length > 0) {
this.fixInitialMatches();
}
}
}, 100);
},
pauseGame() {
this.gameActive = false;
this.showPause = true;
},
resumeGame() {
this.gameActive = true;
this.showPause = false;
},
toggleStats() {
this.statsExpanded = !this.statsExpanded;
},
toggleLevelDescription() {
this.levelDescriptionExpanded = !this.levelDescriptionExpanded;
},
getTileColor(type) {
const colors = {
gem: '#FF6B6B',
star: '#FFD93D',
heart: '#FF8E8E',
diamond: '#4ECDC4',
circle: '#95E1D3',
square: '#A8E6CF'
};
return colors[type] || '#666';
},
getTileIcon(type) {
const icons = {
gem: 'mdi-diamond-stone',
star: 'mdi-star',
heart: 'mdi-heart',
diamond: 'mdi-diamond',
circle: 'mdi-circle',
square: 'mdi-square'
};
return icons[type] || 'mdi-help';
},
getTileSymbol(type) {
const symbols = {
gem: '💎',
star: '⭐',
heart: '❤️',
diamond: '🔷',
circle: '⭕',
square: '🟦',
crown: '👑',
rainbow: '🌈',
// Spezielle Items
rocket: '🚀',
'rocket-horizontal': '🚀',
'rocket-vertical': '🚀',
bomb: '💣'
};
return symbols[type] || '❓';
},
// Neue Methode: Board-Layout parsen
parseBoardLayout(layoutString) {
if (!layoutString) return [];
// Teile den String in Zeilen auf
const rows = layoutString.split('\n').filter(row => row.trim().length > 0);
const layout = [];
for (let row = 0; row < rows.length; row++) {
const rowString = rows[row];
const rowArray = [];
for (let col = 0; col < rowString.length; col++) {
const char = rowString[col];
// Backend-Format: 'x' = Feld (tile), 'o' = kein Feld, andere = spezifischer Tile-Typ
// Fallback-Format: 't' = Feld (tile), 'f' = kein Feld
if (char === 'o' || char === 'f') {
rowArray.push({ type: 'empty', char: char });
} else if (char === 'x' || char === 't') {
rowArray.push({ type: 'tile', char: char });
} else {
// Spezifischer Tile-Typ
rowArray.push({ type: 'specific', char: char });
}
}
layout.push(rowArray);
}
return layout;
},
// Neue Methode: Board basierend auf Layout generieren
generateBoardFromLayout() {
const board = [];
let attempts = 0;
const maxAttempts = 100;
do {
board.length = 0; // Board leeren
// Gehe durch jede Zeile und Spalte des Layouts
for (let row = 0; row < this.boardHeight; row++) {
for (let col = 0; col < this.boardWidth; col++) {
// Prüfe ob an dieser Position ein Feld sein soll
if (this.boardLayout[row] && this.boardLayout[row][col]) {
const layoutCell = this.boardLayout[row][col];
if (layoutCell.type === 'empty') {
// Kein Feld - null hinzufügen
board.push(null);
} else if (layoutCell.type === 'tile') {
// Tile-Feld - zufälliger Tile-Typ (nur normale Tiles, keine Power-Ups)
const tileType = this.tileTypes[Math.floor(Math.random() * this.tileTypes.length)];
const tile = {
type: tileType,
id: Date.now() + row * this.boardWidth + col + Math.random(),
row: row,
col: col
};
board.push(tile);
} else if (layoutCell.type === 'specific') {
// Spezifischer Tile-Typ basierend auf dem Char
let tileType = 'gem'; // Standard
if (layoutCell.char === 'g') tileType = 'gem';
else if (layoutCell.char === 'c') tileType = 'crown';
else if (layoutCell.char === 'r') tileType = 'rainbow';
else {
// Fallback für unbekannte Zeichen - verwende zufälligen Tile-Typ
tileType = this.tileTypes[Math.floor(Math.random() * this.tileTypes.length)];
}
const tile = {
type: tileType,
id: Date.now() + row * this.boardWidth + col + Math.random(),
row: row,
col: col
};
board.push(tile);
}
} else {
// Kein Feld - null hinzufügen
board.push(null);
}
}
}
attempts++;
// Prüfe auf initiale Matches
const initialMatches = this.findMatchesOnBoard(board, false); // Reduziere Debug-Ausgaben
if (initialMatches.length === 0) {
break;
}
} while (attempts < maxAttempts);
return board;
},
// Hilfsmethode: Index zu Koordinaten konvertieren
indexToCoords(index) {
if (index === null || index < 0) return { row: -1, col: -1 };
const row = Math.floor(index / this.boardWidth);
const col = index % this.boardWidth;
return { row, col };
},
// Hilfsmethode: Koordinaten zu Index konvertieren
coordsToIndex(row, col) {
if (row < 0 || row >= this.boardHeight || col < 0 || col >= this.boardWidth) return null;
return row * this.boardWidth + col;
},
// Hilfsmethode: Prüfe ob Position im Layout gültig ist
isValidPosition(row, col) {
// Prüfe Board-Grenzen
if (row < 0 || row >= this.boardHeight || col < 0 || col >= this.boardWidth) {
return false;
}
// Prüfe Board-Layout
if (this.boardLayout && this.boardLayout[row] && this.boardLayout[row][col]) {
const layoutCell = this.boardLayout[row][col];
// Alle Positionen sind gültig, auch leere Felder werden gerendert
return true;
}
// Fallback: Alle Positionen innerhalb der Board-Grenzen sind gültig
return true;
},
// Hilfsmethode: Prüfe ob ein Tile ein Power-Up ist
isPowerUpTile(tile) {
return tile && this.powerUpTypes.includes(tile.type);
},
// Hilfsmethode: Prüfe ob zwei Tiles matchen können
canTilesMatch(tile1, tile2) {
// Power-Up Tiles können nicht mit anderen Tiles matchen (auch nicht mit sich selbst)
if (this.isPowerUpTile(tile1) || this.isPowerUpTile(tile2)) {
return false;
}
// Normale Tiles können nur mit dem gleichen Typ matchen
return tile1 && tile2 && tile1.type === tile2.type;
},
// Hilfsmethode: Zeige Fortschritt für Level-Objekte
getObjectiveProgress(objective) {
switch (objective.type) {
case 'score':
return `${this.levelScore}/${objective.target}`;
case 'matches':
return `${this.matchesMade}/${objective.target}`;
case 'moves':
return `${this.moves}/${objective.target}`;
default:
return 'N/A';
}
},
// Neue Methode: Erstelle spezielle Items basierend auf der Match-Anordnung
createSpecialItems(matches) {
const powerUpPositions = new Set(); // Sammle Positionen, wo Power-Ups erstellt werden
matches.forEach(match => {
if (match.length >= 4) {
// Mindestens 4 Felder gematcht - prüfe auf spezielle Anordnungen
const specialItem = this.detectSpecialItemPattern(match);
if (specialItem) {
// Erstelle das spezielle Item an der ersten Position des Matches
const centerIndex = match[Math.floor(match.length / 2)];
this.createSpecialItem(centerIndex, specialItem);
powerUpPositions.add(centerIndex);
}
}
});
// Entferne Power-Up Positionen aus den Matches, damit sie nicht als normale Matches behandelt werden
return powerUpPositions;
},
// Neue Methode: Erkenne spezielle Item-Muster
detectSpecialItemPattern(match) {
if (match.length < 4) return null;
// Konvertiere Match-Indizes zu Koordinaten
const coords = match.map(index => this.indexToCoords(index));
// WICHTIG: Prüfe zuerst die komplexesten Muster (höchste Priorität)
// UND stelle sicher, dass nur EIN Pattern erkannt wird
// Prüfe auf I-Form (Regenbogen) - höchste Priorität, mindestens 5 Tiles
if (coords.length >= 5 && this.isIRainbow(coords)) {
return 'rainbow';
}
// Prüfe auf L-Form (Bombe) - zweite Priorität, mindestens 5 Tiles
if (coords.length >= 5 && this.isLBomb(coords)) {
return 'bomb';
}
// Prüfe auf T-Form (Bombe) - dritte Priorität, mindestens 5 Tiles
if (coords.length >= 5 && this.isTBomb(coords)) {
return 'bomb';
}
// Prüfe auf Kreuz-Form (Bombe) - vierte Priorität, mindestens 5 Tiles
if (coords.length >= 5 && this.isCrossBomb(coords)) {
return 'bomb';
}
// Prüfe auf horizontale Anordnung (Rakete) - niedrigste Priorität, mindestens 4 Tiles
if (coords.length >= 4 && this.isHorizontalRocket(coords)) {
return 'rocket';
}
// Prüfe auf vertikale Anordnung (Rakete) - niedrigste Priorität, mindestens 4 Tiles
if (coords.length >= 4 && this.isVerticalRocket(coords)) {
return 'rocket';
}
return null;
},
// Hilfsmethode: Prüfe auf horizontale Rakete (4+ Felder in einer Reihe)
isHorizontalRocket(coords) {
if (coords.length < 4) return false;
// Alle Koordinaten müssen in der gleichen Zeile sein
const row = coords[0].row;
return coords.every(coord => coord.row === row);
},
// Hilfsmethode: Prüfe auf vertikale Rakete (4+ Felder in einer Spalte)
isVerticalRocket(coords) {
if (coords.length < 4) return false;
// Alle Koordinaten müssen in der gleichen Spalte sein
const col = coords[0].col;
return coords.every(coord => coord.col === col);
},
// Hilfsmethode: Prüfe auf L-Form Bombe
isLBomb(coords) {
// L-Form Bombe benötigt mindestens 5 Tiles
if (coords.length < 5) {
return false;
}
// Für 5+ Tiles: Suche nach L-Form mit langen Zeilen/Spalten
const rows = new Set(coords.map(c => c.row));
const cols = new Set(coords.map(c => c.col));
// L-Form: Mindestens 3 Zeilen UND 3 Spalten
if (rows.size >= 3 && cols.size >= 3) {
// Prüfe ob es eine echte L-Form gibt
return this.hasLShape(coords);
}
return false;
},
// Hilfsmethode: Prüfe auf T-Form Bombe
isTBomb(coords) {
if (coords.length < 5) return false; // Mindestens 5 Tiles für T-Form Bombe
// Suche nach T-Form: 3+ horizontal + 3+ vertikal
const rows = new Set(coords.map(c => c.row));
const cols = new Set(coords.map(c => c.col));
// Mindestens 3 Zeilen und 3 Spalten
if (rows.size >= 3 && cols.size >= 3) {
// Prüfe ob es eine T-Form gibt
return this.hasTShape(coords);
}
return false;
},
// Hilfsmethode: Prüfe auf Kreuz-Form Bombe
isCrossBomb(coords) {
if (coords.length < 5) return false; // Mindestens 5 Tiles für Kreuz-Form Bombe
// Suche nach Kreuz-Form: 3+ horizontal + 3+ vertikal
const rows = new Set(coords.map(c => c.row));
const cols = new Set(coords.map(c => c.col));
// Mindestens 3 Zeilen und 3 Spalten
if (rows.size >= 3 && cols.size >= 3) {
// Prüfe ob es eine Kreuz-Form gibt
return this.hasCrossShape(coords);
}
return false;
},
// Hilfsmethode: Prüfe auf I-Form Regenbogen (genau 5 Felder in einer Reihe/Spalte)
isIRainbow(coords) {
if (coords.length !== 5) return false; // Genau 5 Tiles für Regenbogen
// Alle Koordinaten müssen in der gleichen Zeile ODER Spalte sein
const row = coords[0].row;
const col = coords[0].col;
return coords.every(coord => coord.row === row) ||
coords.every(coord => coord.col === col);
},
// Hilfsmethode: Prüfe auf L-Form
hasLShape(coords) {
if (coords.length < 5) return false; // Mindestens 5 Tiles für L-Form
// Gruppiere nach Zeilen und Spalten
const rowGroups = {};
const colGroups = {};
coords.forEach(coord => {
if (!rowGroups[coord.row]) rowGroups[coord.row] = [];
if (!colGroups[coord.col]) colGroups[coord.col] = [];
rowGroups[coord.row].push(coord);
colGroups[coord.col].push(coord);
});
// Prüfe auf L-Form: Eine Zeile mit 3+ Tiles UND eine Spalte mit 3+ Tiles
// und sie müssen sich an einer Ecke treffen
const rows = Object.keys(rowGroups).map(r => parseInt(r));
const cols = Object.keys(colGroups).map(c => parseInt(c));
// Finde Zeilen mit mindestens 3 Tiles
const longRows = rows.filter(row => rowGroups[row].length >= 3);
// Finde Spalten mit mindestens 2 Tiles (für L-Form reicht das)
const longCols = cols.filter(col => colGroups[col].length >= 2);
// Prüfe ob eine lange Zeile und eine lange Spalte sich schneiden
for (const row of longRows) {
for (const col of longCols) {
// Prüfe ob es ein Tile an der Kreuzung gibt
const intersection = coords.find(coord => coord.row === row && coord.col === col);
if (intersection) {
// Prüfe ob es wirklich eine L-Form ist
const rowTiles = rowGroups[row];
const colTiles = colGroups[col];
// L-Form: horizontale Linie + vertikale Linie, die sich treffen
if (rowTiles.length >= 3 && colTiles.length >= 2) {
return true;
}
}
}
}
return false;
},
// Hilfsmethode: Prüfe auf T-Form
hasTShape(coords) {
if (coords.length < 5) return false; // Mindestens 5 Tiles für T-Form
// Gruppiere nach Zeilen und Spalten
const rowGroups = {};
const colGroups = {};
coords.forEach(coord => {
if (!rowGroups[coord.row]) rowGroups[coord.row] = [];
if (!colGroups[coord.col]) colGroups[coord.col] = [];
rowGroups[coord.row].push(coord);
colGroups[coord.col].push(coord);
});
// Finde Zeilen und Spalten mit mindestens 3 Tiles
const longRows = Object.values(rowGroups).filter(group => group.length >= 3);
const longCols = Object.values(colGroups).filter(group => group.length >= 3);
// T-Form: mindestens eine lange Zeile UND eine lange Spalte
// Zusätzlich: mindestens ein Tile muss an der Kreuzung sein
if (longRows.length > 0 && longCols.length > 0) {
// Prüfe ob es eine Kreuzung gibt
const intersection = coords.filter(coord =>
longRows.some(row => row.some(r => r.row === coord.row)) &&
longCols.some(col => col.some(c => c.col === coord.col))
);
return intersection.length > 0;
}
return false;
},
// Hilfsmethode: Prüfe auf Kreuz-Form
hasCrossShape(coords) {
if (coords.length < 5) return false; // Mindestens 5 Tiles für Kreuz-Form
// Gruppiere nach Zeilen und Spalten
const rowGroups = {};
const colGroups = {};
coords.forEach(coord => {
if (!rowGroups[coord.row]) rowGroups[coord.row] = [];
if (!colGroups[coord.col]) colGroups[coord.col] = [];
rowGroups[coord.row].push(coord);
colGroups[coord.col].push(coord);
});
// Finde Zeilen und Spalten mit mindestens 3 Tiles
const longRows = Object.values(rowGroups).filter(group => group.length >= 3);
const longCols = Object.values(colGroups).filter(group => group.length >= 3);
// Kreuz-Form: mindestens eine lange Zeile UND eine lange Spalte
// Zusätzlich: mindestens ein Tile muss an der Kreuzung sein
if (longRows.length > 0 && longCols.length > 0) {
// Prüfe ob es eine Kreuzung gibt
const intersection = coords.filter(coord =>
longRows.some(row => row.some(r => r.row === coord.row)) &&
longCols.some(col => col.some(c => c.col === coord.col))
);
return intersection.length > 0;
}
return false;
},
// Neue Methode: Erstelle ein spezielles Item
createSpecialItem(index, itemType) {
if (!this.board[index]) return;
// Erstelle das spezielle Item
const specialItem = {
type: itemType,
id: Date.now() + index + Math.random(),
row: this.indexToCoords(index).row,
col: this.indexToCoords(index).col,
isSpecial: true,
specialType: itemType
};
// Ersetze das normale Tile mit dem speziellen Item
this.board[index] = specialItem;
// Zeige eine visuelle Bestätigung
this.showSpecialItemCreated(index, itemType);
},
// Neue Methode: Zeige visuelle Bestätigung für erstelltes spezielles Item
showSpecialItemCreated(index, itemType) {
const tileElement = document.querySelector(`[data-index="${index}"]`);
if (tileElement) {
// Füge eine spezielle Klasse hinzu
tileElement.classList.add('special-item-created');
// Zeige eine kurze Animation
setTimeout(() => {
tileElement.classList.remove('special-item-created');
}, 1000);
}
},
// Neue Methode: Zähle Power-Up als Zug
countPowerUpMove() {
this.moves++;
this.movesLeft--;
},
// Neue Methode: Behandle Doppelklick auf Power-Up Tiles
async handleDoubleClick(index, event) {
event.preventDefault();
event.stopPropagation();
const tile = this.board[index];
if (!tile) return;
// Prüfe auf Raketen-Tiles (neue Power-ups)
if (this.isRocketTile(tile.type)) {
console.log(`🚀 Doppelklick auf Rakete ${tile.type} an Position ${index}`);
// Rakete durch Doppelklick aktivieren
await this.activateRocketByDoubleClick(index);
return;
}
// Prüfe auf Bomben-Tiles (neue Power-ups)
if (tile.type === 'bomb') {
console.log(`💣 Doppelklick auf Bombe an Position ${index}`);
// Bombe durch Doppelklick aktivieren
await this.activateBombByDoubleClick(index);
return;
}
if (tile.isSpecial) {
// Power-Up als Zug zählen
this.countPowerUpMove();
if (tile.type === 'rainbow') {
// Spiele Regenbogen-Sound
this.playSound('rainbow');
// Regenbogen-Tile: Zufälligen Tile-Typ entfernen
const randomTileType = this.tileTypes[Math.floor(Math.random() * this.tileTypes.length)];
this.removeAllTilesOfType(randomTileType);
} else if (tile.type === 'bomb') {
// Bomben-Tile: 9 Tiles rundherum entfernen
this.explodeBomb(index, 1); // 1 Ring = 3x3 Bereich
} else if (tile.type === 'rocket') {
// Raketen-Tile: 4 Nachbarfelder löschen und Rakete starten
this.handleRocketDoubleClick(index);
}
}
},
// Aktiviere Rakete durch Doppelklick
async activateRocketByDoubleClick(rocketIndex) {
console.log(`🚀 Aktiviere Rakete durch Doppelklick an Position ${rocketIndex}`);
// Hole alle Tiles um die Rakete herum
const tilesToRemove = this.getRocketExplosionTiles(rocketIndex);
if (tilesToRemove.size > 0) {
console.log(`🚀 Entferne ${tilesToRemove.size} Tiles um Rakete herum`);
// Starte Schrumpf-Animation für alle zu entfernenden Tiles
await this.animateTileRemoval(Array.from(tilesToRemove));
// Entferne alle Tiles nach der Animation
tilesToRemove.forEach(index => {
if (this.board[index]) {
console.log(`🚀 Entferne Tile ${this.board[index].type} an Position ${index}`);
this.board[index] = null;
}
});
// Entferne auch die Rakete selbst
console.log(`🚀 Entferne Rakete an Position ${rocketIndex}`);
this.board[rocketIndex] = null;
// Aktualisiere die Anzeige
this.$forceUpdate();
// Zeige Raketen-Explosions-Animation
await this.showRocketExplosionAnimation(rocketIndex);
// Führe Fall-Down-Logik aus
await this.fallTilesDown();
// Fülle leere Positionen mit neuen Tiles auf
await this.fillEmptyPositions();
// Erhöhe den Zug-Zähler
this.moves++;
this.movesLeft--;
}
},
// Aktiviere Bombe durch Doppelklick
async activateBombByDoubleClick(bombIndex) {
console.log(`💣 Aktiviere Bombe durch Doppelklick an Position ${bombIndex}`);
// Spiele Bomben-Sound
this.playSound('bomb');
// Zeige Bomben-Explosions-Animation
await this.showBombEffectAnimation(bombIndex);
// Aktiviere die Bombe (3x3 Bereich)
this.explodeBomb(bombIndex, 1);
// Entferne die Bombe selbst
console.log(`💣 Entferne Bombe an Position ${bombIndex}`);
this.board[bombIndex] = null;
// Aktualisiere die Anzeige
this.$forceUpdate();
// Führe Fall-Down-Logik aus
await this.fallTilesDown();
// Fülle leere Positionen mit neuen Tiles auf
await this.fillEmptyPositions();
// Erhöhe den Zug-Zähler
this.moves++;
this.movesLeft--;
},
// Neue Methode: Entferne alle Tiles eines bestimmten Typs
removeAllTilesOfType(tileType) {
const tilesToRemove = [];
// Sammle alle Indizes der zu entfernenden Tiles
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && this.board[i].type === tileType) {
tilesToRemove.push(i);
}
}
if (tilesToRemove.length > 0) {
// Zeige Regenbogen-Effekt-Animation für das erste Tile
if (tilesToRemove.length > 0) {
this.showRainbowEffectAnimation(tilesToRemove[0]);
}
// Entferne die Tiles (außer Regenbögen)
tilesToRemove.forEach(index => {
this.safeRemoveTile(index);
});
// Starte Fall-Animation
this.startFallAnimation(tilesToRemove);
// Punkte hinzufügen
const points = tilesToRemove.length * 20 * this.currentLevel;
this.levelScore += points;
this.score += points;
}
},
// Neue Methode: Behandle Power-Up Tile Tausch
handleRainbowSwap(originalTile1, originalTile2) {
// Power-Up Tausch als Zug zählen (wird auch über swapTiles aufgerufen)
this.countPowerUpMove();
if (originalTile1.type === 'rainbow' && originalTile2.type === 'rainbow') {
// Spiele Regenbogen-Sound
this.playSound('rainbow');
// Zwei Regenbogen-Tiles getauscht: Entferne alle Tiles
this.removeAllTilesFromBoard();
return true;
} else if (originalTile1.type === 'rainbow' || originalTile2.type === 'rainbow') {
// Spiele Regenbogen-Sound
this.playSound('rainbow');
// Ein Regenbogen-Tile mit normalem Tile getauscht: Entferne alle Tiles des normalen Typs
const normalTile = originalTile1.type === 'rainbow' ? originalTile2 : originalTile1;
this.removeAllTilesOfType(normalTile.type);
return true;
} else if (originalTile1.type === 'bomb' && originalTile2.type === 'bomb') {
// Zwei Bomben-Tiles getauscht: Entferne 2 Ringe (5x5 Bereich)
const bombIndex = this.findBombIndex(originalTile1, originalTile2);
if (bombIndex !== null) {
this.explodeBomb(bombIndex, 2); // 2 Ringe = 5x5 Bereich
}
return true;
} else if (originalTile1.type === 'bomb' || originalTile2.type === 'bomb') {
// Ein Bomben-Tile mit normalem Tile getauscht: Entferne 9 Tiles rund um das Ziel
const bombTile = originalTile1.type === 'bomb' ? originalTile1 : originalTile2;
const targetTile = originalTile1.type === 'bomb' ? originalTile2 : originalTile1;
// Finde die neue Position der Bombe
const newBombIndex = this.findTileIndex(bombTile);
if (newBombIndex !== null) {
this.explodeBomb(newBombIndex, 1); // 1 Ring = 3x3 Bereich
}
return true;
} else if ((originalTile1.type === 'bomb' && originalTile2.type === 'rainbow') ||
(originalTile1.type === 'rainbow' && originalTile2.type === 'bomb')) {
// Spiele Regenbogen-Sound
this.playSound('rainbow');
// Bombe + Regenbogen: Erstelle zufällige Bomben und löse sie aus
this.createRandomBombs(20);
setTimeout(() => {
this.detonateAllBombs();
}, 500); // Kurze Verzögerung für visuellen Effekt
return true;
} else if (originalTile1.type === 'rocket' && originalTile2.type === 'rocket') {
// Zwei Raketen verbunden: Lösche Nachbarfelder beider Raketen und starte 3 Raketen
this.handleRocketConnection(originalTile1, originalTile2);
return true;
} else if ((originalTile1.type === 'rocket' && originalTile2.type === 'rainbow') ||
(originalTile1.type === 'rainbow' && originalTile2.type === 'rocket')) {
// Spiele Regenbogen-Sound
this.playSound('rainbow');
// Rakete + Regenbogen: Erstelle zufällige Raketen und starte sie
this.createRandomRockets(10);
setTimeout(() => {
this.launchAllRockets();
}, 500); // Kurze Verzögerung für visuellen Effekt
return true;
} else if ((originalTile1.type === 'rocket' && originalTile2.type === 'bomb') ||
(originalTile1.type === 'bomb' && originalTile2.type === 'rocket')) {
// Rakete + Bombe: Lösche 4 Nachbarfelder und starte Bomben-Rakete
this.handleRocketBombCombination(originalTile1, originalTile2);
return true;
} else if (originalTile1.type === 'rocket' || originalTile2.type === 'rocket') {
// Eine Rakete mit normalem Tile: Rakete fliegt auf zufälliges Feld
const rocketTile = originalTile1.type === 'rocket' ? originalTile1 : originalTile2;
const targetTile = originalTile1.type === 'rocket' ? originalTile2 : originalTile1;
this.handleRocketLaunch(rocketTile, targetTile);
return true;
}
return false; // Kein Power-Up Tausch
},
// Hilfsmethode: Finde den Index einer Bombe nach dem Tausch
findBombIndex(originalBomb1, originalBomb2) {
// Suche nach der Bombe auf dem Board
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && this.board[i].type === 'bomb') {
return i;
}
}
return null;
},
// Hilfsmethode: Finde den Index eines Tiles nach dem Tausch
findTileIndex(originalTile) {
// Suche nach dem Tile auf dem Board
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && this.board[i].id === originalTile.id) {
return i;
}
}
return null;
},
// Neue Methode: Entferne alle Tiles vom Board (außer Regenbögen)
removeAllTilesFromBoard() {
const allTileIndices = [];
// Sammle alle Tile-Indizes (außer Regenbögen)
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && this.board[i].type !== 'rainbow') {
allTileIndices.push(i);
}
}
if (allTileIndices.length > 0) {
// Zeige Regenbogen-Effekt-Animation in der Mitte des Boards
const centerIndex = Math.floor(this.board.length / 2);
this.showRainbowEffectAnimation(centerIndex);
// Entferne alle Tiles (außer Regenbögen)
allTileIndices.forEach(index => {
this.safeRemoveTile(index);
});
// Starte Fall-Animation
this.startFallAnimation(allTileIndices);
// Punkte hinzufügen
const points = allTileIndices.length * 30 * this.currentLevel;
this.levelScore += points;
this.score += points;
}
},
// Neue Methode: Explodiere Bombe mit bestimmter Reichweite
explodeBomb(centerIndex, rings) {
const { row: centerRow, col: centerCol } = this.indexToCoords(centerIndex);
const tilesToRemove = [];
const powerUpsToTrigger = []; // Sammle Power-Ups für Kettenreaktionen
// Sammle alle Tiles in den angegebenen Ringen (außer Regenbögen)
for (let ring = 0; ring <= rings; ring++) {
for (let r = centerRow - ring; r <= centerRow + ring; r++) {
for (let c = centerCol - ring; c <= centerCol + ring; c++) {
if (r >= 0 && r < this.boardHeight && c >= 0 && c < this.boardWidth) {
const index = this.coordsToIndex(r, c);
if (index !== null && this.board[index] && this.board[index].type !== 'rainbow') {
tilesToRemove.push(index);
// Prüfe auf Power-Ups für Kettenreaktionen (außer Regenbögen)
if (this.isPowerUpTile(this.board[index]) && this.board[index].type !== 'rainbow') {
powerUpsToTrigger.push({
index: index,
type: this.board[index].type
});
}
}
}
}
}
}
if (tilesToRemove.length > 0) {
// Spiele Bomb-Sound
this.playSound('bomb');
// Zeige Explosions-Animation
this.showExplosionAnimation(centerIndex);
// WICHTIG: Entferne zuerst nur normale Tiles, nicht Power-Ups
const normalTilesToRemove = tilesToRemove.filter(index =>
!this.isPowerUpTile(this.board[index]) || this.board[index].type === 'rainbow'
);
normalTilesToRemove.forEach(index => {
this.safeRemoveTile(index);
});
// Starte Fall-Animation nur für normale Tiles
this.startFallAnimation(normalTilesToRemove);
// Punkte hinzufügen
const points = normalTilesToRemove.length * 25 * this.currentLevel;
this.levelScore += points;
this.score += points;
// Löse Kettenreaktionen nur für Power-Ups aus, die noch existieren
if (powerUpsToTrigger.length > 0) {
setTimeout(() => {
// Prüfe nochmal, ob die Power-Ups noch existieren, bevor sie ausgelöst werden
const validPowerUps = powerUpsToTrigger.filter(powerUp =>
this.board[powerUp.index] &&
this.board[powerUp.index].type === powerUp.type
);
if (validPowerUps.length > 0) {
this.triggerChainReaction(validPowerUps);
}
}, 800);
}
}
},
// Neue Methode: Löse Kettenreaktionen für Power-Ups aus
triggerChainReaction(powerUps) {
powerUps.forEach(powerUp => {
if (powerUp.type === 'bomb') {
// Bombe explodiert mit 1 Ring (3x3 Bereich)
this.explodeBomb(powerUp.index, 1);
} else if (powerUp.type === 'rocket') {
// Rakete startet auf zufälliges Feld
this.launchRocketToRandomField(powerUp.index);
}
// Regenbögen lösen keine Kettenreaktionen aus
// WICHTIG: Entferne das Power-Up nach der Auslösung
setTimeout(() => {
this.safeRemoveTile(powerUp.index);
}, 500);
});
},
// Neue Methode: Erstelle zufällige Bomben auf 20% der Tiles
createRandomBombs(percentage = 20) {
const totalTiles = this.board.filter(tile => tile !== null).length;
const bombCount = Math.floor(totalTiles * percentage / 100);
const bombIndices = [];
// Sammle alle verfügbaren Tile-Indizes (außer Bomben und Regenbögen)
const availableIndices = [];
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && this.board[i].type !== 'bomb' && this.board[i].type !== 'rainbow') {
availableIndices.push(i);
}
}
// Wähle zufällige Tiles aus und verwandle sie in Bomben
for (let i = 0; i < Math.min(bombCount, availableIndices.length); i++) {
const randomIndex = Math.floor(Math.random() * availableIndices.length);
const tileIndex = availableIndices.splice(randomIndex, 1)[0];
// Verwandle das Tile in eine Bombe
this.board[tileIndex] = {
type: 'bomb',
id: Date.now() + tileIndex + Math.random(),
row: this.indexToCoords(tileIndex).row,
col: this.indexToCoords(tileIndex).col,
isSpecial: true,
specialType: 'bomb'
};
bombIndices.push(tileIndex);
}
return bombIndices;
},
// Neue Methode: Löse alle Bomben auf dem Feld aus
detonateAllBombs() {
const bombIndices = [];
// Sammle alle Bomben-Indizes
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && this.board[i].type === 'bomb') {
bombIndices.push(i);
}
}
// Löse alle Bomben aus
bombIndices.forEach(index => {
this.explodeBomb(index, 1);
});
},
// Neue Methode: Erstelle zufällige Raketen
createRandomRockets(percentage = 10) {
const totalTiles = this.board.filter(tile => tile !== null).length;
const rocketCount = Math.floor(totalTiles * percentage / 100);
const availableIndices = [];
// Sammle alle verfügbaren Positionen (keine Power-Ups, aber explizit keine Regenbögen)
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && !this.isPowerUpTile(this.board[i]) && this.board[i].type !== 'rainbow') {
availableIndices.push(i);
}
}
// Wähle zufällige Positionen für Raketen
for (let i = 0; i < Math.min(rocketCount, availableIndices.length); i++) {
const randomIndex = Math.floor(Math.random() * availableIndices.length);
const boardIndex = availableIndices.splice(randomIndex, 1)[0];
// Ersetze das Tile durch eine Rakete
this.board[boardIndex] = {
id: Math.random().toString(36).substr(2, 9),
type: 'rocket',
isSpecial: true
};
}
},
// Neue Methode: Starte alle Raketen
launchAllRockets() {
const rocketIndices = [];
// Finde alle Raketen auf dem Board
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && this.board[i].type === 'rocket') {
rocketIndices.push(i);
}
}
// Starte jede Rakete
rocketIndices.forEach(rocketIndex => {
this.handleRocketDoubleClick(rocketIndex);
});
},
// Neue Methode: Behandle Rakete + Bombe Kombination
handleRocketBombCombination(originalTile1, originalTile2) {
// Finde die Position einer der Power-Ups (für die 4 Nachbarfelder)
const rocketTile = originalTile1.type === 'rocket' ? originalTile1 : originalTile2;
const bombTile = originalTile1.type === 'bomb' ? originalTile1 : originalTile2;
// Finde die Position auf dem Board
const rocketIndex = this.findTileIndex(rocketTile);
if (rocketIndex !== null) {
// Lösche die 4 Nachbarfelder der Rakete
const { row: rocketRow, col: rocketCol } = this.indexToCoords(rocketIndex);
const tilesToRemove = [];
// Sammle die 4 Nachbarfelder (oben, unten, links, rechts)
const directions = [
{ row: rocketRow - 1, col: rocketCol }, // Oben
{ row: rocketRow + 1, col: rocketCol }, // Unten
{ row: rocketRow, col: rocketCol - 1 }, // Links
{ row: rocketRow, col: rocketCol + 1 } // Rechts
];
directions.forEach(({ row, col }) => {
if (row >= 0 && row < this.boardHeight && col >= 0 && col < this.boardWidth) {
const index = this.coordsToIndex(row, col);
if (this.board[index]) {
tilesToRemove.push(index);
}
}
});
// Entferne die Nachbarfelder (außer Regenbögen)
if (tilesToRemove.length > 0) {
tilesToRemove.forEach(index => {
this.safeRemoveTile(index);
});
// Starte Fall-Animation
this.startFallAnimation(tilesToRemove);
// Punkte hinzufügen
const points = tilesToRemove.length * 25 * this.currentLevel;
this.levelScore += points;
this.score += points;
}
}
// Starte die Bomben-Rakete nach kurzer Verzögerung
setTimeout(() => {
this.launchBombRocket();
}, 200);
},
// Neue Methode: Starte eine Bomben-Rakete
launchBombRocket() {
// Finde alle verfügbaren Positionen für die Landung
const availableIndices = [];
for (let i = 0; i < this.board.length; i++) {
if (this.board[i]) {
availableIndices.push(i);
}
}
if (availableIndices.length > 0) {
// Wähle zufällige Landungsposition
const randomIndex = Math.floor(Math.random() * availableIndices.length);
const landingIndex = availableIndices[randomIndex];
// Explodiere am Landungsort (9 Felder = 1 Ring)
this.explodeBomb(landingIndex, 1);
}
},
// Neue Methode: Zeige Explosions-Animation
showExplosionAnimation(centerIndex) {
// Warte bis das DOM gerendert ist
this.$nextTick(() => {
const tileElement = document.querySelector(`[data-index="${centerIndex}"]`);
const gameBoard = document.querySelector('.game-board-container');
if (tileElement && gameBoard) {
const tileRect = tileElement.getBoundingClientRect();
const boardRect = gameBoard.getBoundingClientRect();
// Berechne relative Position zum Game-Board-Container
this.explosionPosition = {
x: tileRect.left - boardRect.left + tileRect.width / 2 - 40,
y: tileRect.top - boardRect.top + tileRect.height / 2 - 40
};
this.showExplosion = true;
// Verstecke Animation nach der Dauer
setTimeout(() => {
this.showExplosion = false;
}, 800);
}
});
},
// Neue Methode: Zeige Raketen-Flug-Animation
showRocketFlightAnimation(startIndex, endIndex) {
// Warte bis das DOM gerendert ist
this.$nextTick(() => {
const startElement = document.querySelector(`[data-index="${startIndex}"]`);
const endElement = document.querySelector(`[data-index="${endIndex}"]`);
const gameBoard = document.querySelector('.game-board-container');
if (startElement && endElement && gameBoard) {
const startRect = startElement.getBoundingClientRect();
const endRect = endElement.getBoundingClientRect();
const boardRect = gameBoard.getBoundingClientRect();
this.rocketStartPos = {
x: startRect.left - boardRect.left + startRect.width / 2 - 20,
y: startRect.top - boardRect.top + startRect.height / 2 - 20
};
this.rocketEndPos = {
x: endRect.left - boardRect.left + endRect.width / 2 - 20,
y: endRect.top - boardRect.top + endRect.height / 2 - 20
};
this.showRocketFlight = true;
// Verstecke Animation nach der Dauer
setTimeout(() => {
this.showRocketFlight = false;
}, 1200);
}
});
},
// Neue Methode: Zeige Regenbogen-Effekt-Animation
showRainbowEffectAnimation(centerIndex) {
// Warte bis das DOM gerendert ist
this.$nextTick(() => {
const tileElement = document.querySelector(`[data-index="${centerIndex}"]`);
const gameBoard = document.querySelector('.game-board-container');
if (tileElement && gameBoard) {
const tileRect = tileElement.getBoundingClientRect();
const boardRect = gameBoard.getBoundingClientRect();
this.rainbowCenter = {
x: tileRect.left - boardRect.left + tileRect.width / 2 - 50,
y: tileRect.top - boardRect.top + tileRect.height / 2 - 50
};
this.showRainbowEffect = true;
// Verstecke Animation nach der Dauer
setTimeout(() => {
this.showRainbowEffect = false;
}, 1500);
}
});
},
// Neue Methode: Zeige Bomben-Effekt-Animation
showBombEffectAnimation(centerIndex) {
// Warte bis das DOM gerendert ist
this.$nextTick(() => {
const tileElement = document.querySelector(`[data-index="${centerIndex}"]`);
const gameBoard = document.querySelector('.game-board-container');
if (tileElement && gameBoard) {
const tileRect = tileElement.getBoundingClientRect();
const boardRect = gameBoard.getBoundingClientRect();
this.bombCenter = {
x: tileRect.left - boardRect.left + tileRect.width / 2 - 30,
y: tileRect.top - boardRect.top + tileRect.height / 2 - 30
};
this.showBombEffect = true;
// Verstecke Animation nach der Dauer
setTimeout(() => {
this.showBombEffect = false;
}, 300);
}
});
},
// Neue Methode: Behandle Doppelklick auf Rakete
handleRocketDoubleClick(rocketIndex) {
const { row: rocketRow, col: rocketCol } = this.indexToCoords(rocketIndex);
const tilesToRemove = [];
// Sammle die 4 Nachbarfelder (oben, unten, links, rechts)
const directions = [
{ row: rocketRow - 1, col: rocketCol }, // Oben
{ row: rocketRow + 1, col: rocketCol }, // Unten
{ row: rocketRow, col: rocketCol - 1 }, // Links
{ row: rocketRow, col: rocketCol + 1 } // Rechts
];
directions.forEach(({ row, col }) => {
if (row >= 0 && row < this.boardHeight && col >= 0 && col < this.boardWidth) {
const index = this.coordsToIndex(row, col);
if (index !== null && this.board[index]) {
tilesToRemove.push(index);
}
}
});
if (tilesToRemove.length > 0) {
// Spiele Rocket-Sound
this.playSound('rocket');
// Zeige Bomben-Effekt-Animation für die Rakete
this.showBombEffectAnimation(rocketIndex);
// Entferne die Nachbar-Tiles (außer Regenbögen)
tilesToRemove.forEach(index => {
this.safeRemoveTile(index);
});
// Starte Fall-Animation
this.startFallAnimation(tilesToRemove);
// Punkte hinzufügen
const points = tilesToRemove.length * 15 * this.currentLevel;
this.levelScore += points;
this.score += points;
// Starte Rakete auf zufälliges Feld
this.launchRocketToRandomField(rocketIndex);
}
},
// Neue Methode: Rakete auf zufälliges Feld starten
launchRocketToRandomField(rocketIndex) {
// Finde alle verfügbaren Felder (außer der Rakete selbst)
const availableFields = [];
for (let i = 0; i < this.board.length; i++) {
if (i !== rocketIndex && this.board[i] && this.isValidPosition(this.indexToCoords(i).row, this.indexToCoords(i).col)) {
availableFields.push(i);
}
}
if (availableFields.length > 0) {
// Wähle ein zufälliges Feld aus
const randomIndex = availableFields[Math.floor(Math.random() * availableFields.length)];
// Zeige visuellen Effekt auf dem Ziel-Tile (kurzer Farbwechsel)
this.showRocketTargetEffect(randomIndex);
// Kurze Verzögerung, damit der Effekt sichtbar ist
setTimeout(() => {
// Spiele Rocket-Sound
this.playSound('rocket');
// Prüfe ob das Ziel-Feld ein Power-Up ist (außer Regenbogen)
const targetTile = this.board[randomIndex];
let chainReaction = false;
if (targetTile && this.isPowerUpTile(targetTile) && targetTile.type !== 'rainbow') {
// Kettenreaktion auslösen
chainReaction = true;
setTimeout(() => {
// Prüfe nochmal, ob das Power-Up noch existiert, bevor es ausgelöst wird
if (this.board[randomIndex] && this.board[randomIndex].type === targetTile.type) {
this.triggerChainReaction([{
index: randomIndex,
type: targetTile.type
}]);
}
}, 500);
// WICHTIG: Entferne das Ziel-Feld erst nach der Kettenreaktion
setTimeout(() => {
this.safeRemoveTile(randomIndex);
// Starte Fall-Animation für das Ziel-Feld
this.startFallAnimation([randomIndex]);
}, 1000); // Längere Verzögerung für Kettenreaktion
} else {
// Keine Kettenreaktion - lösche das Feld sofort
this.safeRemoveTile(randomIndex);
// Starte Fall-Animation für das Ziel-Feld
this.startFallAnimation([randomIndex]);
}
// Entferne die Rakete vom ursprünglichen Feld (außer Regenbögen)
this.safeRemoveTile(rocketIndex);
// Zeige Raketen-Flug-Animation
this.showRocketFlightAnimation(rocketIndex, randomIndex);
// Starte Fall-Animation für die Rakete
this.startFallAnimation([rocketIndex]);
}, 800); // 800ms Verzögerung für den visuellen Effekt
// Punkte hinzufügen
const points = 20 * this.currentLevel;
this.levelScore += points;
this.score += points;
}
},
// Neue Methode: Zeige visuellen Effekt auf dem Raketen-Ziel
showRocketTargetEffect(targetIndex) {
const targetTile = document.querySelector(`[data-index="${targetIndex}"]`);
if (targetTile) {
// Füge eine CSS-Klasse für den Raketen-Treffer-Effekt hinzu
targetTile.classList.add('rocket-target-hit');
// Entferne die Klasse nach der Animation
setTimeout(() => {
targetTile.classList.remove('rocket-target-hit');
}, 300);
}
},
// Neue Methode: Behandle Verbindung zweier Raketen
handleRocketConnection(rocket1, rocket2) {
const rocket1Index = this.findTileIndex(rocket1);
const rocket2Index = this.findTileIndex(rocket2);
if (rocket1Index === null || rocket2Index === null) return;
const tilesToRemove = [];
// Sammle Nachbarfelder beider Raketen
[rocket1Index, rocket2Index].forEach(rocketIndex => {
const { row: rocketRow, col: rocketCol } = this.indexToCoords(rocketIndex);
const directions = [
{ row: rocketRow - 1, col: rocketCol }, // Oben
{ row: rocketRow + 1, col: rocketCol }, // Unten
{ row: rocketRow, col: rocketCol - 1 }, // Links
{ row: rocketRow, col: rocketCol + 1 } // Rechts
];
directions.forEach(({ row, col }) => {
if (row >= 0 && row < this.boardHeight && col >= 0 && col < this.boardWidth) {
const index = this.coordsToIndex(row, col);
if (index !== null && this.board[index]) {
tilesToRemove.push(index);
}
}
});
});
if (tilesToRemove.length > 0) {
// Spiele Rocket-Sound
this.playSound('rocket');
// Entferne die Nachbar-Tiles (außer Regenbögen)
tilesToRemove.forEach(index => {
this.safeRemoveTile(index);
});
// Starte Fall-Animation
this.startFallAnimation(tilesToRemove);
// Punkte hinzufügen
const points = tilesToRemove.length * 20 * this.currentLevel;
this.levelScore += points;
this.score += points;
// Starte 3 Raketen auf zufällige Felder
this.launchThreeRockets([rocket1Index, rocket2Index]);
}
},
// Neue Methode: Starte 3 Raketen auf zufällige Felder
launchThreeRockets(rocketIndices) {
// Entferne die ursprünglichen Raketen (außer Regenbögen)
rocketIndices.forEach(index => {
this.safeRemoveTile(index);
});
// Finde alle verfügbaren Felder
const availableFields = [];
for (let i = 0; i < this.board.length; i++) {
if (!rocketIndices.includes(i) && this.board[i] && this.isValidPosition(this.indexToCoords(i).row, this.indexToCoords(i).col)) {
availableFields.push(i);
}
}
if (availableFields.length > 0) {
// Wähle 3 zufällige Felder aus
const selectedFields = [];
for (let i = 0; i < Math.min(3, availableFields.length); i++) {
const randomIndex = Math.floor(Math.random() * availableFields.length);
const fieldIndex = availableFields.splice(randomIndex, 1)[0];
selectedFields.push(fieldIndex);
}
// Prüfe auf Power-Ups für Kettenreaktionen (außer Regenbögen)
const powerUpsToTrigger = [];
const normalFieldsToRemove = [];
selectedFields.forEach(index => {
const tile = this.board[index];
if (tile && this.isPowerUpTile(tile) && tile.type !== 'rainbow') {
powerUpsToTrigger.push({
index: index,
type: tile.type
});
} else {
normalFieldsToRemove.push(index);
}
});
// Lösche nur normale Felder sofort
normalFieldsToRemove.forEach(index => {
this.safeRemoveTile(index);
});
// Starte Fall-Animation für normale Felder und Raketen
this.startFallAnimation([...rocketIndices, ...normalFieldsToRemove]);
// Punkte hinzufügen
const points = (rocketIndices.length + normalFieldsToRemove.length) * 25 * this.currentLevel;
this.levelScore += points;
this.score += points;
// Löse Kettenreaktionen nur für Power-Ups aus, die noch existieren
if (powerUpsToTrigger.length > 0) {
setTimeout(() => {
// Prüfe nochmal, ob die Power-Ups noch existieren, bevor sie ausgelöst werden
const validPowerUps = powerUpsToTrigger.filter(powerUp =>
this.board[powerUp.index] &&
this.board[powerUp.index].type === powerUp.type
);
if (validPowerUps.length > 0) {
this.triggerChainReaction(validPowerUps);
}
}, 800);
}
}
},
// Neue Methode: Rakete mit normalem Tile tauschen
handleRocketLaunch(rocketTile, targetTile) {
const targetIndex = this.findTileIndex(targetTile);
if (targetIndex === null) return;
const { row: targetRow, col: targetCol } = this.indexToCoords(targetIndex);
const tilesToRemove = [];
// Sammle die 4 Nachbarfelder des Ziel-Tiles
const directions = [
{ row: targetRow - 1, col: targetCol }, // Oben
{ row: targetRow + 1, col: targetCol }, // Unten
{ row: targetRow, col: targetCol - 1 }, // Links
{ row: targetRow, col: targetCol + 1 } // Rechts
];
directions.forEach(({ row, col }) => {
if (row >= 0 && row < this.boardHeight && col >= 0 && col < this.boardWidth) {
const index = this.coordsToIndex(row, col);
if (index !== null && this.board[index]) {
tilesToRemove.push(index);
}
}
});
if (tilesToRemove.length > 0) {
// Spiele Rocket-Sound
this.playSound('rocket');
// Prüfe auf Power-Ups für Kettenreaktionen (außer Regenbögen)
const powerUpsToTrigger = [];
const normalTilesToRemove = [];
tilesToRemove.forEach(index => {
const tile = this.board[index];
if (tile && this.isPowerUpTile(tile) && tile.type !== 'rainbow') {
powerUpsToTrigger.push({
index: index,
type: tile.type
});
} else {
normalTilesToRemove.push(index);
}
});
// Entferne nur normale Tiles sofort
normalTilesToRemove.forEach(index => {
this.safeRemoveTile(index);
});
// Starte Fall-Animation für normale Tiles
this.startFallAnimation(normalTilesToRemove);
// Punkte hinzufügen
const points = normalTilesToRemove.length * 15 * this.currentLevel;
this.levelScore += points;
this.score += points;
// Löse Kettenreaktionen nur für Power-Ups aus, die noch existieren
if (powerUpsToTrigger.length > 0) {
setTimeout(() => {
// Prüfe nochmal, ob die Power-Ups noch existieren, bevor sie ausgelöst werden
const validPowerUps = powerUpsToTrigger.filter(powerUp =>
this.board[powerUp.index] &&
this.board[powerUp.index].type === powerUp.type
);
if (validPowerUps.length > 0) {
this.triggerChainReaction(validPowerUps);
}
}, 800);
}
// Starte Rakete auf zufälliges Feld
this.launchRocketToRandomField(targetIndex);
}
}
},
computed: {
// Computed Eigenschaften für UI
isStatsExpanded() {
return this.statsExpanded;
},
toggleIcon() {
return this.statsExpanded ? '▼' : '▶';
},
safeMovesLeft() {
return this.movesLeft || 0;
}
}
}
</script>
<style scoped>
/* Minimalistischer Style - nur für Match3Game */
/* Verwendet globale Scroll-Klassen: .contenthidden und .contentscroll */
.game-title {
text-align: center;
margin-bottom: 30px;
padding-top: 20px;
}
.game-title h1 {
margin: 0 0 10px 0;
font-size: 2rem;
font-weight: 600;
color: #333;
}
.game-title p {
margin: 0;
font-size: 1.1rem;
color: #666;
line-height: 1.5;
}
/* Spiel-Layout */
.game-layout {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 20px;
align-items: center;
}
.stats-section {
width: 100%;
max-width: 400px;
}
.game-content {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
/* Verbleibende Züge Anzeige */
.moves-left-display {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 5px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.moves-left-label {
font-size: 16px;
font-weight: 500;
color: #333;
}
.moves-left-value {
font-size: 24px;
font-weight: bold;
color: #FFC107;
}
/* Statistik-Bereich */
.stats-card {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 5px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stats-header {
cursor: pointer;
padding: 8px;
border-bottom: 1px solid #eee;
margin-bottom: 10px;
user-select: none;
transition: background-color 0.2s ease;
border-radius: 4px;
}
.stats-header:hover {
background-color: #f8f9fa;
}
.stats-header-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.stats-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
pointer-events: none;
}
.toggle-button {
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
margin: 0;
border-radius: 4px;
transition: all 0.2s ease;
}
.toggle-button:hover {
background-color: rgba(249, 162, 44, 0.1);
transform: scale(1.1);
}
.toggle-icon {
font-size: 18px;
color: #F9A22C;
transition: transform 0.3s ease;
user-select: none;
pointer-events: none;
min-width: 20px;
text-align: center;
font-weight: bold;
}
.stats-card.expanded .toggle-icon {
transform: rotate(180deg);
}
.stats-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.stat-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.stat-value {
font-size: 14px;
font-weight: bold;
}
.stat-label {
font-size: 12px;
color: #666;
}
/* Statistik-Werte Farben */
.score-value { color: #F9A22C; }
.moves-value { color: #28a745; }
.level-value { color: #17a2b8; }
.stars-value { color: #FFD700; }
/* Level-Info */
.level-info-card {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 5px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
width: 100%;
max-width: 600px;
}
.level-header {
cursor: pointer;
padding: 8px;
border-bottom: 1px solid #eee;
margin-bottom: 10px;
user-select: none;
transition: background-color 0.2s ease;
border-radius: 4px;
}
.level-header:hover {
background-color: #f8f9fa;
}
.level-header-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.level-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
flex-grow: 1;
text-align: left;
pointer-events: none;
}
.level-content.expanded {
display: block; /* Wenn expanded */
}
.level-info-card p {
margin: 0 0 15px 0;
text-align: left;
color: #666;
line-height: 1.6;
}
.level-objectives {
display: flex;
flex-direction: column;
gap: 6px;
}
.objective-item {
display: flex;
align-items: center;
margin-bottom: 8px;
padding: 8px;
background-color: #f8f9fa;
border-radius: 4px;
}
.objective-icon {
margin-right: 8px;
font-size: 16px;
color: #28a745;
}
.objective-item.completed .objective-icon {
color: #28a745;
}
.objective-progress {
margin-left: auto;
font-size: 12px;
color: #6c757d;
font-weight: 500;
}
/* Spiel-Brett */
.game-board-container {
display: inline-block;
padding: 20px;
background: white;
border-radius: 8px;
border: 1px solid #ddd;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
position: relative; /* Für absolute Positionierung der Animationen */
}
.game-board {
display: grid;
gap: 2px;
padding: 10px;
background: #f0f0f0;
border-radius: 8px;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative; /* Wichtig für absolute Positionierung der animierten Tiles */
}
.game-tile {
width: 30px;
height: 30px;
background: linear-gradient(135deg, #eeeded, #f1eeee);
border: 2px solid #c0c0c0;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
pointer-events: auto;
user-select: none;
position: relative;
}
/* Vergrößere den klickbaren Bereich für besseres Drag&Drop */
.game-tile::before {
content: '';
position: absolute;
top: -5px;
left: -5px;
right: -5px;
bottom: -5px;
z-index: -1;
}
.game-tile.empty {
background: transparent;
border: 2px solid transparent;
box-shadow: none;
pointer-events: none;
/* Wichtig: Behalte die gleiche Größe wie normale Tiles */
width: 30px;
height: 30px;
/* Optional: Zeige einen subtilen Rahmen für bessere Sichtbarkeit */
background: rgba(0, 0, 0, 0.05);
}
.game-tile:hover {
transform: scale(1.05);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
border-color: #F9A22C;
}
.game-tile.swapping {
animation: swap 0.8s ease-in-out;
}
.game-tile.matched {
animation: match 1.2s ease-in-out;
}
/* Verschwind-Animation für matched Tiles */
.game-tile.disappearing {
transition: all 0.8s ease-out;
transform: scale(0.1) rotate(180deg);
opacity: 0;
z-index: 200;
}
/* Schrumpf-Animation für das Entfernen von Tiles */
.game-tile.removing {
transition: all 0.75s ease-out;
transform: scale(0.1) rotate(360deg);
opacity: 0;
z-index: 200;
}
/* Spezielle Items */
.game-tile.special-item-created {
animation: specialItemCreated 2.5s ease-in-out;
box-shadow: 0 0 20px rgba(255, 215, 0, 0.8);
}
@keyframes specialItemCreated {
0% { transform: scale(1); }
50% { transform: scale(1.3) rotate(10deg); }
100% { transform: scale(1); }
}
/* Fall-Animation */
.game-tile.falling {
transition: transform 0.5s ease-in, opacity 0.3s ease-out;
z-index: 100;
}
.game-tile.new-tile {
transition: opacity 0.5s ease-in;
z-index: 100;
}
/* Drag & Drop Styles */
.game-tile.dragging {
transform: scale(1.1) !important;
z-index: 200 !important;
box-shadow: 0 8px 16px rgba(249, 162, 44, 0.6) !important;
border-color: #F9A22C !important;
cursor: grabbing !important;
background: linear-gradient(135deg, #F9A22C, #e8941a) !important;
}
/* Preview Animation - Priorisiert transform über andere CSS-Regeln */
.game-tile.preview-animating {
/* transform wird über JavaScript gesetzt */
z-index: 999 !important;
}
.game-tile.drag-hover {
transform: scale(1.05);
border-color: #F9A22C;
box-shadow: 0 4px 8px rgba(249, 162, 44, 0.4);
}
.game-tile.adjacent-available {
transform: scale(1.02) !important;
border-color: #4CAF50 !important;
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3) !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
background: linear-gradient(135deg, #4CAF50, #45a049) !important;
}
.game-tile.adjacent-available:hover {
transform: scale(1.05) !important;
border-color: #45a049 !important;
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.5) !important;
background: linear-gradient(135deg, #45a049, #388E3C) !important;
}
/* Drag-Effekt */
.drag-tile {
background: white;
border: 2px solid #F9A22C;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transform: rotate(5deg) scale(1.1);
transition: transform 0.1s ease;
}
.drag-tile .tile-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.drag-tile .tile-icon {
font-size: 18px;
user-select: none;
}
.tile-icon {
font-size: 18px;
user-select: none;
}
/* Spiel-Kontrollen */
.game-controls {
text-align: center;
margin-bottom: 20px;
}
.btn {
display: inline-block;
padding: 10px 20px;
margin: 0 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
text-decoration: none;
transition: all 0.2s ease;
}
.btn-primary {
background-color: #F9A22C;
color: #000;
}
.btn-primary:hover {
background-color: #e8941a;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
/* Animationen */
@keyframes swap {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
@keyframes match {
0% { transform: scale(1); }
50% { transform: scale(1.2) rotate(10deg); }
100% { transform: scale(1); }
}
/* Power-Up Animationen */
@keyframes explosion {
0% {
transform: scale(0.1);
opacity: 1;
}
50% {
transform: scale(1.5);
opacity: 0.8;
}
100% {
transform: scale(2.5);
opacity: 0;
}
}
@keyframes rocketFlight {
0% {
transform: translate(0, 0) rotate(0deg);
opacity: 1;
}
50% {
transform: translate(var(--dx), var(--dy)) rotate(45deg);
opacity: 0.8;
}
100% {
transform: translate(calc(var(--dx) * 2), calc(var(--dy) * 2)) rotate(90deg);
opacity: 0;
}
}
@keyframes rainbowPulse {
0% {
transform: scale(1) rotate(0deg);
filter: hue-rotate(0deg);
}
25% {
transform: scale(1.2) rotate(90deg);
filter: hue-rotate(90deg);
}
50% {
transform: scale(1.4) rotate(180deg);
filter: hue-rotate(180deg);
}
75% {
transform: scale(1.2) rotate(270deg);
filter: hue-rotate(270deg);
}
100% {
transform: scale(1) rotate(360deg);
filter: hue-rotate(360deg);
}
}
@keyframes bombShake {
0%, 100% { transform: translateX(0) rotate(0deg); }
25% { transform: translateX(-2px) rotate(-1deg); }
75% { transform: translateX(2px) rotate(1deg); }
}
@keyframes rocketTargetHit {
0% {
background: linear-gradient(135deg, #eeeded, #f1eeee);
transform: scale(1);
}
25% {
background: linear-gradient(135deg, #ff6b6b, #ff8e53);
transform: scale(1.1);
}
50% {
background: linear-gradient(135deg, #ffd93d, #ff6b6b);
transform: scale(1.2);
}
75% {
background: linear-gradient(135deg, #ff8e53, #ffd93d);
transform: scale(1.1);
}
100% {
background: linear-gradient(135deg, #eeeded, #f1eeee);
transform: scale(1);
}
}
/* Power-Up Animation Container */
.power-up-animation {
position: absolute;
pointer-events: none;
z-index: 1000;
transform: translate(-50%, -50%); /* Zentriere die Animation */
}
/* Raketen-Treffer-Effekt */
.game-tile.rocket-target-hit {
animation: rocketTargetHit 0.8s ease-in-out;
z-index: 100;
}
.explosion-effect {
width: 80px;
height: 80px;
background: radial-gradient(circle, #ff6b6b, #ff8e53, #ffd93d, #6bcf7f);
border-radius: 50%;
animation: explosion 2s ease-out forwards;
}
.rocket-flight {
width: 40px;
height: 40px;
background: linear-gradient(45deg, #ff6b6b, #ffd93d);
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
animation: rocketFlight 3s ease-in-out forwards;
}
.rainbow-effect {
width: 100px;
height: 100px;
background: conic-gradient(from 0deg, #ff6b6b, #ffd93d, #6bcf7f, #4ecdc4, #45b7d1, #96ceb4, #feca57, #ff9ff3, #ff6b6b);
border-radius: 50%;
animation: rainbowPulse 3.5s ease-in-out forwards;
}
.bomb-effect {
width: 60px;
height: 60px;
background: radial-gradient(circle, #ff6b6b, #ff8e53);
border-radius: 50%;
animation: bombShake 0.8s ease-in-out infinite;
}
/* Loading State */
.game-board-loading {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
background-color: #f5f5f5;
border-radius: 8px;
border: 2px dashed #ddd;
}
.game-board-loading p {
color: #666;
font-size: 16px;
margin: 0;
}
/* Fall-Animation für Tiles */
.game-tile.falling {
z-index: 150;
/* transform wird über JavaScript gesetzt */
}
/* Erscheinungs-Animation für neue Tiles */
.game-tile.new-tile {
transition: all 0.5s ease-out;
transform: scale(0.1);
opacity: 0;
animation: newTileAppear 0.5s ease-out forwards;
}
/* Raketen-Power-ups */
.game-tile.rocket-horizontal,
.game-tile.rocket-vertical {
background: white !important;
position: relative;
}
/* Raketen-Symbol */
.rocket-symbol {
font-size: 24px;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
font-weight: bold;
}
/* Bomben-Power-ups */
.game-tile.bomb {
background: linear-gradient(45deg, #ff6b6b, #ff8e53) !important;
position: relative;
}
/* Bomben-Symbol */
.bomb-symbol {
font-size: 24px;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
font-weight: bold;
}
/* Raketen-Explosions-Effekt */
.rocket-explosion {
width: 80px;
height: 80px;
background: radial-gradient(circle, #ff6b6b, #ffd93d, #ff6b6b);
border-radius: 50%;
animation: rocketExplosion 1s ease-out forwards;
position: relative;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
@keyframes rocketExplosion {
0% {
transform: scale(0.1);
opacity: 0;
}
50% {
transform: scale(1.2);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
/* Raketen-Flug-Animation */
.rocket-flight-animation {
position: fixed;
z-index: 2000;
pointer-events: none;
}
.rocket-flight {
width: 40px;
height: 40px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
animation: rocketFlight 1s ease-in-out forwards;
}
@keyframes rocketFlight {
0% {
transform: scale(0.5);
opacity: 0;
}
50% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(0.8);
opacity: 0;
}
}
/* Fliegende Rakete */
.flying-rocket {
position: fixed;
z-index: 2000;
pointer-events: none;
animation: rocketFly 1s ease-in-out forwards;
}
.rocket-icon {
width: 40px;
height: 40px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
box-shadow: 0 0 10px rgba(255, 107, 107, 0.8);
}
@keyframes rocketFly {
0% {
transform: translate(0, 0) scale(1);
opacity: 1;
}
100% {
transform: translate(var(--fly-x), var(--fly-y)) scale(0.8);
opacity: 0;
}
}
@keyframes newTileAppear {
0% {
transform: scale(0.1);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
/* Responsive */
@media (max-width: 768px) {
.game-layout {
gap: 15px;
}
.stats-section {
max-width: 100%;
}
.game-tile {
width: 25px;
height: 25px;
}
.game-board {
padding: 12px;
gap: 3px;
}
.tile-icon {
font-size: 14px;
}
.moves-left-display {
padding: 5px;
}
.moves-left-label {
font-size: 14px;
}
.moves-left-value {
font-size: 20px;
}
}
</style>