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:
@@ -57,11 +57,12 @@ export default {
|
||||
loading: false,
|
||||
error: null,
|
||||
isDragging: false,
|
||||
_daemonMessageHandler: null
|
||||
_daemonMessageHandler: null,
|
||||
pendingFetchTimer: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['socket', 'daemonSocket']),
|
||||
...mapState(['socket', 'daemonSocket', 'user']),
|
||||
isFalukantWidget() {
|
||||
return this.endpoint && String(this.endpoint).includes('falukant');
|
||||
},
|
||||
@@ -89,25 +90,51 @@ export default {
|
||||
if (this.isFalukantWidget) this.setupSocketListeners();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.pendingFetchTimer) {
|
||||
clearTimeout(this.pendingFetchTimer);
|
||||
this.pendingFetchTimer = null;
|
||||
}
|
||||
if (this.isFalukantWidget) this.teardownSocketListeners();
|
||||
},
|
||||
methods: {
|
||||
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));
|
||||
},
|
||||
setupSocketListeners() {
|
||||
this.teardownSocketListeners();
|
||||
const daemonEvents = ['falukantUpdateStatus', 'stock_change', 'familychanged'];
|
||||
const daemonEvents = ['falukantUpdateStatus', 'falukantUpdateFamily', 'children_update', 'stock_change', 'familychanged'];
|
||||
if (this.daemonSocket) {
|
||||
this._daemonMessageHandler = (event) => {
|
||||
if (event.data === 'ping') return;
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (daemonEvents.includes(data.event)) this.fetchData();
|
||||
if (daemonEvents.includes(data.event) && this.matchesCurrentUser(data)) this.queueFetchData();
|
||||
} catch (_) {}
|
||||
};
|
||||
this.daemonSocket.addEventListener('message', this._daemonMessageHandler);
|
||||
}
|
||||
if (this.socket) {
|
||||
this.socket.on('falukantUpdateStatus', () => this.fetchData());
|
||||
this.socket.on('falukantBranchUpdate', () => this.fetchData());
|
||||
this._statusSocketHandler = (data) => {
|
||||
if (this.matchesCurrentUser(data)) this.queueFetchData();
|
||||
};
|
||||
this._familySocketHandler = (data) => {
|
||||
if (this.matchesCurrentUser(data)) this.queueFetchData();
|
||||
};
|
||||
this._childrenSocketHandler = (data) => {
|
||||
if (this.matchesCurrentUser(data)) this.queueFetchData();
|
||||
};
|
||||
this._branchSocketHandler = () => this.queueFetchData();
|
||||
|
||||
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
|
||||
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
|
||||
this.socket.on('children_update', this._childrenSocketHandler);
|
||||
this.socket.on('falukantBranchUpdate', this._branchSocketHandler);
|
||||
}
|
||||
},
|
||||
teardownSocketListeners() {
|
||||
@@ -116,10 +143,21 @@ export default {
|
||||
this._daemonMessageHandler = null;
|
||||
}
|
||||
if (this.socket) {
|
||||
this.socket.off('falukantUpdateStatus');
|
||||
this.socket.off('falukantBranchUpdate');
|
||||
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
|
||||
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
|
||||
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
|
||||
if (this._branchSocketHandler) this.socket.off('falukantBranchUpdate', this._branchSocketHandler);
|
||||
}
|
||||
},
|
||||
queueFetchData() {
|
||||
if (this.pendingFetchTimer) {
|
||||
clearTimeout(this.pendingFetchTimer);
|
||||
}
|
||||
this.pendingFetchTimer = setTimeout(() => {
|
||||
this.pendingFetchTimer = null;
|
||||
this.fetchData();
|
||||
}, 120);
|
||||
},
|
||||
async fetchData() {
|
||||
if (!this.endpoint || this.pauseFetch) return;
|
||||
this.loading = true;
|
||||
|
||||
@@ -60,10 +60,11 @@ export default {
|
||||
{ key: "children", icon: "👶", value: null },
|
||||
],
|
||||
unreadCount: 0,
|
||||
pendingStatusRefresh: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["socket", "daemonSocket"]),
|
||||
...mapState(["socket", "daemonSocket", "user"]),
|
||||
...mapGetters(['menu']),
|
||||
},
|
||||
watch: {
|
||||
@@ -100,6 +101,10 @@ export default {
|
||||
beforeUnmount() {
|
||||
this.teardownSocketListeners();
|
||||
this.teardownDaemonListeners();
|
||||
if (this.pendingStatusRefresh) {
|
||||
clearTimeout(this.pendingStatusRefresh);
|
||||
this.pendingStatusRefresh = null;
|
||||
}
|
||||
EventBus.off('open-falukant-messages', this.openMessages);
|
||||
},
|
||||
methods: {
|
||||
@@ -169,15 +174,25 @@ export default {
|
||||
setupSocketListeners() {
|
||||
this.teardownSocketListeners();
|
||||
if (!this.socket) return;
|
||||
this.socket.on('falukantUpdateStatus', (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data }));
|
||||
this.socket.on('stock_change', (data) => this.handleEvent({ event: 'stock_change', ...data }));
|
||||
this.socket.on('familychanged', (data) => this.handleEvent({ event: 'familychanged', ...data }));
|
||||
this._statusSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
|
||||
this._familySocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
|
||||
this._childrenSocketHandler = (data) => this.handleEvent({ event: 'children_update', ...data });
|
||||
this._stockSocketHandler = (data) => this.handleEvent({ event: 'stock_change', ...data });
|
||||
this._familyChangedSocketHandler = (data) => this.handleEvent({ event: 'familychanged', ...data });
|
||||
|
||||
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
|
||||
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
|
||||
this.socket.on('children_update', this._childrenSocketHandler);
|
||||
this.socket.on('stock_change', this._stockSocketHandler);
|
||||
this.socket.on('familychanged', this._familyChangedSocketHandler);
|
||||
},
|
||||
teardownSocketListeners() {
|
||||
if (this.socket) {
|
||||
this.socket.off('falukantUpdateStatus');
|
||||
this.socket.off('stock_change');
|
||||
this.socket.off('familychanged');
|
||||
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
|
||||
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
|
||||
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
|
||||
if (this._stockSocketHandler) this.socket.off('stock_change', this._stockSocketHandler);
|
||||
if (this._familyChangedSocketHandler) this.socket.off('familychanged', this._familyChangedSocketHandler);
|
||||
}
|
||||
},
|
||||
setupDaemonListeners() {
|
||||
@@ -186,13 +201,22 @@ export default {
|
||||
this._daemonHandler = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (['falukantUpdateStatus', 'stock_change', 'familychanged'].includes(data.event)) {
|
||||
if (['falukantUpdateStatus', 'falukantUpdateFamily', 'children_update', 'stock_change', 'familychanged'].includes(data.event)) {
|
||||
this.handleEvent(data);
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
this.daemonSocket.addEventListener('message', this._daemonHandler);
|
||||
},
|
||||
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));
|
||||
},
|
||||
teardownDaemonListeners() {
|
||||
const sock = this.daemonSocket;
|
||||
if (sock && this._daemonHandler) {
|
||||
@@ -200,12 +224,26 @@ export default {
|
||||
this._daemonHandler = null;
|
||||
}
|
||||
},
|
||||
queueStatusRefresh() {
|
||||
if (this.pendingStatusRefresh) {
|
||||
clearTimeout(this.pendingStatusRefresh);
|
||||
}
|
||||
this.pendingStatusRefresh = setTimeout(async () => {
|
||||
this.pendingStatusRefresh = null;
|
||||
await this.fetchStatus();
|
||||
}, 120);
|
||||
},
|
||||
handleEvent(eventData) {
|
||||
if (!this.matchesCurrentUser(eventData)) {
|
||||
return;
|
||||
}
|
||||
switch (eventData.event) {
|
||||
case 'falukantUpdateStatus':
|
||||
case 'falukantUpdateFamily':
|
||||
case 'children_update':
|
||||
case 'stock_change':
|
||||
case 'familychanged':
|
||||
this.fetchStatus();
|
||||
this.queueStatusRefresh();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -486,6 +486,8 @@
|
||||
"name": "Name",
|
||||
"age": "Alter",
|
||||
"status": "Status",
|
||||
"marriageSatisfaction": "Ehe-Zufriedenheit",
|
||||
"marriageState": "Ehezustand",
|
||||
"none": "Kein Ehepartner vorhanden.",
|
||||
"search": "Ehepartner suchen",
|
||||
"found": "Ehepartner gefunden",
|
||||
@@ -517,6 +519,17 @@
|
||||
"progress": "Zuneigung",
|
||||
"jumpToPartyForm": "Hochzeitsfeier veranstalten (Nötig für Hochzeit und Kinder)"
|
||||
},
|
||||
"marriageState": {
|
||||
"stable": "Stabil",
|
||||
"strained": "Angespannt",
|
||||
"crisis": "Krise"
|
||||
},
|
||||
"householdTension": {
|
||||
"label": "Hausfrieden",
|
||||
"low": "Ruhig",
|
||||
"medium": "Unruhig",
|
||||
"high": "Belastet"
|
||||
},
|
||||
"relationships": {
|
||||
"name": "Name"
|
||||
},
|
||||
@@ -538,14 +551,62 @@
|
||||
"baptism": "Taufen",
|
||||
"notBaptized": "Noch nicht getauft",
|
||||
"baptismNotice": "Dieses Kind wurde noch nicht getauft und hat daher noch keinen Namen.",
|
||||
"legitimacy": {
|
||||
"legitimate": "Ehelich",
|
||||
"acknowledged_bastard": "Anerkannt unehelich",
|
||||
"hidden_bastard": "Unehelich"
|
||||
},
|
||||
"details": {
|
||||
"title": "Kind-Details"
|
||||
}
|
||||
},
|
||||
"lovers": {
|
||||
"title": "Liebhaber",
|
||||
"title": "Liebhaber und Mätressen",
|
||||
"none": "Keine Liebhaber vorhanden.",
|
||||
"affection": "Zuneigung"
|
||||
"affection": "Zuneigung",
|
||||
"visibility": "Sichtbarkeit",
|
||||
"discretion": "Diskretion",
|
||||
"maintenance": "Unterhalt",
|
||||
"monthlyCost": "Monatskosten",
|
||||
"statusFit": "Standespassung",
|
||||
"acknowledged": "Anerkannt",
|
||||
"underfunded": "{count} Monate unterversorgt",
|
||||
"role": {
|
||||
"secret_affair": "Heimliche Liebschaft",
|
||||
"lover": "Geliebte Beziehung",
|
||||
"mistress_or_favorite": "Mätresse oder Favorit"
|
||||
},
|
||||
"risk": {
|
||||
"low": "Geringes Risiko",
|
||||
"medium": "Mittleres Risiko",
|
||||
"high": "Hohes Risiko"
|
||||
},
|
||||
"actions": {
|
||||
"start": "Liebschaft beginnen",
|
||||
"startSuccess": "Die neue Liebschaft wurde begonnen.",
|
||||
"startError": "Die Liebschaft konnte nicht begonnen werden.",
|
||||
"maintenanceLow": "Unterhalt 25",
|
||||
"maintenanceMedium": "Unterhalt 50",
|
||||
"maintenanceHigh": "Unterhalt 75",
|
||||
"maintenanceSuccess": "Der Unterhalt wurde angepasst.",
|
||||
"maintenanceError": "Der Unterhalt konnte nicht angepasst werden.",
|
||||
"acknowledge": "Anerkennen",
|
||||
"acknowledgeSuccess": "Die Beziehung wurde offiziell anerkannt.",
|
||||
"acknowledgeError": "Die Beziehung konnte nicht anerkannt werden.",
|
||||
"end": "Beenden",
|
||||
"endConfirm": "Soll diese Beziehung wirklich beendet werden?",
|
||||
"endSuccess": "Die Beziehung wurde beendet.",
|
||||
"endError": "Die Beziehung konnte nicht beendet werden."
|
||||
},
|
||||
"candidates": {
|
||||
"title": "Mögliche Liebschaften",
|
||||
"roleLabel": "Form der Beziehung",
|
||||
"none": "Derzeit gibt es keine passenden neuen Liebschaften."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"scandal": "Ein Familienskandal erschüttert dein Haus.",
|
||||
"loverBirth": "Aus einer Liebschaft ist ein Kind hervorgegangen."
|
||||
},
|
||||
"statuses": {
|
||||
"wooing": "In Werbung",
|
||||
@@ -1120,10 +1181,16 @@
|
||||
"type": "Aktivitätstyp",
|
||||
"victim": "Zielperson",
|
||||
"cost": "Kosten",
|
||||
"status": "Status",
|
||||
"additionalInfo": "Zusätzliche Informationen",
|
||||
"blackmailAmount": "Erpressungssumme",
|
||||
"discoveries": "Erkenntnisse",
|
||||
"visibilityDelta": "Sichtbarkeit",
|
||||
"reputationDelta": "Ansehen",
|
||||
"victimPlaceholder": "Benutzername eingeben",
|
||||
"sabotageTarget": "Sabotageziel",
|
||||
"corruptGoal": "Ziel der Korruption"
|
||||
"corruptGoal": "Ziel der Korruption",
|
||||
"affairGoal": "Ziel der Untersuchung"
|
||||
},
|
||||
"attacks": {
|
||||
"target": "Angreifer",
|
||||
@@ -1136,7 +1203,8 @@
|
||||
"assassin": "Attentat",
|
||||
"sabotage": "Sabotage",
|
||||
"corrupt_politician": "Korruption",
|
||||
"rob": "Raub"
|
||||
"rob": "Raub",
|
||||
"investigate_affair": "Liebschaft untersuchen"
|
||||
},
|
||||
"targets": {
|
||||
"house": "Wohnhaus",
|
||||
@@ -1145,8 +1213,15 @@
|
||||
"goals": {
|
||||
"elect": "Amtseinsetzung",
|
||||
"taxIncrease": "Steuern erhöhen",
|
||||
"taxDecrease": "Steuern senken"
|
||||
"taxDecrease": "Steuern senken",
|
||||
"expose": "Aufdecken",
|
||||
"blackmail": "Erpressen"
|
||||
},
|
||||
"status": {
|
||||
"pending": "Ausstehend",
|
||||
"resolved": "Abgeschlossen",
|
||||
"failed": "Gescheitert"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,6 +434,11 @@
|
||||
"baptism": "Baptize",
|
||||
"notBaptized": "Not yet baptized",
|
||||
"baptismNotice": "This child has not been baptized yet and therefore has no name.",
|
||||
"legitimacy": {
|
||||
"legitimate": "Legitimate",
|
||||
"acknowledged_bastard": "Acknowledged illegitimate",
|
||||
"hidden_bastard": "Illegitimate"
|
||||
},
|
||||
"details": {
|
||||
"title": "Child Details"
|
||||
}
|
||||
@@ -449,6 +454,8 @@
|
||||
}
|
||||
},
|
||||
"spouse": {
|
||||
"marriageSatisfaction": "Marriage Satisfaction",
|
||||
"marriageState": "Marriage State",
|
||||
"wooing": {
|
||||
"cancel": "Cancel wooing",
|
||||
"cancelConfirm": "Do you really want to cancel wooing? Progress will be lost.",
|
||||
@@ -457,6 +464,65 @@
|
||||
"cancelTooSoon": "You can only cancel wooing after 24 hours."
|
||||
}
|
||||
},
|
||||
"marriageState": {
|
||||
"stable": "Stable",
|
||||
"strained": "Strained",
|
||||
"crisis": "Crisis"
|
||||
},
|
||||
"householdTension": {
|
||||
"label": "Household Tension",
|
||||
"low": "Calm",
|
||||
"medium": "Uneasy",
|
||||
"high": "Strained"
|
||||
},
|
||||
"lovers": {
|
||||
"title": "Lovers and Mistresses",
|
||||
"none": "No lovers present.",
|
||||
"affection": "Affection",
|
||||
"visibility": "Visibility",
|
||||
"discretion": "Discretion",
|
||||
"maintenance": "Maintenance",
|
||||
"monthlyCost": "Monthly Cost",
|
||||
"statusFit": "Status Fit",
|
||||
"acknowledged": "Acknowledged",
|
||||
"underfunded": "{count} months underfunded",
|
||||
"role": {
|
||||
"secret_affair": "Secret affair",
|
||||
"lover": "Lover",
|
||||
"mistress_or_favorite": "Mistress or favorite"
|
||||
},
|
||||
"risk": {
|
||||
"low": "Low risk",
|
||||
"medium": "Medium risk",
|
||||
"high": "High risk"
|
||||
},
|
||||
"actions": {
|
||||
"start": "Start affair",
|
||||
"startSuccess": "The new affair has begun.",
|
||||
"startError": "The affair could not be started.",
|
||||
"maintenanceLow": "Maintenance 25",
|
||||
"maintenanceMedium": "Maintenance 50",
|
||||
"maintenanceHigh": "Maintenance 75",
|
||||
"maintenanceSuccess": "Maintenance has been updated.",
|
||||
"maintenanceError": "Maintenance could not be updated.",
|
||||
"acknowledge": "Acknowledge",
|
||||
"acknowledgeSuccess": "The relationship has been officially acknowledged.",
|
||||
"acknowledgeError": "The relationship could not be acknowledged.",
|
||||
"end": "End",
|
||||
"endConfirm": "Do you really want to end this relationship?",
|
||||
"endSuccess": "The relationship has been ended.",
|
||||
"endError": "The relationship could not be ended."
|
||||
},
|
||||
"candidates": {
|
||||
"title": "Possible affairs",
|
||||
"roleLabel": "Relationship form",
|
||||
"none": "There are currently no suitable new affairs."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"scandal": "A family scandal is shaking your house.",
|
||||
"loverBirth": "A child has been born from an affair."
|
||||
},
|
||||
"sendgift": {
|
||||
"error": {
|
||||
"nogiftselected": "Please select a gift.",
|
||||
@@ -604,6 +670,60 @@
|
||||
"cost": "Cost",
|
||||
"date": "Date"
|
||||
}
|
||||
},
|
||||
"underground": {
|
||||
"title": "Underground",
|
||||
"tabs": {
|
||||
"activities": "Activities",
|
||||
"attacks": "Attacks"
|
||||
},
|
||||
"activities": {
|
||||
"none": "No activities available.",
|
||||
"create": "Create new activity",
|
||||
"type": "Activity type",
|
||||
"victim": "Target person",
|
||||
"cost": "Cost",
|
||||
"status": "Status",
|
||||
"additionalInfo": "Additional information",
|
||||
"blackmailAmount": "Blackmail amount",
|
||||
"discoveries": "Discoveries",
|
||||
"visibilityDelta": "Visibility",
|
||||
"reputationDelta": "Reputation",
|
||||
"victimPlaceholder": "Enter username",
|
||||
"sabotageTarget": "Sabotage target",
|
||||
"corruptGoal": "Corruption goal",
|
||||
"affairGoal": "Investigation goal"
|
||||
},
|
||||
"attacks": {
|
||||
"target": "Attacker",
|
||||
"date": "Date",
|
||||
"success": "Success",
|
||||
"none": "No attacks recorded."
|
||||
},
|
||||
"types": {
|
||||
"spyin": "Espionage",
|
||||
"assassin": "Assassination",
|
||||
"sabotage": "Sabotage",
|
||||
"corrupt_politician": "Corruption",
|
||||
"rob": "Robbery",
|
||||
"investigate_affair": "Investigate affair"
|
||||
},
|
||||
"targets": {
|
||||
"house": "House",
|
||||
"storage": "Storage"
|
||||
},
|
||||
"goals": {
|
||||
"elect": "Appointment",
|
||||
"taxIncrease": "Raise taxes",
|
||||
"taxDecrease": "Lower taxes",
|
||||
"expose": "Expose",
|
||||
"blackmail": "Blackmail"
|
||||
},
|
||||
"status": {
|
||||
"pending": "Pending",
|
||||
"resolved": "Resolved",
|
||||
"failed": "Failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,6 +470,8 @@
|
||||
"name": "Nombre",
|
||||
"age": "Edad",
|
||||
"status": "Estado",
|
||||
"marriageSatisfaction": "Satisfacción matrimonial",
|
||||
"marriageState": "Estado del matrimonio",
|
||||
"none": "No hay cónyuge.",
|
||||
"search": "Buscar pareja",
|
||||
"found": "Pareja encontrada",
|
||||
@@ -501,6 +503,17 @@
|
||||
"progress": "Afecto",
|
||||
"jumpToPartyForm": "Organizar banquete de boda (necesario para boda e hijos)"
|
||||
},
|
||||
"marriageState": {
|
||||
"stable": "Estable",
|
||||
"strained": "Tenso",
|
||||
"crisis": "Crisis"
|
||||
},
|
||||
"householdTension": {
|
||||
"label": "Tensión del hogar",
|
||||
"low": "Calmo",
|
||||
"medium": "Inquieto",
|
||||
"high": "Tenso"
|
||||
},
|
||||
"relationships": {
|
||||
"name": "Nombre"
|
||||
},
|
||||
@@ -522,14 +535,62 @@
|
||||
"baptism": "Bautizar",
|
||||
"notBaptized": "Aún no bautizado",
|
||||
"baptismNotice": "Este niño aún no ha sido bautizado y por lo tanto todavía no tiene nombre.",
|
||||
"legitimacy": {
|
||||
"legitimate": "Legítimo",
|
||||
"acknowledged_bastard": "Ilegítimo reconocido",
|
||||
"hidden_bastard": "Ilegítimo"
|
||||
},
|
||||
"details": {
|
||||
"title": "Detalles del hijo"
|
||||
}
|
||||
},
|
||||
"lovers": {
|
||||
"title": "Amantes",
|
||||
"title": "Amantes y favoritas",
|
||||
"none": "No hay amantes.",
|
||||
"affection": "Afecto"
|
||||
"affection": "Afecto",
|
||||
"visibility": "Visibilidad",
|
||||
"discretion": "Discreción",
|
||||
"maintenance": "Mantenimiento",
|
||||
"monthlyCost": "Coste mensual",
|
||||
"statusFit": "Adecuación social",
|
||||
"acknowledged": "Reconocido",
|
||||
"underfunded": "{count} meses con fondos insuficientes",
|
||||
"role": {
|
||||
"secret_affair": "Aventura secreta",
|
||||
"lover": "Amante",
|
||||
"mistress_or_favorite": "Favorita o favorito"
|
||||
},
|
||||
"risk": {
|
||||
"low": "Riesgo bajo",
|
||||
"medium": "Riesgo medio",
|
||||
"high": "Riesgo alto"
|
||||
},
|
||||
"actions": {
|
||||
"start": "Iniciar relación",
|
||||
"startSuccess": "La nueva relación ha comenzado.",
|
||||
"startError": "No se pudo iniciar la relación.",
|
||||
"maintenanceLow": "Mantenimiento 25",
|
||||
"maintenanceMedium": "Mantenimiento 50",
|
||||
"maintenanceHigh": "Mantenimiento 75",
|
||||
"maintenanceSuccess": "Se ha ajustado el mantenimiento.",
|
||||
"maintenanceError": "No se pudo ajustar el mantenimiento.",
|
||||
"acknowledge": "Reconocer",
|
||||
"acknowledgeSuccess": "La relación ha sido reconocida oficialmente.",
|
||||
"acknowledgeError": "No se pudo reconocer la relación.",
|
||||
"end": "Finalizar",
|
||||
"endConfirm": "¿De verdad quieres finalizar esta relación?",
|
||||
"endSuccess": "La relación ha finalizado.",
|
||||
"endError": "No se pudo finalizar la relación."
|
||||
},
|
||||
"candidates": {
|
||||
"title": "Posibles relaciones",
|
||||
"roleLabel": "Forma de la relación",
|
||||
"none": "Actualmente no hay nuevas relaciones adecuadas."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"scandal": "Un escándalo familiar sacude tu casa.",
|
||||
"loverBirth": "Ha nacido un hijo de una relación amorosa."
|
||||
},
|
||||
"statuses": {
|
||||
"wooing": "En cortejo",
|
||||
@@ -1008,10 +1069,16 @@
|
||||
"type": "Tipo de actividad",
|
||||
"victim": "Objetivo",
|
||||
"cost": "Coste",
|
||||
"status": "Estado",
|
||||
"additionalInfo": "Información adicional",
|
||||
"blackmailAmount": "Suma del chantaje",
|
||||
"discoveries": "Hallazgos",
|
||||
"visibilityDelta": "Visibilidad",
|
||||
"reputationDelta": "Reputación",
|
||||
"victimPlaceholder": "Introduce el nombre de usuario",
|
||||
"sabotageTarget": "Objetivo del sabotaje",
|
||||
"corruptGoal": "Objetivo de la corrupción"
|
||||
"corruptGoal": "Objetivo de la corrupción",
|
||||
"affairGoal": "Objetivo de la investigación"
|
||||
},
|
||||
"attacks": {
|
||||
"target": "Atacante",
|
||||
@@ -1024,7 +1091,8 @@
|
||||
"assassin": "Atentado",
|
||||
"sabotage": "Sabotaje",
|
||||
"corrupt_politician": "Corrupción",
|
||||
"rob": "Robo"
|
||||
"rob": "Robo",
|
||||
"investigate_affair": "Investigar relación"
|
||||
},
|
||||
"targets": {
|
||||
"house": "Vivienda",
|
||||
@@ -1033,7 +1101,14 @@
|
||||
"goals": {
|
||||
"elect": "Nombramiento",
|
||||
"taxIncrease": "Subir impuestos",
|
||||
"taxDecrease": "Bajar impuestos"
|
||||
"taxDecrease": "Bajar impuestos",
|
||||
"expose": "Exponer",
|
||||
"blackmail": "Chantajear"
|
||||
},
|
||||
"status": {
|
||||
"pending": "Pendiente",
|
||||
"resolved": "Resuelto",
|
||||
"failed": "Fallido"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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