Entkopple Partnersuche vom Altersabstand und entferne garantierte Schwangerschaften
All checks were successful
Deploy to production / deploy (push) Successful in 2m41s

This commit is contained in:
Torsten Schulz (local)
2026-11-04 15:09:49 +01:00
parent 0178d33a21
commit 6e31a114cf
4 changed files with 98 additions and 148 deletions

View File

@@ -5,7 +5,8 @@ import FalukantPredefineFirstname from '../models/falukant/predefine/firstname.j
import FalukantPredefineLastname from '../models/falukant/predefine/lastname.js';
import FalukantUser from '../models/falukant/data/user.js';
import FalukantCharacter from '../models/falukant/data/character.js';
import { getCharacterDevelopment, hasReachedFertility } from '../utils/falukant/characterDevelopment.js';
import { getCharacterDevelopment } from '../utils/falukant/characterDevelopment.js';
import { buildPartnerCandidateQuery } from '../utils/falukant/partnerCandidates.js';
import RegionData from '../models/falukant/data/region.js';
import RegionType from '../models/falukant/type/region.js';
import FalukantStock from '../models/falukant/data/stock.js';
@@ -3610,108 +3611,6 @@ class FalukantService extends BaseService {
}
}
/**
* Nach der Trauung (relationship = married): geplante Geburt auf der Mutter-Charakterzeile setzen,
* damit der Daemon den UserCharacterWorker-Pfad mit pregnancy_due_at nutzen kann.
* Läuft idempotent pro Ehe über relationship_state.flags_json.honeymoonPregnancyScheduled.
* @returns {{ scheduled: boolean, motherCharacterId?: number }}
*/
async _ensureMarriageHoneymoonPregnancyScheduled({
playerCharacterId,
playerGender,
spouseCharacterId,
spouseGender,
relationshipId,
}) {
if (!relationshipId || !playerCharacterId || !spouseCharacterId) {
return { scheduled: false };
}
const norm = (g) => String(g || '').trim().toLowerCase();
const isFemale = (g) => {
const s = norm(g);
return s === 'female' || s === 'f';
};
const isMale = (g) => {
const s = norm(g);
return s === 'male' || s === 'm';
};
let motherId;
let fatherId;
if (isFemale(playerGender) && isMale(spouseGender)) {
motherId = playerCharacterId;
fatherId = spouseCharacterId;
} else if (isFemale(spouseGender) && isMale(playerGender)) {
motherId = spouseCharacterId;
fatherId = playerCharacterId;
} else {
return { scheduled: false };
}
const parents = await FalukantCharacter.unscoped().findAll({
where: { id: [motherId, fatherId] },
attributes: ['id', 'birthdate', 'fertileFromAge'],
});
if (
parents.length !== 2
|| parents.some((parent) => !hasReachedFertility(parent, calcAge(parent.birthdate)))
) {
return { scheduled: false };
}
const stateRow = await RelationshipState.findOne({ where: { relationshipId } });
const flags = stateRow?.flagsJson && typeof stateRow.flagsJson === 'object'
? { ...stateRow.flagsJson }
: {};
if (flags.honeymoonPregnancyScheduled) {
return { scheduled: false };
}
const pregRows = await sequelize.query(
`SELECT pregnancy_due_at FROM falukant_data."character" WHERE id = :id`,
{ replacements: { id: motherId }, type: Sequelize.QueryTypes.SELECT }
);
if (pregRows[0]?.pregnancy_due_at) {
return { scheduled: false };
}
// Automatische Hochzeits-Schwangerschaften nutzen die kurze Spielzeit von 18 Stunden.
const BIRTH_DUE_HOURS = 18;
const due = new Date();
due.setHours(due.getHours() + BIRTH_DUE_HOURS);
await FalukantCharacter.unscoped().update(
{
pregnancyDueAt: due,
pregnancyFatherCharacterId: fatherId,
},
{ where: { id: motherId } }
);
const mergedFlags = { ...flags, honeymoonPregnancyScheduled: true };
if (stateRow) {
await stateRow.update({ flagsJson: mergedFlags });
} else {
await RelationshipState.create({
relationshipId,
...this.buildDefaultRelationshipState('married'),
flagsJson: mergedFlags,
});
}
const mother = await FalukantCharacter.unscoped().findByPk(motherId, { attributes: ['id', 'userId'] });
if (mother?.userId) {
const fu = await FalukantUser.findByPk(mother.userId);
if (fu) {
const u = await User.findByPk(fu.userId);
if (u?.hashedId) {
await notifyUser(u.hashedId, 'familychanged', {});
}
}
}
return { scheduled: true, motherCharacterId: motherId };
}
async getFamily(hashedUserId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
if (!user) throw new Error('User not found');
@@ -3926,7 +3825,7 @@ class FalukantService extends BaseService {
if (!child.hasName || child.relationship || !kid || calcAge(kid.birthdate) < FAMILY_AGE.MIN_WOOING) continue;
child.possiblePartners = await this.getPossiblePartners(kid.id);
if (child.possiblePartners.length === 0) {
await this.createPossiblePartners(kid.id, kid.gender, kid.regionId, kid.titleOfNobility, calcAge(kid.birthdate));
await this.createPossiblePartners(kid.id, kid.gender, kid.regionId, kid.titleOfNobility);
child.possiblePartners = await this.getPossiblePartners(kid.id);
}
}
@@ -4039,25 +3938,11 @@ class FalukantService extends BaseService {
character.id,
character.gender,
character.regionId,
character.titleOfNobility,
ownAge
character.titleOfNobility
);
family.possiblePartners = await this.getPossiblePartners(character.id);
}
}
const marriedRel = family.relationships.find((r) => r.relationshipType === 'married');
if (marriedRel?.id && marriedRel.character2?.id != null) {
const honeymoon = await this._ensureMarriageHoneymoonPregnancyScheduled({
playerCharacterId: character.id,
playerGender: character.gender,
spouseCharacterId: marriedRel.character2.id,
spouseGender: marriedRel.character2.gender,
relationshipId: marriedRel.id,
});
if (honeymoon.scheduled && honeymoon.motherCharacterId === character.id) {
family.pregnancy = await this._getCharacterPregnancyOptional(character.id);
}
}
return family;
}
@@ -4663,7 +4548,7 @@ class FalukantService extends BaseService {
});
}
async createPossiblePartners(requestingCharacterId, requestingCharacterGender, requestingRegionId, requestingCharacterTitleOfNobility, ownAge) {
async createPossiblePartners(requestingCharacterId, requestingCharacterGender, requestingRegionId, requestingCharacterTitleOfNobility) {
try {
const minTitleResult = await TitleOfNobility.findOne({
order: [['id', 'ASC']],
@@ -4674,20 +4559,13 @@ class FalukantService extends BaseService {
}
const minTitle = minTitleResult.id;
const potentialPartners = await FalukantCharacter.findAll({
where: {
id: { [Op.ne]: requestingCharacterId },
gender: { [Op.ne]: requestingCharacterGender },
regionId: requestingRegionId,
birthdate: { [Op.lte]: new Date(Date.now() - FAMILY_AGE.MIN_WOOING * 24 * 60 * 60 * 1000) },
createdAt: { [Op.lt]: new Date(Date.now() - FAMILY_AGE.MIN_WOOING * 24 * 60 * 60 * 1000) },
titleOfNobility: { [Op.between]: [requestingCharacterTitleOfNobility - 1, requestingCharacterTitleOfNobility + 1] }
},
order: [
[Sequelize.literal(`ABS((EXTRACT(EPOCH FROM (NOW() - "birthdate")) / 86400) - ${ownAge})`), 'ASC']
],
limit: 5,
});
const potentialPartners = await FalukantCharacter.findAll(buildPartnerCandidateQuery({
characterId: requestingCharacterId,
gender: requestingCharacterGender,
regionId: requestingRegionId,
titleId: requestingCharacterTitleOfNobility,
minimumAge: FAMILY_AGE.MIN_WOOING,
}));
const proposals = potentialPartners.map(partner => {
const age = calcAge(partner.birthdate);