feat(match3): Erweiterung der Power-Up-Mechanik und Verbesserung der Benutzeroberfläche

- Hinzufügen von Raketen-Power-ups für 4er-Matches, einschließlich Logik zur Erstellung und Anzeige dieser Power-ups.
- Anpassungen an der Darstellung von Tiles, um Raketen visuell hervorzuheben.
- Einführung neuer Soundeffekte für das Fallen von Tiles und die Erstellung von Power-ups.
- Verbesserung der Logik zur Handhabung von Matches, um 3er- und 4er-Matches zu berücksichtigen.
- Implementierung einer Methode zur Überprüfung und Auffüllung leerer gültiger Felder nach dem Fallen von Tiles.
This commit is contained in:
Torsten Schulz (local)
2025-08-25 22:16:45 +02:00
parent 285607a3ee
commit 39401840f2

View File

@@ -88,8 +88,15 @@
}">
<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) }]"
: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'
}]"
:data-index="index"
:data-type="tile ? tile.type : null"
@mousedown="onTileMouseDown($event, index)"
@mouseup="onTileMouseUp($event, index)"
@mousemove="onTileMouseMove($event)"
@@ -98,7 +105,10 @@
@touchstart="onTileMouseDown($event, index)"
@touchend="onTileMouseUp($event, index)"
@dblclick="handleDoubleClick(index, $event)">
<span v-if="tile" class="tile-symbol">{{ getTileSymbol(tile.type) }}</span>
<span v-if="tile && !isRocketTile(tile.type)" class="tile-symbol">{{ getTileSymbol(tile.type) }}</span>
<span v-else-if="tile && isRocketTile(tile.type)" class="rocket-symbol">
🚀
</span>
</div>
</template>
</div>
@@ -354,25 +364,29 @@ export default {
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.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
rainbow: this.sounds.rainbow.readyState,
falling: this.sounds.falling.readyState
});
}).catch(error => {
console.warn('⚠️ Fehler beim Laden der Sounds:', error);
@@ -1136,6 +1150,11 @@ export default {
return matches;
},
// Hilfsmethode: Prüfe ob ein Tile ein Raketen-Power-up ist
isRocketTile(tileType) {
return tileType === 'rocket-horizontal' || tileType === 'rocket-vertical';
},
// Hilfsmethode: Prüfe ob 3 Tiles einen gültigen Match bilden
isValidMatch(index1, index2, index3, board) {
// Prüfe ob alle Indizes gültig sind
@@ -1352,25 +1371,33 @@ export default {
console.log(`🔧 Verarbeite ${matches.length} Matches...`);
// WICHTIG: Filtere zuerst nach 3er-Matches und entferne Duplikate
const validMatches = matches.filter(match => match.length === 3);
// Erstelle Power-ups für 4er-Matches (BEVOR die Tiles entfernt werden)
await this.createPowerUpsForMatches(matches);
if (validMatches.length === 0) {
console.log('🔧 Keine 3er-Matches zum Verarbeiten');
// Sammle alle zu entfernenden Tile-Indizes (aus 3er-Matches UND 4er-Matches)
const tilesToRemove = new Set();
matches.forEach((match, index) => {
if (match.length >= 3) {
console.log(`🔧 ${match.length}er-Match ${index + 1}: [${match.join(', ')}]`);
match.forEach(tileIndex => {
// Raketen-Power-ups nicht entfernen
if (this.board[tileIndex] && this.board[tileIndex].type &&
(this.board[tileIndex].type === 'rocket-horizontal' || this.board[tileIndex].type === 'rocket-vertical')) {
console.log(`🔧 Rakete ${this.board[tileIndex].type} an Position ${tileIndex} wird NICHT entfernt`);
return; // Überspringe dieses Tile
}
tilesToRemove.add(tileIndex);
});
}
});
if (tilesToRemove.size === 0) {
console.log('🔧 Keine Tiles zum Entfernen gefunden');
return;
}
console.log(`🔧 ${validMatches.length} gültige 3er-Matches gefunden`);
// Sammle alle zu entfernenden Tile-Indizes (nur aus 3er-Matches)
const tilesToRemove = new Set();
validMatches.forEach((match, index) => {
console.log(`🔧 3er-Match ${index + 1}: [${match.join(', ')}]`);
match.forEach(tileIndex => {
tilesToRemove.add(tileIndex);
});
});
console.log(`🔧 ${tilesToRemove.size} Tiles zum Entfernen gefunden`);
// Starte Schrumpf-Animation für alle zu entfernenden Tiles
console.log(`🔧 Starte Schrumpf-Animation für ${tilesToRemove.size} Tiles...`);
@@ -1487,6 +1514,9 @@ export default {
}
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
@@ -1556,6 +1586,105 @@ export default {
console.log(`🔧 ${newTilesAdded} neue Tiles hinzugefügt`);
},
// Erstelle Power-ups für 4er-Matches
async createPowerUpsForMatches(matches) {
console.log('🔧 Prüfe auf Power-up-Erstellung...');
matches.forEach(match => {
if (match.length === 4) {
console.log(`🔧 4er-Match gefunden: ${match.join(', ')} - erstelle Rakete`);
// Rakete erscheint an der Position des zweiten Tiles
const rocketIndex = match[1];
const rocketTile = this.board[rocketIndex];
if (rocketTile) {
// 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`);
}
}
});
},
// 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`);
}
}
}
}
}
}
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...`);
@@ -1637,6 +1766,9 @@ export default {
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);
@@ -1932,22 +2064,23 @@ export default {
// Warte auf Animation (0,75 Sekunden)
await this.wait(750);
// Prüfe ob der Move einen 3er-Match erzeugt hat
const matches = this.findMatchesOnBoard(this.board, false);
const hasValidMatch = matches.some(match => match.length === 3);
// Prüfe ob der Move einen 3er-Match oder 4er-Match erzeugt hat
const matches = this.findMatchesOnBoard(this.board, false);
const hasValidMatch = matches.some(match => match.length >= 3);
if (hasValidMatch) {
console.log('✅ Move erfolgreich - 3er-Match 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;
}
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;
}
},
// Hilfsmethode: Warte für eine bestimmte Zeit
@@ -4963,6 +5096,21 @@ export default {
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;
}
@keyframes newTileAppear {
0% {
transform: scale(0.1);