|
|
|
|
@@ -94,7 +94,7 @@ function calcAge(birthdate) {
|
|
|
|
|
|
|
|
|
|
// Ein realer Tag entspricht einem Falukant-Spieljahr.
|
|
|
|
|
const FAMILY_AGE = Object.freeze({
|
|
|
|
|
MIN_WOOING: 12,
|
|
|
|
|
MIN_WOOING: 10,
|
|
|
|
|
MIN_MARRIAGE: 14,
|
|
|
|
|
MIN_HOUSEHOLD_AND_CHILDREN: 16,
|
|
|
|
|
});
|
|
|
|
|
@@ -3820,7 +3820,7 @@ class FalukantService extends BaseService {
|
|
|
|
|
const childChars = childCharIds.length
|
|
|
|
|
? await FalukantCharacter.findAll({
|
|
|
|
|
where: { id: childCharIds },
|
|
|
|
|
attributes: ['id', 'birthdate', 'gender'],
|
|
|
|
|
attributes: ['id', 'birthdate', 'gender', 'regionId', 'titleOfNobility'],
|
|
|
|
|
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }]
|
|
|
|
|
})
|
|
|
|
|
: [];
|
|
|
|
|
@@ -3892,6 +3892,42 @@ class FalukantService extends BaseService {
|
|
|
|
|
otherParent: otherParentId != null ? (otherParentMap[otherParentId] || null) : null,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
const childIds = children.map((child) => child.childCharacterId);
|
|
|
|
|
const childRelationships = childIds.length
|
|
|
|
|
? await Relationship.findAll({
|
|
|
|
|
where: { [Op.or]: [{ character1Id: { [Op.in]: childIds } }, { character2Id: { [Op.in]: childIds } }] },
|
|
|
|
|
include: [
|
|
|
|
|
{ model: RelationshipType, as: 'relationshipType', attributes: ['tr'] },
|
|
|
|
|
{ model: FalukantCharacter, as: 'character1', attributes: ['id'], include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }] },
|
|
|
|
|
{ model: FalukantCharacter, as: 'character2', attributes: ['id'], include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }] },
|
|
|
|
|
],
|
|
|
|
|
attributes: ['id', 'character1Id', 'character2Id', 'nextStepProgress']
|
|
|
|
|
})
|
|
|
|
|
: [];
|
|
|
|
|
const childRelationshipMap = new Map();
|
|
|
|
|
for (const relation of childRelationships) {
|
|
|
|
|
const type = relation.relationshipType?.tr;
|
|
|
|
|
if (!['wooing', 'engaged', 'married'].includes(type)) continue;
|
|
|
|
|
const childId = childIds.includes(relation.character1Id) ? relation.character1Id : relation.character2Id;
|
|
|
|
|
const partner = relation.character1Id === childId ? relation.character2 : relation.character1;
|
|
|
|
|
childRelationshipMap.set(childId, {
|
|
|
|
|
id: relation.id,
|
|
|
|
|
status: type,
|
|
|
|
|
progress: relation.nextStepProgress || 0,
|
|
|
|
|
partnerName: partner?.definedFirstName?.name || 'Unbekannt',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
for (const child of children) {
|
|
|
|
|
child.relationship = childRelationshipMap.get(child.childCharacterId) || null;
|
|
|
|
|
child.possiblePartners = [];
|
|
|
|
|
const kid = childCharMap[child.childCharacterId];
|
|
|
|
|
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));
|
|
|
|
|
child.possiblePartners = await this.getPossiblePartners(kid.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 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'];
|
|
|
|
|
@@ -4637,7 +4673,7 @@ class FalukantService extends BaseService {
|
|
|
|
|
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(new Date() - 12 * 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: [
|
|
|
|
|
@@ -4684,7 +4720,7 @@ class FalukantService extends BaseService {
|
|
|
|
|
|| calcAge(character.birthdate) < FAMILY_AGE.MIN_WOOING
|
|
|
|
|
|| calcAge(proposedCharacter.birthdate) < FAMILY_AGE.MIN_WOOING
|
|
|
|
|
) {
|
|
|
|
|
const error = new Error('Werbung ist erst ab 12 Spieljahren möglich');
|
|
|
|
|
const error = new Error('Werbung ist erst ab 10 Spieljahren möglich');
|
|
|
|
|
error.status = 422;
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
@@ -4723,6 +4759,60 @@ class FalukantService extends BaseService {
|
|
|
|
|
return { success: true, message: 'Marriage proposal accepted', relationshipId: newRel.id };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async acceptChildMarriageProposal(hashedUserId, childCharacterId, proposedCharacterId) {
|
|
|
|
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
|
|
|
|
const childId = Number(childCharacterId);
|
|
|
|
|
const partnerId = Number(proposedCharacterId);
|
|
|
|
|
if (!Number.isInteger(childId) || !Number.isInteger(partnerId)) throw { status: 400, message: 'Ungültige Partnerauswahl' };
|
|
|
|
|
|
|
|
|
|
const childRelation = await ChildRelation.findOne({
|
|
|
|
|
where: { childCharacterId: childId, [Op.or]: [{ fatherCharacterId: user.character.id }, { motherCharacterId: user.character.id }] }
|
|
|
|
|
});
|
|
|
|
|
if (!childRelation) throw { status: 404, message: 'Kind gehört nicht zu deinem Charakter' };
|
|
|
|
|
|
|
|
|
|
const [child, partner, proposal, activeRelation] = await Promise.all([
|
|
|
|
|
FalukantCharacter.findByPk(childId, { attributes: ['id', 'birthdate'] }),
|
|
|
|
|
FalukantCharacter.findByPk(partnerId, { attributes: ['id', 'birthdate'] }),
|
|
|
|
|
MarriageProposal.findOne({ where: { requesterCharacterId: childId, proposedCharacterId: partnerId } }),
|
|
|
|
|
Relationship.findOne({ where: { [Op.or]: [{ character1Id: childId }, { character2Id: childId }] }, include: [{ model: RelationshipType, as: 'relationshipType', where: { tr: { [Op.in]: ['wooing', 'engaged', 'married'] } } }] })
|
|
|
|
|
]);
|
|
|
|
|
if (!proposal || !child || !partner) throw { status: 404, message: 'Partnervorschlag nicht gefunden' };
|
|
|
|
|
if (activeRelation) throw { status: 409, message: 'Das Kind ist bereits in einer Beziehung' };
|
|
|
|
|
if (calcAge(child.birthdate) < FAMILY_AGE.MIN_WOOING || calcAge(partner.birthdate) < FAMILY_AGE.MIN_WOOING) {
|
|
|
|
|
throw { status: 422, message: 'Verkuppelung ist erst ab 10 Spieljahren möglich' };
|
|
|
|
|
}
|
|
|
|
|
if (Number(user.money) < Number(proposal.cost)) throw { status: 422, message: 'Nicht genügend Guthaben' };
|
|
|
|
|
const wooingType = await RelationshipType.findOne({ where: { tr: 'wooing' } });
|
|
|
|
|
const moneyResult = await updateFalukantUserMoney(user.id, -proposal.cost, 'Child marriage arrangement', user.id);
|
|
|
|
|
if (!moneyResult.success) throw new Error('Geld konnte nicht abgezogen werden');
|
|
|
|
|
const relationship = await Relationship.create({
|
|
|
|
|
character1Id: childId,
|
|
|
|
|
character2Id: partnerId,
|
|
|
|
|
relationshipTypeId: wooingType.id,
|
|
|
|
|
nextStepProgress: Math.ceil(FalukantService.WOOING_PROGRESS_TARGET / 2),
|
|
|
|
|
});
|
|
|
|
|
await MarriageProposal.destroy({ where: { requesterCharacterId: childId } });
|
|
|
|
|
return { success: true, relationshipId: relationship.id };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async advanceChildWooing(hashedUserId, childCharacterId) {
|
|
|
|
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
|
|
|
|
const childId = Number(childCharacterId);
|
|
|
|
|
const isOwnChild = await ChildRelation.findOne({
|
|
|
|
|
where: { childCharacterId: childId, [Op.or]: [{ fatherCharacterId: user.character.id }, { motherCharacterId: user.character.id }] },
|
|
|
|
|
attributes: ['childCharacterId']
|
|
|
|
|
});
|
|
|
|
|
if (!isOwnChild) throw { status: 404, message: 'Kind gehört nicht zu deinem Charakter' };
|
|
|
|
|
const relationship = await Relationship.findOne({
|
|
|
|
|
where: { [Op.or]: [{ character1Id: childId }, { character2Id: childId }] },
|
|
|
|
|
include: [{ model: RelationshipType, as: 'relationshipType', where: { tr: 'wooing' } }]
|
|
|
|
|
});
|
|
|
|
|
if (!relationship) throw { status: 409, message: 'Keine laufende Werbung für dieses Kind' };
|
|
|
|
|
const engagedType = await RelationshipType.findOne({ where: { tr: 'engaged' } });
|
|
|
|
|
await relationship.update({ nextStepProgress: 0, relationshipTypeId: engagedType.id });
|
|
|
|
|
return { success: true, status: 'engaged' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async cancelWooing(hashedUserId) {
|
|
|
|
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
|
|
|
|
if (!user || !user.character) {
|
|
|
|
|
@@ -5608,44 +5698,37 @@ class FalukantService extends BaseService {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getWeddingCandidates(falukantUser) {
|
|
|
|
|
const parent = await FalukantCharacter.findOne({ where: { userId: falukantUser.id }, attributes: ['id'] });
|
|
|
|
|
if (!parent) return [];
|
|
|
|
|
const childRelations = await ChildRelation.findAll({
|
|
|
|
|
where: { [Op.or]: [{ fatherCharacterId: parent.id }, { motherCharacterId: parent.id }] },
|
|
|
|
|
attributes: ['childCharacterId']
|
|
|
|
|
});
|
|
|
|
|
const familyCharacterIds = [parent.id, ...childRelations.map((relation) => relation.childCharacterId)];
|
|
|
|
|
const relationships = await Relationship.findAll({
|
|
|
|
|
where: { [Op.or]: [{ character1Id: { [Op.in]: familyCharacterIds } }, { character2Id: { [Op.in]: familyCharacterIds } }] },
|
|
|
|
|
include: [
|
|
|
|
|
{ model: RelationshipType, as: 'relationshipType', where: { tr: 'engaged' } },
|
|
|
|
|
{ model: FalukantCharacter, as: 'character1', attributes: ['id', 'birthdate'], include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }] },
|
|
|
|
|
{ model: FalukantCharacter, as: 'character2', attributes: ['id', 'birthdate'], include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }] },
|
|
|
|
|
],
|
|
|
|
|
attributes: ['id', 'character1Id', 'character2Id']
|
|
|
|
|
});
|
|
|
|
|
return relationships
|
|
|
|
|
.filter((relationship) => calcAge(relationship.character1.birthdate) >= FAMILY_AGE.MIN_MARRIAGE
|
|
|
|
|
&& calcAge(relationship.character2.birthdate) >= FAMILY_AGE.MIN_MARRIAGE)
|
|
|
|
|
.map((relationship) => ({
|
|
|
|
|
relationshipId: relationship.id,
|
|
|
|
|
label: `${relationship.character1.definedFirstName?.name || 'Unbekannt'} & ${relationship.character2.definedFirstName?.name || 'Unbekannt'}`,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getPartyTypes(hashedUserId) {
|
|
|
|
|
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
|
|
|
|
const character = await FalukantCharacter.findOne({
|
|
|
|
|
where: { userId: falukantUser.id },
|
|
|
|
|
attributes: ['id', 'birthdate'],
|
|
|
|
|
});
|
|
|
|
|
const engagedCount = character && calcAge(character.birthdate) >= FAMILY_AGE.MIN_MARRIAGE
|
|
|
|
|
? await Relationship.count({
|
|
|
|
|
include: [
|
|
|
|
|
{
|
|
|
|
|
model: RelationshipType,
|
|
|
|
|
as: 'relationshipType',
|
|
|
|
|
where: { tr: 'engaged' },
|
|
|
|
|
required: true
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
model: FalukantCharacter,
|
|
|
|
|
as: 'character1',
|
|
|
|
|
where: { userId: falukantUser.id },
|
|
|
|
|
required: false
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
model: FalukantCharacter,
|
|
|
|
|
as: 'character2',
|
|
|
|
|
where: { userId: falukantUser.id },
|
|
|
|
|
required: false
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
where: {
|
|
|
|
|
[Op.or]: [
|
|
|
|
|
{ '$character1.user_id$': falukantUser.id },
|
|
|
|
|
{ '$character2.user_id$': falukantUser.id }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
: 0;
|
|
|
|
|
const weddingCandidates = await this.getWeddingCandidates(falukantUser);
|
|
|
|
|
const orConditions = [{ forMarriage: false }];
|
|
|
|
|
if (engagedCount > 0) {
|
|
|
|
|
if (weddingCandidates.length > 0) {
|
|
|
|
|
orConditions.push({ forMarriage: true });
|
|
|
|
|
}
|
|
|
|
|
const partyTypes = await PartyType.findAll({
|
|
|
|
|
@@ -5656,10 +5739,10 @@ class FalukantService extends BaseService {
|
|
|
|
|
});
|
|
|
|
|
const musicTypes = await MusicType.findAll();
|
|
|
|
|
const banquetteTypes = await BanquetteType.findAll();
|
|
|
|
|
return { partyTypes, musicTypes, banquetteTypes };
|
|
|
|
|
return { partyTypes, musicTypes, banquetteTypes, weddingCandidates };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createParty(hashedUserId, partyTypeId, musicId, banquetteId, nobilityIds = [], servantRatio) {
|
|
|
|
|
async createParty(hashedUserId, partyTypeId, musicId, banquetteId, nobilityIds = [], servantRatio, relationshipId = null) {
|
|
|
|
|
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
|
|
|
|
const since = new Date(Date.now() - 24 * 3600 * 1000);
|
|
|
|
|
const already = await Party.findOne({
|
|
|
|
|
@@ -5694,34 +5777,9 @@ class FalukantService extends BaseService {
|
|
|
|
|
|
|
|
|
|
const character = await FalukantCharacter.findOne({ where: { userId: falukantUser.id }, attributes: ['id', 'birthdate', 'titleOfNobility'] });
|
|
|
|
|
if (ptype.forMarriage) {
|
|
|
|
|
if (!character || calcAge(character.birthdate) < FAMILY_AGE.MIN_MARRIAGE) {
|
|
|
|
|
const error = new Error('Eine Hochzeit ist erst ab 14 Spieljahren möglich');
|
|
|
|
|
error.status = 422;
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
const engagement = await Relationship.findOne({
|
|
|
|
|
where: {
|
|
|
|
|
[Op.or]: [
|
|
|
|
|
{ character1Id: character.id },
|
|
|
|
|
{ character2Id: character.id },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
include: [{ model: RelationshipType, as: 'relationshipType', where: { tr: 'engaged' } }],
|
|
|
|
|
attributes: ['character1Id', 'character2Id'],
|
|
|
|
|
});
|
|
|
|
|
const partnerId = engagement?.character1Id === character.id
|
|
|
|
|
? engagement.character2Id
|
|
|
|
|
: engagement?.character1Id;
|
|
|
|
|
const partner = partnerId
|
|
|
|
|
? await FalukantCharacter.findByPk(partnerId, { attributes: ['birthdate'] })
|
|
|
|
|
: null;
|
|
|
|
|
if (!engagement) {
|
|
|
|
|
const error = new Error('Für eine Hochzeitsfeier musst du mit deinem Partner verlobt sein');
|
|
|
|
|
error.status = 422;
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
if (!partner || calcAge(partner.birthdate) < FAMILY_AGE.MIN_MARRIAGE) {
|
|
|
|
|
const error = new Error('Beide Verlobten müssen mindestens 14 Spieljahre alt sein');
|
|
|
|
|
const validRelationshipIds = (await this.getWeddingCandidates(falukantUser)).map((candidate) => candidate.relationshipId);
|
|
|
|
|
if (!validRelationshipIds.includes(Number(relationshipId))) {
|
|
|
|
|
const error = new Error('Für eine Hochzeitsfeier muss ein verlobtes Paar ab 14 Spieljahren ausgewählt werden');
|
|
|
|
|
error.status = 422;
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
@@ -5748,6 +5806,7 @@ class FalukantService extends BaseService {
|
|
|
|
|
falukantUserId: falukantUser.id,
|
|
|
|
|
musicTypeId: musicId,
|
|
|
|
|
banquetteTypeId: banquetteId,
|
|
|
|
|
relationshipId: ptype.forMarriage ? Number(relationshipId) : null,
|
|
|
|
|
servantRatio,
|
|
|
|
|
cost: cost
|
|
|
|
|
});
|
|
|
|
|
|