Enhance NPC creation functionality and validation in Admin module

- Updated AdminController to validate the count parameter, ensuring it is between 1 and 500.
- Refactored NPC creation logic in AdminService to create NPCs for each city-title combination, improving efficiency.
- Enhanced frontend localization files to reflect changes in count descriptions and validation messages.
- Updated CreateNPCView to provide user guidance on count input and display detailed creation results.
This commit is contained in:
Torsten Schulz (local)
2026-01-07 16:57:50 +01:00
parent 4d967fe7a2
commit 5996f819e8
5 changed files with 96 additions and 64 deletions

View File

@@ -389,13 +389,17 @@ class AdminController {
try { try {
const { userid: userId } = req.headers; const { userid: userId } = req.headers;
const { regionIds, minAge, maxAge, minTitleId, maxTitleId, count } = req.body; const { regionIds, minAge, maxAge, minTitleId, maxTitleId, count } = req.body;
const countValue = parseInt(count) || 1;
if (countValue < 1 || countValue > 500) {
return res.status(400).json({ error: 'Count must be between 1 and 500' });
}
const result = await AdminService.createNPCs(userId, { const result = await AdminService.createNPCs(userId, {
regionIds: regionIds && regionIds.length > 0 ? regionIds : null, regionIds: regionIds && regionIds.length > 0 ? regionIds : null,
minAge: parseInt(minAge) || 0, minAge: parseInt(minAge) || 0,
maxAge: parseInt(maxAge) || 100, maxAge: parseInt(maxAge) || 100,
minTitleId: parseInt(minTitleId) || 1, minTitleId: parseInt(minTitleId) || 1,
maxTitleId: parseInt(maxTitleId) || 19, maxTitleId: parseInt(maxTitleId) || 19,
count: parseInt(count) || 1 count: countValue
}); });
res.status(200).json(result); res.status(200).json(result);
} catch (error) { } catch (error) {

View File

@@ -1156,75 +1156,78 @@ class AdminService {
const genders = ['male', 'female']; const genders = ['male', 'female'];
const createdNPCs = []; const createdNPCs = [];
const totalNPCs = targetRegions.length * titles.length * count;
// Erstelle NPCs in einer Transaktion // Erstelle NPCs in einer Transaktion
// Für jede Stadt-Titel-Kombination wird die angegebene Anzahl erstellt
await sequelize.transaction(async (t) => { await sequelize.transaction(async (t) => {
for (let i = 0; i < count; i++) { for (const region of targetRegions) {
// Zufällige Region for (const title of titles) {
const region = targetRegions[Math.floor(Math.random() * targetRegions.length)]; // Erstelle 'count' NPCs für diese Stadt-Titel-Kombination
for (let i = 0; i < count; i++) {
// Zufälliges Geschlecht
const gender = genders[Math.floor(Math.random() * genders.length)];
// Zufälliges Geschlecht // Zufälliger Vorname
const gender = genders[Math.floor(Math.random() * genders.length)]; 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 Vorname // Zufälliger Nachname
const firstName = await FalukantPredefineFirstname.findAll({ const lastName = await FalukantPredefineLastname.findAll({
where: { gender }, order: sequelize.fn('RANDOM'),
order: sequelize.fn('RANDOM'), limit: 1,
limit: 1, transaction: t
transaction: t });
}); if (lastName.length === 0) {
if (firstName.length === 0) { throw new Error('No last names found');
throw new Error(`No first names found for gender: ${gender}`); }
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
// 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
});
}
} }
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 { return {
success: true, success: true,
count: createdNPCs.length, count: createdNPCs.length,
countPerCombination: count,
totalCombinations: targetRegions.length * titles.length,
npcs: createdNPCs npcs: createdNPCs
}; };
} }

View File

@@ -122,18 +122,20 @@
"to": "bis", "to": "bis",
"years": "Jahre", "years": "Jahre",
"titleRange": "Titel-Bereich", "titleRange": "Titel-Bereich",
"count": "Anzahl", "count": "Anzahl pro Stadt-Titel-Kombination",
"countHelp": "Diese Anzahl wird für jede Kombination aus gewählter Stadt und Titel erstellt.",
"create": "NPCs erstellen", "create": "NPCs erstellen",
"creating": "Erstelle...", "creating": "Erstelle...",
"result": "Ergebnis", "result": "Ergebnis",
"createdCount": "{count} NPCs wurden erstellt.", "createdCount": "{count} NPCs wurden erstellt.",
"combinationInfo": "{perCombination} NPCs pro Kombination × {combinations} Kombinationen = {count} NPCs insgesamt",
"age": "Alter", "age": "Alter",
"errorLoadingRegions": "Fehler beim Laden der Städte.", "errorLoadingRegions": "Fehler beim Laden der Städte.",
"errorLoadingTitles": "Fehler beim Laden der Titel.", "errorLoadingTitles": "Fehler beim Laden der Titel.",
"errorCreating": "Fehler beim Erstellen der NPCs.", "errorCreating": "Fehler beim Erstellen der NPCs.",
"invalidAgeRange": "Ungültiger Altersbereich.", "invalidAgeRange": "Ungültiger Altersbereich.",
"invalidTitleRange": "Ungültiger Titel-Bereich.", "invalidTitleRange": "Ungültiger Titel-Bereich.",
"invalidCount": "Ungültige Anzahl (1-100)." "invalidCount": "Ungültige Anzahl (1-500)."
} }
}, },
"chatrooms": { "chatrooms": {

View File

@@ -149,18 +149,20 @@
"to": "to", "to": "to",
"years": "years", "years": "years",
"titleRange": "Title Range", "titleRange": "Title Range",
"count": "Count", "count": "Count per City-Title Combination",
"countHelp": "This count will be created for each combination of selected city and title.",
"create": "Create NPCs", "create": "Create NPCs",
"creating": "Creating...", "creating": "Creating...",
"result": "Result", "result": "Result",
"createdCount": "{count} NPCs have been created.", "createdCount": "{count} NPCs have been created.",
"combinationInfo": "{perCombination} NPCs per combination × {combinations} combinations = {count} NPCs total",
"age": "Age", "age": "Age",
"errorLoadingRegions": "Error loading cities.", "errorLoadingRegions": "Error loading cities.",
"errorLoadingTitles": "Error loading titles.", "errorLoadingTitles": "Error loading titles.",
"errorCreating": "Error creating NPCs.", "errorCreating": "Error creating NPCs.",
"invalidAgeRange": "Invalid age range.", "invalidAgeRange": "Invalid age range.",
"invalidTitleRange": "Invalid title range.", "invalidTitleRange": "Invalid title range.",
"invalidCount": "Invalid count (1-100)." "invalidCount": "Invalid count (1-500)."
} }
}, },
"chatrooms": { "chatrooms": {

View File

@@ -47,7 +47,8 @@
<div class="form-group"> <div class="form-group">
<label>{{ $t('admin.falukant.createNPC.count') }}:</label> <label>{{ $t('admin.falukant.createNPC.count') }}:</label>
<input type="number" v-model.number="count" min="1" max="100" class="form-input" /> <input type="number" v-model.number="count" min="1" max="500" class="form-input" />
<div class="help-text">{{ $t('admin.falukant.createNPC.countHelp') }}</div>
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
@@ -61,6 +62,13 @@
<div v-if="result" class="result-section"> <div v-if="result" class="result-section">
<h2>{{ $t('admin.falukant.createNPC.result') }}</h2> <h2>{{ $t('admin.falukant.createNPC.result') }}</h2>
<p>{{ $t('admin.falukant.createNPC.createdCount', { count: result.count }) }}</p> <p>{{ $t('admin.falukant.createNPC.createdCount', { count: result.count }) }}</p>
<p v-if="result.totalCombinations" class="info-text">
{{ $t('admin.falukant.createNPC.combinationInfo', {
perCombination: result.countPerCombination,
combinations: result.totalCombinations,
total: result.count
}) }}
</p>
<div v-if="result.npcs && result.npcs.length > 0" class="npcs-list"> <div v-if="result.npcs && result.npcs.length > 0" class="npcs-list">
<div v-for="npc in result.npcs" :key="npc.id" class="npc-item"> <div v-for="npc in result.npcs" :key="npc.id" class="npc-item">
{{ $t(`falukant.titles.${npc.gender}.${npc.title}`) }} {{ npc.firstName }} {{ npc.lastName }} {{ $t(`falukant.titles.${npc.gender}.${npc.title}`) }} {{ npc.firstName }} {{ npc.lastName }}
@@ -142,7 +150,7 @@ export default {
return; return;
} }
if (this.count < 1 || this.count > 100) { if (this.count < 1 || this.count > 500) {
this.error = this.$t('admin.falukant.createNPC.invalidCount'); this.error = this.$t('admin.falukant.createNPC.invalidCount');
return; return;
} }
@@ -286,4 +294,17 @@ export default {
border-radius: 4px; border-radius: 4px;
margin-top: 20px; margin-top: 20px;
} }
.help-text {
font-size: 0.9em;
color: #666;
margin-top: 5px;
font-style: italic;
}
.info-text {
font-size: 0.9em;
color: #155724;
margin-top: 5px;
}
</style> </style>