feat(bisaya): implement child marriage proposal and wooing features, update age rules and relationships
All checks were successful
Deploy to production / deploy (push) Successful in 3m15s
All checks were successful
Deploy to production / deploy (push) Successful in 3m15s
This commit is contained in:
@@ -102,6 +102,10 @@ class FalukantController {
|
|||||||
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
|
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
|
||||||
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId), { blockInDebtorsPrison: true });
|
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId), { blockInDebtorsPrison: true });
|
||||||
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId), { blockInDebtorsPrison: true });
|
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId), { blockInDebtorsPrison: true });
|
||||||
|
this.acceptChildMarriageProposal = this._wrapWithUser((userId, req) =>
|
||||||
|
this.service.acceptChildMarriageProposal(userId, req.params.childCharacterId, req.body.proposedCharacterId), { blockInDebtorsPrison: true });
|
||||||
|
this.advanceChildWooing = this._wrapWithUser((userId, req) =>
|
||||||
|
this.service.advanceChildWooing(userId, req.params.childCharacterId), { blockInDebtorsPrison: true });
|
||||||
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId), { blockInDebtorsPrison: true });
|
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId), { blockInDebtorsPrison: true });
|
||||||
this.cancelWooing = this._wrapWithUser(async (userId) => {
|
this.cancelWooing = this._wrapWithUser(async (userId) => {
|
||||||
try {
|
try {
|
||||||
@@ -162,8 +166,8 @@ class FalukantController {
|
|||||||
|
|
||||||
this.getPartyTypes = this._wrapWithUser((userId) => this.service.getPartyTypes(userId));
|
this.getPartyTypes = this._wrapWithUser((userId) => this.service.getPartyTypes(userId));
|
||||||
this.createParty = this._wrapWithUser((userId, req) => {
|
this.createParty = this._wrapWithUser((userId, req) => {
|
||||||
const { partyTypeId, musicId, banquetteId, nobilityIds, servantRatio } = req.body;
|
const { partyTypeId, musicId, banquetteId, nobilityIds, servantRatio, relationshipId } = req.body;
|
||||||
return this.service.createParty(userId, partyTypeId, musicId, banquetteId, nobilityIds, servantRatio);
|
return this.service.createParty(userId, partyTypeId, musicId, banquetteId, nobilityIds, servantRatio, relationshipId);
|
||||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||||
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));
|
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.party
|
||||||
|
ADD COLUMN IF NOT EXISTS relationship_id INTEGER NULL
|
||||||
|
REFERENCES falukant_data.relationship(id) ON DELETE SET NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS party_relationship_id_idx
|
||||||
|
ON falukant_data.party (relationship_id);
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`ALTER TABLE falukant_data.party DROP COLUMN IF EXISTS relationship_id;`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -633,6 +633,7 @@ export default function setupAssociations() {
|
|||||||
|
|
||||||
FalukantUser.hasMany(Party, { foreignKey: 'falukantUserId', as: 'parties' });
|
FalukantUser.hasMany(Party, { foreignKey: 'falukantUserId', as: 'parties' });
|
||||||
Party.belongsTo(FalukantUser, { foreignKey: 'falukantUserId', as: 'partyUser' });
|
Party.belongsTo(FalukantUser, { foreignKey: 'falukantUserId', as: 'partyUser' });
|
||||||
|
Party.belongsTo(Relationship, { foreignKey: 'relationshipId', as: 'marriageRelationship' });
|
||||||
|
|
||||||
Party.belongsToMany(TitleOfNobility, {
|
Party.belongsToMany(TitleOfNobility, {
|
||||||
through: PartyInvitedNobility,
|
through: PartyInvitedNobility,
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ Party.init({
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
field: 'falukant_user_id'
|
field: 'falukant_user_id'
|
||||||
},
|
},
|
||||||
|
relationshipId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'relationship_id'
|
||||||
|
},
|
||||||
musicTypeId: {
|
musicTypeId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
@@ -41,4 +46,4 @@ Party.init({
|
|||||||
timestamps: true,
|
timestamps: true,
|
||||||
underscored: true});
|
underscored: true});
|
||||||
|
|
||||||
export default Party;
|
export default Party;
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ router.get('/dashboard-widget', falukantController.getDashboardWidget);
|
|||||||
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
|
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
|
||||||
router.post('/family/cancel-wooing', falukantController.cancelWooing);
|
router.post('/family/cancel-wooing', falukantController.cancelWooing);
|
||||||
router.post('/family/set-heir', falukantController.setHeir);
|
router.post('/family/set-heir', falukantController.setHeir);
|
||||||
|
router.post('/family/children/:childCharacterId/accept-marriage-proposal', falukantController.acceptChildMarriageProposal);
|
||||||
|
router.post('/family/children/:childCharacterId/advance-wooing', falukantController.advanceChildWooing);
|
||||||
router.post('/family/lover', falukantController.createLoverRelationship);
|
router.post('/family/lover', falukantController.createLoverRelationship);
|
||||||
router.post('/family/marriage/spend-time', falukantController.spendTimeWithSpouse);
|
router.post('/family/marriage/spend-time', falukantController.spendTimeWithSpouse);
|
||||||
router.post('/family/marriage/gift', falukantController.giftToSpouse);
|
router.post('/family/marriage/gift', falukantController.giftToSpouse);
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ function calcAge(birthdate) {
|
|||||||
|
|
||||||
// Ein realer Tag entspricht einem Falukant-Spieljahr.
|
// Ein realer Tag entspricht einem Falukant-Spieljahr.
|
||||||
const FAMILY_AGE = Object.freeze({
|
const FAMILY_AGE = Object.freeze({
|
||||||
MIN_WOOING: 12,
|
MIN_WOOING: 10,
|
||||||
MIN_MARRIAGE: 14,
|
MIN_MARRIAGE: 14,
|
||||||
MIN_HOUSEHOLD_AND_CHILDREN: 16,
|
MIN_HOUSEHOLD_AND_CHILDREN: 16,
|
||||||
});
|
});
|
||||||
@@ -3820,7 +3820,7 @@ class FalukantService extends BaseService {
|
|||||||
const childChars = childCharIds.length
|
const childChars = childCharIds.length
|
||||||
? await FalukantCharacter.findAll({
|
? await FalukantCharacter.findAll({
|
||||||
where: { id: childCharIds },
|
where: { id: childCharIds },
|
||||||
attributes: ['id', 'birthdate', 'gender'],
|
attributes: ['id', 'birthdate', 'gender', 'regionId', 'titleOfNobility'],
|
||||||
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }]
|
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }]
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
@@ -3892,6 +3892,42 @@ class FalukantService extends BaseService {
|
|||||||
otherParent: otherParentId != null ? (otherParentMap[otherParentId] || null) : null,
|
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)
|
// Sort children globally by relation createdAt ascending (older first)
|
||||||
children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt));
|
children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt));
|
||||||
const inProgress = ['wooing', 'engaged', 'married'];
|
const inProgress = ['wooing', 'engaged', 'married'];
|
||||||
@@ -4637,7 +4673,7 @@ class FalukantService extends BaseService {
|
|||||||
gender: { [Op.ne]: requestingCharacterGender },
|
gender: { [Op.ne]: requestingCharacterGender },
|
||||||
regionId: requestingRegionId,
|
regionId: requestingRegionId,
|
||||||
birthdate: { [Op.lte]: new Date(Date.now() - FAMILY_AGE.MIN_WOOING * 24 * 60 * 60 * 1000) },
|
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] }
|
titleOfNobility: { [Op.between]: [requestingCharacterTitleOfNobility - 1, requestingCharacterTitleOfNobility + 1] }
|
||||||
},
|
},
|
||||||
order: [
|
order: [
|
||||||
@@ -4684,7 +4720,7 @@ class FalukantService extends BaseService {
|
|||||||
|| calcAge(character.birthdate) < FAMILY_AGE.MIN_WOOING
|
|| calcAge(character.birthdate) < FAMILY_AGE.MIN_WOOING
|
||||||
|| calcAge(proposedCharacter.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;
|
error.status = 422;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -4723,6 +4759,60 @@ class FalukantService extends BaseService {
|
|||||||
return { success: true, message: 'Marriage proposal accepted', relationshipId: newRel.id };
|
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) {
|
async cancelWooing(hashedUserId) {
|
||||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
if (!user || !user.character) {
|
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) {
|
async getPartyTypes(hashedUserId) {
|
||||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||||
const character = await FalukantCharacter.findOne({
|
const weddingCandidates = await this.getWeddingCandidates(falukantUser);
|
||||||
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 orConditions = [{ forMarriage: false }];
|
const orConditions = [{ forMarriage: false }];
|
||||||
if (engagedCount > 0) {
|
if (weddingCandidates.length > 0) {
|
||||||
orConditions.push({ forMarriage: true });
|
orConditions.push({ forMarriage: true });
|
||||||
}
|
}
|
||||||
const partyTypes = await PartyType.findAll({
|
const partyTypes = await PartyType.findAll({
|
||||||
@@ -5656,10 +5739,10 @@ class FalukantService extends BaseService {
|
|||||||
});
|
});
|
||||||
const musicTypes = await MusicType.findAll();
|
const musicTypes = await MusicType.findAll();
|
||||||
const banquetteTypes = await BanquetteType.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 falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||||
const since = new Date(Date.now() - 24 * 3600 * 1000);
|
const since = new Date(Date.now() - 24 * 3600 * 1000);
|
||||||
const already = await Party.findOne({
|
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'] });
|
const character = await FalukantCharacter.findOne({ where: { userId: falukantUser.id }, attributes: ['id', 'birthdate', 'titleOfNobility'] });
|
||||||
if (ptype.forMarriage) {
|
if (ptype.forMarriage) {
|
||||||
if (!character || calcAge(character.birthdate) < FAMILY_AGE.MIN_MARRIAGE) {
|
const validRelationshipIds = (await this.getWeddingCandidates(falukantUser)).map((candidate) => candidate.relationshipId);
|
||||||
const error = new Error('Eine Hochzeit ist erst ab 14 Spieljahren möglich');
|
if (!validRelationshipIds.includes(Number(relationshipId))) {
|
||||||
error.status = 422;
|
const error = new Error('Für eine Hochzeitsfeier muss ein verlobtes Paar ab 14 Spieljahren ausgewählt werden');
|
||||||
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');
|
|
||||||
error.status = 422;
|
error.status = 422;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -5748,6 +5806,7 @@ class FalukantService extends BaseService {
|
|||||||
falukantUserId: falukantUser.id,
|
falukantUserId: falukantUser.id,
|
||||||
musicTypeId: musicId,
|
musicTypeId: musicId,
|
||||||
banquetteTypeId: banquetteId,
|
banquetteTypeId: banquetteId,
|
||||||
|
relationshipId: ptype.forMarriage ? Number(relationshipId) : null,
|
||||||
servantRatio,
|
servantRatio,
|
||||||
cost: cost
|
cost: cost
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ verwendet daher die Differenz zwischen `CURRENT_DATE` und `birthdate` in Tagen.
|
|||||||
|
|
||||||
| Bereich | Mindestalter | Regel |
|
| Bereich | Mindestalter | Regel |
|
||||||
| --- | ---: | --- |
|
| --- | ---: | --- |
|
||||||
| Werbung | 12 | Partner können vorgeschlagen und ein Vorschlag kann angenommen werden. |
|
| Werbung | 10 | Partner können vorgeschlagen und ein Vorschlag kann angenommen werden. |
|
||||||
| Hochzeit | 14 | Eine Verlobung kann durch eine Hochzeitsfeier vollzogen werden. |
|
| Hochzeit | 14 | Eine Verlobung kann durch eine Hochzeitsfeier vollzogen werden. |
|
||||||
| Gemeinsamer Haushalt und Kinder | 16 | Erst dann darf die Spielmechanik eine Schwangerschaft oder Geburt aus einer Ehe erzeugen. |
|
| Gemeinsamer Haushalt und Kinder | 16 | Erst dann darf die Spielmechanik eine Schwangerschaft oder Geburt aus einer Ehe erzeugen. |
|
||||||
|
|
||||||
@@ -15,10 +15,16 @@ Die Regeln gelten für **beide** Personen einer Beziehung. Sie bilden bewusst
|
|||||||
Spielphasen ab; es gibt keine individuelle Prüfung körperlicher Reife und keine
|
Spielphasen ab; es gibt keine individuelle Prüfung körperlicher Reife und keine
|
||||||
explizite Sexualmechanik.
|
explizite Sexualmechanik.
|
||||||
|
|
||||||
|
Eltern können ihre benannten Kinder ab 10 Spieljahren verkuppeln. Diese
|
||||||
|
Werbung ist verkürzt: Nach der Auswahl eines Kandidaten genügt ein weiterer
|
||||||
|
Verkuppelungsschritt zur Verlobung. Die Hochzeitsfeier bleibt trotzdem ab 14
|
||||||
|
Spieljahren erforderlich und wird dabei ausdrücklich dem verlobten Paar
|
||||||
|
zugeordnet. Erst ab 16 kann die normale automatische Kinderlogik greifen.
|
||||||
|
|
||||||
## Bereits im Backend und Daemon umgesetzt
|
## Bereits im Backend und Daemon umgesetzt
|
||||||
|
|
||||||
- `backend/services/falukantService.js`
|
- `backend/services/falukantService.js`
|
||||||
- erzeugt Heiratsvorschläge nur für mindestens 12 Jahre alte Partner;
|
- erzeugt Heiratsvorschläge nur für mindestens 10 Jahre alte Partner;
|
||||||
- prüft beim Annehmen eines Vorschlags beide Alter erneut;
|
- prüft beim Annehmen eines Vorschlags beide Alter erneut;
|
||||||
- bietet den Hochzeitstyp erst ab 14 an und prüft beim Bestellen der Feier
|
- bietet den Hochzeitstyp erst ab 14 an und prüft beim Bestellen der Feier
|
||||||
beide Verlobte serverseitig;
|
beide Verlobte serverseitig;
|
||||||
|
|||||||
@@ -207,10 +207,17 @@ export default {
|
|||||||
.app-section-bar__back {
|
.app-section-bar__back {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
background: rgba(255, 255, 255, 0.82);
|
background: rgba(255, 255, 255, 0.82);
|
||||||
|
color: var(--color-text-primary);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-section-bar__back:hover:not(:disabled) {
|
||||||
|
background: var(--color-secondary-soft);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.app-section-bar {
|
.app-section-bar {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -775,6 +775,20 @@
|
|||||||
"setAsHeir": "Als Erben festlegen",
|
"setAsHeir": "Als Erben festlegen",
|
||||||
"heirSetSuccess": "Das Kind wurde erfolgreich als Erbe festgelegt.",
|
"heirSetSuccess": "Das Kind wurde erfolgreich als Erbe festgelegt.",
|
||||||
"heirSetError": "Fehler beim Festlegen des Erben.",
|
"heirSetError": "Fehler beim Festlegen des Erben.",
|
||||||
|
"matchmaking": "Verkuppelung",
|
||||||
|
"selectPartner": "Partner auswählen",
|
||||||
|
"arrange": "Werbung arrangieren",
|
||||||
|
"arrangeSuccess": "Die Werbung des Kindes wurde arrangiert.",
|
||||||
|
"arrangeError": "Die Verkuppelung konnte nicht durchgeführt werden.",
|
||||||
|
"completeWooing": "Werbung abschließen",
|
||||||
|
"engagedSuccess": "Das Kind ist nun verlobt.",
|
||||||
|
"weddingHint": "Hochzeitsfeier ab 14 Jahren möglich.",
|
||||||
|
"noPartnerProposal": "Noch keine passenden Vorschläge.",
|
||||||
|
"relationshipStatus": {
|
||||||
|
"wooing": "Werbung läuft",
|
||||||
|
"engaged": "Verlobt",
|
||||||
|
"married": "Verheiratet"
|
||||||
|
},
|
||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
"none": "Keine Kinder vorhanden.",
|
"none": "Keine Kinder vorhanden.",
|
||||||
"detailButton": "Details anzeigen",
|
"detailButton": "Details anzeigen",
|
||||||
|
|||||||
@@ -314,6 +314,7 @@
|
|||||||
<th>{{ $t('falukant.family.children.otherParent') }}</th>
|
<th>{{ $t('falukant.family.children.otherParent') }}</th>
|
||||||
<th>{{ $t('falukant.family.children.age') }}</th>
|
<th>{{ $t('falukant.family.children.age') }}</th>
|
||||||
<th>{{ $t('falukant.family.children.heir') }}</th>
|
<th>{{ $t('falukant.family.children.heir') }}</th>
|
||||||
|
<th>{{ $t('falukant.family.children.matchmaking') }}</th>
|
||||||
<th>{{ $t('falukant.family.children.actions') }}</th>
|
<th>{{ $t('falukant.family.children.actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -365,6 +366,38 @@
|
|||||||
{{ $t('falukant.family.children.setAsHeir') }}
|
{{ $t('falukant.family.children.setAsHeir') }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="child-matchmaking-cell">
|
||||||
|
<template v-if="child.relationship">
|
||||||
|
<strong>{{ child.relationship.partnerName }}</strong>
|
||||||
|
<span>{{ $t('falukant.family.children.relationshipStatus.' + child.relationship.status) }}</span>
|
||||||
|
<button
|
||||||
|
v-if="child.relationship.status === 'wooing'"
|
||||||
|
type="button"
|
||||||
|
@click="advanceChildWooing(child)"
|
||||||
|
>
|
||||||
|
{{ $t('falukant.family.children.completeWooing') }}
|
||||||
|
</button>
|
||||||
|
<span v-else-if="child.relationship.status === 'engaged'" class="child-matchmaking-hint">
|
||||||
|
{{ $t('falukant.family.children.weddingHint') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="child.hasName && child.possiblePartners?.length">
|
||||||
|
<select v-model="selectedChildProposalIds[child.childCharacterId]">
|
||||||
|
<option :value="null">{{ $t('falukant.family.children.selectPartner') }}</option>
|
||||||
|
<option v-for="proposal in child.possiblePartners" :key="proposal.id" :value="proposal.proposedCharacterId">
|
||||||
|
{{ proposal.proposedCharacterName }} ({{ proposal.proposedCharacterAge }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:disabled="!selectedChildProposalIds[child.childCharacterId]"
|
||||||
|
@click="acceptChildProposal(child)"
|
||||||
|
>
|
||||||
|
{{ $t('falukant.family.children.arrange') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<span v-else-if="child.hasName">{{ $t('falukant.family.children.noPartnerProposal') }}</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button @click="showChildDetails(child)">
|
<button @click="showChildDetails(child)">
|
||||||
{{ $t('falukant.family.children.detailButton') }}
|
{{ $t('falukant.family.children.detailButton') }}
|
||||||
@@ -670,6 +703,7 @@ export default {
|
|||||||
},
|
},
|
||||||
pregnancy: null,
|
pregnancy: null,
|
||||||
selectedChild: null,
|
selectedChild: null,
|
||||||
|
selectedChildProposalIds: {},
|
||||||
pendingFamilyRefresh: null,
|
pendingFamilyRefresh: null,
|
||||||
familyTab: 'partner',
|
familyTab: 'partner',
|
||||||
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
|
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
|
||||||
@@ -854,6 +888,9 @@ export default {
|
|||||||
const response = await apiClient.get('/api/falukant/family');
|
const response = await apiClient.get('/api/falukant/family');
|
||||||
this.relationships = response.data.relationships;
|
this.relationships = response.data.relationships;
|
||||||
this.children = response.data.children;
|
this.children = response.data.children;
|
||||||
|
this.selectedChildProposalIds = Object.fromEntries(
|
||||||
|
this.children.map((child) => [child.childCharacterId, this.selectedChildProposalIds[child.childCharacterId] || null])
|
||||||
|
);
|
||||||
this.lovers = response.data.lovers;
|
this.lovers = response.data.lovers;
|
||||||
this.politicalFreeLoverSlots = Number(response.data.politicalFreeLoverSlots) || 0;
|
this.politicalFreeLoverSlots = Number(response.data.politicalFreeLoverSlots) || 0;
|
||||||
this.possibleLovers = response.data.possibleLovers || [];
|
this.possibleLovers = response.data.possibleLovers || [];
|
||||||
@@ -978,6 +1015,30 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async acceptChildProposal(child) {
|
||||||
|
const proposedCharacterId = this.selectedChildProposalIds[child.childCharacterId];
|
||||||
|
if (!proposedCharacterId) return;
|
||||||
|
try {
|
||||||
|
await apiClient.post(`/api/falukant/family/children/${child.childCharacterId}/accept-marriage-proposal`, { proposedCharacterId });
|
||||||
|
await this.loadFamilyData();
|
||||||
|
showSuccess(this, this.$t('falukant.family.children.arrangeSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error arranging child marriage:', error);
|
||||||
|
showError(this, error?.response?.data?.message || this.$t('falukant.family.children.arrangeError'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async advanceChildWooing(child) {
|
||||||
|
try {
|
||||||
|
await apiClient.post(`/api/falukant/family/children/${child.childCharacterId}/advance-wooing`);
|
||||||
|
await this.loadFamilyData();
|
||||||
|
showSuccess(this, this.$t('falukant.family.children.engagedSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error advancing child wooing:', error);
|
||||||
|
showError(this, error?.response?.data?.message || this.$t('falukant.family.children.arrangeError'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async setLoverMaintenance(lover, maintenanceLevel) {
|
async setLoverMaintenance(lover, maintenanceLevel) {
|
||||||
try {
|
try {
|
||||||
await apiClient.post(`/api/falukant/family/lover/${lover.relationshipId}/maintenance`, {
|
await apiClient.post(`/api/falukant/family/lover/${lover.relationshipId}/maintenance`, {
|
||||||
|
|||||||
@@ -76,6 +76,15 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div v-if="newPartyTypeId" class="party-options">
|
<div v-if="newPartyTypeId" class="party-options">
|
||||||
|
<label v-if="selectedPartyType?.forMarriage">
|
||||||
|
Hochzeitspaar:
|
||||||
|
<select v-model.number="relationshipId">
|
||||||
|
<option :value="null">Bitte Verlobte auswählen</option>
|
||||||
|
<option v-for="candidate in weddingCandidates" :key="candidate.relationshipId" :value="candidate.relationshipId">
|
||||||
|
{{ candidate.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{{ $t('falukant.reputation.party.music.label') }}:
|
{{ $t('falukant.reputation.party.music.label') }}:
|
||||||
<select v-model.number="musicId">
|
<select v-model.number="musicId">
|
||||||
@@ -214,6 +223,8 @@ export default {
|
|||||||
nobilityTitles: [],
|
nobilityTitles: [],
|
||||||
selectedNobilityIds: [],
|
selectedNobilityIds: [],
|
||||||
servantRatio: 50,
|
servantRatio: 50,
|
||||||
|
relationshipId: null,
|
||||||
|
weddingCandidates: [],
|
||||||
inProgressParties: [],
|
inProgressParties: [],
|
||||||
completedParties: [],
|
completedParties: [],
|
||||||
reputation: null,
|
reputation: null,
|
||||||
@@ -235,6 +246,7 @@ export default {
|
|||||||
this.partyTypes = data.partyTypes;
|
this.partyTypes = data.partyTypes;
|
||||||
this.musicTypes = data.musicTypes;
|
this.musicTypes = data.musicTypes;
|
||||||
this.banquetteTypes = data.banquetteTypes;
|
this.banquetteTypes = data.banquetteTypes;
|
||||||
|
this.weddingCandidates = data.weddingCandidates || [];
|
||||||
this.musicId = this.musicTypes[0]?.id;
|
this.musicId = this.musicTypes[0]?.id;
|
||||||
this.banquetteId = this.banquetteTypes[0]?.id;
|
this.banquetteId = this.banquetteTypes[0]?.id;
|
||||||
},
|
},
|
||||||
@@ -310,7 +322,8 @@ export default {
|
|||||||
musicId: this.musicId,
|
musicId: this.musicId,
|
||||||
banquetteId: this.banquetteId,
|
banquetteId: this.banquetteId,
|
||||||
nobilityIds: this.selectedNobilityIds.map(n => n.id ?? n),
|
nobilityIds: this.selectedNobilityIds.map(n => n.id ?? n),
|
||||||
servantRatio: this.servantRatio
|
servantRatio: this.servantRatio,
|
||||||
|
relationshipId: this.relationshipId
|
||||||
});
|
});
|
||||||
this.toggleNewPartyView();
|
this.toggleNewPartyView();
|
||||||
},
|
},
|
||||||
@@ -343,6 +356,10 @@ export default {
|
|||||||
maximumFractionDigits: 2
|
maximumFractionDigits: 2
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
,
|
||||||
|
selectedPartyType() {
|
||||||
|
return this.partyTypes.find((type) => type.id === this.newPartyTypeId) || null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const tabFromQuery = this.$route?.query?.tab;
|
const tabFromQuery = this.$route?.query?.tab;
|
||||||
|
|||||||
@@ -112,8 +112,11 @@ private:
|
|||||||
JOIN falukant_data."character" AS c
|
JOIN falukant_data."character" AS c
|
||||||
ON c.user_id = fu.id
|
ON c.user_id = fu.id
|
||||||
JOIN falukant_data.relationship AS rel2
|
JOIN falukant_data.relationship AS rel2
|
||||||
ON rel2.character1_id = c.id
|
ON p.relationship_id = rel2.id
|
||||||
OR rel2.character2_id = c.id
|
OR (
|
||||||
|
p.relationship_id IS NULL
|
||||||
|
AND (rel2.character1_id = c.id OR rel2.character2_id = c.id)
|
||||||
|
)
|
||||||
JOIN falukant_type.relationship AS rt2
|
JOIN falukant_type.relationship AS rt2
|
||||||
ON rt2.id = rel2.relationship_type_id
|
ON rt2.id = rel2.relationship_type_id
|
||||||
AND rt2.tr = 'engaged'
|
AND rt2.tr = 'engaged'
|
||||||
@@ -121,19 +124,15 @@ private:
|
|||||||
-- Die Feier muss für diese bereits bestehende Verlobung bestellt worden sein.
|
-- Die Feier muss für diese bereits bestehende Verlobung bestellt worden sein.
|
||||||
AND p.created_at >= rel2.created_at
|
AND p.created_at >= rel2.created_at
|
||||||
-- Ein realer Tag entspricht einem Spieljahr: Hochzeit ab 14.
|
-- Ein realer Tag entspricht einem Spieljahr: Hochzeit ab 14.
|
||||||
AND c.birthdate <= CURRENT_DATE - INTERVAL '14 days'
|
AND EXISTS (
|
||||||
AND (
|
SELECT 1 FROM falukant_data."character" spouse1
|
||||||
(rel2.character1_id = c.id AND EXISTS (
|
WHERE spouse1.id = rel2.character1_id
|
||||||
SELECT 1 FROM falukant_data."character" partner
|
AND spouse1.birthdate <= CURRENT_DATE - INTERVAL '14 days'
|
||||||
WHERE partner.id = rel2.character2_id
|
)
|
||||||
AND partner.birthdate <= CURRENT_DATE - INTERVAL '14 days'
|
AND EXISTS (
|
||||||
))
|
SELECT 1 FROM falukant_data."character" spouse2
|
||||||
OR
|
WHERE spouse2.id = rel2.character2_id
|
||||||
(rel2.character2_id = c.id AND EXISTS (
|
AND spouse2.birthdate <= CURRENT_DATE - INTERVAL '14 days'
|
||||||
SELECT 1 FROM falukant_data."character" partner
|
|
||||||
WHERE partner.id = rel2.character1_id
|
|
||||||
AND partner.birthdate <= CURRENT_DATE - INTERVAL '14 days'
|
|
||||||
))
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
RETURNING character1_id, character2_id
|
RETURNING character1_id, character2_id
|
||||||
|
|||||||
Reference in New Issue
Block a user