diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js index 05b37d2..7443a24 100644 --- a/backend/controllers/adminController.js +++ b/backend/controllers/adminController.js @@ -43,6 +43,8 @@ class AdminController { this.getRegionDistances = this.getRegionDistances.bind(this); this.upsertRegionDistance = this.upsertRegionDistance.bind(this); this.deleteRegionDistance = this.deleteRegionDistance.bind(this); + this.createNPCs = this.createNPCs.bind(this); + this.getTitlesOfNobility = this.getTitlesOfNobility.bind(this); } async getOpenInterests(req, res) { @@ -383,6 +385,38 @@ class AdminController { } } + async createNPCs(req, res) { + try { + const { userid: userId } = req.headers; + const { regionIds, minAge, maxAge, minTitleId, maxTitleId, count } = req.body; + const result = await AdminService.createNPCs(userId, { + regionIds: regionIds && regionIds.length > 0 ? regionIds : null, + minAge: parseInt(minAge) || 0, + maxAge: parseInt(maxAge) || 100, + minTitleId: parseInt(minTitleId) || 1, + maxTitleId: parseInt(maxTitleId) || 19, + count: parseInt(count) || 1 + }); + res.status(200).json(result); + } catch (error) { + console.log(error); + const status = error.message === 'noaccess' ? 403 : 500; + res.status(status).json({ error: error.message }); + } + } + + async getTitlesOfNobility(req, res) { + try { + const { userid: userId } = req.headers; + const titles = await AdminService.getTitlesOfNobility(userId); + res.status(200).json(titles); + } catch (error) { + console.log(error); + const status = error.message === 'noaccess' ? 403 : 500; + res.status(status).json({ error: error.message }); + } + } + async getRoomTypes(req, res) { try { const userId = req.headers.userid; diff --git a/backend/controllers/navigationController.js b/backend/controllers/navigationController.js index ad5fc4b..772da0b 100644 --- a/backend/controllers/navigationController.js +++ b/backend/controllers/navigationController.js @@ -280,6 +280,10 @@ const menuStructure = { visible: ["mainadmin", "falukant"], path: "/admin/falukant/map" }, + createNPC: { + visible: ["mainadmin", "falukant"], + path: "/admin/falukant/create-npc" + }, } }, minigames: { diff --git a/backend/routers/adminRouter.js b/backend/routers/adminRouter.js index b31d6a7..af99583 100644 --- a/backend/routers/adminRouter.js +++ b/backend/routers/adminRouter.js @@ -46,6 +46,8 @@ router.put('/falukant/regions/:id/map', authenticate, adminController.updateFalu router.get('/falukant/region-distances', authenticate, adminController.getRegionDistances); router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance); router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance); +router.post('/falukant/npcs/create', authenticate, adminController.createNPCs); +router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility); // --- Minigames Admin --- router.get('/minigames/match3/campaigns', authenticate, adminController.getMatch3Campaigns); diff --git a/backend/services/adminService.js b/backend/services/adminService.js index 3aac8f0..35888da 100644 --- a/backend/services/adminService.js +++ b/backend/services/adminService.js @@ -10,7 +10,7 @@ import UserParamType from "../models/type/user_param.js"; import ContactMessage from "../models/service/contactmessage.js"; import ContactService from "./ContactService.js"; import { sendAnswerEmail } from './emailService.js'; -import { Op } from 'sequelize'; +import { Op, Sequelize } from 'sequelize'; import FalukantUser from "../models/falukant/data/user.js"; import FalukantCharacter from "../models/falukant/data/character.js"; import FalukantPredefineFirstname from "../models/falukant/predefine/firstname.js"; @@ -24,6 +24,8 @@ import BranchType from "../models/falukant/type/branch.js"; import RegionDistance from "../models/falukant/data/region_distance.js"; import Room from '../models/chat/room.js'; import UserParam from '../models/community/user_param.js'; +import TitleOfNobility from "../models/falukant/type/title_of_nobility.js"; +import { sequelize } from '../utils/sequelize.js'; class AdminService { async hasUserAccess(userId, section) { @@ -321,6 +323,17 @@ class AdminService { return regions; } + async getTitlesOfNobility(userId) { + if (!(await this.hasUserAccess(userId, 'falukantusers'))) { + throw new Error('noaccess'); + } + const titles = await TitleOfNobility.findAll({ + order: [['id', 'ASC']], + attributes: ['id', 'labelTr', 'level'] + }); + return titles; + } + async updateFalukantRegionMap(userId, regionId, map) { if (!(await this.hasUserAccess(userId, 'falukantusers'))) { throw new Error('noaccess'); @@ -1085,6 +1098,136 @@ class AdminService { ageGroups }; } + + async createNPCs(userId, options) { + if (!(await this.hasUserAccess(userId, 'falukantusers'))) { + throw new Error('noaccess'); + } + + const { + regionIds, // Array von Region-IDs oder null für alle Städte + minAge, // Mindestalter in Jahren + maxAge, // Maximalalter in Jahren + minTitleId, // Minimale Title-ID + maxTitleId, // Maximale Title-ID + count // Anzahl der zu erstellenden NPCs + } = options; + + // Hole alle Städte, wenn keine spezifischen Regionen angegeben + let targetRegions = []; + if (regionIds && regionIds.length > 0) { + targetRegions = await RegionData.findAll({ + where: { + id: { [Op.in]: regionIds } + }, + include: [{ + model: RegionType, + as: 'regionType', + where: { labelTr: 'city' } + }] + }); + } else { + targetRegions = await RegionData.findAll({ + include: [{ + model: RegionType, + as: 'regionType', + where: { labelTr: 'city' } + }] + }); + } + + if (targetRegions.length === 0) { + throw new Error('No cities found'); + } + + // Hole alle Titles im Bereich + const titles = await TitleOfNobility.findAll({ + where: { + id: { + [Op.between]: [minTitleId, maxTitleId] + } + }, + order: [['id', 'ASC']] + }); + + if (titles.length === 0) { + throw new Error('No titles found in specified range'); + } + + const genders = ['male', 'female']; + const createdNPCs = []; + + // Erstelle NPCs in einer Transaktion + await sequelize.transaction(async (t) => { + for (let i = 0; i < count; i++) { + // Zufällige Region + const region = targetRegions[Math.floor(Math.random() * targetRegions.length)]; + + // Zufälliges Geschlecht + const gender = genders[Math.floor(Math.random() * genders.length)]; + + // Zufälliger Vorname + const firstName = await FalukantPredefineFirstname.findAll({ + where: { gender }, + order: sequelize.fn('RANDOM'), + limit: 1, + transaction: t + }); + if (firstName.length === 0) { + throw new Error(`No first names found for gender: ${gender}`); + } + const fnObj = firstName[0]; + + // Zufälliger Nachname + const lastName = await FalukantPredefineLastname.findAll({ + order: sequelize.fn('RANDOM'), + limit: 1, + transaction: t + }); + if (lastName.length === 0) { + throw new Error('No last names found'); + } + const lnObj = lastName[0]; + + // Zufälliges Alter (in Jahren, wird in Tage umgerechnet) + const randomAge = Math.floor(Math.random() * (maxAge - minAge + 1)) + minAge; + const birthdate = new Date(); + birthdate.setDate(birthdate.getDate() - randomAge); // 5 Tage = 5 Jahre alt + + // Zufälliger Title + const title = titles[Math.floor(Math.random() * titles.length)]; + + // Erstelle den NPC-Charakter (ohne userId = NPC) + const npc = await FalukantCharacter.create({ + userId: null, // Wichtig: null = NPC + regionId: region.id, + firstName: fnObj.id, + lastName: lnObj.id, + gender: gender, + birthdate: birthdate, + titleOfNobility: title.id, + health: 100, + moodId: 1 + }, { transaction: t }); + + createdNPCs.push({ + id: npc.id, + firstName: fnObj.name, + lastName: lnObj.name, + gender: gender, + age: randomAge, + region: region.name, + title: title.labelTr + }); + } + }); + + return { + success: true, + count: createdNPCs.length, + npcs: createdNPCs + }; + } } export default new AdminService(); \ No newline at end of file diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 7e5a56e..8673f24 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -635,20 +635,27 @@ class FalukantService extends BaseService { }); if (already > 0) return null; - // Choose an event (guaranteed once/day, random type) + // Choose an event (reduced frequency - not guaranteed every day) + // Total weight: 50 (50% chance per day, or adjust as needed) const events = [ - { id: 'windfall', weight: 25 }, - { id: 'theft', weight: 20 }, - { id: 'character_illness', weight: 20 }, - { id: 'character_recovery', weight: 15 }, - { id: 'character_accident', weight: 10 }, - { id: 'regional_festival', weight: 10 }, - // Regionale Events sind sehr selten und sollten nicht alle Charaktere töten + { id: 'windfall', weight: 10 }, + { id: 'theft', weight: 8 }, + { id: 'character_illness', weight: 8 }, + { id: 'character_recovery', weight: 6 }, + { id: 'character_accident', weight: 4 }, + { id: 'regional_festival', weight: 4 }, + // Regionale Events sind sehr selten { id: 'regional_storm', weight: 1 }, { id: 'regional_epidemic', weight: 1 }, { id: 'earthquake', weight: 1 }, ]; const total = events.reduce((s, e) => s + e.weight, 0); + // Reduzierte Wahrscheinlichkeit: Nur 30% Chance pro Tag, dass ein Event auftritt + const eventChance = 0.3; + if (Math.random() > eventChance) { + return null; // Kein Event heute + } + let r = Math.random() * total; let chosen = events[0].id; for (const e of events) { @@ -686,9 +693,9 @@ class FalukantService extends BaseService { const name = [character?.definedFirstName?.name, character?.definedLastName?.name].filter(Boolean).join(' ').trim(); payload.characterName = name || null; let delta = 0; - if (chosen === 'character_illness') delta = -(Math.floor(Math.random() * 11) + 5); // -5..-15 + if (chosen === 'character_illness') delta = -(Math.floor(Math.random() * 10) + 1); // -1..-10 if (chosen === 'character_recovery') delta = (Math.floor(Math.random() * 11) + 5); // +5..+15 - if (chosen === 'character_accident') delta = -(Math.floor(Math.random() * 16) + 10); // -10..-25 + if (chosen === 'character_accident') delta = -(Math.floor(Math.random() * 24) + 2); // -2..-25 payload.healthChange = delta > 0 ? `+${delta}` : `${delta}`; if (character) { const next = Math.min(100, Math.max(0, Number(character.health || 0) + delta)); @@ -704,20 +711,20 @@ class FalukantService extends BaseService { // Regionale Events sollten nur einen moderaten Health-Verlust verursachen // NICHT alle Charaktere töten! if (chosen === 'regional_epidemic' && character) { - // Moderate Health-Reduktion: -10 bis -20 (nicht tödlich!) - const delta = -(Math.floor(Math.random() * 11) + 10); // -10..-20 + // Moderate Health-Reduktion: -3 bis -7 (reduziert) + const delta = -(Math.floor(Math.random() * 5) + 3); // -3..-7 payload.healthChange = `${delta}`; const next = Math.min(100, Math.max(0, Number(character.health || 0) + delta)); await character.update({ health: next }, { transaction: t }); } else if (chosen === 'regional_storm' && character) { - // Sehr geringer Health-Verlust: -5 bis -10 - const delta = -(Math.floor(Math.random() * 6) + 5); // -5..-10 + // Sehr geringer Health-Verlust: -2 bis -5 + const delta = -(Math.floor(Math.random() * 4) + 2); // -2..-5 payload.healthChange = `${delta}`; const next = Math.min(100, Math.max(0, Number(character.health || 0) + delta)); await character.update({ health: next }, { transaction: t }); } else if (chosen === 'earthquake' && character) { - // Moderate Health-Reduktion: -15 bis -25 (kann gefährlich sein, aber nicht tödlich) - const delta = -(Math.floor(Math.random() * 11) + 15); // -15..-25 + // Moderate Health-Reduktion: -5 bis -10 (reduziert) + const delta = -(Math.floor(Math.random() * 6) + 5); // -5..-10 payload.healthChange = `${delta}`; const next = Math.min(100, Math.max(0, Number(character.health || 0) + delta)); await character.update({ health: next }, { transaction: t }); diff --git a/frontend/src/i18n/locales/de/admin.json b/frontend/src/i18n/locales/de/admin.json index 7994643..81db431 100644 --- a/frontend/src/i18n/locales/de/admin.json +++ b/frontend/src/i18n/locales/de/admin.json @@ -113,6 +113,27 @@ "errorSaveConnection": "Die Verbindung konnte nicht gespeichert werden.", "errorDeleteConnection": "Die Verbindung konnte nicht gelöscht werden.", "confirmDeleteConnection": "Verbindung wirklich löschen?" + }, + "createNPC": { + "title": "NPCs erstellen", + "region": "Stadt", + "allRegions": "Alle Städte", + "ageRange": "Altersbereich", + "to": "bis", + "years": "Jahre", + "titleRange": "Titel-Bereich", + "count": "Anzahl", + "create": "NPCs erstellen", + "creating": "Erstelle...", + "result": "Ergebnis", + "createdCount": "{count} NPCs wurden erstellt.", + "age": "Alter", + "errorLoadingRegions": "Fehler beim Laden der Städte.", + "errorLoadingTitles": "Fehler beim Laden der Titel.", + "errorCreating": "Fehler beim Erstellen der NPCs.", + "invalidAgeRange": "Ungültiger Altersbereich.", + "invalidTitleRange": "Ungültiger Titel-Bereich.", + "invalidCount": "Ungültige Anzahl (1-100)." } }, "chatrooms": { diff --git a/frontend/src/i18n/locales/de/navigation.json b/frontend/src/i18n/locales/de/navigation.json index d3662a6..a44a225 100644 --- a/frontend/src/i18n/locales/de/navigation.json +++ b/frontend/src/i18n/locales/de/navigation.json @@ -62,7 +62,8 @@ "logentries": "Log-Einträge", "edituser": "Benutzer bearbeiten", "database": "Datenbank", - "mapEditor": "Karteneditor" + "mapEditor": "Karteneditor", + "createNPC": "NPCs erstellen" }, "minigames": "Minispiele", "m-minigames": { diff --git a/frontend/src/i18n/locales/en/admin.json b/frontend/src/i18n/locales/en/admin.json index ce3a9f0..43f823a 100644 --- a/frontend/src/i18n/locales/en/admin.json +++ b/frontend/src/i18n/locales/en/admin.json @@ -140,6 +140,27 @@ "errorAddingStock": "Error adding warehouse.", "stockAdded": "Warehouse successfully added.", "invalidStockData": "Please enter valid warehouse type and quantity." + }, + "createNPC": { + "title": "Create NPCs", + "region": "City", + "allRegions": "All Cities", + "ageRange": "Age Range", + "to": "to", + "years": "years", + "titleRange": "Title Range", + "count": "Count", + "create": "Create NPCs", + "creating": "Creating...", + "result": "Result", + "createdCount": "{count} NPCs have been created.", + "age": "Age", + "errorLoadingRegions": "Error loading cities.", + "errorLoadingTitles": "Error loading titles.", + "errorCreating": "Error creating NPCs.", + "invalidAgeRange": "Invalid age range.", + "invalidTitleRange": "Invalid title range.", + "invalidCount": "Invalid count (1-100)." } }, "chatrooms": { diff --git a/frontend/src/i18n/locales/en/navigation.json b/frontend/src/i18n/locales/en/navigation.json index c0604ba..e722a9e 100644 --- a/frontend/src/i18n/locales/en/navigation.json +++ b/frontend/src/i18n/locales/en/navigation.json @@ -62,7 +62,8 @@ "logentries": "Log entries", "edituser": "Edit user", "database": "Database", - "mapEditor": "Map editor" + "mapEditor": "Map editor", + "createNPC": "Create NPCs" }, "minigames": "Mini games", "m-minigames": { diff --git a/frontend/src/router/adminRoutes.js b/frontend/src/router/adminRoutes.js index d5b436f..613d2b4 100644 --- a/frontend/src/router/adminRoutes.js +++ b/frontend/src/router/adminRoutes.js @@ -5,6 +5,7 @@ import UserRightsView from '../views/admin/UserRightsView.vue'; import ForumAdminView from '../dialogues/admin/ForumAdminView.vue'; import AdminFalukantEditUserView from '../views/admin/falukant/EditUserView.vue'; import AdminFalukantMapRegionsView from '../views/admin/falukant/MapRegionsView.vue'; +import AdminFalukantCreateNPCView from '../views/admin/falukant/CreateNPCView.vue'; import AdminMinigamesView from '../views/admin/MinigamesView.vue'; import AdminTaxiToolsView from '../views/admin/TaxiToolsView.vue'; import AdminUsersView from '../views/admin/UsersView.vue'; @@ -66,6 +67,12 @@ const adminRoutes = [ component: AdminFalukantMapRegionsView, meta: { requiresAuth: true } }, + { + path: '/admin/falukant/create-npc', + name: 'AdminFalukantCreateNPCView', + component: AdminFalukantCreateNPCView, + meta: { requiresAuth: true } + }, { path: '/admin/minigames/match3', name: 'AdminMinigames', diff --git a/frontend/src/views/admin/falukant/CreateNPCView.vue b/frontend/src/views/admin/falukant/CreateNPCView.vue new file mode 100644 index 0000000..06fcc4e --- /dev/null +++ b/frontend/src/views/admin/falukant/CreateNPCView.vue @@ -0,0 +1,289 @@ + + + + +