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:
@@ -37,6 +37,15 @@
|
||||
<td>{{ $t('falukant.family.spouse.status') }}</td>
|
||||
<td>{{ $t('falukant.family.statuses.' + relationships[0].relationshipType) }}</td>
|
||||
</tr>
|
||||
<tr v-if="relationships[0].marriageSatisfaction != null">
|
||||
<td>{{ $t('falukant.family.spouse.marriageSatisfaction') }}</td>
|
||||
<td>
|
||||
{{ relationships[0].marriageSatisfaction }}
|
||||
<span class="inline-status-pill" :class="`inline-status-pill--${relationships[0].marriageState || 'stable'}`">
|
||||
{{ $t('falukant.family.marriageState.' + (relationships[0].marriageState || 'stable')) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="relationships[0].relationshipType === 'wooing'">
|
||||
<td>{{ $t('falukant.family.spouse.progress') }}</td>
|
||||
<td>
|
||||
@@ -123,6 +132,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="marriageSatisfaction != null || householdTension" class="marriage-overview surface-card">
|
||||
<div class="marriage-overview__item" v-if="marriageSatisfaction != null">
|
||||
<span class="marriage-overview__label">{{ $t('falukant.family.spouse.marriageSatisfaction') }}</span>
|
||||
<strong>{{ marriageSatisfaction }}</strong>
|
||||
</div>
|
||||
<div class="marriage-overview__item" v-if="marriageState">
|
||||
<span class="marriage-overview__label">{{ $t('falukant.family.spouse.marriageState') }}</span>
|
||||
<span class="inline-status-pill" :class="`inline-status-pill--${marriageState}`">
|
||||
{{ $t('falukant.family.marriageState.' + marriageState) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="marriage-overview__item" v-if="householdTension">
|
||||
<span class="marriage-overview__label">{{ $t('falukant.family.householdTension.label') }}</span>
|
||||
<span class="inline-status-pill" :class="`inline-status-pill--${householdTension}`">
|
||||
{{ $t('falukant.family.householdTension.' + householdTension) }}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="children-section">
|
||||
<h3>{{ $t('falukant.family.children.title') }}</h3>
|
||||
<div v-if="children && children.length > 0" class="children-container">
|
||||
@@ -141,6 +169,9 @@
|
||||
<tr v-for="(child, index) in children" :key="index">
|
||||
<td v-if="child.hasName">
|
||||
{{ child.name }}
|
||||
<span v-if="child.legitimacy && child.legitimacy !== 'legitimate'" class="child-origin-badge">
|
||||
{{ $t('falukant.family.children.legitimacy.' + child.legitimacy) }}
|
||||
</span>
|
||||
</td>
|
||||
<td v-else>
|
||||
<button @click="jumpToChurchForm">{{ $t('falukant.family.children.baptism')
|
||||
@@ -177,17 +208,106 @@
|
||||
<!-- Liebhaber / Geliebte -->
|
||||
<div class="lovers-section">
|
||||
<h3>{{ $t('falukant.family.lovers.title') }}</h3>
|
||||
<div v-if="lovers && lovers.length > 0">
|
||||
<ul>
|
||||
<li v-for="(lover, idx) in lovers" :key="idx">
|
||||
{{ $t('falukant.titles.' + lover.gender + '.' + lover.title) }} {{ lover.name }}
|
||||
({{ $t('falukant.family.lovers.affection') }}: {{ lover.affection }})
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="lovers && lovers.length > 0" class="lovers-grid">
|
||||
<article v-for="lover in lovers" :key="lover.relationshipId" class="lover-card surface-card">
|
||||
<div class="lover-card__header">
|
||||
<div>
|
||||
<strong>{{ $t('falukant.titles.' + lover.gender + '.' + lover.title) }} {{ lover.name }}</strong>
|
||||
<div class="lover-card__role">
|
||||
{{ $t('falukant.family.lovers.role.' + (lover.role || 'lover')) }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="inline-status-pill" :class="`inline-status-pill--${lover.riskState || 'low'}`">
|
||||
{{ $t('falukant.family.lovers.risk.' + (lover.riskState || 'low')) }}
|
||||
</span>
|
||||
</div>
|
||||
<dl class="lover-card__stats">
|
||||
<div>
|
||||
<dt>{{ $t('falukant.family.lovers.affection') }}</dt>
|
||||
<dd>{{ lover.affection }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ $t('falukant.family.lovers.visibility') }}</dt>
|
||||
<dd>{{ lover.visibility }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ $t('falukant.family.lovers.discretion') }}</dt>
|
||||
<dd>{{ lover.discretion }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ $t('falukant.family.lovers.maintenance') }}</dt>
|
||||
<dd>{{ lover.maintenanceLevel }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ $t('falukant.family.lovers.monthlyCost') }}</dt>
|
||||
<dd>{{ formatCost(lover.monthlyCost || 0) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ $t('falukant.family.lovers.statusFit') }}</dt>
|
||||
<dd>{{ lover.statusFit }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="lover-card__meta">
|
||||
<span v-if="lover.acknowledged" class="lover-meta-badge">
|
||||
{{ $t('falukant.family.lovers.acknowledged') }}
|
||||
</span>
|
||||
<span v-if="lover.monthsUnderfunded > 0" class="lover-meta-badge lover-meta-badge--warning">
|
||||
{{ $t('falukant.family.lovers.underfunded', { count: lover.monthsUnderfunded }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="lover-card__actions">
|
||||
<button class="button button--secondary" @click="setLoverMaintenance(lover, 25)">
|
||||
{{ $t('falukant.family.lovers.actions.maintenanceLow') }}
|
||||
</button>
|
||||
<button class="button button--secondary" @click="setLoverMaintenance(lover, 50)">
|
||||
{{ $t('falukant.family.lovers.actions.maintenanceMedium') }}
|
||||
</button>
|
||||
<button class="button button--secondary" @click="setLoverMaintenance(lover, 75)">
|
||||
{{ $t('falukant.family.lovers.actions.maintenanceHigh') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!lover.acknowledged"
|
||||
class="button button--secondary"
|
||||
@click="acknowledgeLover(lover)"
|
||||
>
|
||||
{{ $t('falukant.family.lovers.actions.acknowledge') }}
|
||||
</button>
|
||||
<button class="button button--danger" @click="endLoverRelationship(lover)">
|
||||
{{ $t('falukant.family.lovers.actions.end') }}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>{{ $t('falukant.family.lovers.none') }}</p>
|
||||
</div>
|
||||
<div class="lover-candidates surface-card">
|
||||
<h4>{{ $t('falukant.family.lovers.candidates.title') }}</h4>
|
||||
<div v-if="possibleLovers && possibleLovers.length > 0" class="lover-candidates__grid">
|
||||
<article v-for="candidate in possibleLovers" :key="candidate.characterId" class="lover-candidate-card">
|
||||
<div class="lover-candidate-card__main">
|
||||
<strong>{{ $t('falukant.titles.' + candidate.gender + '.' + candidate.title) }} {{ candidate.name }}</strong>
|
||||
<span>{{ $t('falukant.family.spouse.age') }}: {{ candidate.age }}</span>
|
||||
<span>{{ $t('falukant.family.lovers.statusFit') }}: {{ candidate.statusFit }}</span>
|
||||
<span>{{ $t('falukant.family.lovers.monthlyCost') }}: {{ formatCost(candidate.estimatedMonthlyCost || 0) }}</span>
|
||||
</div>
|
||||
<div class="lover-candidate-card__actions">
|
||||
<label class="lover-candidate-card__label">
|
||||
{{ $t('falukant.family.lovers.candidates.roleLabel') }}
|
||||
</label>
|
||||
<select class="lover-candidate-card__select" v-model="candidateRoles[candidate.characterId]">
|
||||
<option value="secret_affair">{{ $t('falukant.family.lovers.role.secret_affair') }}</option>
|
||||
<option value="lover">{{ $t('falukant.family.lovers.role.lover') }}</option>
|
||||
<option value="mistress_or_favorite">{{ $t('falukant.family.lovers.role.mistress_or_favorite') }}</option>
|
||||
</select>
|
||||
<button class="button button--secondary" @click="createLoverRelationship(candidate)">
|
||||
{{ $t('falukant.family.lovers.actions.start') }}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else>{{ $t('falukant.family.lovers.candidates.none') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,7 +321,7 @@ import ChildDetailsDialog from '@/dialogues/falukant/ChildDetailsDialog.vue'
|
||||
import Character3D from '@/components/Character3D.vue'
|
||||
|
||||
import apiClient from '@/utils/axios.js'
|
||||
import { confirmAction, showError, showSuccess } from '@/utils/feedback.js'
|
||||
import { confirmAction, showError, showInfo, showSuccess } from '@/utils/feedback.js'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
const WOOING_PROGRESS_TARGET = 70
|
||||
@@ -218,6 +338,8 @@ export default {
|
||||
relationships: [],
|
||||
children: [],
|
||||
lovers: [],
|
||||
possibleLovers: [],
|
||||
candidateRoles: {},
|
||||
deathPartners: [],
|
||||
proposals: [],
|
||||
selectedProposalId: null,
|
||||
@@ -226,11 +348,25 @@ export default {
|
||||
moodAffects: [],
|
||||
characterAffects: [],
|
||||
ownCharacter: null,
|
||||
selectedChild: null
|
||||
marriageSatisfaction: null,
|
||||
marriageState: null,
|
||||
householdTension: null,
|
||||
selectedChild: null,
|
||||
pendingFamilyRefresh: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['socket'])
|
||||
...mapState(['socket', 'daemonSocket', 'user'])
|
||||
},
|
||||
watch: {
|
||||
socket(newVal, oldVal) {
|
||||
if (oldVal) this.teardownSocketEvents();
|
||||
if (newVal) this.setupSocketEvents();
|
||||
},
|
||||
daemonSocket(newVal, oldVal) {
|
||||
if (oldVal) this.teardownDaemonListeners();
|
||||
if (newVal) this.setupDaemonListeners();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadOwnCharacter();
|
||||
@@ -239,25 +375,110 @@ export default {
|
||||
await this.loadMoodAffects();
|
||||
await this.loadCharacterAffects();
|
||||
this.setupSocketEvents();
|
||||
this.setupDaemonListeners();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.teardownSocketEvents();
|
||||
this.teardownDaemonListeners();
|
||||
if (this.pendingFamilyRefresh) {
|
||||
clearTimeout(this.pendingFamilyRefresh);
|
||||
this.pendingFamilyRefresh = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setupSocketEvents() {
|
||||
this.teardownSocketEvents();
|
||||
if (this.socket) {
|
||||
this.socket.on('falukantUpdateStatus', (data) => {
|
||||
this.handleEvent({ event: 'falukantUpdateStatus', ...data });
|
||||
});
|
||||
this.socket.on('familychanged', (data) => {
|
||||
this.handleEvent({ event: 'familychanged', ...data });
|
||||
});
|
||||
this._falukantUpdateStatusHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
|
||||
this._falukantUpdateFamilyHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
|
||||
this._childrenUpdateHandler = (data) => this.handleEvent({ event: 'children_update', ...data });
|
||||
this._familyChangedHandler = (data) => this.handleEvent({ event: 'familychanged', ...data });
|
||||
|
||||
this.socket.on('falukantUpdateStatus', this._falukantUpdateStatusHandler);
|
||||
this.socket.on('falukantUpdateFamily', this._falukantUpdateFamilyHandler);
|
||||
this.socket.on('children_update', this._childrenUpdateHandler);
|
||||
this.socket.on('familychanged', this._familyChangedHandler);
|
||||
} else {
|
||||
setTimeout(() => this.setupSocketEvents(), 1000);
|
||||
}
|
||||
},
|
||||
teardownSocketEvents() {
|
||||
if (!this.socket) return;
|
||||
if (this._falukantUpdateStatusHandler) this.socket.off('falukantUpdateStatus', this._falukantUpdateStatusHandler);
|
||||
if (this._falukantUpdateFamilyHandler) this.socket.off('falukantUpdateFamily', this._falukantUpdateFamilyHandler);
|
||||
if (this._childrenUpdateHandler) this.socket.off('children_update', this._childrenUpdateHandler);
|
||||
if (this._familyChangedHandler) this.socket.off('familychanged', this._familyChangedHandler);
|
||||
},
|
||||
setupDaemonListeners() {
|
||||
this.teardownDaemonListeners();
|
||||
if (!this.daemonSocket) return;
|
||||
this._daemonFamilyHandler = (event) => {
|
||||
if (event.data === 'ping') return;
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
if ([
|
||||
'falukantUpdateStatus',
|
||||
'falukantUpdateFamily',
|
||||
'children_update',
|
||||
'familychanged',
|
||||
'falukant_family_scandal_hint'
|
||||
].includes(message.event)) {
|
||||
this.handleEvent(message);
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
this.daemonSocket.addEventListener('message', this._daemonFamilyHandler);
|
||||
},
|
||||
teardownDaemonListeners() {
|
||||
if (this.daemonSocket && this._daemonFamilyHandler) {
|
||||
this.daemonSocket.removeEventListener('message', this._daemonFamilyHandler);
|
||||
this._daemonFamilyHandler = null;
|
||||
}
|
||||
},
|
||||
matchesCurrentUser(eventData) {
|
||||
if (eventData?.user_id == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentIds = [this.user?.id, this.user?.hashedId]
|
||||
.filter(Boolean)
|
||||
.map((value) => String(value));
|
||||
|
||||
return currentIds.includes(String(eventData.user_id));
|
||||
},
|
||||
queueFamilyRefresh({ reloadCharacter = false } = {}) {
|
||||
if (this.pendingFamilyRefresh) {
|
||||
clearTimeout(this.pendingFamilyRefresh);
|
||||
}
|
||||
|
||||
this.pendingFamilyRefresh = setTimeout(async () => {
|
||||
this.pendingFamilyRefresh = null;
|
||||
await this.loadFamilyData();
|
||||
if (reloadCharacter) {
|
||||
await this.loadOwnCharacter();
|
||||
}
|
||||
}, 120);
|
||||
},
|
||||
handleEvent(eventData) {
|
||||
if (!this.matchesCurrentUser(eventData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (eventData.event) {
|
||||
case 'falukantUpdateStatus':
|
||||
case 'familychanged':
|
||||
this.loadFamilyData();
|
||||
this.queueFamilyRefresh({ reloadCharacter: true });
|
||||
break;
|
||||
case 'children_update':
|
||||
this.queueFamilyRefresh({ reloadCharacter: false });
|
||||
break;
|
||||
case 'falukantUpdateFamily':
|
||||
if (eventData.reason === 'scandal') {
|
||||
showInfo(this, this.$t('falukant.family.notifications.scandal'));
|
||||
} else if (eventData.reason === 'lover_birth') {
|
||||
showInfo(this, this.$t('falukant.family.notifications.loverBirth'));
|
||||
}
|
||||
this.queueFamilyRefresh({ reloadCharacter: eventData.reason === 'monthly' || eventData.reason === 'daily' });
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -267,8 +488,13 @@ export default {
|
||||
this.relationships = response.data.relationships;
|
||||
this.children = response.data.children;
|
||||
this.lovers = response.data.lovers;
|
||||
this.possibleLovers = response.data.possibleLovers || [];
|
||||
this.syncCandidateRoles();
|
||||
this.proposals = response.data.possiblePartners;
|
||||
this.deathPartners = response.data.deathPartners;
|
||||
this.marriageSatisfaction = response.data.marriageSatisfaction;
|
||||
this.marriageState = response.data.marriageState;
|
||||
this.householdTension = response.data.householdTension;
|
||||
} catch (error) {
|
||||
console.error('Error loading family data:', error);
|
||||
}
|
||||
@@ -305,6 +531,73 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async setLoverMaintenance(lover, maintenanceLevel) {
|
||||
try {
|
||||
await apiClient.post(`/api/falukant/family/lover/${lover.relationshipId}/maintenance`, {
|
||||
maintenanceLevel
|
||||
});
|
||||
await this.loadFamilyData();
|
||||
showSuccess(this, this.$t('falukant.family.lovers.actions.maintenanceSuccess'));
|
||||
} catch (error) {
|
||||
console.error('Error updating lover maintenance:', error);
|
||||
showError(this, this.$t('falukant.family.lovers.actions.maintenanceError'));
|
||||
}
|
||||
},
|
||||
|
||||
async acknowledgeLover(lover) {
|
||||
try {
|
||||
await apiClient.post(`/api/falukant/family/lover/${lover.relationshipId}/acknowledge`);
|
||||
await this.loadFamilyData();
|
||||
showSuccess(this, this.$t('falukant.family.lovers.actions.acknowledgeSuccess'));
|
||||
} catch (error) {
|
||||
console.error('Error acknowledging lover:', error);
|
||||
showError(this, this.$t('falukant.family.lovers.actions.acknowledgeError'));
|
||||
}
|
||||
},
|
||||
|
||||
async endLoverRelationship(lover) {
|
||||
const confirmed = await confirmAction(this, {
|
||||
title: this.$t('falukant.family.lovers.actions.end'),
|
||||
message: this.$t('falukant.family.lovers.actions.endConfirm')
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await apiClient.post(`/api/falukant/family/lover/${lover.relationshipId}/end`);
|
||||
await this.loadFamilyData();
|
||||
showSuccess(this, this.$t('falukant.family.lovers.actions.endSuccess'));
|
||||
} catch (error) {
|
||||
console.error('Error ending lover relationship:', error);
|
||||
showError(this, this.$t('falukant.family.lovers.actions.endError'));
|
||||
}
|
||||
},
|
||||
|
||||
async createLoverRelationship(candidate) {
|
||||
try {
|
||||
await apiClient.post('/api/falukant/family/lover', {
|
||||
targetCharacterId: candidate.characterId,
|
||||
loverRole: this.getCandidateRole(candidate.characterId)
|
||||
});
|
||||
await this.loadFamilyData();
|
||||
showSuccess(this, this.$t('falukant.family.lovers.actions.startSuccess'));
|
||||
} catch (error) {
|
||||
console.error('Error creating lover relationship:', error);
|
||||
showError(this, this.$t('falukant.family.lovers.actions.startError'));
|
||||
}
|
||||
},
|
||||
|
||||
syncCandidateRoles() {
|
||||
const nextRoles = {};
|
||||
for (const candidate of this.possibleLovers) {
|
||||
nextRoles[candidate.characterId] = this.candidateRoles[candidate.characterId] || 'secret_affair';
|
||||
}
|
||||
this.candidateRoles = nextRoles;
|
||||
},
|
||||
|
||||
getCandidateRole(characterId) {
|
||||
return this.candidateRoles[characterId] || 'secret_affair';
|
||||
},
|
||||
|
||||
formatCost(value) {
|
||||
return new Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
|
||||
},
|
||||
@@ -442,16 +735,6 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
handleDaemonMessage(event) {
|
||||
if (event.data === 'ping') {
|
||||
return;
|
||||
}
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.event === 'children_update') {
|
||||
this.loadFamilyData();
|
||||
}
|
||||
},
|
||||
|
||||
getEffect(gift) {
|
||||
// aktueller Partner
|
||||
const partner = this.relationships[0].character2;
|
||||
@@ -512,6 +795,196 @@ export default {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.marriage-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.marriage-overview__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.marriage-overview__label {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.inline-status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.inline-status-pill--stable,
|
||||
.inline-status-pill--low {
|
||||
background: rgba(66, 140, 87, 0.16);
|
||||
color: #2e6b42;
|
||||
}
|
||||
|
||||
.inline-status-pill--strained,
|
||||
.inline-status-pill--medium {
|
||||
background: rgba(230, 172, 52, 0.18);
|
||||
color: #875e08;
|
||||
}
|
||||
|
||||
.inline-status-pill--crisis,
|
||||
.inline-status-pill--high {
|
||||
background: rgba(188, 84, 61, 0.16);
|
||||
color: #9a3c26;
|
||||
}
|
||||
|
||||
.child-origin-badge {
|
||||
margin-left: 8px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(131, 104, 73, 0.14);
|
||||
color: #6a4d2f;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lovers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.lover-card {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.lover-card__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.lover-card__role {
|
||||
margin-top: 4px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.lover-card__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lover-card__stats div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.lover-card__stats dt {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.lover-card__stats dd {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lover-card__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.lover-card__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.lover-card__actions .button {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lover-candidates {
|
||||
margin-top: 18px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.lover-candidates__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.lover-candidate-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 250, 243, 0.88);
|
||||
}
|
||||
|
||||
.lover-candidate-card__main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.lover-candidate-card__main span {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.lover-candidate-card__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.lover-candidate-card__label {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lover-candidate-card__select {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.lover-meta-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 9px;
|
||||
border-radius: 999px;
|
||||
background: rgba(66, 140, 87, 0.14);
|
||||
color: #2e6b42;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lover-meta-badge--warning {
|
||||
background: rgba(188, 84, 61, 0.14);
|
||||
color: #9a3c26;
|
||||
}
|
||||
|
||||
.self-character-3d {
|
||||
width: 250px;
|
||||
height: 350px;
|
||||
|
||||
@@ -213,10 +213,11 @@ export default {
|
||||
productions: [],
|
||||
potentialHeirs: [],
|
||||
loadingHeirs: false,
|
||||
pendingOverviewRefresh: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['socket', 'daemonSocket']),
|
||||
...mapState(['socket', 'daemonSocket', 'user']),
|
||||
getAvatarStyle() {
|
||||
if (!this.falukantUser || !this.falukantUser.character) return {};
|
||||
const { gender, age } = this.falukantUser.character;
|
||||
@@ -335,12 +336,18 @@ export default {
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.pendingOverviewRefresh) {
|
||||
clearTimeout(this.pendingOverviewRefresh);
|
||||
this.pendingOverviewRefresh = null;
|
||||
}
|
||||
if (this.daemonSocket) {
|
||||
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
|
||||
}
|
||||
if (this.socket) {
|
||||
this.socket.off("falukantUserUpdated", this.fetchFalukantUser);
|
||||
this.socket.off("falukantUpdateStatus");
|
||||
this.socket.off("falukantUpdateFamily");
|
||||
this.socket.off("children_update");
|
||||
this.socket.off("falukantBranchUpdate");
|
||||
this.socket.off("stock_change");
|
||||
}
|
||||
@@ -352,6 +359,12 @@ export default {
|
||||
this.socket.on("falukantUpdateStatus", (data) => {
|
||||
this.handleEvent({ event: 'falukantUpdateStatus', ...data });
|
||||
});
|
||||
this.socket.on("falukantUpdateFamily", (data) => {
|
||||
this.handleEvent({ event: 'falukantUpdateFamily', ...data });
|
||||
});
|
||||
this.socket.on("children_update", (data) => {
|
||||
this.handleEvent({ event: 'children_update', ...data });
|
||||
});
|
||||
this.socket.on("falukantBranchUpdate", (data) => {
|
||||
this.handleEvent({ event: 'falukantBranchUpdate', ...data });
|
||||
});
|
||||
@@ -387,16 +400,37 @@ export default {
|
||||
console.error('Overview: Error processing daemon message:', err);
|
||||
}
|
||||
},
|
||||
matchesCurrentUser(eventData) {
|
||||
if (eventData?.user_id == null) {
|
||||
return true;
|
||||
}
|
||||
const currentIds = [this.user?.id, this.user?.hashedId]
|
||||
.filter(Boolean)
|
||||
.map((value) => String(value));
|
||||
return currentIds.includes(String(eventData.user_id));
|
||||
},
|
||||
queueOverviewRefresh() {
|
||||
if (this.pendingOverviewRefresh) {
|
||||
clearTimeout(this.pendingOverviewRefresh);
|
||||
}
|
||||
this.pendingOverviewRefresh = setTimeout(async () => {
|
||||
this.pendingOverviewRefresh = null;
|
||||
await this.fetchFalukantUser();
|
||||
if (this.falukantUser?.character) {
|
||||
await this.fetchProductions();
|
||||
await this.fetchAllStock();
|
||||
}
|
||||
}, 120);
|
||||
},
|
||||
async handleEvent(eventData) {
|
||||
if (!this.falukantUser?.character) return;
|
||||
if (!this.matchesCurrentUser(eventData)) return;
|
||||
switch (eventData.event) {
|
||||
case 'falukantUpdateStatus':
|
||||
case 'falukantUpdateFamily':
|
||||
case 'children_update':
|
||||
case 'falukantBranchUpdate':
|
||||
await this.fetchFalukantUser();
|
||||
if (this.falukantUser?.character) {
|
||||
await this.fetchProductions();
|
||||
await this.fetchAllStock();
|
||||
}
|
||||
this.queueOverviewRefresh();
|
||||
break;
|
||||
case 'production_ready':
|
||||
case 'production_started':
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user