feat(admin): implement pregnancy and birth management features
Some checks failed
Deploy to production / deploy (push) Failing after 2m6s

- Added new admin functionalities to force pregnancy, clear pregnancy, and trigger birth for characters.
- Introduced corresponding routes and controller methods in adminRouter and adminController.
- Enhanced the FalukantCharacter model to include pregnancy-related fields.
- Created database migration for adding pregnancy columns to the character table.
- Updated frontend views and internationalization files to support new pregnancy and birth management features.
- Improved user feedback and error handling for these new actions.
This commit is contained in:
Torsten Schulz (local)
2026-03-30 13:44:43 +02:00
parent b2591da428
commit c52d4b60f9
18 changed files with 628 additions and 160 deletions

View File

@@ -13,6 +13,9 @@ class AdminController {
this.searchUser = this.searchUser.bind(this);
this.getFalukantUserById = this.getFalukantUserById.bind(this);
this.changeFalukantUser = this.changeFalukantUser.bind(this);
this.adminForceFalukantPregnancy = this.adminForceFalukantPregnancy.bind(this);
this.adminClearFalukantPregnancy = this.adminClearFalukantPregnancy.bind(this);
this.adminForceFalukantBirth = this.adminForceFalukantBirth.bind(this);
this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this);
this.updateFalukantStock = this.updateFalukantStock.bind(this);
this.addFalukantStock = this.addFalukantStock.bind(this);
@@ -372,6 +375,50 @@ class AdminController {
}
}
async adminForceFalukantPregnancy(req, res) {
try {
const { userid: userId } = req.headers;
const { characterId, fatherCharacterId, dueInDays } = req.body;
const response = await AdminService.adminForceFalukantPregnancy(userId, characterId, {
fatherCharacterId,
dueInDays,
});
res.status(200).json(response);
} catch (error) {
console.log(error);
res.status(400).json({ error: error.message });
}
}
async adminClearFalukantPregnancy(req, res) {
try {
const { userid: userId } = req.headers;
const { characterId } = req.body;
const response = await AdminService.adminClearFalukantPregnancy(userId, characterId);
res.status(200).json(response);
} catch (error) {
console.log(error);
res.status(400).json({ error: error.message });
}
}
async adminForceFalukantBirth(req, res) {
try {
const { userid: userId } = req.headers;
const { motherCharacterId, fatherCharacterId, birthContext, legitimacy, gender } = req.body;
const response = await AdminService.adminForceFalukantBirth(userId, motherCharacterId, {
fatherCharacterId,
birthContext,
legitimacy,
gender,
});
res.status(200).json(response);
} catch (error) {
console.log(error);
res.status(400).json({ error: error.message });
}
}
async getFalukantUserBranches(req, res) {
try {
const { userid: userId } = req.headers;

View File

@@ -0,0 +1,36 @@
"use strict";
/** Schwangerschaft (Admin / Spiel): erwarteter Geburtstermin + optionaler Vater-Charakter */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_due_at'
) THEN
ALTER TABLE falukant_data."character"
ADD COLUMN pregnancy_due_at TIMESTAMPTZ NULL;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_father_character_id'
) THEN
ALTER TABLE falukant_data."character"
ADD COLUMN pregnancy_father_character_id INTEGER NULL
REFERENCES falukant_data."character"(id) ON DELETE SET NULL;
END IF;
END$$;
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_father_character_id;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_due_at;
`);
},
};

View File

@@ -379,6 +379,8 @@ export default function setupAssociations() {
FalukantCharacter.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(FalukantCharacter, { foreignKey: 'regionId', as: 'charactersInRegion' });
FalukantCharacter.belongsTo(FalukantCharacter, { foreignKey: 'pregnancyFatherCharacterId', as: 'pregnancyFather' });
FalukantStock.belongsTo(FalukantStockType, { foreignKey: 'stockTypeId', as: 'stockType' });
FalukantStockType.hasMany(FalukantStock, { foreignKey: 'stockTypeId', as: 'stocks' });

View File

@@ -45,6 +45,14 @@ FalukantCharacter.init(
min: 0,
max: 100
}
},
pregnancyDueAt: {
type: DataTypes.DATE,
allowNull: true,
},
pregnancyFatherCharacterId: {
type: DataTypes.INTEGER,
allowNull: true,
}
},
{

View File

@@ -43,6 +43,9 @@ router.post('/contacts/answer', authenticate, adminController.answerContact);
router.post('/falukant/searchuser', authenticate, adminController.searchUser);
router.get('/falukant/getuser/:id', authenticate, adminController.getFalukantUserById);
router.post('/falukant/edituser', authenticate, adminController.changeFalukantUser);
router.post('/falukant/character/force-pregnancy', authenticate, adminController.adminForceFalukantPregnancy);
router.post('/falukant/character/clear-pregnancy', authenticate, adminController.adminClearFalukantPregnancy);
router.post('/falukant/character/force-birth', authenticate, adminController.adminForceFalukantBirth);
router.get('/falukant/branches/:falukantUserId', authenticate, adminController.getFalukantUserBranches);
router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock);
router.post('/falukant/stock', authenticate, adminController.addFalukantStock);

View File

@@ -28,6 +28,7 @@ import Image from '../models/community/image.js';
import EroticVideo from '../models/community/erotic_video.js';
import EroticContentReport from '../models/community/erotic_content_report.js';
import TitleOfNobility from "../models/falukant/type/title_of_nobility.js";
import ChildRelation from "../models/falukant/data/child_relation.js";
import { sequelize } from '../utils/sequelize.js';
import npcCreationJobService from './npcCreationJobService.js';
import { v4 as uuidv4 } from 'uuid';
@@ -669,7 +670,6 @@ class AdminService {
include: [{
model: FalukantCharacter,
as: 'character',
attributes: ['birthdate', 'health', 'title_of_nobility'],
include: [{
model: FalukantPredefineFirstname,
as: 'definedFirstName',
@@ -945,6 +945,145 @@ class AdminService {
await character.save();
}
/**
* Admin: Charakter als schwanger markieren (erwarteter Geburtstermin).
* @param {number} fatherCharacterId - optional; Vater-Charakter-ID
* @param {number} dueInDays - Tage bis zur „Geburt“ (Default 21)
*/
async adminForceFalukantPregnancy(userId, characterId, { fatherCharacterId = null, dueInDays = 21 } = {}) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
}
const mother = await FalukantCharacter.findByPk(characterId);
if (!mother) throw new Error('notfound');
if (fatherCharacterId != null) {
const father = await FalukantCharacter.findByPk(Number(fatherCharacterId));
if (!father) throw new Error('fatherNotFound');
}
const days = Math.max(1, Math.min(365, Number(dueInDays) || 21));
const due = new Date();
due.setDate(due.getDate() + days);
await mother.update({
pregnancyDueAt: due,
pregnancyFatherCharacterId: fatherCharacterId != null ? Number(fatherCharacterId) : null,
});
const fu = mother.userId ? await FalukantUser.findByPk(mother.userId) : null;
if (fu) {
const u = await User.findByPk(fu.userId);
if (u?.hashedId) await notifyUser(u.hashedId, 'familychanged', {});
}
return {
success: true,
pregnancyDueAt: due.toISOString(),
pregnancyFatherCharacterId: fatherCharacterId != null ? Number(fatherCharacterId) : null,
};
}
async adminClearFalukantPregnancy(userId, characterId) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
}
const mother = await FalukantCharacter.findByPk(characterId);
if (!mother) throw new Error('notfound');
await mother.update({
pregnancyDueAt: null,
pregnancyFatherCharacterId: null,
});
const fu = mother.userId ? await FalukantUser.findByPk(mother.userId) : null;
if (fu) {
const u = await User.findByPk(fu.userId);
if (u?.hashedId) await notifyUser(u.hashedId, 'familychanged', {});
}
return { success: true };
}
/**
* Admin: Geburt auslösen Kind-Charakter + child_relation; setzt Schwangerschaft zurück.
*/
async adminForceFalukantBirth(userId, motherCharacterId, {
fatherCharacterId,
birthContext = 'marriage',
legitimacy = 'legitimate',
gender = null,
} = {}) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
}
if (fatherCharacterId == null || fatherCharacterId === '') {
throw new Error('fatherRequired');
}
const mother = await FalukantCharacter.findByPk(motherCharacterId);
if (!mother) throw new Error('notfound');
const father = await FalukantCharacter.findByPk(Number(fatherCharacterId));
if (!father) throw new Error('fatherNotFound');
if (Number(fatherCharacterId) === Number(motherCharacterId)) {
throw new Error('invalidParents');
}
const ctx = ['marriage', 'lover'].includes(birthContext) ? birthContext : 'marriage';
const leg = ['legitimate', 'acknowledged_bastard', 'hidden_bastard'].includes(legitimacy)
? legitimacy
: 'legitimate';
const childGender = gender === 'male' || gender === 'female'
? gender
: (Math.random() < 0.5 ? 'male' : 'female');
const nobility = await TitleOfNobility.findOne({ where: { labelTr: 'noncivil' } });
if (!nobility) throw new Error('titleNotFound');
const fnObj = await FalukantPredefineFirstname.findOne({
where: { gender: childGender },
order: Sequelize.fn('RANDOM'),
});
if (!fnObj) throw new Error('firstNameNotFound');
let childCharacterId;
await sequelize.transaction(async (t) => {
const baby = await FalukantCharacter.create({
userId: null,
regionId: mother.regionId,
firstName: fnObj.id,
lastName: mother.lastName,
gender: childGender,
birthdate: new Date(),
titleOfNobility: nobility.id,
health: 100,
moodId: 1,
}, { transaction: t });
childCharacterId = baby.id;
await ChildRelation.create({
fatherCharacterId: father.id,
motherCharacterId: mother.id,
childCharacterId: baby.id,
nameSet: false,
isHeir: false,
legitimacy: leg,
birthContext: ctx,
publicKnown: ctx === 'marriage',
}, { transaction: t });
await mother.update({
pregnancyDueAt: null,
pregnancyFatherCharacterId: null,
}, { transaction: t });
});
const notifyParent = async (char) => {
if (!char?.userId) return;
const fu = await FalukantUser.findByPk(char.userId);
if (!fu) return;
const u = await User.findByPk(fu.userId);
if (u?.hashedId) {
await notifyUser(u.hashedId, 'familychanged', {});
await notifyUser(u.hashedId, 'falukantUpdateStatus', {});
}
};
await notifyParent(mother);
await notifyParent(father);
return { success: true, childCharacterId, gender: childGender };
}
// --- User Administration ---
async searchUsers(requestingHashedUserId, query) {
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {

View File

@@ -3518,7 +3518,13 @@ class FalukantService extends BaseService {
deathPartners: relationships.filter(r => r.relationshipType === 'widowed'),
children: children.map(({ _createdAt, ...rest }) => rest),
possiblePartners: [],
possibleLovers: []
possibleLovers: [],
pregnancy: character.pregnancyDueAt
? {
dueAt: character.pregnancyDueAt,
fatherCharacterId: character.pregnancyFatherCharacterId,
}
: null,
};
const ownAge = calcAge(character.birthdate);
if (ownAge >= 12) {

View File

@@ -0,0 +1,6 @@
-- Optional: manuell ausführen, falls Migration nicht genutzt wird
ALTER TABLE falukant_data."character"
ADD COLUMN IF NOT EXISTS pregnancy_due_at TIMESTAMPTZ NULL;
ALTER TABLE falukant_data."character"
ADD COLUMN IF NOT EXISTS pregnancy_father_character_id INTEGER NULL
REFERENCES falukant_data."character"(id) ON DELETE SET NULL;