- Implementierung neuer Endpunkte für die Verwaltung von Match3-Kampagnen, Levels, Objectives und Tile-Typen im Admin-Bereich. - Anpassung der Admin-Services zur Unterstützung von Benutzerberechtigungen und Fehlerbehandlung. - Einführung von neuen Modellen und Assoziationen für Match3-Levels und Tile-Typen in der Datenbank. - Verbesserung der Internationalisierung für Match3-spezifische Texte in Deutsch und Englisch. - Aktualisierung der Frontend-Routen und -Komponenten zur Verwaltung von Match3-Inhalten.
504 lines
18 KiB
JavaScript
504 lines
18 KiB
JavaScript
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' };
|
|
}
|
|
|
|
return level;
|
|
}
|
|
|
|
// 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();
|