Implement lover relationship management features: Add endpoints for creating, acknowledging, and managing lover relationships in the FalukantController. Enhance backend models with RelationshipState for tracking relationship statuses. Update frontend components to display and manage lover details, including marriage satisfaction and household tension. Improve localization for new features in multiple languages.

This commit is contained in:
Torsten Schulz (local)
2026-03-20 11:37:46 +01:00
parent c7d33525ff
commit 2977b152a2
29 changed files with 4551 additions and 86 deletions

View File

@@ -80,6 +80,18 @@
</select>
</label>
<label v-if="selectedType && selectedType.tr === 'investigate_affair'" class="form-label">
{{ $t('falukant.underground.activities.affairGoal') }}
<select v-model="newAffairGoal" class="form-control">
<option value="expose">
{{ $t('falukant.underground.goals.expose') }}
</option>
<option value="blackmail">
{{ $t('falukant.underground.goals.blackmail') }}
</option>
</select>
</label>
<button class="btn-create-activity" :disabled="!canCreate" @click="createActivity">
{{ $t('falukant.underground.activities.create') }}
</button>
@@ -96,6 +108,7 @@
<th>{{ $t('falukant.underground.activities.type') }}</th>
<th>{{ $t('falukant.underground.activities.victim') }}</th>
<th>{{ $t('falukant.underground.activities.cost') }}</th>
<th>{{ $t('falukant.underground.activities.status') }}</th>
<th>{{ $t('falukant.underground.activities.additionalInfo') }}</th>
</tr>
</thead>
@@ -105,16 +118,73 @@
<td>{{ act.victimName }}</td>
<td>{{ formatCost(act.cost) }}</td>
<td>
<template v-if="act.type === 'sabotage'">
{{ $t(`falukant.underground.targets.${act.target}`) }}
</template>
<template v-else-if="act.type === 'corrupt_politician'">
{{ $t(`falukant.underground.goals.${act.goal}`) }}
</template>
<div class="activity-status">
<span class="activity-status__badge" :class="`is-${act.status}`">
{{ $t(`falukant.underground.status.${act.status}`) }}
</span>
</div>
</td>
<td>
<div class="activity-details">
<div class="activity-details__summary">
<template v-if="act.type === 'sabotage'">
{{ $t(`falukant.underground.targets.${act.target}`) }}
</template>
<template v-else-if="act.type === 'corrupt_politician'">
{{ $t(`falukant.underground.goals.${act.goal}`) }}
</template>
<template v-else-if="act.type === 'investigate_affair'">
{{ $t(`falukant.underground.goals.${act.goal}`) }}
</template>
</div>
<template v-if="act.type === 'investigate_affair' && hasAffairDetails(act)">
<div
v-if="getAffairDiscoveries(act).length"
class="activity-details__block"
>
<div class="activity-details__label">
{{ $t('falukant.underground.activities.discoveries') }}
</div>
<ul class="activity-details__list">
<li v-for="entry in getAffairDiscoveries(act)" :key="entry">
{{ entry }}
</li>
</ul>
</div>
<div
v-if="hasAffairImpact(act)"
class="activity-details__block activity-details__metrics"
>
<span
v-if="hasNumericValue(act.additionalInfo?.visibilityDelta)"
class="activity-metric"
>
{{ $t('falukant.underground.activities.visibilityDelta') }}:
{{ formatSignedNumber(act.additionalInfo.visibilityDelta) }}
</span>
<span
v-if="hasNumericValue(act.additionalInfo?.reputationDelta)"
class="activity-metric"
>
{{ $t('falukant.underground.activities.reputationDelta') }}:
{{ formatSignedNumber(act.additionalInfo.reputationDelta) }}
</span>
<span
v-if="act.additionalInfo?.blackmailAmount > 0"
class="activity-metric"
>
{{ $t('falukant.underground.activities.blackmailAmount') }}:
{{ formatCost(act.additionalInfo.blackmailAmount) }}
</span>
</div>
</template>
</div>
</td>
</tr>
<tr v-if="!activities.length">
<td colspan="4">{{ $t('falukant.underground.activities.none') }}</td>
<td colspan="5">{{ $t('falukant.underground.activities.none') }}</td>
</tr>
</tbody>
</table>
@@ -179,7 +249,8 @@ export default {
victimSearchTimeout: null,
newPoliticalTargets: [],
newSabotageTarget: 'house',
newCorruptGoal: 'elect'
newCorruptGoal: 'elect',
newAffairGoal: 'expose'
};
},
computed: {
@@ -202,6 +273,12 @@ export default {
) {
return false;
}
if (
this.selectedType?.tr === 'investigate_affair' &&
!this.newAffairGoal
) {
return false;
}
return true;
}
},
@@ -234,7 +311,6 @@ export default {
}
},
async searchVictims(q) {
console.log('Searching victims for:', q);
try {
const { data } = await apiClient.get('/api/falukant/users/search', {
params: { q }
@@ -264,6 +340,9 @@ export default {
payload.politicalTargets = this.newPoliticalTargets;
}
}
if (this.selectedType.tr === 'investigate_affair') {
payload.goal = this.newAffairGoal;
}
try {
await apiClient.post(
'/api/falukant/underground/activities',
@@ -273,6 +352,7 @@ export default {
this.newPoliticalTargets = [];
this.newSabotageTarget = 'house';
this.newCorruptGoal = 'elect';
this.newAffairGoal = 'expose';
await this.loadActivities();
} catch (err) {
console.error('Error creating activity', err);
@@ -287,8 +367,6 @@ export default {
},
async loadActivities() {
return; // TODO: Aktivierung der Methode geplant
/* Temporär deaktiviert:
this.loading.activities = true;
try {
const { data } = await apiClient.get(
@@ -298,7 +376,6 @@ export default {
} finally {
this.loading.activities = false;
}
*/
},
async loadAttacks() {
@@ -328,6 +405,48 @@ export default {
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(v);
},
hasNumericValue(value) {
return typeof value === 'number' && !Number.isNaN(value);
},
formatSignedNumber(value) {
if (!this.hasNumericValue(value)) return '0';
return new Intl.NumberFormat(navigator.language, {
minimumFractionDigits: 0,
maximumFractionDigits: 1,
signDisplay: 'always'
}).format(value);
},
getAffairDiscoveries(activity) {
const discoveries = activity?.additionalInfo?.discoveries;
if (Array.isArray(discoveries)) {
return discoveries.filter(Boolean).map(entry => String(entry));
}
if (discoveries && typeof discoveries === 'object') {
return Object.entries(discoveries)
.filter(([, value]) => value !== null && value !== undefined && value !== '')
.map(([key, value]) => `${key}: ${value}`);
}
if (typeof discoveries === 'string' && discoveries.trim()) {
return [discoveries.trim()];
}
return [];
},
hasAffairImpact(activity) {
const info = activity?.additionalInfo || {};
return (
this.hasNumericValue(info.visibilityDelta) ||
this.hasNumericValue(info.reputationDelta) ||
(typeof info.blackmailAmount === 'number' && info.blackmailAmount > 0)
);
},
hasAffairDetails(activity) {
return this.getAffairDiscoveries(activity).length > 0 || this.hasAffairImpact(activity);
}
}
};
@@ -407,6 +526,7 @@ h2 {
padding: 8px;
border: 1px solid #ddd;
text-align: left;
vertical-align: top;
}
.suggestions {
@@ -432,4 +552,72 @@ h2 {
.suggestions li:hover {
background: #eee;
}
.activity-status__badge {
display: inline-flex;
align-items: center;
padding: 0.2rem 0.55rem;
border-radius: 999px;
font-size: 0.88rem;
font-weight: 600;
background: #ececec;
color: #333;
}
.activity-status__badge.is-pending {
background: #fff2cc;
color: #7a5600;
}
.activity-status__badge.is-resolved {
background: #dff3e2;
color: #25613a;
}
.activity-status__badge.is-failed {
background: #f8d7da;
color: #8a2632;
}
.activity-details {
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.activity-details__summary {
font-weight: 600;
}
.activity-details__block {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.activity-details__label {
font-size: 0.85rem;
font-weight: 600;
color: #666;
}
.activity-details__list {
margin: 0;
padding-left: 1.1rem;
}
.activity-details__metrics {
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
}
.activity-metric {
display: inline-flex;
align-items: center;
padding: 0.2rem 0.55rem;
border-radius: 999px;
background: #f0f0f0;
font-size: 0.88rem;
}
</style>