Implement lover installment logic in FalukantFamilyWorker: Added support for processing lover maintenance payments every 2 hours, including new SQL queries for installment tracking and updates to relationship states. Enhanced WebSocket events to notify UI of installment changes, improving family dynamics and financial interactions.

This commit is contained in:
Torsten Schulz (local)
2026-03-23 10:01:34 +01:00
parent fe0361971d
commit c82fbc0f7c
5 changed files with 198 additions and 61 deletions

View File

@@ -1,7 +1,8 @@
//! Liebhaber, Ehezufriedenheit, Ansehen, Monatskosten (Handoff: docs/FALUKANT_DAEMON_HANDOFF.md).
//! Benötigt `migrations/001_falukant_family_lovers.sql` (ggf. `002` bei Altbestand), `003` für Ehe-Buffs,
//! optional `004` + Backend-Stammdaten für Dienerschaft (`falukant_servants`),
//! `005` Ehe öffentliche Stabilität + Hausfrieden (`household_tension_score`).
//! `005` Ehe öffentliche Stabilität + Hausfrieden (`household_tension_score`),
//! `006` Liebschafts-Unterhalt 12× pro Spieltag (alle 2 h), 1 Spieltag = 1 Spieljahr.
//!
//! WebSocket: `falukantUpdateFamily` (reason) + `falukantUpdateStatus` für betroffene Nutzer.
@@ -18,10 +19,11 @@ use super::base::{BaseWorker, Worker, WorkerState};
use super::sql::{
QUERY_COUNT_LOVER_CHILDREN_FOR_USER, QUERY_FAMILY_SCHEMA_READY,
QUERY_GET_ACTIVE_LOVER_ROWS_FOR_DAILY, QUERY_GET_ACTIVE_LOVER_ROWS_FOR_MONTHLY,
QUERY_GET_LOVER_PREGNANCY_CANDIDATES, QUERY_GET_MARRIAGE_ROWS,
QUERY_GET_USER_HOUSE_ROW_BY_USER, QUERY_INSERT_CHILD, QUERY_INSERT_CHILD_RELATION_LOVER,
QUERY_LOVER_BIRTH_PENALTY_MARRIAGE, QUERY_LOVER_BIRTH_PENALTY_REPUTATION,
QUERY_MARK_LOVER_DAILY_DONE, QUERY_MARK_LOVER_MONTHLY_DONE,
QUERY_GET_ACTIVE_LOVER_ROWS_FOR_INSTALLMENT, QUERY_GET_LOVER_PREGNANCY_CANDIDATES,
QUERY_GET_MARRIAGE_ROWS, QUERY_GET_USER_HOUSE_ROW_BY_USER, QUERY_INSERT_CHILD,
QUERY_INSERT_CHILD_RELATION_LOVER, QUERY_LOVER_BIRTH_PENALTY_MARRIAGE,
QUERY_LOVER_BIRTH_PENALTY_REPUTATION, QUERY_LOVER_INSTALLMENT_SCHEMA_READY,
QUERY_MARK_LOVER_DAILY_DONE, QUERY_MARK_LOVER_INSTALLMENT_AT, QUERY_MARK_LOVER_MONTHLY_DONE,
QUERY_MARRIAGE_SUBTRACT_SATISFACTION, QUERY_RESET_LOVER_UNDERPAY_COUNTERS,
QUERY_UPDATE_CHARACTER_REPUTATION, QUERY_UPDATE_LOVER_UNDERPAY_STATE,
QUERY_UPDATE_LOVER_VISIBILITY_DISCRETION, QUERY_UPDATE_MARRIAGE_STATE_AND_BUFFS,
@@ -32,6 +34,8 @@ use crate::message_broker::MessageBroker;
const DAILY_INTERVAL: Duration = Duration::from_secs(24 * 3600);
const MONTHLY_INTERVAL: Duration = Duration::from_secs(30 * 24 * 3600);
/// 12 Teilzahlungen pro Spieltag (24 h = 1 Spieljahr); 2 h = 1 Spielmonat.
const LOVER_INSTALLMENT_INTERVAL: Duration = Duration::from_secs(2 * 3600);
pub struct FalukantFamilyWorker {
base: BaseWorker,
@@ -39,9 +43,12 @@ pub struct FalukantFamilyWorker {
dist: Uniform<f64>,
last_daily: Option<Instant>,
last_monthly: Option<Instant>,
last_lover_installment: Option<Instant>,
schema_ready: bool,
/// Migration `004_falukant_servants_daemon.sql` (Dienerschaft-Ticks).
servants_schema_ready: bool,
/// Migration `006_falukant_lover_installments.sql`.
lover_installment_schema_ready: bool,
}
impl FalukantFamilyWorker {
@@ -52,8 +59,10 @@ impl FalukantFamilyWorker {
dist: Uniform::from(0.0..1.0),
last_daily: None,
last_monthly: None,
last_lover_installment: None,
schema_ready: false,
servants_schema_ready: false,
lover_installment_schema_ready: false,
}
}
@@ -84,6 +93,15 @@ impl FalukantFamilyWorker {
self.last_monthly = Some(now);
}
if self.lover_installment_schema_ready {
if Self::should_run(self.last_lover_installment, now, LOVER_INSTALLMENT_INTERVAL) {
if let Err(e) = self.process_lover_installments() {
eprintln!("[FalukantFamilyWorker] process_lover_installments: {e}");
}
self.last_lover_installment = Some(now);
}
}
std::thread::sleep(Duration::from_secs(1));
if !state.running_worker.load(Ordering::Relaxed) {
// stopping
@@ -112,6 +130,13 @@ impl FalukantFamilyWorker {
.unwrap_or(false);
self.servants_schema_ready =
super::falukant_servants::servants_schema_ready(&self.base.pool).unwrap_or(false);
conn.prepare("lover_inst_schema", QUERY_LOVER_INSTALLMENT_SCHEMA_READY)?;
let inst_rows = conn.execute("lover_inst_schema", &[])?;
self.lover_installment_schema_ready = inst_rows
.first()
.and_then(|r| r.get("ready"))
.map(|v| v == "true" || v == "t")
.unwrap_or(false);
Ok(family_ok)
}
@@ -638,13 +663,46 @@ impl FalukantFamilyWorker {
conn.prepare("get_lovers_m", QUERY_GET_ACTIVE_LOVER_ROWS_FOR_MONTHLY)?;
let lover_rows = conn.execute("get_lovers_m", &[])?;
conn.prepare("mark_monthly", QUERY_MARK_LOVER_MONTHLY_DONE)?;
let mut monthly_notify: HashSet<i32> = HashSet::new();
for r in lover_rows {
let rel_id = parse_i32(&r, "rel_id", -1);
if rel_id < 0 {
continue;
}
let u1 = parse_opt_i32(&r, "user1_id");
let u2 = parse_opt_i32(&r, "user2_id");
conn.execute("mark_monthly", &[&rel_id])?;
push_user_id(&mut monthly_notify, u1);
push_user_id(&mut monthly_notify, u2);
}
self.publish_falukant_update_family_batch(&monthly_notify, "monthly");
drop(conn);
self.process_lover_births()?;
Ok(())
}
/// Ein Zwölftel des bisherigen Monatsunterhalts, alle 2 h (12 Spielmonate pro Spieljahr).
fn process_lover_installments(&mut self) -> Result<(), DbError> {
let mut conn = self
.base
.pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("get_lovers_i", QUERY_GET_ACTIVE_LOVER_ROWS_FOR_INSTALLMENT)?;
let lover_rows = conn.execute("get_lovers_i", &[])?;
conn.prepare("upd_under", QUERY_UPDATE_LOVER_UNDERPAY_STATE)?;
conn.prepare("reset_under", QUERY_RESET_LOVER_UNDERPAY_COUNTERS)?;
conn.prepare("mar_sub", QUERY_MARRIAGE_SUBTRACT_SATISFACTION)?;
conn.prepare("mark_monthly", QUERY_MARK_LOVER_MONTHLY_DONE)?;
conn.prepare("mark_inst", QUERY_MARK_LOVER_INSTALLMENT_AT)?;
let mut monthly_notify: HashSet<i32> = HashSet::new();
let mut notify: HashSet<i32> = HashSet::new();
for r in lover_rows {
let rel_id = parse_i32(&r, "rel_id", -1);
@@ -674,6 +732,7 @@ impl FalukantFamilyWorker {
_ => 1.0,
};
let cost = ((base as f64) * rank_m * maint_f * sf_m).round() as i32;
let installment = ((cost as f64) / 12.0 * 100.0).round() / 100.0;
let u1 = parse_opt_i32(&r, "user1_id");
let u2 = parse_opt_i32(&r, "user2_id");
@@ -686,66 +745,69 @@ impl FalukantFamilyWorker {
let mut scandal_extra = parse_i32(&r, "scandal_extra_daily_pct", 0);
if let Some(uid) = payer {
let money = self.get_user_money(uid)?;
if money >= cost as f64 {
self.base.change_falukant_user_money(
uid,
-(cost as f64),
"lover maintenance",
)?;
conn.execute("reset_under", &[&rel_id])?;
if installment <= 0.0 {
conn.execute("mark_inst", &[&rel_id])?;
} else {
let new_aff = clamp_i32(affection - 8, 0, 100);
let new_disc = clamp_i32(discretion - 6, 0, 100);
let new_vis = clamp_i32(visibility + 8, 0, 100);
consec += 1;
if consec >= 2 {
scandal_extra = (scandal_extra + 2).min(100);
}
conn.execute(
"upd_under",
&[
&new_aff,
&new_disc,
&new_vis,
&consec,
&scandal_extra,
&rel_id,
],
)?;
for cid in [parse_i32(&r, "c1", 0), parse_i32(&r, "c2", 0)] {
if cid <= 0 {
continue;
let money = self.get_user_money(uid)?;
if money >= installment {
self.base.change_falukant_user_money(
uid,
-installment,
"lover maintenance",
)?;
conn.execute("reset_under", &[&rel_id])?;
conn.execute("mark_inst", &[&rel_id])?;
} else {
let new_aff = clamp_i32(affection - 8, 0, 100);
let new_disc = clamp_i32(discretion - 6, 0, 100);
let new_vis = clamp_i32(visibility + 8, 0, 100);
consec += 1;
if consec >= 2 {
scandal_extra = (scandal_extra + 2).min(100);
}
if let Ok(Some(mid)) = marriage_id_for_character(&mut conn, cid) {
conn.execute("mar_sub", &[&mid, &4])?;
}
}
conn.execute(
"upd_under",
&[
&new_aff,
&new_disc,
&new_vis,
&consec,
&scandal_extra,
&rel_id,
],
)?;
if visibility >= 40 {
for cid in [parse_i32(&r, "c1", 0), parse_i32(&r, "c2", 0)] {
if cid > 0 {
let cur = fetch_reputation(&mut conn, cid)?;
let s = format!("{:.2}", (cur - 1.0).max(0.0));
conn.prepare("upd_rep", QUERY_UPDATE_CHARACTER_REPUTATION)?;
conn.execute("upd_rep", &[&s, &cid])?;
if cid <= 0 {
continue;
}
if let Ok(Some(mid)) = marriage_id_for_character(&mut conn, cid) {
conn.execute("mar_sub", &[&mid, &4])?;
}
}
if visibility >= 40 {
for cid in [parse_i32(&r, "c1", 0), parse_i32(&r, "c2", 0)] {
if cid > 0 {
let cur = fetch_reputation(&mut conn, cid)?;
let s = format!("{:.2}", (cur - 1.0).max(0.0));
conn.prepare("upd_rep", QUERY_UPDATE_CHARACTER_REPUTATION)?;
conn.execute("upd_rep", &[&s, &cid])?;
}
}
}
conn.execute("mark_inst", &[&rel_id])?;
}
}
} else {
conn.execute("mark_inst", &[&rel_id])?;
}
conn.execute("mark_monthly", &[&rel_id])?;
push_user_id(&mut monthly_notify, u1);
push_user_id(&mut monthly_notify, u2);
push_user_id(&mut notify, u1);
push_user_id(&mut notify, u2);
}
self.publish_falukant_update_family_batch(&monthly_notify, "monthly");
drop(conn);
self.process_lover_births()?;
self.publish_falukant_update_family_batch(&notify, "lover_installment");
Ok(())
}