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

This commit is contained in:
Torsten Schulz (local)
2026-09-08 14:46:26 +02:00
parent 0a9174c486
commit 3d235a621a
12 changed files with 281 additions and 90 deletions

View File

@@ -102,6 +102,10 @@ class FalukantController {
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.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.cancelWooing = this._wrapWithUser(async (userId) => {
try {
@@ -162,8 +166,8 @@ class FalukantController {
this.getPartyTypes = this._wrapWithUser((userId) => this.service.getPartyTypes(userId));
this.createParty = this._wrapWithUser((userId, req) => {
const { partyTypeId, musicId, banquetteId, nobilityIds, servantRatio } = req.body;
return this.service.createParty(userId, partyTypeId, musicId, banquetteId, nobilityIds, servantRatio);
const { partyTypeId, musicId, banquetteId, nobilityIds, servantRatio, relationshipId } = req.body;
return this.service.createParty(userId, partyTypeId, musicId, banquetteId, nobilityIds, servantRatio, relationshipId);
}, { successStatus: 201, blockInDebtorsPrison: true });
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));

View File

@@ -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;`);
}
};

View File

@@ -633,6 +633,7 @@ export default function setupAssociations() {
FalukantUser.hasMany(Party, { foreignKey: 'falukantUserId', as: 'parties' });
Party.belongsTo(FalukantUser, { foreignKey: 'falukantUserId', as: 'partyUser' });
Party.belongsTo(Relationship, { foreignKey: 'relationshipId', as: 'marriageRelationship' });
Party.belongsToMany(TitleOfNobility, {
through: PartyInvitedNobility,

View File

@@ -14,6 +14,11 @@ Party.init({
allowNull: false,
field: 'falukant_user_id'
},
relationshipId: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'relationship_id'
},
musicTypeId: {
type: DataTypes.INTEGER,
allowNull: false,
@@ -41,4 +46,4 @@ Party.init({
timestamps: true,
underscored: true});
export default Party;
export default Party;

View File

@@ -49,6 +49,8 @@ router.get('/dashboard-widget', falukantController.getDashboardWidget);
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
router.post('/family/cancel-wooing', falukantController.cancelWooing);
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/marriage/spend-time', falukantController.spendTimeWithSpouse);
router.post('/family/marriage/gift', falukantController.giftToSpouse);

View File

@@ -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
});

View File

@@ -7,7 +7,7 @@ verwendet daher die Differenz zwischen `CURRENT_DATE` und `birthdate` in Tagen.
| 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. |
| 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
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
- `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;
- bietet den Hochzeitstyp erst ab 14 an und prüft beim Bestellen der Feier
beide Verlobte serverseitig;

View File

@@ -207,10 +207,17 @@ export default {
.app-section-bar__back {
flex: 0 0 auto;
background: rgba(255, 255, 255, 0.82);
color: var(--color-text-primary);
box-shadow: none;
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) {
.app-section-bar {
flex-direction: column;

View File

@@ -775,6 +775,20 @@
"setAsHeir": "Als Erben festlegen",
"heirSetSuccess": "Das Kind wurde erfolgreich als Erbe festgelegt.",
"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",
"none": "Keine Kinder vorhanden.",
"detailButton": "Details anzeigen",

View File

@@ -314,6 +314,7 @@
<th>{{ $t('falukant.family.children.otherParent') }}</th>
<th>{{ $t('falukant.family.children.age') }}</th>
<th>{{ $t('falukant.family.children.heir') }}</th>
<th>{{ $t('falukant.family.children.matchmaking') }}</th>
<th>{{ $t('falukant.family.children.actions') }}</th>
</tr>
</thead>
@@ -365,6 +366,38 @@
{{ $t('falukant.family.children.setAsHeir') }}
</button>
</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>
<button @click="showChildDetails(child)">
{{ $t('falukant.family.children.detailButton') }}
@@ -670,6 +703,7 @@ export default {
},
pregnancy: null,
selectedChild: null,
selectedChildProposalIds: {},
pendingFamilyRefresh: null,
familyTab: 'partner',
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
@@ -854,6 +888,9 @@ export default {
const response = await apiClient.get('/api/falukant/family');
this.relationships = response.data.relationships;
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.politicalFreeLoverSlots = Number(response.data.politicalFreeLoverSlots) || 0;
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) {
try {
await apiClient.post(`/api/falukant/family/lover/${lover.relationshipId}/maintenance`, {

View File

@@ -76,6 +76,15 @@
</label>
<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>
{{ $t('falukant.reputation.party.music.label') }}:
<select v-model.number="musicId">
@@ -214,6 +223,8 @@ export default {
nobilityTitles: [],
selectedNobilityIds: [],
servantRatio: 50,
relationshipId: null,
weddingCandidates: [],
inProgressParties: [],
completedParties: [],
reputation: null,
@@ -235,6 +246,7 @@ export default {
this.partyTypes = data.partyTypes;
this.musicTypes = data.musicTypes;
this.banquetteTypes = data.banquetteTypes;
this.weddingCandidates = data.weddingCandidates || [];
this.musicId = this.musicTypes[0]?.id;
this.banquetteId = this.banquetteTypes[0]?.id;
},
@@ -310,7 +322,8 @@ export default {
musicId: this.musicId,
banquetteId: this.banquetteId,
nobilityIds: this.selectedNobilityIds.map(n => n.id ?? n),
servantRatio: this.servantRatio
servantRatio: this.servantRatio,
relationshipId: this.relationshipId
});
this.toggleNewPartyView();
},
@@ -343,6 +356,10 @@ export default {
maximumFractionDigits: 2
});
}
,
selectedPartyType() {
return this.partyTypes.find((type) => type.id === this.newPartyTypeId) || null;
}
},
async mounted() {
const tabFromQuery = this.$route?.query?.tab;

View File

@@ -112,8 +112,11 @@ private:
JOIN falukant_data."character" AS c
ON c.user_id = fu.id
JOIN falukant_data.relationship AS rel2
ON rel2.character1_id = c.id
OR rel2.character2_id = c.id
ON p.relationship_id = rel2.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
ON rt2.id = rel2.relationship_type_id
AND rt2.tr = 'engaged'
@@ -121,19 +124,15 @@ private:
-- Die Feier muss für diese bereits bestehende Verlobung bestellt worden sein.
AND p.created_at >= rel2.created_at
-- Ein realer Tag entspricht einem Spieljahr: Hochzeit ab 14.
AND c.birthdate <= CURRENT_DATE - INTERVAL '14 days'
AND (
(rel2.character1_id = c.id AND EXISTS (
SELECT 1 FROM falukant_data."character" partner
WHERE partner.id = rel2.character2_id
AND partner.birthdate <= CURRENT_DATE - INTERVAL '14 days'
))
OR
(rel2.character2_id = c.id AND EXISTS (
SELECT 1 FROM falukant_data."character" partner
WHERE partner.id = rel2.character1_id
AND partner.birthdate <= CURRENT_DATE - INTERVAL '14 days'
))
AND EXISTS (
SELECT 1 FROM falukant_data."character" spouse1
WHERE spouse1.id = rel2.character1_id
AND spouse1.birthdate <= CURRENT_DATE - INTERVAL '14 days'
)
AND EXISTS (
SELECT 1 FROM falukant_data."character" spouse2
WHERE spouse2.id = rel2.character2_id
AND spouse2.birthdate <= CURRENT_DATE - INTERVAL '14 days'
)
)
RETURNING character1_id, character2_id