- Ü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.
6013 lines
209 KiB
Vue
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>
|
|
|