Enhance usability and localization across components: Update USABILITY_CONCEPT.md with new focus areas, improve user feedback in AppFooter and FamilyView components, and refine text in various UI elements for better clarity and consistency. Replace console logs with user-friendly messages, correct German translations, and streamline interaction logic in multiple components.

This commit is contained in:
Torsten Schulz (local)
2026-03-20 09:41:03 +01:00
parent 1774d7df88
commit c7d33525ff
48 changed files with 1161 additions and 481 deletions

View File

@@ -129,7 +129,9 @@
@mouseenter="onTileMouseEnter($event, index)"
@mouseleave="onTileMouseLeave($event, index)"
@touchstart="onTileMouseDown($event, index)"
@touchmove.prevent="onTileMouseMove($event)"
@touchend="onTileMouseUp($event, index)"
@touchcancel="endDrag($event)"
@dblclick="handleDoubleClick(index, $event)">
<span v-if="tile" class="tile-symbol">{{ getTileSymbol(tile.type) }}</span>
</div>
@@ -148,7 +150,9 @@
</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 class="rocket-flight-path" :style="{ '--dx': (rocketEndPos.x - rocketStartPos.x) + 'px', '--dy': (rocketEndPos.y - rocketStartPos.y) + 'px' }">
<div class="rocket-flight-icon">🚀</div>
</div>
</div>
<div v-if="showRainbowEffect" class="power-up-animation" :style="{ left: rainbowCenter.x + 'px', top: rainbowCenter.y + 'px' }">
@@ -164,8 +168,8 @@
</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 v-if="rocketTarget.x > 0 && rocketTarget.y > 0" class="rocket-target-marker" :style="{ left: rocketTarget.x + 'px', top: rocketTarget.y + 'px' }">
<div class="rocket-target-marker__icon">🎯</div>
</div>
<!-- Fliegende Rakete -->
@@ -383,14 +387,110 @@ export default {
// Füge globale Event-Listener hinzu
document.addEventListener('mousemove', this.onGlobalMouseMove);
document.addEventListener('mouseup', this.onGlobalMouseUp);
document.addEventListener('touchmove', this.onGlobalMouseMove, { passive: false });
document.addEventListener('touchend', this.onGlobalMouseUp);
document.addEventListener('touchcancel', this.onGlobalMouseUp);
},
beforeUnmount() {
// Entferne globale Event-Listener
document.removeEventListener('mousemove', this.onGlobalMouseMove);
document.removeEventListener('mouseup', this.onGlobalMouseUp);
document.removeEventListener('touchmove', this.onGlobalMouseMove);
document.removeEventListener('touchend', this.onGlobalMouseUp);
document.removeEventListener('touchcancel', this.onGlobalMouseUp);
},
methods: {
createTile(type, extra = {}) {
return {
type,
id: extra.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
...extra
};
},
getTileType(tileOrType) {
if (!tileOrType) {
return null;
}
return typeof tileOrType === 'string' ? tileOrType : tileOrType.type;
},
getPowerUpKind(tileOrType) {
const tileType = this.getTileType(tileOrType);
if (this.isRocketTile(tileType)) {
return 'rocket';
}
if (tileType === 'bomb' || tileType === 'rainbow') {
return tileType;
}
return null;
},
getPointerCoordinates(event) {
const pointer = event?.touches?.[0] || event?.changedTouches?.[0];
const x = pointer?.clientX ?? event?.clientX ?? null;
const y = pointer?.clientY ?? event?.clientY ?? null;
if (x === null || y === null) {
return null;
}
return { x, y };
},
getSwipeDirection(clientX, clientY, threshold = 18) {
if (this.dragStartX === null || this.dragStartY === null) {
return null;
}
const deltaX = clientX - this.dragStartX;
const deltaY = clientY - this.dragStartY;
if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) {
return null;
}
return Math.abs(deltaX) >= Math.abs(deltaY)
? (deltaX > 0 ? 'right' : 'left')
: (deltaY > 0 ? 'down' : 'up');
},
resolveDragTargetIndex(event, fallbackTileIndex = null) {
if (this.currentlyAnimatingTile !== null && this.currentlyAnimatingTile !== this.draggedTileIndex) {
return this.currentlyAnimatingTile;
}
if (
fallbackTileIndex !== null &&
fallbackTileIndex !== this.draggedTileIndex &&
this.areTilesAdjacent(this.draggedTileIndex, fallbackTileIndex)
) {
return fallbackTileIndex;
}
const pointer = this.getPointerCoordinates(event);
if (!pointer) {
return null;
}
const pointerTargetIndex = this.findTileAtPosition(pointer.x, pointer.y);
if (
pointerTargetIndex !== null &&
pointerTargetIndex !== this.draggedTileIndex &&
this.areTilesAdjacent(this.draggedTileIndex, pointerTargetIndex)
) {
return pointerTargetIndex;
}
const swipeDirection = this.getSwipeDirection(pointer.x, pointer.y);
return swipeDirection ? this.getAdjacentIndex(this.draggedTileIndex, swipeDirection) : null;
},
// Initialisiere Sound-Effekte
initializeSounds() {
try {
@@ -1467,18 +1567,20 @@ export default {
// Hilfsmethode: Prüfe ob ein Tile ein Raketen-Power-up ist
isRocketTile(tileType) {
return tileType === 'rocket' || tileType === 'rocket-horizontal' || tileType === 'rocket-vertical';
const normalizedType = this.getTileType(tileType);
return normalizedType === 'rocket' || normalizedType === 'rocket-horizontal' || normalizedType === 'rocket-vertical';
},
// Hilfsmethode: Prüfe ob ein Tile ein Regenbogen-Power-up ist
isRainbowTile(tileType) {
return tileType === 'rainbow';
return this.getTileType(tileType) === 'rainbow';
},
// Hilfsmethode: Prüfe ob ein Tile ein Power-up ist
isPowerUpTile(tileType) {
if (!tileType) return false;
return this.isRocketTile(tileType) || tileType === 'bomb' || tileType === 'rocket-horizontal' || tileType === 'rocket-vertical' || this.isRainbowTile(tileType);
const normalizedType = this.getTileType(tileType);
if (!normalizedType) return false;
return this.isRocketTile(normalizedType) || normalizedType === 'bomb' || this.isRainbowTile(normalizedType);
},
// Hilfsmethode: Debug-Ausgabe für Power-ups
@@ -1932,21 +2034,11 @@ export default {
// JETZT erst Power-ups erstellen, nachdem die Tiles entfernt wurden
debugLog('🔧 Erstelle Power-ups nach der Tile-Entfernung...');
const powerUpsCreated = await this.createPowerUpsForMatches(matches);
await this.createPowerUpsForMatches(matches);
// Wenn Raketen erstellt wurden, lass sie im nächsten Zug starten
if (powerUpsCreated && powerUpsCreated.rockets && powerUpsCreated.rockets.length > 0) {
debugLog(`🚀 ${powerUpsCreated.rockets.length} Raketen erstellt - werden im nächsten Zug aktiviert`);
// Aktualisiere die Anzeige
this.$forceUpdate();
// Warte kurz, damit die Rakete sichtbar wird
await this.wait(300);
// KEINE automatische Aktivierung - Raketen bleiben auf dem Board
// und werden erst durch Spieler-Aktionen aktiviert
}
this.$forceUpdate();
await this.wait(120);
// Debug: Zeige alle Power-ups nach der Erstellung
debugLog('🔧 Debug: Alle Power-ups nach createPowerUpsForMatches:');
@@ -1994,36 +2086,12 @@ export default {
debugLog('🔧 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 || powerUpsCreated.count === 0) {
// Nur Cascade-Matches prüfen, wenn keine Power-ups erstellt wurden
await this.checkForCascadeMatches();
} else {
debugLog(`🔧 ${powerUpsCreated.count} Power-ups erstellt - überspringe Cascade-Match-Prüfung`);
// Debug: Zeige alle Power-ups nach der Verarbeitung
debugLog('🔧 Debug: Alle Power-ups nach Power-up-Verarbeitung:');
this.debugPowerUps();
// Debug: Zeige alle Power-ups im Template nach der Verarbeitung
debugLog('🔧 Debug: Power-ups im Template nach Verarbeitung:');
for (let i = 0; i < this.board.length; i++) {
if (this.board[i]) {
const isPowerUp = this.isPowerUpTile(this.board[i].type);
debugLog(`🔧 Position ${i}: Tile ${this.board[i].type}, isPowerUpTile: ${isPowerUp}`);
if (isPowerUp) {
debugLog(`🔧 ✅ Power-up im Template: ${this.board[i].type} an Position ${i}`);
}
}
}
await this.checkForCascadeMatches();
// WICHTIG: Prüfe Level-Objekte nach dem Verarbeiten der Matches
if (isPlayerMove && !this.isInitializingLevel) {
debugLog('🎯 Prüfe Level-Objekte nach Match-Verarbeitung...');
this.checkLevelObjectives();
}
this.checkLevelObjectives();
}
},
@@ -2051,15 +2119,9 @@ export default {
// Wenn Position leer ist, prüfe ob sie im Layout gültig ist
if (!this.board[index]) {
if (this.currentLevelData && this.currentLevelData.boardLayout) {
const layoutRows = this.currentLevelData.boardLayout.split('\n');
if (row < layoutRows.length && col < layoutRows[row].length) {
const targetChar = layoutRows[row][col];
if (targetChar !== 'x') {
debugLog(`🔧 Position [${row}, ${col}] ist ungültig im Layout (${targetChar}) - überspringe`);
continue; // Überspringe ungültige Positionen
}
}
if (!this.isPlayableBoardCell(row, col)) {
debugLog(`🔧 Position [${row}, ${col}] ist ein Design-Leerfeld - überspringe`);
continue;
}
debugLog(`🔧 Leere Position gefunden: [${row}, ${col}] -> Index ${index}`);
@@ -2069,7 +2131,7 @@ export default {
for (let searchRow = row - 1; searchRow >= 0; searchRow--) {
const searchIndex = this.coordsToIndex(searchRow, col);
if (this.board[searchIndex] && !this.isPowerUpTile(this.board[searchIndex].type)) {
if (this.board[searchIndex]) {
debugLog(`🔧 Tile ${this.board[searchIndex].type} gefunden an [${searchRow}, ${col}] -> verschiebe nach [${row}, ${col}]`);
// Verschiebe Tile nach unten
@@ -2079,8 +2141,8 @@ export default {
// Aktualisiere DOM
this.$forceUpdate();
// Warte kurz für Animation
await this.wait(500);
// Kurze Wartezeit, damit die Bewegung sichtbar bleibt ohne das Spiel zu bremsen
await this.wait(120);
hasChanges = true;
tileFound = true;
@@ -2104,15 +2166,9 @@ export default {
// Wenn oberste Position leer ist, prüfe ob sie im Layout gültig ist
if (!this.board[index]) {
if (this.currentLevelData && this.currentLevelData.boardLayout) {
const layoutRows = this.currentLevelData.boardLayout.split('\n');
if (0 < layoutRows.length && col < layoutRows[0].length) {
const targetChar = layoutRows[0][col];
if (targetChar !== 'x') {
debugLog(`🔧 Oberste Position [0, ${col}] ist ungültig im Layout (${targetChar}) - überspringe`);
continue;
}
}
if (!this.isPlayableBoardCell(0, col)) {
debugLog(`🔧 Oberste Position [0, ${col}] ist ein Design-Leerfeld - überspringe`);
continue;
}
// Erstelle neues Tile
@@ -2125,7 +2181,7 @@ export default {
// Aktualisiere DOM nach dem Auffüllen
this.$forceUpdate();
await this.wait(100);
await this.wait(40);
}
debugLog(`🔧 Iteration ${iteration} abgeschlossen - Änderungen: ${hasChanges}`);
@@ -2137,6 +2193,37 @@ export default {
debugLog('🔧 Fall-Down-Logik abgeschlossen');
},
isPlayableBoardCell(row, col) {
if (row < 0 || row >= this.boardHeight || col < 0 || col >= this.boardWidth) {
return false;
}
if (this.boardLayout?.[row]?.[col]) {
return this.boardLayout[row][col].type !== 'empty';
}
return true;
},
collectEmptyPlayableFields() {
const emptyPlayableFields = [];
for (let row = 0; row < this.boardHeight; row++) {
for (let col = 0; col < this.boardWidth; col++) {
if (!this.isPlayableBoardCell(row, col)) {
continue;
}
const index = this.coordsToIndex(row, col);
if (index !== null && !this.board[index]) {
emptyPlayableFields.push({ index, row, col });
}
}
}
return emptyPlayableFields;
},
// Hilfsfunktion: Zeige den aktuellen Board-Zustand in der Konsole
printBoardState() {
@@ -2226,8 +2313,8 @@ export default {
if (newTilesAdded > 0) {
debugLog('🔍 Überprüfe das Brett auf Matches nach dem Füllen der leeren Positionen...');
// Warte kurz, damit die neuen Tiles vollständig angezeigt werden
await this.wait(300);
// Nur kurz warten, damit das Brett aktualisiert ist
await this.wait(80);
// Prüfe auf Matches auf dem aktuellen Board
const matchesAfterFill = this.findMatchesOnBoard(this.board, false);
@@ -2285,7 +2372,7 @@ export default {
match.forEach(pos => usedPositions.add(pos));
// Erstelle Regenbogen-Tile
this.board[rainbowIndex] = { type: 'rainbow' };
this.board[rainbowIndex] = this.createTile('rainbow');
debugLog(`🌈 Regenbogen-Tile an Position ${rainbowIndex} erstellt`);
debugLog(`🔧 Board[${rainbowIndex}] = ${JSON.stringify(this.board[rainbowIndex])}`);
@@ -2309,7 +2396,7 @@ export default {
usedPositions.add(match.corner);
// Bombe an der Ecke erstellen
this.board[match.corner] = { type: 'bomb' };
this.board[match.corner] = this.createTile('bomb');
debugLog(`💣 Bombe an Position ${match.corner} erstellt`);
debugLog(`🔧 Board[${match.corner}] = ${JSON.stringify(this.board[match.corner])}`);
@@ -2337,7 +2424,7 @@ export default {
// Erstelle Rakete basierend auf der Richtung des Matches
const rocketType = this.determineRocketType(match);
this.board[rocketIndex] = { type: rocketType };
this.board[rocketIndex] = this.createTile(rocketType);
debugLog(`🚀 Rakete ${rocketType} an Position ${rocketIndex} erstellt`);
debugLog(`🔧 Board[${rocketIndex}] = ${JSON.stringify(this.board[rocketIndex])}`);
@@ -2380,99 +2467,45 @@ export default {
async checkAndFillEmptyValidFields() {
debugLog('🔧 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;
debugLog(`🔧 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
debugLog(`🔧 Position [${row}, ${col}] enthält bereits Power-up ${this.board[index].type} - wird nicht überschrieben`);
}
}
}
if (hasEmptyValidFields) {
debugLog(`🔧 ${emptyValidFields.length} leere gültige Felder gefunden - starte erneuten Fall-Prozess`);
// Fülle alle leeren gültigen Felder mit neuen Tiles
let safetyCounter = 0;
let emptyValidFields = this.collectEmptyPlayableFields();
while (emptyValidFields.length > 0 && safetyCounter < 8) {
safetyCounter++;
debugLog(`🔧 ${emptyValidFields.length} leere gültige Felder gefunden - Füllrunde ${safetyCounter}`);
for (const field of emptyValidFields) {
const newTile = this.createRandomTile();
this.board[field.index] = newTile;
debugLog(`🔧 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
await this.animateNewTilesAppearing(emptyValidFields.map(field => ({
index: field.index,
row: field.row,
col: field.col,
type: this.board[field.index].type
})));
debugLog(`🔧 Alle leeren gültigen Felder gefüllt`);
// WICHTIG: Nach dem Füllen der leeren gültigen Felder das Brett auf Matches überprüfen
debugLog('🔍 Überprüfe das Brett auf Matches nach dem Füllen der leeren gültigen Felder...');
// Warte kurz, damit die neuen Tiles vollständig angezeigt werden
await this.wait(300);
// Prüfe auf Matches auf dem aktuellen Board
const matchesAfterFill = this.findMatchesOnBoard(this.board, false);
if (matchesAfterFill.length > 0) {
debugLog(`🔍 ${matchesAfterFill.length} Match(es) nach dem Füllen der leeren gültigen Felder gefunden - starte automatische Behandlung`);
// Behandle die gefundenen Matches automatisch (kein Spieler-Move)
await this.handleMatches(matchesAfterFill, false);
// WICHTIG: Rekursiver Aufruf, falls durch die Matches neue leere Positionen entstehen
// Das verhindert Endlosschleifen durch max. 3 Rekursionen
if (this.recursionDepth === undefined) {
this.recursionDepth = 0;
}
if (this.recursionDepth < 3) {
this.recursionDepth++;
debugLog(`🔄 Rekursiver Aufruf ${this.recursionDepth}/3 - prüfe auf weitere leere gültige Felder`);
// Prüfe erneut auf leere gültige Felder und fülle sie auf
await this.checkAndFillEmptyValidFields();
this.recursionDepth--;
} else {
debugLog('⚠️ Maximale Rekursionstiefe erreicht - stoppe automatische Match-Behandlung');
}
} else {
debugLog('✅ Keine Matches nach dem Füllen der leeren gültigen Felder gefunden - Board ist bereit');
}
} else {
debugLog('🔧 Alle gültigen Felder enthalten Tiles - Board ist vollständig');
await this.wait(40);
await this.fallTilesDown();
emptyValidFields = this.collectEmptyPlayableFields();
}
if (safetyCounter >= 8 && emptyValidFields.length > 0) {
debugLog(`⚠️ Brett konnte nach ${safetyCounter} Füllrunden nicht vollständig stabilisiert werden`);
}
if (emptyValidFields.length === 0) {
debugLog('🔧 Alle spielbaren Felder enthalten Tiles - Board ist vollständig');
}
const matchesAfterFill = this.findMatchesOnBoard(this.board, false);
if (matchesAfterFill.length > 0) {
debugLog(`🔍 ${matchesAfterFill.length} Match(es) nach der Stabilisierung gefunden - starte automatische Behandlung`);
await this.handleMatches(matchesAfterFill, false);
}
},
@@ -2498,8 +2531,8 @@ export default {
debugLog(`🎬 ${tileElements.length} DOM-Elemente für Animation vorbereitet`);
// Warte auf die Animation (0,75 Sekunden)
await this.wait(750);
// Warte auf die Animation
await this.wait(280);
// Entferne die CSS-Klassen
tileElements.forEach(element => {
@@ -2525,7 +2558,7 @@ export default {
// Setze das Tile an seine ursprüngliche Position (oben) mit transform
element.style.transform = `translateY(-${fallPixels}px)`;
element.style.transition = 'transform 0.4s ease-out';
element.style.transition = 'transform 0.18s ease-out';
// Füge CSS-Klasse für die Fall-Animation hinzu
element.classList.add('falling');
@@ -2553,8 +2586,8 @@ export default {
// Spiele Fall-Sound ab
this.playSound('falling');
// Warte auf die Fall-Animation (0,4 Sekunden)
await this.wait(400);
// Warte auf die Fall-Animation
await this.wait(180);
// Entferne die CSS-Klassen und transform-Eigenschaften
tileElements.forEach(element => {
@@ -2588,8 +2621,8 @@ export default {
debugLog(`🎬 ${tileElements.length} DOM-Elemente für Erscheinungs-Animation vorbereitet`);
// Warte auf die Erscheinungs-Animation (0,5 Sekunden)
await this.wait(500);
// Warte auf die Erscheinungs-Animation
await this.wait(180);
// Entferne die CSS-Klassen
tileElements.forEach(element => {
@@ -2606,12 +2639,12 @@ export default {
const randomType = this.currentLevelData.tileTypes[
Math.floor(Math.random() * this.currentLevelData.tileTypes.length)
];
return { type: randomType };
return this.createTile(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 };
return this.createTile(randomType);
}
},
@@ -2802,28 +2835,15 @@ export default {
async checkForCascadeMatches() {
debugLog('🔧 Prüfe auf Cascade-Matches...');
// Warte kurz, damit alle Animationen abgeschlossen sind
await this.wait(200);
// Nur kurze Synchronisierung nach dem Fallen
await this.wait(60);
// 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') {
debugLog('🔧 L-Form Match in Cascade gefunden - überspringe');
return false;
}
if (Array.isArray(match) && match.length === 4) {
debugLog('🔧 4er-Match in Cascade gefunden - überspringe');
return false;
}
return true;
});
if (filteredMatches.length > 0) {
debugLog(`🔧 ${filteredMatches.length} neue Cascade-Matches gefunden`);
await this.handleMatches(filteredMatches, false);
if (newMatches.length > 0) {
debugLog(`🔧 ${newMatches.length} neue Cascade-Matches gefunden`);
await this.handleMatches(newMatches, false);
} else {
debugLog('🔧 Keine neuen Cascade-Matches gefunden');
}
@@ -3411,14 +3431,19 @@ export default {
}
debugLog(`🔧 Starte Drag für Tile ${tileIndex}`);
const pointer = this.getPointerCoordinates(event);
if (!pointer) {
return;
}
// Setze Drag-Status
this.draggedTileIndex = tileIndex;
this.isDragging = true;
// Speichere Start-Position für Drag-Offset
this.dragStartX = event.clientX;
this.dragStartY = event.clientY;
this.dragStartX = pointer.x;
this.dragStartY = pointer.y;
// WICHTIG: Speichere die ursprüngliche Position des gedraggten Tiles
const tileElement = event.target.closest('.game-tile');
@@ -3483,12 +3508,21 @@ export default {
if (!this.isDragging || this.draggedTileIndex === null) {
return;
}
const pointer = this.getPointerCoordinates(event);
if (!pointer) {
return;
}
if (event?.cancelable) {
event.preventDefault();
}
debugLog(`🔧 onTileMouseMove: clientX=${event.clientX}, clientY=${event.clientY}`);
debugLog(`🔧 onTileMouseMove: clientX=${pointer.x}, clientY=${pointer.y}`);
// Berechne Drag-Offset
const deltaX = event.clientX - this.dragStartX;
const deltaY = event.clientY - this.dragStartY;
const deltaX = pointer.x - this.dragStartX;
const deltaY = pointer.y - this.dragStartY;
debugLog(`🔧 Drag-Offset: deltaX=${deltaX}px, deltaY=${deltaY}px`);
@@ -3724,7 +3758,7 @@ export default {
onGlobalMouseUp(event) {
if (this.isDragging) {
debugLog(`🔧 Globaler MouseUp während Drag - beende Drag`);
this.endDrag();
this.endDrag(event);
}
},
@@ -3744,23 +3778,11 @@ export default {
debugLog(`🔧 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;
debugLog(`🔧 Ziel-Tile ${tileIndex} ist animiert - führe Move durch`);
} else {
// Das Ziel-Tile ist nicht animiert - kein Move
debugLog(`🔧 Ziel-Tile ${tileIndex} ist nicht animiert - kein Move`);
}
} else {
debugLog(`🔧 Gleiches Tile, kein Move erforderlich`);
}
const targetTileIndex = this.resolveDragTargetIndex(event, tileIndex);
const shouldPerformMove =
targetTileIndex !== null &&
targetTileIndex !== this.draggedTileIndex &&
this.areTilesAdjacent(this.draggedTileIndex, targetTileIndex);
// Setze alle Animationen zurück
this.resetAllTileAnimations();
@@ -3786,7 +3808,7 @@ export default {
},
// Beende den Drag korrekt
endDrag() {
endDrag(event = null) {
debugLog(`🔧 endDrag aufgerufen`);
if (!this.isDragging) {
@@ -3795,20 +3817,11 @@ export default {
}
// 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;
debugLog(`🔧 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
debugLog(`🔧 Gedraggtes Tile ist auf sich selbst animiert - kein Move`);
} else {
debugLog(`🔧 Kein Tile animiert - kein Move`);
}
let targetTileIndex = this.resolveDragTargetIndex(event);
let shouldPerformMove =
targetTileIndex !== null &&
targetTileIndex !== this.draggedTileIndex &&
this.areTilesAdjacent(this.draggedTileIndex, targetTileIndex);
// Zusätzliche Prüfung: Wenn das gedraggte Tile fast an seiner ursprünglichen Position ist, kein Move
if (shouldPerformMove && this.originalTilePosition) {
@@ -4396,7 +4409,8 @@ export default {
// Hilfsmethode: Prüfe ob ein Tile ein Power-Up ist
isPowerUpTile(tile) {
return tile && this.powerUpTypes.includes(tile.type);
const tileType = this.getTileType(tile);
return this.getPowerUpKind(tileType) !== null;
},
// Hilfsmethode: Prüfe ob zwei Tiles matchen können
@@ -4992,8 +5006,17 @@ export default {
// Power-Up Tausch als Zug zählen (wird auch über swapTiles aufgerufen)
// WICHTIG: Zähle den Zug nur einmal hier, nicht in den nachfolgenden Funktionen
this.countPowerUpMove();
const tile1Type = this.getTileType(originalTile1);
const tile2Type = this.getTileType(originalTile2);
const tile1IsRocket = this.isRocketTile(tile1Type);
const tile2IsRocket = this.isRocketTile(tile2Type);
const tile1IsBomb = tile1Type === 'bomb';
const tile2IsBomb = tile2Type === 'bomb';
const tile1IsRainbow = tile1Type === 'rainbow';
const tile2IsRainbow = tile2Type === 'rainbow';
if (originalTile1.type === 'rainbow' && originalTile2.type === 'rainbow') {
if (tile1IsRainbow && tile2IsRainbow) {
// Spiele Regenbogen-Sound
this.playSound('rainbow');
@@ -5001,29 +5024,48 @@ export default {
debugLog('🌈 Zwei Regenbogen-Tiles kombiniert - entferne alle Tiles vom Board!');
await this.removeAllTilesFromBoardIncludingRainbows();
return true;
} else if (originalTile1.type === 'rainbow' || originalTile2.type === 'rainbow') {
} else if ((tile1IsBomb && tile2IsRainbow) || (tile1IsRainbow && tile2IsBomb)) {
this.playSound('rainbow');
this.createRandomBombs(20);
setTimeout(() => {
this.detonateAllBombs();
}, 500);
return true;
} else if ((tile1IsRocket && tile2IsRainbow) || (tile1IsRainbow && tile2IsRocket)) {
this.playSound('rainbow');
this.createRandomRockets(10);
setTimeout(() => {
this.launchAllRockets();
}, 500);
return true;
} else if (tile1IsRainbow || tile2IsRainbow) {
// 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;
const rainbowIndex = originalTile1.type === 'rainbow' ? this.findTileIndex(originalTile1) : this.findTileIndex(originalTile2);
const normalTile = tile1IsRainbow ? originalTile2 : originalTile1;
const rainbowIndex = tile1IsRainbow ? this.findTileIndex(originalTile1) : this.findTileIndex(originalTile2);
if (rainbowIndex !== null) {
await this.activateRainbowByType(rainbowIndex, normalTile.type);
}
return true;
} else if (originalTile1.type === 'bomb' && originalTile2.type === 'bomb') {
} else if (tile1IsBomb && tile2IsBomb) {
// Zwei Bomben-Tiles getauscht: Entferne 2 Ringe (5x5 Bereich)
const bombIndex = this.findBombIndex(originalTile1, originalTile2);
if (bombIndex !== null) {
this.explodeBomb(bombIndex, 2, true); // 2 Ringe = 5x5 Bereich - manuelle Aktivierung
}
return true;
} else if (originalTile1.type === 'bomb' || originalTile2.type === 'bomb') {
} else if ((tile1IsRocket && tile2IsBomb) || (tile1IsBomb && tile2IsRocket)) {
this.handleRocketBombCombination(originalTile1, originalTile2);
return true;
} else if (tile1IsRocket && tile2IsRocket) {
this.handleRocketConnection(originalTile1, originalTile2);
return true;
} else if (tile1IsBomb || tile2IsBomb) {
// 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;
const bombTile = tile1IsBomb ? originalTile1 : originalTile2;
// Finde die neue Position der Bombe
const newBombIndex = this.findTileIndex(bombTile);
@@ -5031,41 +5073,10 @@ export default {
this.explodeBomb(newBombIndex, 1, true); // 1 Ring = 3x3 Bereich - manuelle Aktivierung
}
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') {
} else if (tile1IsRocket || tile2IsRocket) {
// 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;
const rocketTile = tile1IsRocket ? originalTile1 : originalTile2;
const targetTile = tile1IsRocket ? originalTile2 : originalTile1;
this.handleRocketLaunch(rocketTile, targetTile);
return true;
}
@@ -5087,7 +5098,10 @@ export default {
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) {
if (this.board[i] === originalTile) {
return i;
}
if (this.board[i] && originalTile && this.board[i].id && originalTile.id && this.board[i].id === originalTile.id) {
return i;
}
}
@@ -5291,7 +5305,7 @@ export default {
if (powerUp.type === 'bomb') {
// Bombe explodiert mit 1 Ring (3x3 Bereich)
this.explodeBomb(powerUp.index, 1, true);
} else if (powerUp.type === 'rocket') {
} else if (this.isRocketTile(powerUp.type)) {
// Rakete startet auf zufälliges Feld
this.launchRocketToRandomField(powerUp.index);
}
@@ -5401,7 +5415,7 @@ export default {
// Finde alle Raketen auf dem Board
for (let i = 0; i < this.board.length; i++) {
if (this.board[i] && this.board[i].type === 'rocket') {
if (this.board[i] && this.isRocketTile(this.board[i].type)) {
rocketIndices.push(i);
}
}
@@ -5424,7 +5438,7 @@ export default {
// 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 rocketTile = this.isRocketTile(originalTile1.type) ? originalTile1 : originalTile2;
const bombTile = originalTile1.type === 'bomb' ? originalTile1 : originalTile2;
// Finde die Position auf dem Board
@@ -5562,12 +5576,18 @@ export default {
x: endRect.left - boardRect.left + endRect.width / 2 - 20,
y: endRect.top - boardRect.top + endRect.height / 2 - 20
};
this.rocketTarget = {
x: endRect.left - boardRect.left + endRect.width / 2,
y: endRect.top - boardRect.top + endRect.height / 2
};
this.showRocketFlight = true;
// Verstecke Animation nach der Dauer
setTimeout(() => {
this.showRocketFlight = false;
this.rocketTarget = { x: -1, y: -1 };
}, 1200);
}
});
@@ -6410,6 +6430,7 @@ export default {
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 */
touch-action: none;
}
.game-tile {
@@ -6427,6 +6448,7 @@ export default {
pointer-events: auto;
user-select: none;
position: relative;
touch-action: none;
}
/* Vergrößere den klickbaren Bereich für besseres Drag&Drop */
@@ -6478,7 +6500,7 @@ export default {
/* Schrumpf-Animation für das Entfernen von Tiles */
.game-tile.removing {
transition: all 0.75s ease-out;
transition: all 0.28s ease-out;
transform: scale(0.1) rotate(360deg);
opacity: 0;
z-index: 200;
@@ -6498,12 +6520,12 @@ export default {
/* Fall-Animation */
.game-tile.falling {
transition: transform 0.5s ease-in, opacity 0.3s ease-out;
transition: transform 0.18s ease-in, opacity 0.18s ease-out;
z-index: 100;
}
.game-tile.new-tile {
transition: opacity 0.5s ease-in;
transition: opacity 0.18s ease-in;
z-index: 100;
}
@@ -6741,12 +6763,25 @@ export default {
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;
.rocket-flight-path {
position: relative;
width: 0;
height: 0;
animation: rocketTravel 0.9s ease-in-out forwards;
}
.rocket-flight-icon {
width: 44px;
height: 44px;
margin-left: -22px;
margin-top: -22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
background: linear-gradient(135deg, #fff7e8, #ffd7a8);
box-shadow: 0 10px 24px rgba(181, 94, 21, 0.28);
}
.rainbow-effect {
@@ -6895,35 +6930,51 @@ export default {
}
/* Raketen-Flug-Animation */
.rocket-flight-animation {
position: fixed;
z-index: 2000;
.rocket-target-marker {
position: absolute;
z-index: 1400;
pointer-events: none;
transform: translate(-50%, -50%);
}
.rocket-flight {
.rocket-target-marker__icon {
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;
font-size: 22px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 0 0 6px rgba(255, 107, 107, 0.18);
animation: rocketTargetPulse 1.2s ease-in-out forwards;
}
@keyframes rocketFlight {
@keyframes rocketTravel {
0% {
transform: scale(0.5);
transform: translate(0, 0) scale(0.7) rotate(-10deg);
opacity: 0;
}
50% {
transform: scale(1);
15% {
opacity: 1;
}
100% {
transform: scale(0.8);
transform: translate(var(--dx), var(--dy)) scale(1) rotate(8deg);
opacity: 1;
}
}
@keyframes rocketTargetPulse {
0% {
transform: scale(0.6);
opacity: 0;
}
35% {
transform: scale(1.08);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}

View File

@@ -411,7 +411,6 @@ export default {
}, 2 * 60 * 1000); // alle 2 Minuten
},
beforeUnmount() {
console.log('🚪 Component unmounting, cleaning up...');
this.cleanup();
},
methods: {
@@ -854,8 +853,6 @@ export default {
},
cleanup() {
console.log('🧹 Starting cleanup...');
// Game Loop stoppen
if (this.gameLoop) {
clearTimeout(this.gameLoop);
@@ -929,20 +926,15 @@ export default {
this.passengerImages = {};
this.carImage = null; // Auto-Bild bereinigen
this.tiles = null;
console.log('🧹 Cleanup completed');
},
// Regelmäßige Memory-Cleanup-Methode
performMemoryCleanup() {
console.log('🧹 Performing memory cleanup...');
// Canvas NICHT leeren das verursacht sichtbares Flackern / Grau
// Wir verlassen uns auf das reguläre render() zum Überschreiben des Frames
// Traffic Light States aggressiver bereinigen
if (this.trafficLightStates && Object.keys(this.trafficLightStates).length > 20) {
console.log('🧹 Cleaning up traffic light states');
// Nur States für aktuelle Map behalten
if (this.currentMap && this.currentMap.tiles) {
const currentTileKeys = new Set();
@@ -966,12 +958,10 @@ export default {
// Passagier-Listen aggressiver begrenzen
if (this.waitingPassengersList && this.waitingPassengersList.length > 20) {
console.log('🧹 Trimming waiting passengers list');
this.waitingPassengersList = this.waitingPassengersList.slice(-10);
}
if (this.loadedPassengersList && this.loadedPassengersList.length > 20) {
console.log('🧹 Trimming loaded passengers list');
this.loadedPassengersList = this.loadedPassengersList.slice(-10);
}
@@ -989,8 +979,6 @@ export default {
if (window.gc) {
window.gc();
}
console.log('🧹 Memory cleanup completed');
},
generateLevel() {