Bereinigen und Entfernen von nicht mehr benötigten TinyMCE-Dateien und -Plugins; Aktualisierung der Internationalisierung für Deutsch und Englisch in den Falukant- und Navigationsmodulen; Verbesserung der Statusleiste und Router-Implementierung.
This commit is contained in:
@@ -12,6 +12,7 @@ import forumRouter from './routers/forumRouter.js';
|
||||
import falukantRouter from './routers/falukantRouter.js';
|
||||
import friendshipRouter from './routers/friendshipRouter.js';
|
||||
import blogRouter from './routers/blogRouter.js';
|
||||
import match3Router from './routers/match3Router.js';
|
||||
import cors from 'cors';
|
||||
import './jobs/sessionCleanup.js';
|
||||
|
||||
@@ -21,8 +22,10 @@ const __dirname = path.dirname(__filename);
|
||||
const app = express();
|
||||
|
||||
const corsOptions = {
|
||||
origin: '*',
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
origin: ['http://localhost:3000', 'http://localhost:5173', 'http://127.0.0.1:3000', 'http://127.0.0.1:5173'],
|
||||
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'userId', 'authCode'],
|
||||
credentials: true,
|
||||
preflightContinue: false,
|
||||
optionsSuccessStatus: 204
|
||||
};
|
||||
@@ -35,6 +38,7 @@ app.use('/api/auth', authRouter);
|
||||
app.use('/api/navigation', navigationRouter);
|
||||
app.use('/api/settings', settingsRouter);
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use('/api/match3', match3Router);
|
||||
app.use('/images', express.static(path.join(__dirname, '../frontend/public/images')));
|
||||
app.use('/api/contact', contactRouter);
|
||||
app.use('/api/socialnetwork', socialnetworkRouter);
|
||||
|
||||
@@ -142,6 +142,8 @@ class FalukantController {
|
||||
|
||||
this.getUndergroundTypes = this._wrapWithUser((userId) => this.service.getUndergroundTypes(userId));
|
||||
this.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId));
|
||||
this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size));
|
||||
this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 });
|
||||
this.getUndergroundTargets = this._wrapWithUser((userId) => this.service.getPoliticalOfficeHolders(userId));
|
||||
|
||||
this.searchUsers = this._wrapWithUser((userId, req) => {
|
||||
|
||||
128
backend/controllers/match3Controller.js
Normal file
128
backend/controllers/match3Controller.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import match3Service from '../services/match3Service.js';
|
||||
|
||||
class Match3Controller {
|
||||
/**
|
||||
* Lädt alle aktiven Kampagnen
|
||||
*/
|
||||
async getCampaigns(req, res) {
|
||||
try {
|
||||
const campaigns = await match3Service.getActiveCampaigns();
|
||||
res.json({ success: true, data: campaigns });
|
||||
} catch (error) {
|
||||
console.error('Error in getCampaigns:', error);
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden der Kampagnen' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt eine spezifische Kampagne
|
||||
*/
|
||||
async getCampaign(req, res) {
|
||||
try {
|
||||
const { campaignId } = req.params;
|
||||
const campaign = await match3Service.getCampaign(campaignId);
|
||||
|
||||
if (!campaign) {
|
||||
return res.status(404).json({ success: false, message: 'Kampagne nicht gefunden' });
|
||||
}
|
||||
|
||||
res.json({ success: true, data: campaign });
|
||||
} catch (error) {
|
||||
console.error('Error in getCampaign:', error);
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden der Kampagne' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt den Benutzerfortschritt für eine Kampagne
|
||||
*/
|
||||
async getUserProgress(req, res) {
|
||||
try {
|
||||
const { campaignId } = req.params;
|
||||
const userId = req.headers.userid || req.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ success: false, message: 'Benutzer-ID nicht gefunden' });
|
||||
}
|
||||
|
||||
const userProgress = await match3Service.getUserProgress(userId, campaignId);
|
||||
res.json({ success: true, data: userProgress });
|
||||
} catch (error) {
|
||||
console.error('Error in getUserProgress:', error);
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden des Fortschritts' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert den Level-Fortschritt eines Benutzers
|
||||
*/
|
||||
async updateLevelProgress(req, res) {
|
||||
try {
|
||||
const { campaignId, levelId } = req.params;
|
||||
const userId = req.headers.userid || req.user?.id;
|
||||
const { score, moves, time, stars, isCompleted } = req.body;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ success: false, message: 'Benutzer-ID nicht gefunden' });
|
||||
}
|
||||
|
||||
if (!score || !moves || !stars) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Alle erforderlichen Felder müssen ausgefüllt werden'
|
||||
});
|
||||
}
|
||||
|
||||
const levelData = {
|
||||
score: parseInt(score),
|
||||
moves: parseInt(moves),
|
||||
time: time ? parseInt(time) : 0,
|
||||
stars: parseInt(stars),
|
||||
isCompleted: Boolean(isCompleted)
|
||||
};
|
||||
|
||||
const result = await match3Service.updateLevelProgress(userId, campaignId, levelId, levelData);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
console.error('Error in updateLevelProgress:', error);
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren des Fortschritts' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Bestenliste für eine Kampagne
|
||||
*/
|
||||
async getLeaderboard(req, res) {
|
||||
try {
|
||||
const { campaignId } = req.params;
|
||||
const { limit = 10 } = req.query;
|
||||
|
||||
const leaderboard = await match3Service.getLeaderboard(campaignId, parseInt(limit));
|
||||
res.json({ success: true, data: leaderboard });
|
||||
} catch (error) {
|
||||
console.error('Error in getLeaderboard:', error);
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden der Bestenliste' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Statistiken für einen Benutzer
|
||||
*/
|
||||
async getUserStats(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid || req.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ success: false, message: 'Benutzer-ID nicht gefunden' });
|
||||
}
|
||||
|
||||
const stats = await match3Service.getUserStats(userId);
|
||||
res.json({ success: true, data: stats });
|
||||
} catch (error) {
|
||||
console.error('Error in getUserStats:', error);
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden der Statistiken' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Match3Controller();
|
||||
32
backend/controllers/minigamesController.js
Normal file
32
backend/controllers/minigamesController.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import MinigamesService from '../services/minigamesService.js';
|
||||
|
||||
function extractHashedUserId(req) {
|
||||
return req.headers?.userid;
|
||||
}
|
||||
|
||||
class MinigamesController {
|
||||
constructor() {
|
||||
this.service = MinigamesService;
|
||||
|
||||
this.listCampaigns = this._wrap((userId, req) => this.service.listCampaigns());
|
||||
this.getCampaign = this._wrap((userId, req) => this.service.getCampaign(req.params.code));
|
||||
this.getProgress = this._wrap((userId, req) => this.service.getProgress(userId, req.params.code));
|
||||
this.saveProgress = this._wrap((userId, req) => this.service.saveProgress(userId, req.params.code, req.body));
|
||||
}
|
||||
|
||||
_wrap(fn, { successStatus = 200 } = {}) {
|
||||
return async (req, res) => {
|
||||
try {
|
||||
const userId = extractHashedUserId(req);
|
||||
if (!userId) return res.status(400).json({ error: 'Missing user identifier' });
|
||||
const result = await fn(userId, req, res);
|
||||
res.status(successStatus).json(result);
|
||||
} catch (error) {
|
||||
console.error('Minigames controller error:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal error' });
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new MinigamesController();
|
||||
@@ -170,6 +170,12 @@ const menuStructure = {
|
||||
minigames: {
|
||||
visible: ["all"],
|
||||
icon: "minigames16.png",
|
||||
children: {
|
||||
match3: {
|
||||
visible: ["all"],
|
||||
path: "/minigames/match3"
|
||||
}
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
visible: ["all"],
|
||||
|
||||
@@ -17,3 +17,6 @@ export const authenticate = async (req, res, next) => {
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// Default export für Kompatibilität
|
||||
export default { authenticate };
|
||||
|
||||
@@ -97,10 +97,28 @@ import Underground from './falukant/data/underground.js';
|
||||
import UndergroundType from './falukant/type/underground.js';
|
||||
import Blog from './community/blog.js';
|
||||
import BlogPost from './community/blog_post.js';
|
||||
import MinigameCampaign from './service/minigame_campaign.js';
|
||||
import MinigameCampaignLevel from './service/minigame_campaign_level.js';
|
||||
import MinigameUserProgress from './service/minigame_user_progress.js';
|
||||
|
||||
// Match3 Models
|
||||
import Match3Campaign from './match3/campaign.js';
|
||||
import Match3Level from './match3/level.js';
|
||||
import Match3Objective from './match3/objective.js';
|
||||
import Match3UserProgress from './match3/userProgress.js';
|
||||
import Match3UserLevelProgress from './match3/userLevelProgress.js';
|
||||
|
||||
export default function setupAssociations() {
|
||||
// RoomType 1:n Room
|
||||
RoomType.hasMany(Room, { foreignKey: 'roomTypeId', as: 'rooms' });
|
||||
// Minigames associations
|
||||
MinigameCampaign.hasMany(MinigameCampaignLevel, { foreignKey: 'campaign_id', as: 'levels' });
|
||||
MinigameCampaignLevel.belongsTo(MinigameCampaign, { foreignKey: 'campaign_id', as: 'campaign' });
|
||||
|
||||
User.hasMany(MinigameUserProgress, { foreignKey: 'user_id', as: 'minigameProgress' });
|
||||
MinigameUserProgress.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
|
||||
MinigameCampaign.hasMany(MinigameUserProgress, { foreignKey: 'campaign_id', as: 'userProgress' });
|
||||
MinigameUserProgress.belongsTo(MinigameCampaign, { foreignKey: 'campaign_id', as: 'campaign' });
|
||||
Room.belongsTo(RoomType, { foreignKey: 'roomTypeId', as: 'roomType' });
|
||||
// ChatUser <-> ChatRight n:m
|
||||
ChatUser.belongsToMany(ChatRight, {
|
||||
@@ -768,4 +786,59 @@ export default function setupAssociations() {
|
||||
Blog.hasMany(BlogPost, { foreignKey: 'blog_id', as: 'posts' });
|
||||
BlogPost.belongsTo(User, { foreignKey: 'user_id', as: 'author' });
|
||||
User.hasMany(BlogPost, { foreignKey: 'user_id', as: 'blogPosts' });
|
||||
|
||||
// Match3 associations
|
||||
Match3Campaign.hasMany(Match3Level, {
|
||||
foreignKey: 'campaignId',
|
||||
as: 'levels'
|
||||
});
|
||||
Match3Level.belongsTo(Match3Campaign, {
|
||||
foreignKey: 'campaignId',
|
||||
as: 'campaign'
|
||||
});
|
||||
|
||||
Match3Level.hasMany(Match3Objective, {
|
||||
foreignKey: 'levelId',
|
||||
as: 'objectives'
|
||||
});
|
||||
Match3Objective.belongsTo(Match3Level, {
|
||||
foreignKey: 'levelId',
|
||||
as: 'level'
|
||||
});
|
||||
|
||||
Match3Campaign.hasMany(Match3UserProgress, {
|
||||
foreignKey: 'campaignId',
|
||||
as: 'userProgress'
|
||||
});
|
||||
Match3UserProgress.belongsTo(Match3Campaign, {
|
||||
foreignKey: 'campaignId',
|
||||
as: 'campaign'
|
||||
});
|
||||
|
||||
User.hasMany(Match3UserProgress, {
|
||||
foreignKey: 'userId',
|
||||
as: 'match3Progress'
|
||||
});
|
||||
Match3UserProgress.belongsTo(User, {
|
||||
foreignKey: 'userId',
|
||||
as: 'user'
|
||||
});
|
||||
|
||||
Match3UserProgress.hasMany(Match3UserLevelProgress, {
|
||||
foreignKey: 'userProgressId',
|
||||
as: 'levelProgress'
|
||||
});
|
||||
Match3UserLevelProgress.belongsTo(Match3UserProgress, {
|
||||
foreignKey: 'userProgressId',
|
||||
as: 'userProgress'
|
||||
});
|
||||
|
||||
Match3Level.hasMany(Match3UserLevelProgress, {
|
||||
foreignKey: 'levelId',
|
||||
as: 'userProgress'
|
||||
});
|
||||
Match3UserLevelProgress.belongsTo(Match3Level, {
|
||||
foreignKey: 'levelId',
|
||||
as: 'level'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,6 +86,18 @@ import Credit from './falukant/data/credit.js';
|
||||
import DebtorsPrism from './falukant/data/debtors_prism.js';
|
||||
import HealthActivity from './falukant/log/health_activity.js';
|
||||
|
||||
// Minigames (service)
|
||||
import MinigameCampaign from './service/minigame_campaign.js';
|
||||
import MinigameCampaignLevel from './service/minigame_campaign_level.js';
|
||||
import MinigameUserProgress from './service/minigame_user_progress.js';
|
||||
|
||||
// Match3 Models
|
||||
import Match3Campaign from './match3/campaign.js';
|
||||
import Match3Level from './match3/level.js';
|
||||
import Match3Objective from './match3/objective.js';
|
||||
import Match3UserProgress from './match3/userProgress.js';
|
||||
import Match3UserLevelProgress from './match3/userLevelProgress.js';
|
||||
|
||||
// — Politische Ämter (Politics) —
|
||||
import PoliticalOfficeType from './falukant/type/political_office_type.js';
|
||||
import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js';
|
||||
@@ -194,6 +206,14 @@ const models = {
|
||||
Credit,
|
||||
DebtorsPrism,
|
||||
HealthActivity,
|
||||
MinigameCampaign,
|
||||
MinigameCampaignLevel,
|
||||
MinigameUserProgress,
|
||||
Match3Campaign,
|
||||
Match3Level,
|
||||
Match3Objective,
|
||||
Match3UserProgress,
|
||||
Match3UserLevelProgress,
|
||||
PoliticalOfficeType,
|
||||
PoliticalOfficeRequirement,
|
||||
PoliticalOfficeBenefitType,
|
||||
|
||||
40
backend/models/match3/campaign.js
Normal file
40
backend/models/match3/campaign.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
const Campaign = sequelize.define('Campaign', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
},
|
||||
order: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
tableName: 'match3_campaigns',
|
||||
schema: 'match3',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
export default Campaign;
|
||||
61
backend/models/match3/level.js
Normal file
61
backend/models/match3/level.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
const Level = sequelize.define('Level', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
campaignId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
order: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1
|
||||
},
|
||||
boardSize: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 8
|
||||
},
|
||||
tileTypes: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: false,
|
||||
defaultValue: ['gem', 'star', 'heart', 'diamond', 'circle', 'square']
|
||||
},
|
||||
moveLimit: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
timeLimit: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
tableName: 'match3_levels',
|
||||
schema: 'match3',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
export default Level;
|
||||
52
backend/models/match3/objective.js
Normal file
52
backend/models/match3/objective.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
const Objective = sequelize.define('Objective', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
levelId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.ENUM('score', 'matches', 'moves', 'time', 'special'),
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.STRING(500),
|
||||
allowNull: false
|
||||
},
|
||||
target: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
operator: {
|
||||
type: DataTypes.ENUM('>=', '<=', '=', '>', '<'),
|
||||
defaultValue: '>='
|
||||
},
|
||||
order: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1
|
||||
},
|
||||
isRequired: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
tableName: 'match3_objectives',
|
||||
schema: 'match3',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
export default Objective;
|
||||
78
backend/models/match3/userLevelProgress.js
Normal file
78
backend/models/match3/userLevelProgress.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
const UserLevelProgress = sequelize.define('UserLevelProgress', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userProgressId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
levelId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
score: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
moves: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
time: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
stars: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
isCompleted: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
attempts: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1
|
||||
},
|
||||
bestScore: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
bestMoves: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
bestTime: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
completedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
tableName: 'match3_user_level_progress',
|
||||
schema: 'match3',
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['userProgressId', 'levelId']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default UserLevelProgress;
|
||||
62
backend/models/match3/userProgress.js
Normal file
62
backend/models/match3/userProgress.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
const UserProgress = sequelize.define('UserProgress', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
campaignId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
totalScore: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
totalStars: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
levelsCompleted: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
currentLevel: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1
|
||||
},
|
||||
isCompleted: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
lastPlayed: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
tableName: 'match3_user_progress',
|
||||
schema: 'match3',
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['userId', 'campaignId']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default UserProgress;
|
||||
24
backend/models/service/minigame_campaign.js
Normal file
24
backend/models/service/minigame_campaign.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
const MinigameCampaign = sequelize.define('minigame_campaign', {
|
||||
code: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
}, {
|
||||
tableName: 'minigame_campaign',
|
||||
schema: 'service',
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
export default MinigameCampaign;
|
||||
25
backend/models/service/minigame_campaign_level.js
Normal file
25
backend/models/service/minigame_campaign_level.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
const MinigameCampaignLevel = sequelize.define('minigame_campaign_level', {
|
||||
campaignId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'campaign_id'
|
||||
},
|
||||
index: { // 1-based level number
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
config: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false,
|
||||
defaultValue: {}
|
||||
},
|
||||
}, {
|
||||
tableName: 'minigame_campaign_level',
|
||||
schema: 'service',
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
export default MinigameCampaignLevel;
|
||||
41
backend/models/service/minigame_user_progress.js
Normal file
41
backend/models/service/minigame_user_progress.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
const MinigameUserProgress = sequelize.define('minigame_user_progress', {
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'user_id'
|
||||
},
|
||||
campaignId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'campaign_id'
|
||||
},
|
||||
levelIndex: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
field: 'level_index'
|
||||
},
|
||||
stars: { // 0..3
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
bestScore: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: 'best_score'
|
||||
}
|
||||
}, {
|
||||
tableName: 'minigame_user_progress',
|
||||
schema: 'service',
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ unique: true, fields: ['user_id', 'campaign_id'] }
|
||||
]
|
||||
});
|
||||
|
||||
export default MinigameUserProgress;
|
||||
@@ -71,6 +71,8 @@ router.post('/politics/open', falukantController.applyForElections);
|
||||
router.get('/cities', falukantController.getRegions);
|
||||
router.get('/underground/types', falukantController.getUndergroundTypes);
|
||||
router.get('/notifications', falukantController.getNotifications);
|
||||
router.get('/notifications/all', falukantController.getAllNotifications);
|
||||
router.post('/notifications/mark-shown', falukantController.markNotificationsShown);
|
||||
router.get('/underground/targets', falukantController.getUndergroundTargets);
|
||||
router.post('/underground/activities', falukantController.createUndergroundActivity);
|
||||
router.get('/users/search', falukantController.searchUsers);
|
||||
|
||||
22
backend/routers/match3Router.js
Normal file
22
backend/routers/match3Router.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import express from 'express';
|
||||
import match3Controller from '../controllers/match3Controller.js';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Alle Routen erfordern Authentifizierung
|
||||
router.use(authenticate);
|
||||
|
||||
// Kampagnen-Routen
|
||||
router.get('/campaigns', match3Controller.getCampaigns);
|
||||
router.get('/campaigns/:campaignId', match3Controller.getCampaign);
|
||||
|
||||
// Benutzer-Fortschritt
|
||||
router.get('/campaigns/:campaignId/progress', match3Controller.getUserProgress);
|
||||
router.post('/campaigns/:campaignId/levels/:levelId/progress', match3Controller.updateLevelProgress);
|
||||
|
||||
// Bestenliste und Statistiken
|
||||
router.get('/campaigns/:campaignId/leaderboard', match3Controller.getLeaderboard);
|
||||
router.get('/stats', match3Controller.getUserStats);
|
||||
|
||||
export default router;
|
||||
@@ -298,10 +298,79 @@ class FalukantService extends BaseService {
|
||||
]
|
||||
},
|
||||
],
|
||||
attributes: ['money']
|
||||
attributes: ['id', 'money']
|
||||
});
|
||||
if (!falukantUser) throw new Error('User not found');
|
||||
if (falukantUser.character?.birthdate) falukantUser.character.setDataValue('age', calcAge(falukantUser.character.birthdate));
|
||||
|
||||
// Aggregate status additions: children counts and unread notifications
|
||||
try {
|
||||
const bm = (step, payload = {}) => {
|
||||
try { console.log(`[BLOCKMARKER][falukant.getInfo] ${step}`, payload); } catch (_) { /* ignore */ }
|
||||
};
|
||||
bm('aggregate.start', { userId: user.id, falukantUserId: falukantUser.id });
|
||||
// Determine all character IDs belonging to the user
|
||||
if (!falukantUser.id) {
|
||||
bm('aggregate.noFalukantUserId');
|
||||
throw new Error('Missing falukantUser.id in getInfo aggregation');
|
||||
}
|
||||
const userCharacterIdsRows = await FalukantCharacter.findAll({
|
||||
attributes: ['id'],
|
||||
where: { userId: falukantUser.id },
|
||||
raw: true
|
||||
});
|
||||
const userCharacterIds = userCharacterIdsRows.map(r => r.id);
|
||||
bm('aggregate.userCharacters', { count: userCharacterIds.length, ids: userCharacterIds.slice(0, 5) });
|
||||
|
||||
// Count distinct children for any of the user's characters (as father or mother)
|
||||
let childrenCount = 0;
|
||||
let unbaptisedChildrenCount = 0;
|
||||
if (userCharacterIds.length > 0) {
|
||||
const childRels = await ChildRelation.findAll({
|
||||
attributes: ['childCharacterId'],
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ fatherCharacterId: { [Op.in]: userCharacterIds } },
|
||||
{ motherCharacterId: { [Op.in]: userCharacterIds } },
|
||||
]
|
||||
},
|
||||
raw: true
|
||||
});
|
||||
const distinctChildIds = new Set(childRels.map(r => r.childCharacterId));
|
||||
childrenCount = distinctChildIds.size;
|
||||
bm('aggregate.children', { relations: childRels.length, distinct: childrenCount, sample: Array.from(distinctChildIds).slice(0, 5) });
|
||||
|
||||
const unbaptised = await ChildRelation.findAll({
|
||||
attributes: ['childCharacterId'],
|
||||
where: {
|
||||
nameSet: false,
|
||||
[Op.or]: [
|
||||
{ fatherCharacterId: { [Op.in]: userCharacterIds } },
|
||||
{ motherCharacterId: { [Op.in]: userCharacterIds } },
|
||||
]
|
||||
},
|
||||
raw: true
|
||||
});
|
||||
const distinctUnbaptisedIds = new Set(unbaptised.map(r => r.childCharacterId));
|
||||
unbaptisedChildrenCount = distinctUnbaptisedIds.size;
|
||||
bm('aggregate.unbaptised', { relations: unbaptised.length, distinct: unbaptisedChildrenCount, sample: Array.from(distinctUnbaptisedIds).slice(0, 5) });
|
||||
}
|
||||
|
||||
// Unread notifications count
|
||||
const unreadNotifications = await Notification.count({ where: { userId: falukantUser.id, shown: false } });
|
||||
bm('aggregate.unread', { unreadNotifications });
|
||||
|
||||
falukantUser.setDataValue('childrenCount', childrenCount);
|
||||
falukantUser.setDataValue('unbaptisedChildrenCount', unbaptisedChildrenCount);
|
||||
falukantUser.setDataValue('unreadNotifications', unreadNotifications);
|
||||
bm('aggregate.done', { childrenCount, unbaptisedChildrenCount });
|
||||
} catch (e) {
|
||||
console.error('Error aggregating status info:', e);
|
||||
falukantUser.setDataValue('childrenCount', 0);
|
||||
falukantUser.setDataValue('unbaptisedChildrenCount', 0);
|
||||
falukantUser.setDataValue('unreadNotifications', 0);
|
||||
}
|
||||
|
||||
return falukantUser;
|
||||
}
|
||||
|
||||
@@ -898,12 +967,9 @@ class FalukantService extends BaseService {
|
||||
await this.deleteExpiredProposals();
|
||||
const existingProposals = await this.fetchProposals(falukantUserId, regionId);
|
||||
if (existingProposals.length > 0) {
|
||||
console.log('Existing proposals:', existingProposals);
|
||||
return this.formatProposals(existingProposals);
|
||||
}
|
||||
console.log('No existing proposals, generating new ones');
|
||||
await this.generateProposals(falukantUserId, regionId);
|
||||
console.log('Fetch new proposals');
|
||||
const newProposals = await this.fetchProposals(falukantUserId, regionId);
|
||||
return this.formatProposals(newProposals);
|
||||
}
|
||||
@@ -1320,7 +1386,7 @@ class FalukantService extends BaseService {
|
||||
}
|
||||
]
|
||||
});
|
||||
const children = [];
|
||||
const children = [];
|
||||
for (const parentChar of charsWithChildren) {
|
||||
const allRels = [
|
||||
...(parentChar.childrenFather || []),
|
||||
@@ -1332,16 +1398,19 @@ class FalukantService extends BaseService {
|
||||
name: kid.definedFirstName?.name || 'Unknown',
|
||||
gender: kid.gender,
|
||||
age: calcAge(kid.birthdate),
|
||||
hasName: rel.nameSet,
|
||||
hasName: rel.nameSet,
|
||||
_createdAt: rel.createdAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Sort children globally by relation createdAt ascending (older first)
|
||||
children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt));
|
||||
const inProgress = ['wooing', 'engaged', 'married'];
|
||||
const family = {
|
||||
relationships: relationships.filter(r => inProgress.includes(r.relationshipType)),
|
||||
lovers: relationships.filter(r => r.relationshipType === 'lover'),
|
||||
deathPartners: relationships.filter(r => r.relationshipType === 'widowed'),
|
||||
children,
|
||||
children: children.map(({ _createdAt, ...rest }) => rest),
|
||||
possiblePartners: []
|
||||
};
|
||||
const ownAge = calcAge(character.birthdate);
|
||||
@@ -1983,62 +2052,66 @@ class FalukantService extends BaseService {
|
||||
}
|
||||
|
||||
async baptise(hashedUserId, childId, firstName) {
|
||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||
const parentCharacter = await FalukantCharacter.findOne({
|
||||
where: {
|
||||
userId: falukantUser.id,
|
||||
},
|
||||
});
|
||||
if (!parentCharacter) {
|
||||
throw new Error('Parent character not found');
|
||||
}
|
||||
const child = await FalukantCharacter.findOne({
|
||||
where: {
|
||||
id: childId,
|
||||
},
|
||||
});
|
||||
if (!child) {
|
||||
throw new Error('Child not found');
|
||||
}
|
||||
const childRelation = await ChildRelation.findOne({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{
|
||||
fatherCharacterId: parentCharacter.id,
|
||||
childCharacterId: child.id,
|
||||
},
|
||||
{
|
||||
motherCharacterId: parentCharacter.id,
|
||||
childCharacterId: child.id,
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
if (!childRelation) {
|
||||
throw new Error('Child relation not found');
|
||||
}
|
||||
await childRelation.update({
|
||||
nameSet: true,
|
||||
});
|
||||
let firstNameObject = FalukantPredefineFirstname.findOne({
|
||||
where: {
|
||||
name: firstName,
|
||||
gender: child.gender,
|
||||
},
|
||||
});
|
||||
if (!firstNameObject) {
|
||||
firstNameObject = await FalukantPredefineFirstname.create({
|
||||
name: firstName,
|
||||
gender: child.gender,
|
||||
try {
|
||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||
const parentCharacter = await FalukantCharacter.findOne({
|
||||
where: {
|
||||
userId: falukantUser.id,
|
||||
},
|
||||
});
|
||||
if (!parentCharacter) {
|
||||
throw new Error('Parent character not found');
|
||||
}
|
||||
const child = await FalukantCharacter.findOne({
|
||||
where: {
|
||||
id: childId,
|
||||
},
|
||||
});
|
||||
if (!child) {
|
||||
throw new Error('Child not found');
|
||||
}
|
||||
const childRelation = await ChildRelation.findOne({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{
|
||||
fatherCharacterId: parentCharacter.id,
|
||||
childCharacterId: child.id,
|
||||
},
|
||||
{
|
||||
motherCharacterId: parentCharacter.id,
|
||||
childCharacterId: child.id,
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
if (!childRelation) {
|
||||
throw new Error('Child relation not found');
|
||||
}
|
||||
await childRelation.update({
|
||||
nameSet: true,
|
||||
});
|
||||
let firstNameObject = await FalukantPredefineFirstname.findOne({
|
||||
where: {
|
||||
name: firstName,
|
||||
gender: child.gender,
|
||||
},
|
||||
});
|
||||
if (!firstNameObject) {
|
||||
firstNameObject = await FalukantPredefineFirstname.create({
|
||||
name: firstName,
|
||||
gender: child.gender,
|
||||
});
|
||||
}
|
||||
await child.update({
|
||||
firstName: firstNameObject.id,
|
||||
});
|
||||
updateFalukantUserMoney(falukantUser.id, -50, 'Baptism', falukantUser.id);
|
||||
// Trigger status bar refresh for the user after baptism
|
||||
notifyUser(hashedUserId, 'falukantUpdateStatus', {});
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
await child.update({
|
||||
firstName: firstNameObject.id,
|
||||
});
|
||||
updateFalukantUserMoney(falukantUser.id, -50, 'Baptism', falukantUser.id);
|
||||
return { success: true };
|
||||
} catch(error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
async getEducation(hashedUserId) {
|
||||
@@ -2795,7 +2868,29 @@ class FalukantService extends BaseService {
|
||||
where: { userId: user.id, shown: false },
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
return user.notifications;
|
||||
return notifications;
|
||||
}
|
||||
|
||||
async getAllNotifications(hashedUserId, page = 1, size = 10) {
|
||||
const user = await getFalukantUserOrFail(hashedUserId);
|
||||
const limit = Math.max(1, Math.min(Number(size) || 10, 100));
|
||||
const offset = Math.max(0, ((Number(page) || 1) - 1) * limit);
|
||||
const { rows, count } = await Notification.findAndCountAll({
|
||||
where: { userId: user.id },
|
||||
order: [['createdAt', 'DESC']],
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
return { items: rows, total: count, page: Number(page) || 1, size: limit };
|
||||
}
|
||||
|
||||
async markNotificationsShown(hashedUserId) {
|
||||
const user = await getFalukantUserOrFail(hashedUserId);
|
||||
const [count] = await Notification.update(
|
||||
{ shown: true },
|
||||
{ where: { userId: user.id, shown: false } }
|
||||
);
|
||||
return { updated: count };
|
||||
}
|
||||
|
||||
async getPoliticalOfficeHolders(hashedUserId) {
|
||||
|
||||
313
backend/services/match3Service.js
Normal file
313
backend/services/match3Service.js
Normal file
@@ -0,0 +1,313 @@
|
||||
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';
|
||||
|
||||
class Match3Service {
|
||||
/**
|
||||
* Lädt alle aktiven Kampagnen
|
||||
*/
|
||||
async getActiveCampaigns() {
|
||||
try {
|
||||
const campaigns = await Match3Campaign.findAll({
|
||||
where: { isActive: true },
|
||||
include: [
|
||||
{
|
||||
model: Match3Level,
|
||||
as: 'levels',
|
||||
where: { isActive: true },
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
model: Match3Objective,
|
||||
as: 'objectives',
|
||||
required: false,
|
||||
order: [['order', 'ASC']]
|
||||
}
|
||||
],
|
||||
order: [['order', 'ASC']]
|
||||
}
|
||||
],
|
||||
order: [['order', 'ASC']]
|
||||
});
|
||||
|
||||
return campaigns;
|
||||
} catch (error) {
|
||||
console.error('Error loading active campaigns:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt eine spezifische Kampagne mit allen Leveln
|
||||
*/
|
||||
async getCampaign(campaignId) {
|
||||
try {
|
||||
const campaign = await Match3Campaign.findByPk(campaignId, {
|
||||
include: [
|
||||
{
|
||||
model: Match3Level,
|
||||
as: 'levels',
|
||||
where: { isActive: true },
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
model: Match3Objective,
|
||||
as: 'objectives',
|
||||
required: false,
|
||||
order: [['order', 'ASC']]
|
||||
}
|
||||
],
|
||||
order: [['order', 'ASC']]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return campaign;
|
||||
} catch (error) {
|
||||
console.error('Error loading campaign:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt den Benutzerfortschritt für eine Kampagne
|
||||
*/
|
||||
async getUserProgress(userId, campaignId) {
|
||||
try {
|
||||
let userProgress = await Match3UserProgress.findOne({
|
||||
where: { userId, campaignId },
|
||||
include: [
|
||||
{
|
||||
model: Match3UserLevelProgress,
|
||||
as: 'levelProgress',
|
||||
include: [
|
||||
{
|
||||
model: Match3Level,
|
||||
as: 'level'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!userProgress) {
|
||||
// Erstelle neuen Fortschritt wenn noch nicht vorhanden
|
||||
userProgress = await Match3UserProgress.create({
|
||||
userId,
|
||||
campaignId,
|
||||
totalScore: 0,
|
||||
totalStars: 0,
|
||||
levelsCompleted: 0,
|
||||
currentLevel: 1,
|
||||
isCompleted: false
|
||||
});
|
||||
} else {
|
||||
// Validiere und korrigiere bestehende currentLevel-Werte
|
||||
if (userProgress.currentLevel < 1 || userProgress.currentLevel > 1000) {
|
||||
console.warn(`Invalid currentLevel detected for user ${userId}: ${userProgress.currentLevel}, correcting to ${userProgress.levelsCompleted + 1}`);
|
||||
|
||||
// Korrigiere den ungültigen Wert
|
||||
await userProgress.update({
|
||||
currentLevel: userProgress.levelsCompleted + 1
|
||||
});
|
||||
|
||||
// Lade den aktualisierten Datensatz
|
||||
userProgress = await Match3UserProgress.findByPk(userProgress.id, {
|
||||
include: [
|
||||
{
|
||||
model: Match3UserLevelProgress,
|
||||
as: 'levelProgress',
|
||||
include: [
|
||||
{
|
||||
model: Match3Level,
|
||||
as: 'level'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return userProgress;
|
||||
} catch (error) {
|
||||
console.error('Error loading user progress:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert den Level-Fortschritt eines Benutzers
|
||||
*/
|
||||
async updateLevelProgress(userId, campaignId, levelId, levelData) {
|
||||
try {
|
||||
// Lade oder erstelle Benutzerfortschritt
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
// Aktualisiere Level-Fortschritt
|
||||
const updateData = {
|
||||
score: Math.max(levelProgress.bestScore, levelData.score),
|
||||
moves: levelData.moves,
|
||||
time: levelData.time || 0,
|
||||
stars: Math.max(levelProgress.stars, levelData.stars),
|
||||
isCompleted: levelData.isCompleted || false,
|
||||
attempts: levelProgress.attempts + 1
|
||||
};
|
||||
|
||||
if (levelData.isCompleted) {
|
||||
updateData.completedAt = new Date();
|
||||
}
|
||||
|
||||
await levelProgress.update(updateData);
|
||||
|
||||
// Aktualisiere Bestwerte
|
||||
if (levelData.score > levelProgress.bestScore) {
|
||||
await levelProgress.update({ bestScore: levelData.score });
|
||||
}
|
||||
if (levelData.moves < levelProgress.bestMoves || levelProgress.bestMoves === 0) {
|
||||
await levelProgress.update({ bestMoves: levelData.moves });
|
||||
}
|
||||
if (levelData.time < levelProgress.bestTime || levelProgress.bestTime === 0) {
|
||||
await levelProgress.update({ bestTime: levelData.time });
|
||||
}
|
||||
|
||||
// Aktualisiere Kampagnen-Fortschritt
|
||||
if (levelData.isCompleted) {
|
||||
const totalScore = await Match3UserLevelProgress.sum('score', {
|
||||
where: { userProgressId: userProgress.id, isCompleted: true }
|
||||
});
|
||||
|
||||
const totalStars = await Match3UserLevelProgress.sum('stars', {
|
||||
where: { userProgressId: userProgress.id, isCompleted: true }
|
||||
});
|
||||
|
||||
const levelsCompleted = await Match3UserLevelProgress.count({
|
||||
where: { userProgressId: userProgress.id, isCompleted: true }
|
||||
});
|
||||
|
||||
// Korrigiere currentLevel: Es sollte immer levelsCompleted + 1 sein
|
||||
const correctCurrentLevel = levelsCompleted + 1;
|
||||
|
||||
await userProgress.update({
|
||||
totalScore,
|
||||
totalStars,
|
||||
levelsCompleted,
|
||||
currentLevel: correctCurrentLevel, // Verwende den korrigierten Wert
|
||||
lastPlayed: new Date()
|
||||
});
|
||||
|
||||
// Prüfe ob Kampagne abgeschlossen ist
|
||||
const totalLevels = await Match3Level.count({
|
||||
where: { campaignId, isActive: true }
|
||||
});
|
||||
|
||||
if (levelsCompleted >= totalLevels) {
|
||||
await userProgress.update({ isCompleted: true });
|
||||
}
|
||||
}
|
||||
|
||||
return { userProgress, levelProgress };
|
||||
} catch (error) {
|
||||
console.error('Error updating level progress:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Bestenliste für eine Kampagne
|
||||
*/
|
||||
async getLeaderboard(campaignId, limit = 10) {
|
||||
try {
|
||||
const leaderboard = await Match3UserProgress.findAll({
|
||||
where: { campaignId },
|
||||
include: [
|
||||
{
|
||||
model: Match3UserLevelProgress,
|
||||
as: 'levelProgress',
|
||||
where: { isCompleted: true },
|
||||
required: false
|
||||
}
|
||||
],
|
||||
order: [
|
||||
['totalScore', 'DESC'],
|
||||
['totalStars', 'DESC'],
|
||||
['levelsCompleted', 'DESC']
|
||||
],
|
||||
limit
|
||||
});
|
||||
|
||||
return leaderboard;
|
||||
} catch (error) {
|
||||
console.error('Error loading leaderboard:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Statistiken für einen Benutzer
|
||||
*/
|
||||
async getUserStats(userId) {
|
||||
try {
|
||||
const stats = await Match3UserProgress.findAll({
|
||||
where: { userId },
|
||||
include: [
|
||||
{
|
||||
model: Match3Campaign,
|
||||
as: 'campaign'
|
||||
},
|
||||
{
|
||||
model: Match3UserLevelProgress,
|
||||
as: 'levelProgress',
|
||||
include: [
|
||||
{
|
||||
model: Match3Level,
|
||||
as: 'level'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('Error loading user stats:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Match3Service();
|
||||
53
backend/services/minigamesService.js
Normal file
53
backend/services/minigamesService.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import BaseService from './BaseService.js';
|
||||
import models from '../models/index.js';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
const { MinigameCampaign, MinigameCampaignLevel, MinigameUserProgress, User } = models;
|
||||
|
||||
class MinigamesService extends BaseService {
|
||||
async listCampaigns() {
|
||||
const campaigns = await MinigameCampaign.findAll({ order: [['id', 'ASC']] });
|
||||
return campaigns;
|
||||
}
|
||||
|
||||
async getCampaign(code) {
|
||||
const campaign = await MinigameCampaign.findOne({ where: { code }, include: [{ model: MinigameCampaignLevel, as: 'levels', order: [['index', 'ASC']] }] });
|
||||
if (!campaign) throw new Error('campaign_not_found');
|
||||
return campaign;
|
||||
}
|
||||
|
||||
async getProgress(hashedUserId, code) {
|
||||
const user = await this.getUserByHashedId(hashedUserId);
|
||||
if (!user) throw new Error('user_not_found');
|
||||
const campaign = await MinigameCampaign.findOne({ where: { code } });
|
||||
if (!campaign) throw new Error('campaign_not_found');
|
||||
const progress = await MinigameUserProgress.findOne({ where: { userId: user.id, campaignId: campaign.id } });
|
||||
if (!progress) {
|
||||
return { levelIndex: 1, stars: 0, bestScore: 0 };
|
||||
}
|
||||
return progress;
|
||||
}
|
||||
|
||||
async saveProgress(hashedUserId, code, payload) {
|
||||
const user = await this.getUserByHashedId(hashedUserId);
|
||||
if (!user) throw new Error('user_not_found');
|
||||
const campaign = await MinigameCampaign.findOne({ where: { code } });
|
||||
if (!campaign) throw new Error('campaign_not_found');
|
||||
|
||||
const { levelIndex, stars, bestScore } = payload;
|
||||
const [progress, created] = await MinigameUserProgress.findOrCreate({
|
||||
where: { userId: user.id, campaignId: campaign.id },
|
||||
defaults: { levelIndex, stars, bestScore }
|
||||
});
|
||||
if (!created) {
|
||||
await progress.update({
|
||||
levelIndex: Math.max(progress.levelIndex, levelIndex),
|
||||
stars: Math.max(progress.stars, stars),
|
||||
bestScore: Math.max(progress.bestScore, bestScore),
|
||||
});
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
export default new MinigamesService();
|
||||
63
backend/utils/fixMatch3Data.js
Normal file
63
backend/utils/fixMatch3Data.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { sequelize } from './sequelize.js';
|
||||
import Match3UserProgress from '../models/match3/userProgress.js';
|
||||
|
||||
/**
|
||||
* Korrigiert alle ungültigen currentLevel-Werte in der Match3-Datenbank
|
||||
*/
|
||||
async function fixMatch3Data() {
|
||||
try {
|
||||
console.log('🔧 Starte Korrektur der Match3-Daten...');
|
||||
|
||||
// Finde alle UserProgress-Einträge mit ungültigen currentLevel-Werten
|
||||
const invalidEntries = await Match3UserProgress.findAll({
|
||||
where: {
|
||||
currentLevel: {
|
||||
[sequelize.Op.or]: [
|
||||
{ [sequelize.Op.lt]: 1 },
|
||||
{ [sequelize.Op.gt]: 1000 }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`📊 Gefunden: ${invalidEntries.length} ungültige Einträge`);
|
||||
|
||||
if (invalidEntries.length === 0) {
|
||||
console.log('✅ Alle currentLevel-Werte sind bereits korrekt');
|
||||
return;
|
||||
}
|
||||
|
||||
// Korrigiere jeden ungültigen Eintrag
|
||||
for (const entry of invalidEntries) {
|
||||
const oldValue = entry.currentLevel;
|
||||
const correctValue = entry.levelsCompleted + 1;
|
||||
|
||||
console.log(`🔧 Korrigiere User ${entry.userId}: currentLevel ${oldValue} → ${correctValue}`);
|
||||
|
||||
await entry.update({
|
||||
currentLevel: correctValue
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ Alle ungültigen currentLevel-Werte wurden korrigiert');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Korrigieren der Match3-Daten:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Führe das Skript aus, wenn es direkt aufgerufen wird
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
fixMatch3Data()
|
||||
.then(() => {
|
||||
console.log('🎯 Match3-Datenkorrektur abgeschlossen');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 Match3-Datenkorrektur fehlgeschlagen:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export default fixMatch3Data;
|
||||
104
backend/utils/initializeMatch3.js
Normal file
104
backend/utils/initializeMatch3.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import Match3Campaign from '../models/match3/campaign.js';
|
||||
import Match3Level from '../models/match3/level.js';
|
||||
import Match3Objective from '../models/match3/objective.js';
|
||||
|
||||
export const initializeMatch3Data = async () => {
|
||||
try {
|
||||
// Prüfe ob bereits Daten vorhanden sind
|
||||
const existingCampaigns = await Match3Campaign.count();
|
||||
if (existingCampaigns > 0) {
|
||||
console.log('Match3 data already exists, skipping initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Initializing Match3 data...');
|
||||
|
||||
// Erstelle erste Kampagne
|
||||
const campaign = await Match3Campaign.create({
|
||||
name: 'Juwelen-Meister',
|
||||
description: 'Meistere die Kunst des Juwelen-Matchings',
|
||||
isActive: true,
|
||||
order: 1
|
||||
});
|
||||
|
||||
// Erstelle erste Level
|
||||
const level1 = await Match3Level.create({
|
||||
campaignId: campaign.id,
|
||||
name: 'Der Anfang',
|
||||
description: 'Lerne die Grundlagen des Spiels',
|
||||
order: 1,
|
||||
boardSize: 6,
|
||||
tileTypes: ['gem', 'star', 'heart'],
|
||||
moveLimit: 15,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
const level2 = await Match3Level.create({
|
||||
campaignId: campaign.id,
|
||||
name: 'Erste Herausforderung',
|
||||
description: 'Erweitere deine Fähigkeiten',
|
||||
order: 2,
|
||||
boardSize: 7,
|
||||
tileTypes: ['gem', 'star', 'heart', 'diamond'],
|
||||
moveLimit: 20,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
// Erstelle Objectives für Level 1
|
||||
await Match3Objective.bulkCreate([
|
||||
{
|
||||
levelId: level1.id,
|
||||
type: 'score',
|
||||
description: 'Sammle 100 Punkte',
|
||||
target: 100,
|
||||
operator: '>=',
|
||||
order: 1,
|
||||
isRequired: true
|
||||
},
|
||||
{
|
||||
levelId: level1.id,
|
||||
type: 'matches',
|
||||
description: 'Mache 3 Matches',
|
||||
target: 3,
|
||||
operator: '>=',
|
||||
order: 2,
|
||||
isRequired: true
|
||||
}
|
||||
]);
|
||||
|
||||
// Erstelle Objectives für Level 2
|
||||
await Match3Objective.bulkCreate([
|
||||
{
|
||||
levelId: level2.id,
|
||||
type: 'score',
|
||||
description: 'Sammle 200 Punkte',
|
||||
target: 200,
|
||||
operator: '>=',
|
||||
order: 1,
|
||||
isRequired: true
|
||||
},
|
||||
{
|
||||
levelId: level2.id,
|
||||
type: 'matches',
|
||||
description: 'Mache 5 Matches',
|
||||
target: 5,
|
||||
operator: '>=',
|
||||
order: 2,
|
||||
isRequired: true
|
||||
},
|
||||
{
|
||||
levelId: level2.id,
|
||||
type: 'moves',
|
||||
description: 'Verwende weniger als 20 Züge',
|
||||
target: 20,
|
||||
operator: '<=',
|
||||
order: 3,
|
||||
isRequired: true
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('Match3 data initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Error initializing Match3 data:', error);
|
||||
}
|
||||
};
|
||||
@@ -22,6 +22,7 @@ const createSchemas = async () => {
|
||||
await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_predefine');
|
||||
await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_log');
|
||||
await sequelize.query('CREATE SCHEMA IF NOT EXISTS chat');
|
||||
await sequelize.query('CREATE SCHEMA IF NOT EXISTS match3');
|
||||
};
|
||||
|
||||
const initializeDatabase = async () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import models from '../models/index.js';
|
||||
import { createTriggers } from '../models/trigger.js';
|
||||
import initializeForum from './initializeForum.js';
|
||||
import initializeChat from './initializeChat.js';
|
||||
import { initializeMatch3Data } from './initializeMatch3.js';
|
||||
|
||||
const syncDatabase = async () => {
|
||||
try {
|
||||
@@ -47,6 +48,9 @@ const syncDatabase = async () => {
|
||||
console.log("Initializing chat...");
|
||||
await initializeChat();
|
||||
|
||||
console.log("Initializing Match3...");
|
||||
await initializeMatch3Data();
|
||||
|
||||
console.log('Database synchronization complete.');
|
||||
} catch (error) {
|
||||
console.error('Unable to synchronize the database:', error);
|
||||
|
||||
Reference in New Issue
Block a user