import Match3Campaign from '../models/match3/campaign.js'; import Match3Level from '../models/match3/level.js'; import Match3Objective from '../models/match3/objective.js'; import Match3UserProgress from '../models/match3/userProgress.js'; import Match3UserLevelProgress from '../models/match3/userLevelProgress.js'; import Match3TileType from '../models/match3/tileType.js'; import Match3LevelTileType from '../models/match3/levelTileType.js'; class Match3Service { // Lade alle aktiven Kampagnen async getCampaigns() { const campaigns = await Match3Campaign.findAll({ where: { isActive: true }, include: [ { model: Match3Level, as: 'levels', where: { isActive: true }, required: false, include: [ { model: Match3Objective, as: 'objectives', where: { isRequired: true }, required: false } ] } ], order: [ ['id', 'ASC'], [{ model: Match3Level, as: 'levels' }, 'order', 'ASC'], [{ model: Match3Level, as: 'levels' }, { model: Match3Objective, as: 'objectives' }, 'order', 'ASC'] ] }); return campaigns; } // Lade eine spezifische Kampagne async getCampaign(campaignId) { try { // Lade zuerst die Kampagne ohne levelTileTypes const campaign = await Match3Campaign.findByPk(campaignId, { include: [ { model: Match3Level, as: 'levels', where: { isActive: true }, required: false, include: [ { model: Match3Objective, as: 'objectives', where: { isRequired: true }, required: false } ] } ], order: [ [{ model: Match3Level, as: 'levels' }, 'order', 'ASC'], [{ model: Match3Level, as: 'levels' }, { model: Match3Objective, as: 'objectives' }, 'order', 'ASC'] ] }); if (!campaign) { throw { status: 404, message: 'Campaign not found' }; } // Versuche levelTileTypes zu laden, aber mache es optional try { const levelsWithTileTypes = await Promise.all( campaign.levels.map(async (level) => { try { const levelTileTypes = await Match3LevelTileType.findAll({ where: { levelId: level.id, isActive: true }, include: [ { model: Match3TileType, as: 'tileType', where: { isActive: true }, required: true } ], order: [['weight', 'DESC']] }); return { ...level.toJSON(), levelTileTypes: levelTileTypes }; } catch (error) { console.log(`Warnung: Konnte levelTileTypes für Level ${level.id} nicht laden:`, error.message); // Fallback: Verwende die alten tileTypes return { ...level.toJSON(), levelTileTypes: [] }; } }) ); campaign.levels = levelsWithTileTypes; } catch (error) { console.log('Warnung: Konnte levelTileTypes nicht laden, verwende Fallback:', error.message); // Fallback: Verwende die alten tileTypes campaign.levels = campaign.levels.map(level => ({ ...level.toJSON(), levelTileTypes: [] })); } return campaign; } catch (error) { console.error('Fehler beim Laden der Kampagne:', error); throw error; } } // Lade ein spezifisches Level async getLevel(levelId) { const level = await Match3Level.findByPk(levelId, { include: [ { model: Match3Objective, as: 'objectives', where: { isRequired: true }, required: false, order: [['order', 'ASC']] } ] }); if (!level) { throw { status: 404, message: 'Level not found' }; } // Ergänze levelTileTypes wie in getCampaign (optional) try { const levelTileTypes = await Match3LevelTileType.findAll({ where: { levelId: level.id, isActive: true }, include: [ { model: Match3TileType, as: 'tileType', where: { isActive: true }, required: true } ], order: [['weight', 'DESC']] }); return { ...level.toJSON(), levelTileTypes }; } catch (e) { return { ...level.toJSON(), levelTileTypes: [] }; } } // Lade Benutzer-Fortschritt für eine Kampagne async getUserProgress(userId, campaignId) { let userProgress = await Match3UserProgress.findOne({ where: { userId, campaignId }, include: [ { model: Match3UserLevelProgress, as: 'levelProgress', include: [ { model: Match3Level, as: 'level' } ] } ] }); if (!userProgress) { // Erstelle neuen Fortschritt wenn keiner existiert userProgress = await Match3UserProgress.create({ userId, campaignId, totalScore: 0, totalStars: 0, levelsCompleted: 0, currentLevel: 1, isCompleted: false }); } // Validiere und korrigiere currentLevel falls nötig if (userProgress.currentLevel < 1 || userProgress.currentLevel > 1000) { const correctLevel = userProgress.levelsCompleted + 1; await userProgress.update({ currentLevel: correctLevel }); } return userProgress; } // Aktualisiere Level-Fortschritt async updateLevelProgress(userId, campaignId, levelId, score, moves, time, stars, securityHash, timestamp) { // ANTI-CHEAT-VALIDIERUNG if (!this.validateProgressHash(userId, campaignId, levelId, score, moves, time, stars, securityHash, timestamp)) { throw { status: 403, message: 'Progress validation failed - possible cheating detected' }; } // Lade oder erstelle Benutzer-Fortschritt let userProgress = await Match3UserProgress.findOne({ where: { userId, campaignId } }); if (!userProgress) { userProgress = await Match3UserProgress.create({ userId, campaignId, totalScore: 0, totalStars: 0, levelsCompleted: 0, currentLevel: 1, isCompleted: false }); } // Lade oder erstelle Level-Fortschritt let levelProgress = await Match3UserLevelProgress.findOne({ where: { userProgressId: userProgress.id, levelId } }); if (!levelProgress) { levelProgress = await Match3UserLevelProgress.create({ userProgressId: userProgress.id, levelId, score: 0, moves: 0, time: 0, stars: 0, isCompleted: false, attempts: 0, bestScore: 0, bestMoves: 0, bestTime: 0 }); } // Aktualisiere Level-Fortschritt // WICHTIG: Ein Level ist nur abgeschlossen, wenn es mindestens 1 Stern gibt UND der Score > 0 ist // Das verhindert, dass unvollständige Level als abgeschlossen markiert werden const isCompleted = stars > 0 && score > 0; const attempts = levelProgress.attempts + 1; await levelProgress.update({ score: Math.max(score, levelProgress.bestScore), moves: moves, time: time, stars: Math.max(stars, levelProgress.stars), // WICHTIG: isCompleted wird NUR auf true gesetzt, wenn das Level tatsächlich abgeschlossen ist // Der alte Status wird NICHT beibehalten, da das zu falschen Abschlüssen führen kann isCompleted: isCompleted, attempts: attempts, bestScore: Math.max(score, levelProgress.bestScore), bestMoves: Math.min(moves, levelProgress.bestMoves || moves), bestTime: Math.min(time, levelProgress.bestTime || time), completedAt: isCompleted ? new Date() : levelProgress.completedAt }); // Aktualisiere Gesamt-Fortschritt // WICHTIG: Nur wenn das Level abgeschlossen ist, werden Score und Stars zum Gesamtfortschritt hinzugefügt // Das verhindert, dass unvollständige Level den Gesamtfortschritt beeinflussen let totalScore = userProgress.totalScore; let totalStars = userProgress.totalStars; if (isCompleted) { totalScore += score; totalStars += stars; } // Berechne neue currentLevel // WICHTIG: Zähle nur Level, die tatsächlich abgeschlossen sind const levelsCompleted = await Match3UserLevelProgress.count({ where: { userProgressId: userProgress.id, isCompleted: true } }); // Zusätzliche Sicherheit: Stelle sicher, dass levelsCompleted nicht größer als die verfügbaren Level ist if (levelsCompleted > 10) { // Angenommen, es gibt maximal 10 Level console.warn(`User ${userId} hat ungewöhnlich viele abgeschlossene Level: ${levelsCompleted}`); } // WICHTIG: currentLevel sollte nur aktualisiert werden, wenn das Level tatsächlich abgeschlossen ist // Wenn das Level nicht abgeschlossen ist, behalte den aktuellen currentLevel bei let newCurrentLevel = userProgress.currentLevel; if (isCompleted) { // Nur wenn das Level abgeschlossen ist, setze currentLevel auf das nächste Level newCurrentLevel = levelsCompleted + 1; } await userProgress.update({ totalScore, totalStars, levelsCompleted, currentLevel: newCurrentLevel, lastPlayed: new Date() }); return { levelProgress, userProgress: { totalScore, totalStars, levelsCompleted, currentLevel: newCurrentLevel } }; } // Bereinige falsche Level-Abschlüsse für einen Benutzer async cleanupUserProgress(userId, campaignId) { try { // Lade Benutzer-Fortschritt const userProgress = await Match3UserProgress.findOne({ where: { userId, campaignId } }); if (!userProgress) { return { success: false, message: 'User progress not found' }; } // Lade alle Level-Fortschritte const levelProgresses = await Match3UserLevelProgress.findAll({ where: { userProgressId: userProgress.id } }); let cleanedCount = 0; let totalScore = 0; let totalStars = 0; let levelsCompleted = 0; // Bereinige jeden Level-Fortschritt for (const levelProgress of levelProgresses) { // Ein Level ist nur abgeschlossen, wenn es mindestens 1 Stern UND Score > 0 hat const shouldBeCompleted = levelProgress.stars > 0 && levelProgress.score > 0; if (levelProgress.isCompleted !== shouldBeCompleted) { await levelProgress.update({ isCompleted: shouldBeCompleted, completedAt: shouldBeCompleted ? levelProgress.completedAt : null }); cleanedCount++; } // Sammle korrekte Statistiken if (shouldBeCompleted) { totalScore += levelProgress.score; totalStars += levelProgress.stars; levelsCompleted++; } } // Aktualisiere Gesamt-Fortschritt await userProgress.update({ totalScore, totalStars, levelsCompleted, currentLevel: levelsCompleted + 1 }); return { success: true, message: `Cleaned ${cleanedCount} level progress entries`, cleanedCount, totalScore, totalStars, levelsCompleted, currentLevel: levelsCompleted + 1 }; } catch (error) { console.error('Error cleaning user progress:', error); return { success: false, message: error.message }; } } // ANTI-CHEAT: Validiere den Progress-Hash validateProgressHash(userId, campaignId, levelId, score, moves, time, stars, securityHash, timestamp) { try { // Prüfe ob der Timestamp nicht zu alt ist (5 Minuten) const now = Date.now(); if (now - timestamp > 5 * 60 * 1000) { console.warn(`Progress validation failed: Timestamp too old for user ${userId}`); return false; } // Lade Level-Daten für Hash-Validierung return Match3Level.findByPk(levelId).then(level => { if (!level) { console.warn(`Progress validation failed: Level ${levelId} not found`); return false; } // Erstelle den gleichen Hash wie im Frontend const dataString = `${levelId}|${score}|${moves}|${stars}|true|${level.boardLayout}|${level.moveLimit}`; // Einfache Hash-Funktion (muss mit Frontend übereinstimmen) let hash = 0; for (let i = 0; i < dataString.length; i++) { const char = dataString.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } // Salt hinzufügen (muss mit Frontend übereinstimmen) 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; } const expectedHash = Math.abs(saltedHash).toString(16); // Vergleiche Hash if (expectedHash !== securityHash) { console.warn(`Progress validation failed: Hash mismatch for user ${userId}. Expected: ${expectedHash}, Got: ${securityHash}`); return false; } // Zusätzliche Validierungen if (score < 0 || moves < 0 || stars < 0 || stars > 3) { console.warn(`Progress validation failed: Invalid values for user ${userId}`); return false; } // Prüfe ob der Score realistisch ist (basierend auf Moves und Level) const maxPossibleScore = moves * 100 * levelId; // Vereinfachte Berechnung if (score > maxPossibleScore * 2) { // Erlaube 2x den maximalen Score console.warn(`Progress validation failed: Unrealistic score for user ${userId}`); return false; } return true; }); } catch (error) { console.error('Progress validation error:', error); return false; } } // Lade Benutzer-Statistiken async getUserStats(userId) { const userProgress = await Match3UserProgress.findAll({ where: { userId }, include: [ { model: Match3UserLevelProgress, as: 'levelProgress', include: [ { model: Match3Level, as: 'level' } ] } ] }); if (!userProgress || userProgress.length === 0) { return { totalScore: 0, totalStars: 0, levelsCompleted: 0, totalPlayTime: 0, averageScore: 0, bestLevel: null }; } const totalScore = userProgress.reduce((sum, campaign) => sum + campaign.totalScore, 0); const totalStars = userProgress.reduce((sum, campaign) => sum + campaign.totalStars, 0); const levelsCompleted = userProgress.reduce((sum, campaign) => sum + campaign.levelsCompleted, 0); let totalPlayTime = 0; let totalLevels = 0; let bestLevel = null; let bestScore = 0; userProgress.forEach(campaign => { campaign.levelProgress.forEach(level => { if (level.time) { totalPlayTime += level.time; totalLevels++; } if (level.score > bestScore) { bestScore = level.score; bestLevel = level.level; } }); }); return { totalScore, totalStars, levelsCompleted, totalPlayTime, averageScore: totalLevels > 0 ? Math.round(totalScore / totalLevels) : 0, bestLevel: bestLevel ? { name: bestLevel.name, score: bestScore } : null }; } }; export default new Match3Service();