All checks were successful
Deploy yourpart (blue-green) / deploy (push) Successful in 1m31s
1542 lines
56 KiB
Rust
1542 lines
56 KiB
Rust
//! 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`),
|
||
//! `006` Liebschafts-Unterhalt 12× pro Spieltag (alle 2 h), 1 Spieltag = 1 Spieljahr.
|
||
//! Schuldturm/Verzug/Pfändung: `falukant_debtors::run_daily` (siehe `docs/FALUKANT_DEBTORS_DAEMON.md`).
|
||
//!
|
||
//! WebSocket: `falukantUpdateFamily` (reason) + `falukantUpdateStatus` für betroffene Nutzer.
|
||
|
||
use std::collections::{HashMap, HashSet};
|
||
use std::sync::atomic::Ordering;
|
||
use std::sync::Arc;
|
||
use std::time::{Duration, Instant};
|
||
|
||
use rand::distributions::{Distribution, Uniform};
|
||
use rand::rngs::StdRng;
|
||
use rand::SeedableRng;
|
||
|
||
use super::base::{BaseWorker, Worker, WorkerState};
|
||
use super::sql::{
|
||
QUERY_DEACTIVATE_LOVER_RELATIONSHIP,
|
||
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_ACTIVE_LOVER_ROWS_FOR_INSTALLMENT, QUERY_GET_LOVER_PREGNANCY_CANDIDATES,
|
||
QUERY_HAS_MOTHER_BIRTH_TODAY,
|
||
QUERY_GET_MARRIAGE_ROWS, QUERY_GET_USER_HOUSE_ROW_BY_USER, QUERY_LOVER_INSTALLMENT_SCHEMA_READY,
|
||
QUERY_MARK_LOVER_DAILY_DONE, QUERY_MARK_LOVER_INSTALLMENT_AT, QUERY_MARK_LOVER_MONTHLY_DONE,
|
||
QUERY_SET_CHARACTER_PREGNANCY_FROM_LOVER,
|
||
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,
|
||
QUERY_UPDATE_USER_HOUSE_TENSION_BY_USER,
|
||
};
|
||
use crate::db::{ConnectionPool, DbError};
|
||
use crate::message_broker::MessageBroker;
|
||
use serde_json::json;
|
||
|
||
const DAILY_INTERVAL: Duration = Duration::from_secs(24 * 3600);
|
||
/// Wie `DAILY_INTERVAL`: 1 Spieljahr = 1 Kalendertag — Monats-Stempel/„monthly“-Batch pro Spieltag, nicht alle 30 echten Tage.
|
||
const MONTHLY_INTERVAL: Duration = DAILY_INTERVAL;
|
||
/// Produktionszertifikat: Aufstieg/Herabstufung gegen Mindestwerte (nicht nur 1×/Tag).
|
||
const CERTIFICATE_RECALC_INTERVAL: Duration = Duration::from_secs(60);
|
||
/// 12 Monatsticke pro Spieltag (24 h = 1 Spieljahr); 2 h = 1 Spielmonat (Liebschaft + Dienerschaft).
|
||
const GAME_MONTH_SLICE_INTERVAL: Duration = Duration::from_secs(2 * 3600);
|
||
|
||
pub struct FalukantFamilyWorker {
|
||
base: BaseWorker,
|
||
rng: StdRng,
|
||
dist: Uniform<f64>,
|
||
last_daily: Option<Instant>,
|
||
last_monthly: Option<Instant>,
|
||
/// Gemeinsamer Tick für Liebschafts-Raten + Dienerschaft (Monatstick ≈ 2 h).
|
||
last_game_month_slice: Option<Instant>,
|
||
last_certificate_recalc: 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 {
|
||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||
Self {
|
||
base: BaseWorker::new("FalukantFamilyWorker", pool, broker),
|
||
rng: StdRng::from_entropy(),
|
||
dist: Uniform::from(0.0..1.0),
|
||
last_daily: None,
|
||
last_monthly: None,
|
||
last_game_month_slice: None,
|
||
last_certificate_recalc: None,
|
||
schema_ready: false,
|
||
servants_schema_ready: false,
|
||
lover_installment_schema_ready: false,
|
||
}
|
||
}
|
||
|
||
fn run_iteration(&mut self, state: &WorkerState) {
|
||
self.base.set_current_step("FalukantFamilyWorker iteration");
|
||
let now = Instant::now();
|
||
|
||
if !self.schema_ready
|
||
&& let Ok(true) = self.check_schema() {
|
||
self.schema_ready = true;
|
||
}
|
||
|
||
if Self::should_run(self.last_certificate_recalc, now, CERTIFICATE_RECALC_INTERVAL) {
|
||
if let Err(e) = super::falukant_certificate::run_daily(&self.base, &self.base.broker) {
|
||
eprintln!("[FalukantFamilyWorker] falukant_certificate::run_daily: {e}");
|
||
}
|
||
self.last_certificate_recalc = Some(now);
|
||
}
|
||
|
||
if Self::should_run(self.last_daily, now, DAILY_INTERVAL) {
|
||
if let Err(e) = super::falukant_debtors::run_daily(&self.base, &self.base.broker) {
|
||
eprintln!("[FalukantFamilyWorker] falukant_debtors::run_daily: {e}");
|
||
}
|
||
if self.schema_ready
|
||
&& let Err(e) = self.process_daily() {
|
||
eprintln!("[FalukantFamilyWorker] process_daily: {e}");
|
||
}
|
||
self.last_daily = Some(now);
|
||
}
|
||
|
||
if Self::should_run(self.last_monthly, now, MONTHLY_INTERVAL) {
|
||
if let Err(e) = self.process_monthly() {
|
||
eprintln!("[FalukantFamilyWorker] process_monthly: {e}");
|
||
}
|
||
self.last_monthly = Some(now);
|
||
}
|
||
|
||
if (self.servants_schema_ready || self.lover_installment_schema_ready)
|
||
&& Self::should_run(self.last_game_month_slice, now, GAME_MONTH_SLICE_INTERVAL) {
|
||
if self.servants_schema_ready
|
||
&& let Err(e) =
|
||
super::falukant_servants::run_monthly(&self.base, &self.base.broker)
|
||
{
|
||
eprintln!("[FalukantFamilyWorker] falukant_servants::run_monthly: {e}");
|
||
}
|
||
if self.lover_installment_schema_ready
|
||
&& let Err(e) = self.process_lover_installments() {
|
||
eprintln!("[FalukantFamilyWorker] process_lover_installments: {e}");
|
||
}
|
||
self.last_game_month_slice = Some(now);
|
||
}
|
||
|
||
std::thread::sleep(Duration::from_secs(1));
|
||
if !state.running_worker.load(Ordering::Relaxed) {
|
||
// stopping
|
||
}
|
||
}
|
||
|
||
fn should_run(last: Option<Instant>, now: Instant, interval: Duration) -> bool {
|
||
match last {
|
||
None => true,
|
||
Some(t) => now.saturating_duration_since(t) >= interval,
|
||
}
|
||
}
|
||
|
||
fn check_schema(&mut self) -> Result<bool, DbError> {
|
||
let mut conn = self
|
||
.base
|
||
.pool
|
||
.get()
|
||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||
conn.prepare("family_schema", QUERY_FAMILY_SCHEMA_READY)?;
|
||
let rows = conn.execute("family_schema", &[])?;
|
||
let family_ok = rows
|
||
.first()
|
||
.and_then(|r| r.get("ready"))
|
||
.map(|v| v == "true" || v == "t")
|
||
.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)
|
||
}
|
||
|
||
fn process_daily(&mut self) -> Result<(), DbError> {
|
||
if self.servants_schema_ready {
|
||
super::falukant_servants::run_daily(&self.base, &self.base.broker)?;
|
||
}
|
||
|
||
let mut conn = self
|
||
.base
|
||
.pool
|
||
.get()
|
||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||
|
||
conn.prepare("get_lovers", QUERY_GET_ACTIVE_LOVER_ROWS_FOR_DAILY)?;
|
||
let lover_rows = conn.execute("get_lovers", &[])?;
|
||
|
||
conn.prepare("get_marriages", QUERY_GET_MARRIAGE_ROWS)?;
|
||
let marriage_rows = conn.execute("get_marriages", &[])?;
|
||
|
||
let mut marriages: Vec<MarriageData> = marriage_rows
|
||
.into_iter()
|
||
.map(|r| MarriageData {
|
||
id: parse_i32(&r, "marriage_id", -1),
|
||
m1: parse_i32(&r, "m1", -1),
|
||
m2: parse_i32(&r, "m2", -1),
|
||
satisfaction: parse_i32(&r, "marriage_satisfaction", 55),
|
||
public_stability: parse_i32(&r, "marriage_public_stability", 55),
|
||
drift_high: parse_i32(&r, "marriage_drift_high", 0),
|
||
drift_low: parse_i32(&r, "marriage_drift_low", 0),
|
||
title1_tr: r.get("title1_tr").cloned().unwrap_or_default(),
|
||
title2_tr: r.get("title2_tr").cloned().unwrap_or_default(),
|
||
user1_id: parse_opt_i32(&r, "user1_id"),
|
||
user2_id: parse_opt_i32(&r, "user2_id"),
|
||
gift_days: parse_i32(&r, "marriage_gift_buff_days_remaining", 0),
|
||
feast_pending: parse_i32(&r, "marriage_pending_feast_bonus", 0),
|
||
house_supply: parse_i32(&r, "marriage_house_supply", 50),
|
||
no_lover_counter: parse_i32(&r, "marriage_no_lover_bonus_counter", 0),
|
||
household_order_1: parse_i32(&r, "household_order_1", 55),
|
||
household_order_2: parse_i32(&r, "household_order_2", 55),
|
||
servant_quality_1: parse_i32(&r, "servant_quality_1", 50),
|
||
servant_quality_2: parse_i32(&r, "servant_quality_2", 50),
|
||
})
|
||
.filter(|m| m.id > 0)
|
||
.collect();
|
||
|
||
let mut lovers: Vec<LoverData> = lover_rows
|
||
.into_iter()
|
||
.map(|r| LoverData {
|
||
rel_id: parse_i32(&r, "rel_id", -1),
|
||
c1: parse_i32(&r, "c1", -1),
|
||
c2: parse_i32(&r, "c2", -1),
|
||
lover_role: r.get("lover_role").cloned().unwrap_or_default(),
|
||
affection: parse_i32(&r, "affection", 50),
|
||
visibility: parse_i32(&r, "visibility", 0),
|
||
discretion: parse_i32(&r, "discretion", 50),
|
||
maintenance_level: parse_i32(&r, "maintenance_level", 50),
|
||
status_fit: parse_i32(&r, "status_fit", 0),
|
||
scandal_extra: parse_i32(&r, "scandal_extra_daily_pct", 0),
|
||
months_underfunded: parse_i32(&r, "months_underfunded", 0),
|
||
acknowledged: parse_bool_row(&r, "acknowledged"),
|
||
title1_tr: r.get("title1_tr").cloned().unwrap_or_default(),
|
||
title2_tr: r.get("title2_tr").cloned().unwrap_or_default(),
|
||
user1_id: parse_opt_i32(&r, "user1_id"),
|
||
user2_id: parse_opt_i32(&r, "user2_id"),
|
||
min_age_years: parse_i32(&r, "min_age_years", 99),
|
||
servant_disc_u1: parse_i32(&r, "servant_disc_u1", 0),
|
||
servant_disc_u2: parse_i32(&r, "servant_disc_u2", 0),
|
||
})
|
||
.filter(|l| l.rel_id > 0)
|
||
.collect();
|
||
|
||
let mut char_user: HashMap<i32, Option<i32>> = HashMap::new();
|
||
for l in &lovers {
|
||
char_user.insert(l.c1, l.user1_id);
|
||
char_user.insert(l.c2, l.user2_id);
|
||
}
|
||
|
||
let mut char_lover_count: std::collections::HashMap<i32, usize> =
|
||
std::collections::HashMap::new();
|
||
let mut char_visible_lover_rel_count: std::collections::HashMap<i32, usize> =
|
||
std::collections::HashMap::new();
|
||
for l in &lovers {
|
||
*char_lover_count.entry(l.c1).or_insert(0) += 1;
|
||
*char_lover_count.entry(l.c2).or_insert(0) += 1;
|
||
if l.visibility >= 60 {
|
||
*char_visible_lover_rel_count.entry(l.c1).or_insert(0) += 1;
|
||
*char_visible_lover_rel_count.entry(l.c2).or_insert(0) += 1;
|
||
}
|
||
}
|
||
|
||
conn.prepare("upd_vd", QUERY_UPDATE_LOVER_VISIBILITY_DISCRETION)?;
|
||
conn.prepare("upd_rep", QUERY_UPDATE_CHARACTER_REPUTATION)?;
|
||
conn.prepare("deactivate_lover", QUERY_DEACTIVATE_LOVER_RELATIONSHIP)?;
|
||
let mut ended_rel_ids: HashSet<i32> = HashSet::new();
|
||
let mut breakup_socket_users: HashSet<i32> = HashSet::new();
|
||
let mut breakup_risk_notified: HashSet<(i32, i32)> = HashSet::new();
|
||
|
||
// Sammeln von Ehepartner-Charakteren für Ehezufriedenheits-Deltas
|
||
let mut spouse_satisfaction_delta: HashMap<i32, i32> = HashMap::new();
|
||
|
||
for l in &mut lovers {
|
||
let ehe_conflict = marriages.iter().any(|m| {
|
||
(m.m1 == l.c1 || m.m2 == l.c1 || m.m1 == l.c2 || m.m2 == l.c2) && m.satisfaction < 40
|
||
});
|
||
|
||
let mut v_net = 0i32;
|
||
if l.maintenance_level < 35 {
|
||
v_net += 1;
|
||
}
|
||
if l.affection < 30 {
|
||
v_net += 1;
|
||
}
|
||
if l.status_fit == -2 {
|
||
v_net += 2;
|
||
}
|
||
if ehe_conflict {
|
||
v_net += 1;
|
||
}
|
||
if l.discretion >= 60 {
|
||
v_net -= 1;
|
||
}
|
||
if l.maintenance_level >= 70 {
|
||
v_net -= 1;
|
||
}
|
||
let comb = (l.servant_disc_u1 + l.servant_disc_u2) / 2;
|
||
v_net += comb / 4;
|
||
let new_vis = clamp_i32(l.visibility + v_net, 0, 100);
|
||
|
||
let mut d_net = 0i32;
|
||
if l.maintenance_level >= 70 {
|
||
d_net += 1;
|
||
}
|
||
if l.maintenance_level < 35 {
|
||
d_net -= 1;
|
||
}
|
||
if l.visibility > 60 {
|
||
d_net -= 1;
|
||
}
|
||
let new_disc = clamp_i32(l.discretion + d_net, 0, 100);
|
||
|
||
conn.execute(
|
||
"upd_vd",
|
||
&[&new_vis, &new_disc, &l.rel_id],
|
||
)?;
|
||
l.visibility = new_vis;
|
||
l.discretion = new_disc;
|
||
|
||
// Standsabhängige Auswirkungen auf Reputation der Liebhaber
|
||
let rep_delta = calculate_lover_reputation_delta(l);
|
||
if rep_delta.abs() > 0.01 {
|
||
// Reputation beider Liebhaber erhöhen/senken
|
||
for cid in [l.c1, l.c2] {
|
||
if cid > 0 {
|
||
let rep_s = format!("{:.2}", rep_delta);
|
||
// Nutzen des UPDATE CHARACTER ... reputation = reputation + $1 Pattern
|
||
let _ = conn.execute("upd_rep", &[&rep_s, &cid]);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Standsabhängige Auswirkungen auf Ehezufriedenheit sammeln
|
||
let marriage_sat_delta = calculate_lover_marriage_satisfaction_delta(l);
|
||
if marriage_sat_delta != 0 {
|
||
// Sammeln der Ehepartnerschaften, auf die diese Liebschaft Einfluss hat
|
||
for spouse_char_id in [l.c1, l.c2] {
|
||
let spouse_marriage = marriages.iter().find(|m| {
|
||
m.m1 == spouse_char_id || m.m2 == spouse_char_id
|
||
});
|
||
if let Some(m) = spouse_marriage {
|
||
*spouse_satisfaction_delta.entry(m.id).or_insert(0) += marriage_sat_delta;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Je niedriger die Zuneigung, desto höher das tägliche Trennungsrisiko.
|
||
let breakup_risk_pct = breakup_risk_percent(l);
|
||
if breakup_risk_pct > 50.0 {
|
||
for uid in [l.user1_id, l.user2_id].into_iter().flatten() {
|
||
if uid <= 0 || !breakup_risk_notified.insert((l.rel_id, uid)) {
|
||
continue;
|
||
}
|
||
let payload = json!({
|
||
"tr": "random_event.lover_breakup_risk_high",
|
||
"event_id": "lover_breakup_risk_high",
|
||
"event_type": "family",
|
||
"relationship_id": l.rel_id,
|
||
"risk_percent": breakup_risk_pct,
|
||
"affection": l.affection,
|
||
"visibility": l.visibility,
|
||
"discretion": l.discretion,
|
||
"months_underfunded": l.months_underfunded
|
||
})
|
||
.to_string();
|
||
if let Err(e) = super::notify::insert_notification_conn(&mut conn, uid, &payload, None) {
|
||
eprintln!(
|
||
"[FalukantFamilyWorker] high breakup risk notification failed rel_id={} uid={}: {}",
|
||
l.rel_id, uid, e
|
||
);
|
||
}
|
||
}
|
||
}
|
||
if breakup_risk_pct > 0.0 && self.dist.sample(&mut self.rng) * 100.0 < breakup_risk_pct {
|
||
conn.execute("deactivate_lover", &[&l.rel_id])?;
|
||
ended_rel_ids.insert(l.rel_id);
|
||
push_user_id(&mut breakup_socket_users, l.user1_id);
|
||
push_user_id(&mut breakup_socket_users, l.user2_id);
|
||
}
|
||
}
|
||
lovers.retain(|l| !ended_rel_ids.contains(&l.rel_id));
|
||
|
||
conn.prepare("mark_daily", QUERY_MARK_LOVER_DAILY_DONE)?;
|
||
conn.prepare("mark_monthly", QUERY_MARK_LOVER_MONTHLY_DONE)?;
|
||
for l in &lovers {
|
||
conn.execute("mark_daily", &[&l.rel_id])?;
|
||
// Gleicher Spieltag wie Tageslauf: Geburts-/Schwangerschafts-Logik darf nicht 30 echte Tage warten.
|
||
conn.execute("mark_monthly", &[&l.rel_id])?;
|
||
}
|
||
|
||
let mut marriage_socket_users: HashSet<i32> = HashSet::new();
|
||
|
||
conn.prepare("upd_marriage_full", QUERY_UPDATE_MARRIAGE_STATE_AND_BUFFS)?;
|
||
|
||
for m in marriages.iter_mut() {
|
||
let touching: Vec<&LoverData> = lovers
|
||
.iter()
|
||
.filter(|l| {
|
||
let (a, b) = (l.c1, l.c2);
|
||
((a == m.m1 && b != m.m2) || (b == m.m1 && a != m.m2))
|
||
|| ((a == m.m2 && b != m.m1) || (b == m.m2 && a != m.m1))
|
||
})
|
||
.collect();
|
||
|
||
let touching_empty = touching.is_empty();
|
||
let sat0 = m.satisfaction;
|
||
let pst0 = m.public_stability;
|
||
let dh0 = m.drift_high;
|
||
let dl0 = m.drift_low;
|
||
let g0 = m.gift_days;
|
||
let f0 = m.feast_pending;
|
||
let n0 = m.no_lover_counter;
|
||
|
||
let mut sat = m.satisfaction;
|
||
let mut dh = m.drift_high;
|
||
let mut dl = m.drift_low;
|
||
let mut gift_days = m.gift_days;
|
||
let mut feast = m.feast_pending;
|
||
let mut nl_counter = m.no_lover_counter;
|
||
let house_supply = m.house_supply;
|
||
|
||
if touching_empty {
|
||
if sat > 55 {
|
||
dh += 1;
|
||
if dh >= 3 {
|
||
sat = (sat - 1).max(0);
|
||
dh = 0;
|
||
}
|
||
} else if sat < 55 {
|
||
dl += 1;
|
||
if dl >= 5 {
|
||
sat = (sat + 1).min(100);
|
||
dl = 0;
|
||
}
|
||
}
|
||
} else {
|
||
let mg = marriage_rank_group(&m.title1_tr, &m.title2_tr);
|
||
let mistress_n = touching
|
||
.iter()
|
||
.filter(|l| l.lover_role == "mistress_or_favorite")
|
||
.count();
|
||
let total_lovers_here = touching.len();
|
||
let mut delta = 0i32;
|
||
for l in &touching {
|
||
delta += lover_marriage_daily_delta(
|
||
mg,
|
||
l,
|
||
m.satisfaction,
|
||
mistress_n,
|
||
total_lovers_here,
|
||
);
|
||
}
|
||
if touching.iter().any(|l| l.visibility >= 60) {
|
||
delta -= 2;
|
||
}
|
||
if total_lovers_here >= 2 {
|
||
delta -= 2;
|
||
}
|
||
if touching
|
||
.iter()
|
||
.any(|l| l.lover_role == "mistress_or_favorite" && l.maintenance_level < 35)
|
||
{
|
||
delta -= 1;
|
||
}
|
||
if touching.iter().any(|l| l.min_age_years <= 15) {
|
||
delta -= 1;
|
||
}
|
||
// Standsabhängige Deltas aus der Liebschaften-Verarbeitung anwenden
|
||
if let Some(extra_delta) = spouse_satisfaction_delta.get(&m.id) {
|
||
delta += extra_delta;
|
||
}
|
||
sat = clamp_i32(sat + delta, 0, 100);
|
||
}
|
||
|
||
Self::apply_marriage_positive_buffs(
|
||
&mut sat,
|
||
&mut gift_days,
|
||
&mut feast,
|
||
&mut nl_counter,
|
||
touching_empty,
|
||
house_supply,
|
||
);
|
||
|
||
m.satisfaction = sat;
|
||
m.drift_high = dh;
|
||
m.drift_low = dl;
|
||
m.gift_days = gift_days;
|
||
m.feast_pending = feast;
|
||
m.no_lover_counter = nl_counter;
|
||
|
||
let mut pst = m.public_stability;
|
||
if !touching_empty {
|
||
let max_vis = touching.iter().map(|l| l.visibility).max().unwrap_or(0);
|
||
pst -= (max_vis / 25).min(4);
|
||
if touching.iter().any(|l| l.visibility >= 60) {
|
||
pst -= 2;
|
||
}
|
||
if touching.iter().any(|l| l.months_underfunded >= 1) {
|
||
pst -= 1;
|
||
}
|
||
if touching.iter().any(|l| l.months_underfunded >= 3) {
|
||
pst -= 1;
|
||
}
|
||
if touching.iter().any(|l| l.acknowledged) {
|
||
pst -= 2;
|
||
}
|
||
let g1 = title_group(&m.title1_tr);
|
||
let g2 = title_group(&m.title2_tr);
|
||
if (g1 as i32 - g2 as i32).abs() >= 2 {
|
||
pst -= 1;
|
||
}
|
||
if touching.iter().any(|l| l.min_age_years <= 15) {
|
||
pst -= 2;
|
||
}
|
||
let ho_avg = (m.household_order_1 + m.household_order_2) / 2;
|
||
if ho_avg < 35 {
|
||
pst -= 2;
|
||
} else if ho_avg < 50 {
|
||
pst -= 1;
|
||
}
|
||
let sq_avg = (m.servant_quality_1 + m.servant_quality_2) / 2;
|
||
if sq_avg < 40 {
|
||
pst -= 1;
|
||
}
|
||
if sat < 40 {
|
||
pst -= 1;
|
||
}
|
||
} else if pst < 55 {
|
||
pst += 1;
|
||
} else if pst > 55 {
|
||
pst -= 1;
|
||
}
|
||
pst = clamp_i32(pst, 0, 100);
|
||
m.public_stability = pst;
|
||
|
||
conn.execute(
|
||
"upd_marriage_full",
|
||
&[&sat, &dh, &dl, &gift_days, &feast, &nl_counter, &pst, &m.id],
|
||
)?;
|
||
|
||
if sat != sat0
|
||
|| pst != pst0
|
||
|| dh != dh0
|
||
|| dl != dl0
|
||
|| gift_days != g0
|
||
|| feast != f0
|
||
|| nl_counter != n0
|
||
{
|
||
push_user_id(&mut marriage_socket_users, m.user1_id);
|
||
push_user_id(&mut marriage_socket_users, m.user2_id);
|
||
}
|
||
}
|
||
|
||
// Hausfrieden: Spannungswert 0..100 persistieren (Handoff Ehe & Hausfrieden)
|
||
let mut tension_users: HashSet<i32> = HashSet::new();
|
||
for l in &lovers {
|
||
push_user_id(&mut tension_users, l.user1_id);
|
||
push_user_id(&mut tension_users, l.user2_id);
|
||
}
|
||
for m in &marriages {
|
||
push_user_id(&mut tension_users, m.user1_id);
|
||
push_user_id(&mut tension_users, m.user2_id);
|
||
}
|
||
|
||
let mut tension_agg: HashMap<i32, UserTensionAgg> = HashMap::new();
|
||
for uid in &tension_users {
|
||
tension_agg.insert(*uid, UserTensionAgg::default());
|
||
}
|
||
for l in &lovers {
|
||
for uid in [l.user1_id, l.user2_id].into_iter().flatten() {
|
||
if let Some(a) = tension_agg.get_mut(&uid) {
|
||
a.max_lover_visibility = a.max_lover_visibility.max(l.visibility);
|
||
if l.acknowledged {
|
||
a.n_acknowledged += 1;
|
||
}
|
||
a.max_months_underfunded = a.max_months_underfunded.max(l.months_underfunded);
|
||
}
|
||
}
|
||
}
|
||
for m in &marriages {
|
||
for uid in [m.user1_id, m.user2_id].into_iter().flatten() {
|
||
if let Some(a) = tension_agg.get_mut(&uid) {
|
||
a.min_marriage_sat = a.min_marriage_sat.min(m.satisfaction);
|
||
}
|
||
}
|
||
}
|
||
|
||
conn.prepare("upd_tension", QUERY_UPDATE_USER_HOUSE_TENSION_BY_USER)?;
|
||
conn.prepare("child_cnt_lover", QUERY_COUNT_LOVER_CHILDREN_FOR_USER)?;
|
||
conn.prepare("uh_row_u", QUERY_GET_USER_HOUSE_ROW_BY_USER)?;
|
||
|
||
let mut tension_socket_users: HashSet<i32> = HashSet::new();
|
||
for (&uid, agg) in &tension_agg {
|
||
let child_rows = conn.execute("child_cnt_lover", &[&uid])?;
|
||
let lover_children = child_rows
|
||
.first()
|
||
.map(|r| parse_i32(r, "cnt", 0))
|
||
.unwrap_or(0);
|
||
|
||
let house_rows = conn.execute("uh_row_u", &[&uid])?;
|
||
let Some(hr) = house_rows.first() else {
|
||
continue;
|
||
};
|
||
let ho = parse_i32(hr, "household_order", 55);
|
||
let sq = parse_i32(hr, "servant_quality", 50);
|
||
let pay = hr
|
||
.get("servant_pay_level")
|
||
.cloned()
|
||
.unwrap_or_else(|| "normal".into());
|
||
let prev_ts = parse_i32(hr, "prev_tension_score", 0);
|
||
|
||
let score = compute_household_tension_score(agg, lover_children, ho, sq, pay.as_str());
|
||
if score != prev_ts {
|
||
conn.execute("upd_tension", &[&score, &uid])?;
|
||
tension_socket_users.insert(uid);
|
||
}
|
||
}
|
||
|
||
conn.prepare("upd_rep", QUERY_UPDATE_CHARACTER_REPUTATION)?;
|
||
for l in &lovers {
|
||
let g = pair_rank_group(&l.title1_tr, &l.title2_tr);
|
||
let scandal = l.visibility > 70;
|
||
let order_ok = l.maintenance_level >= 65
|
||
&& l.discretion >= 60
|
||
&& l.visibility <= 35
|
||
&& mistress_count_for_pair(&lovers, l.c1, l.c2) <= 1;
|
||
let rm = rank_rep_modifier(g, scandal, order_ok);
|
||
let vf = 0.4 + (l.visibility as f64 / 100.0) * 1.6;
|
||
let base = match l.lover_role.as_str() {
|
||
"secret_affair" => -0.2,
|
||
"lover" => -0.4,
|
||
"mistress_or_favorite" => -0.6,
|
||
_ => -0.4,
|
||
};
|
||
let mut delta = base * vf * rm;
|
||
|
||
if l.lover_role == "mistress_or_favorite" && order_ok {
|
||
if g == 2 {
|
||
delta = 0.1;
|
||
} else if g == 3 {
|
||
delta = 0.2;
|
||
}
|
||
}
|
||
|
||
delta = delta.clamp(-3.0, 1.0);
|
||
|
||
let age_rep = age_reputation_delta(l.min_age_years);
|
||
let vis_young = visibility_young_penalty(l.min_age_years, l.visibility);
|
||
let final_delta = delta + age_rep + vis_young;
|
||
|
||
for cid in [l.c1, l.c2] {
|
||
let cur = fetch_reputation(&mut conn, cid)?;
|
||
let new_rep = (cur + final_delta).clamp(0.0, 100.0);
|
||
let s = format!("{:.2}", new_rep);
|
||
conn.execute("upd_rep", &[&s, &cid])?;
|
||
}
|
||
|
||
let married_c1 = marriages
|
||
.iter()
|
||
.any(|m| m.m1 == l.c1 || m.m2 == l.c1);
|
||
let married_c2 = marriages
|
||
.iter()
|
||
.any(|m| m.m1 == l.c2 || m.m2 == l.c2);
|
||
let mut p = 1.0
|
||
+ (l.visibility as f64) / 25.0
|
||
+ if married_c1 || married_c2 { 2.0 } else { 0.0 }
|
||
+ if l.status_fit == -2 { 2.0 } else { 0.0 }
|
||
+ if l.maintenance_level < 25 { 3.0 } else { 0.0 }
|
||
+ if char_lover_count.get(&l.c1).copied().unwrap_or(0) >= 2
|
||
|| char_lover_count.get(&l.c2).copied().unwrap_or(0) >= 2
|
||
{
|
||
3.0
|
||
} else {
|
||
0.0
|
||
}
|
||
+ l.scandal_extra as f64
|
||
+ scandal_age_extra_pct(l.min_age_years);
|
||
if l.discretion >= 75 {
|
||
p -= 2.0;
|
||
}
|
||
if g == 3
|
||
&& l.lover_role == "mistress_or_favorite"
|
||
&& l.maintenance_level >= 65
|
||
&& l.visibility <= 40
|
||
{
|
||
p -= 2.0;
|
||
}
|
||
p = p.clamp(0.0, 25.0);
|
||
if self.dist.sample(&mut self.rng) * 100.0 < p {
|
||
self.base.broker.publish(format!(
|
||
r#"{{"event":"falukant_family_scandal_hint","relationship_id":{}}}"#,
|
||
l.rel_id
|
||
));
|
||
if let Some(uid) = l.user1_id.filter(|x| *x > 0) {
|
||
self.publish_falukant_update_family_and_status(uid, "scandal");
|
||
}
|
||
if let Some(uid) = l.user2_id.filter(|x| *x > 0) {
|
||
self.publish_falukant_update_family_and_status(uid, "scandal");
|
||
}
|
||
}
|
||
}
|
||
|
||
// Zwei sichtbare Liebschaften (visibility ≥ 60) pro Person: zusätzlich −4 Ansehen.
|
||
for (&cid, &n_vis) in &char_visible_lover_rel_count {
|
||
if n_vis < 2 {
|
||
continue;
|
||
}
|
||
let cur = fetch_reputation(&mut conn, cid)?;
|
||
let new_rep = (cur - 4.0).clamp(0.0, 100.0);
|
||
let s = format!("{:.2}", new_rep);
|
||
conn.execute("upd_rep", &[&s, &cid])?;
|
||
}
|
||
|
||
// Seltene Einmal-Malus (Gerücht / Tadel) für beteiligte Charaktere.
|
||
let mut unique_cids: HashSet<i32> = HashSet::new();
|
||
for l in &lovers {
|
||
unique_cids.insert(l.c1);
|
||
unique_cids.insert(l.c2);
|
||
}
|
||
let mut rng_malus_cids: Vec<i32> = Vec::new();
|
||
for cid in unique_cids.iter().copied() {
|
||
let r = self.dist.sample(&mut self.rng) * 100.0;
|
||
let malus = if r < 1.0 {
|
||
-5.0
|
||
} else if r < 3.0 {
|
||
-3.0
|
||
} else {
|
||
0.0
|
||
};
|
||
if malus < 0.0 {
|
||
let cur = fetch_reputation(&mut conn, cid)?;
|
||
let new_rep = (cur + malus).clamp(0.0, 100.0);
|
||
let s = format!("{:.2}", new_rep);
|
||
conn.execute("upd_rep", &[&s, &cid])?;
|
||
rng_malus_cids.push(cid);
|
||
}
|
||
}
|
||
|
||
let mut notify: HashSet<i32> = HashSet::new();
|
||
for l in &lovers {
|
||
push_user_id(&mut notify, l.user1_id);
|
||
push_user_id(&mut notify, l.user2_id);
|
||
}
|
||
for (&cid, &n_vis) in &char_visible_lover_rel_count {
|
||
if n_vis >= 2 {
|
||
push_user_id(&mut notify, char_user.get(&cid).copied().flatten());
|
||
}
|
||
}
|
||
for cid in &rng_malus_cids {
|
||
push_user_id(&mut notify, char_user.get(cid).copied().flatten());
|
||
}
|
||
notify.extend(marriage_socket_users);
|
||
notify.extend(tension_socket_users);
|
||
notify.extend(breakup_socket_users);
|
||
self.publish_falukant_update_family_batch(¬ify, "daily");
|
||
|
||
drop(conn);
|
||
// Liebschafts-Geburt: täglich (1 Spieljahr); Monats-Stempel erfolgt oben mit dem Tageslauf.
|
||
if let Err(e) = self.process_lover_births() {
|
||
eprintln!("[FalukantFamilyWorker] process_lover_births: {e}");
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn process_monthly(&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_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_inst", QUERY_MARK_LOVER_INSTALLMENT_AT)?;
|
||
|
||
struct LoverInst {
|
||
rel_id: i32,
|
||
c1: i32,
|
||
c2: i32,
|
||
lover_role: String,
|
||
maintenance_level: i32,
|
||
status_fit: i32,
|
||
base: i32,
|
||
title1: String,
|
||
title2: String,
|
||
u1: Option<i32>,
|
||
u2: Option<i32>,
|
||
affection: i32,
|
||
visibility: i32,
|
||
discretion: i32,
|
||
consec: i32,
|
||
scandal_extra: i32,
|
||
payer_cid: Option<i32>,
|
||
free_political_slot: bool,
|
||
}
|
||
|
||
let mut items: Vec<LoverInst> = Vec::new();
|
||
for r in lover_rows {
|
||
let rel_id = parse_i32(&r, "rel_id", -1);
|
||
if rel_id < 0 {
|
||
continue;
|
||
}
|
||
let c1 = parse_i32(&r, "c1", -1);
|
||
let c2 = parse_i32(&r, "c2", -1);
|
||
let lover_role = r.get("lover_role").cloned().unwrap_or_default();
|
||
let maintenance_level = parse_i32(&r, "maintenance_level", 50);
|
||
let status_fit = parse_i32(&r, "status_fit", 0);
|
||
let mut base = parse_i32(&r, "monthly_base_cost", 0);
|
||
if base <= 0 {
|
||
base = match lover_role.as_str() {
|
||
"secret_affair" => 10,
|
||
"lover" => 30,
|
||
"mistress_or_favorite" => 80,
|
||
_ => 30,
|
||
};
|
||
}
|
||
let title1 = r.get("title1_tr").cloned().unwrap_or_default();
|
||
let title2 = r.get("title2_tr").cloned().unwrap_or_default();
|
||
let u1 = parse_opt_i32(&r, "user1_id");
|
||
let u2 = parse_opt_i32(&r, "user2_id");
|
||
let payer = u1.or(u2).filter(|x| *x > 0);
|
||
let payer_cid = match (u1, u2, payer) {
|
||
(Some(a), _, Some(p)) if p == a => Some(c1),
|
||
(_, Some(b), Some(p)) if p == b => Some(c2),
|
||
_ => None,
|
||
};
|
||
items.push(LoverInst {
|
||
rel_id,
|
||
c1,
|
||
c2,
|
||
lover_role,
|
||
maintenance_level,
|
||
status_fit,
|
||
base,
|
||
title1,
|
||
title2,
|
||
u1,
|
||
u2,
|
||
affection: parse_i32(&r, "affection", 50),
|
||
visibility: parse_i32(&r, "visibility", 0),
|
||
discretion: parse_i32(&r, "discretion", 50),
|
||
consec: parse_i32(&r, "months_underfunded", 0),
|
||
scandal_extra: parse_i32(&r, "scandal_extra_daily_pct", 0),
|
||
payer_cid,
|
||
free_political_slot: false,
|
||
});
|
||
}
|
||
|
||
items.sort_by_key(|x| (x.payer_cid.unwrap_or(-1), x.rel_id));
|
||
|
||
let mut free_slots_by_char: HashMap<i32, i32> = HashMap::new();
|
||
let mut seen_payer: HashSet<i32> = HashSet::new();
|
||
for it in &items {
|
||
if let Some(cid) = it.payer_cid
|
||
&& seen_payer.insert(cid)
|
||
&& let Ok(n) = super::political_benefits::sum_free_lover_slots(&mut conn, cid) {
|
||
free_slots_by_char.insert(cid, n);
|
||
}
|
||
}
|
||
|
||
let mut cur_group = -2i32;
|
||
let mut eligible_ix: usize = 0;
|
||
for it in &mut items {
|
||
let gk = it.payer_cid.unwrap_or(-1);
|
||
if gk != cur_group {
|
||
cur_group = gk;
|
||
eligible_ix = 0;
|
||
}
|
||
let free_n = it
|
||
.payer_cid
|
||
.and_then(|c| free_slots_by_char.get(&c).copied())
|
||
.unwrap_or(0);
|
||
let role_ok = matches!(
|
||
it.lover_role.as_str(),
|
||
"lover" | "mistress_or_favorite"
|
||
);
|
||
if role_ok && it.payer_cid.is_some() {
|
||
if eligible_ix < free_n as usize {
|
||
it.free_political_slot = true;
|
||
}
|
||
eligible_ix += 1;
|
||
}
|
||
}
|
||
|
||
let mut notify: HashSet<i32> = HashSet::new();
|
||
|
||
for it in items {
|
||
let rel_id = it.rel_id;
|
||
let maintenance_level = it.maintenance_level;
|
||
let status_fit = it.status_fit;
|
||
let base = it.base;
|
||
let title1 = it.title1.clone();
|
||
let title2 = it.title2.clone();
|
||
let g = pair_rank_group(&title1, &title2);
|
||
let rank_m = rank_cost_multiplier(g);
|
||
let maint_f = 0.6 + (maintenance_level as f64 / 100.0) * 1.2;
|
||
let sf_m = match status_fit {
|
||
-2 => 1.35,
|
||
-1 => 1.15,
|
||
_ => 1.0,
|
||
};
|
||
let cost = ((base as f64) * rank_m * maint_f * sf_m).round() as i32;
|
||
let mut installment = ((cost as f64) / 12.0 * 100.0).round() / 100.0;
|
||
if it.free_political_slot {
|
||
installment = 0.0;
|
||
}
|
||
|
||
let u1 = it.u1;
|
||
let u2 = it.u2;
|
||
let payer = u1.or(u2).filter(|x| *x > 0);
|
||
|
||
let affection = it.affection;
|
||
let visibility = it.visibility;
|
||
let discretion = it.discretion;
|
||
let mut consec = it.consec;
|
||
let mut scandal_extra = it.scandal_extra;
|
||
|
||
if let Some(uid) = payer {
|
||
if installment <= 0.0 {
|
||
conn.execute("mark_inst", &[&rel_id])?;
|
||
} else {
|
||
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);
|
||
}
|
||
conn.execute(
|
||
"upd_under",
|
||
&[
|
||
&new_aff,
|
||
&new_disc,
|
||
&new_vis,
|
||
&consec,
|
||
&scandal_extra,
|
||
&rel_id,
|
||
],
|
||
)?;
|
||
|
||
for cid in [it.c1, it.c2] {
|
||
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 [it.c1, it.c2] {
|
||
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])?;
|
||
}
|
||
|
||
push_user_id(&mut notify, u1);
|
||
push_user_id(&mut notify, u2);
|
||
}
|
||
|
||
self.publish_falukant_update_family_batch(¬ify, "lover_installment");
|
||
Ok(())
|
||
}
|
||
|
||
fn get_user_money(&mut self, user_id: i32) -> Result<f64, DbError> {
|
||
use super::sql::{QUERY_GET_CURRENT_MONEY};
|
||
let mut conn = self
|
||
.base
|
||
.pool
|
||
.get()
|
||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||
conn.prepare("gcm", QUERY_GET_CURRENT_MONEY)?;
|
||
let rows = conn.execute("gcm", &[&user_id])?;
|
||
Ok(rows
|
||
.first()
|
||
.and_then(|r| r.get("sum"))
|
||
.and_then(|v| v.parse::<f64>().ok())
|
||
.unwrap_or(0.0))
|
||
}
|
||
|
||
fn process_lover_births(&mut self) -> Result<(), DbError> {
|
||
let mut conn = self
|
||
.base
|
||
.pool
|
||
.get()
|
||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||
|
||
conn.prepare("lover_preg", QUERY_GET_LOVER_PREGNANCY_CANDIDATES)?;
|
||
conn.prepare("set_lover_preg", QUERY_SET_CHARACTER_PREGNANCY_FROM_LOVER)?;
|
||
let rows = conn.execute("lover_preg", &[])?;
|
||
|
||
conn.prepare("has_mother_birth_today", QUERY_HAS_MOTHER_BIRTH_TODAY)?;
|
||
|
||
for row in rows {
|
||
let father_cid = parse_i32(&row, "father_cid", -1);
|
||
let mother_cid = parse_i32(&row, "mother_cid", -1);
|
||
if father_cid < 0 || mother_cid < 0 {
|
||
continue;
|
||
}
|
||
let father_uid = parse_opt_i32(&row, "father_uid");
|
||
let mother_uid = parse_opt_i32(&row, "mother_uid");
|
||
if Self::mother_already_had_birth_today(&mut conn, mother_cid)? {
|
||
continue;
|
||
}
|
||
|
||
let set_rows = conn.execute("set_lover_preg", &[&mother_cid, &father_cid])?;
|
||
if set_rows.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
if let Some(uid) = father_uid {
|
||
self.publish_falukant_update_family_and_status(uid, "lover_pregnancy");
|
||
}
|
||
if let Some(uid) = mother_uid {
|
||
self.publish_falukant_update_family_and_status(uid, "lover_pregnancy");
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn mother_already_had_birth_today(
|
||
conn: &mut crate::db::DbConnection,
|
||
mother_character_id: i32,
|
||
) -> Result<bool, DbError> {
|
||
let rows = conn.execute("has_mother_birth_today", &[&mother_character_id])?;
|
||
Ok(rows
|
||
.first()
|
||
.and_then(|r| r.get("has_birth_today"))
|
||
.map(|v| v == "t" || v == "true")
|
||
.unwrap_or(false))
|
||
}
|
||
|
||
/// `falukantUpdateFamily` (mit `reason`) + `falukantUpdateStatus` — für UI-Refresh.
|
||
fn publish_falukant_update_family_and_status(&self, user_id: i32, reason: &str) {
|
||
let family = format!(
|
||
r#"{{"event":"falukantUpdateFamily","user_id":{},"reason":"{}"}}"#,
|
||
user_id, reason
|
||
);
|
||
self.base.broker.publish(family);
|
||
let status = format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
|
||
self.base.broker.publish(status);
|
||
}
|
||
|
||
fn publish_falukant_update_family_batch(&self, user_ids: &HashSet<i32>, reason: &str) {
|
||
for &uid in user_ids {
|
||
self.publish_falukant_update_family_and_status(uid, reason);
|
||
}
|
||
}
|
||
|
||
/// Fest (einmal), Geschenk (−1 Tag, +1/Tag), Haus hoch & keine Liebschaft: alle 4 Tage +1.
|
||
fn apply_marriage_positive_buffs(
|
||
sat: &mut i32,
|
||
gift_days: &mut i32,
|
||
feast: &mut i32,
|
||
nl_counter: &mut i32,
|
||
touching_empty: bool,
|
||
house_supply: i32,
|
||
) {
|
||
if *feast > 0 {
|
||
let bonus = (*feast).min(20).min(5);
|
||
*sat = (*sat + bonus).min(100);
|
||
*feast = 0;
|
||
}
|
||
if *gift_days > 0 {
|
||
*sat = (*sat + 1).min(100);
|
||
*gift_days -= 1;
|
||
}
|
||
if touching_empty && house_supply >= 65 {
|
||
*nl_counter += 1;
|
||
if *nl_counter >= 4 {
|
||
*sat = (*sat + 1).min(100);
|
||
*nl_counter = 0;
|
||
}
|
||
} else {
|
||
*nl_counter = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
struct MarriageData {
|
||
id: i32,
|
||
m1: i32,
|
||
m2: i32,
|
||
satisfaction: i32,
|
||
/// Öffentliche Ehe-Stabilität (Daemon-Drift).
|
||
public_stability: i32,
|
||
drift_high: i32,
|
||
drift_low: i32,
|
||
title1_tr: String,
|
||
title2_tr: String,
|
||
user1_id: Option<i32>,
|
||
user2_id: Option<i32>,
|
||
/// Tage mit +1 Zufriedenheit (Geschenk), runtergezählt.
|
||
gift_days: i32,
|
||
/// Einmal-Bonus (z. B. Fest), wird beim nächsten Daily verbraucht.
|
||
feast_pending: i32,
|
||
/// Hausversorgung 0–100; ab ~65: Bonus alle 4 Tage ohne Liebschaft.
|
||
house_supply: i32,
|
||
/// Tageszähler für Haus-Bonus (0..4).
|
||
no_lover_counter: i32,
|
||
household_order_1: i32,
|
||
household_order_2: i32,
|
||
servant_quality_1: i32,
|
||
servant_quality_2: i32,
|
||
}
|
||
|
||
struct LoverData {
|
||
rel_id: i32,
|
||
c1: i32,
|
||
c2: i32,
|
||
lover_role: String,
|
||
affection: i32,
|
||
visibility: i32,
|
||
discretion: i32,
|
||
maintenance_level: i32,
|
||
status_fit: i32,
|
||
scandal_extra: i32,
|
||
months_underfunded: i32,
|
||
acknowledged: bool,
|
||
title1_tr: String,
|
||
title2_tr: String,
|
||
user1_id: Option<i32>,
|
||
user2_id: Option<i32>,
|
||
/// Jüngeres Alter beider Partner (Jahre, ganzzahlig); für Altersmalus / Skandal / Ehe.
|
||
min_age_years: i32,
|
||
/// Dienerschaft: Diskretionsmodifikator je Haushalt (user_house), MAX pro User.
|
||
servant_disc_u1: i32,
|
||
servant_disc_u2: i32,
|
||
}
|
||
|
||
/// Aggregation für `household_tension_score` (0..100) pro Falukant-User.
|
||
#[derive(Clone)]
|
||
struct UserTensionAgg {
|
||
max_lover_visibility: i32,
|
||
n_acknowledged: i32,
|
||
max_months_underfunded: i32,
|
||
/// Mindest-Ehezufriedenheit aller Ehen dieses Users (100 = keine belastende Ehe).
|
||
min_marriage_sat: i32,
|
||
}
|
||
|
||
impl Default for UserTensionAgg {
|
||
fn default() -> Self {
|
||
Self {
|
||
max_lover_visibility: 0,
|
||
n_acknowledged: 0,
|
||
max_months_underfunded: 0,
|
||
min_marriage_sat: 100,
|
||
}
|
||
}
|
||
}
|
||
|
||
fn push_user_id(set: &mut HashSet<i32>, uid: Option<i32>) {
|
||
if let Some(id) = uid.filter(|x| *x > 0) {
|
||
set.insert(id);
|
||
}
|
||
}
|
||
|
||
fn parse_i32(row: &crate::db::Row, key: &str, default: i32) -> i32 {
|
||
row.get(key)
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(default)
|
||
}
|
||
|
||
fn parse_opt_i32(row: &crate::db::Row, key: &str) -> Option<i32> {
|
||
row.get(key).and_then(|v| v.parse::<i32>().ok())
|
||
}
|
||
|
||
fn parse_bool_row(row: &crate::db::Row, key: &str) -> bool {
|
||
row.get(key)
|
||
.map(|v| {
|
||
matches!(
|
||
v.as_str(),
|
||
"t" | "true" | "True" | "1" | "TRUE"
|
||
)
|
||
})
|
||
.unwrap_or(false)
|
||
}
|
||
|
||
fn clamp_i32(v: i32, lo: i32, hi: i32) -> i32 {
|
||
v.max(lo).min(hi)
|
||
}
|
||
|
||
/// Hausfrieden 0..100 (persistiert als `user_house.household_tension_score`).
|
||
fn compute_household_tension_score(
|
||
agg: &UserTensionAgg,
|
||
lover_children: i32,
|
||
household_order: i32,
|
||
servant_quality: i32,
|
||
servant_pay: &str,
|
||
) -> i32 {
|
||
let mut s: f64 = 0.0;
|
||
s += agg.max_lover_visibility as f64 * 0.35;
|
||
s += agg.n_acknowledged as f64 * 12.0;
|
||
s += agg.max_months_underfunded as f64 * 8.0;
|
||
s += lover_children as f64 * 10.0;
|
||
s += (100 - household_order.clamp(0, 100)) as f64 * 0.22;
|
||
if servant_pay == "low" {
|
||
s += 6.0;
|
||
}
|
||
s += (50 - servant_quality.min(50)).max(0) as f64 * 0.35;
|
||
if agg.min_marriage_sat < 100 {
|
||
if agg.min_marriage_sat < 40 {
|
||
s += 18.0;
|
||
} else if agg.min_marriage_sat < 55 {
|
||
s += 8.0;
|
||
}
|
||
}
|
||
s = s.clamp(0.0, 100.0);
|
||
s.round() as i32
|
||
}
|
||
|
||
fn title_group(tr: &str) -> u8 {
|
||
match tr {
|
||
"noncivil" | "civil" | "sir" => 0,
|
||
"townlord" | "by" | "landlord" => 1,
|
||
"knight" | "baron" | "count" | "palsgrave" | "margrave" | "landgrave" => 2,
|
||
"ruler" | "elector"
|
||
| "imperial-prince"
|
||
| "duke"
|
||
| "grand-duke"
|
||
| "prince-regent"
|
||
| "king" => 3,
|
||
_ => 0,
|
||
}
|
||
}
|
||
|
||
fn marriage_rank_group(t1: &str, t2: &str) -> u8 {
|
||
title_group(t1).max(title_group(t2))
|
||
}
|
||
|
||
fn pair_rank_group(t1: &str, t2: &str) -> u8 {
|
||
title_group(t1).max(title_group(t2))
|
||
}
|
||
|
||
fn rank_cost_multiplier(g: u8) -> f64 {
|
||
match g {
|
||
0 => 1.0,
|
||
1 => 1.6,
|
||
2 => 2.6,
|
||
3 => 4.0,
|
||
_ => 1.0,
|
||
}
|
||
}
|
||
|
||
/// Gruppe 3: 0,7 nur bei „geordneter“ Liebschaft; sonst 1,0; Skandal 1,5.
|
||
fn rank_rep_modifier(g: u8, scandal: bool, order_ok: bool) -> f64 {
|
||
if g == 3 && scandal {
|
||
return 1.5;
|
||
}
|
||
match g {
|
||
0 => 1.8,
|
||
1 => 1.3,
|
||
2 => 1.0,
|
||
3 => if order_ok { 0.7 } else { 1.0 },
|
||
_ => 1.0,
|
||
}
|
||
}
|
||
|
||
/// Spec 5a: Altersmalus (jüngeres Alter der Beteiligten), zusätzlich zur Basis-Reputation.
|
||
fn age_reputation_delta(min_age_years: i32) -> f64 {
|
||
if min_age_years <= 13 {
|
||
-1.5
|
||
} else if min_age_years <= 15 {
|
||
-0.8
|
||
} else if min_age_years <= 17 {
|
||
-0.3
|
||
} else {
|
||
0.0
|
||
}
|
||
}
|
||
|
||
/// Spec 5a: minAge <= 15 und hohe Sichtbarkeit.
|
||
fn visibility_young_penalty(min_age_years: i32, visibility: i32) -> f64 {
|
||
if min_age_years <= 15 && visibility >= 50 {
|
||
-0.5
|
||
} else {
|
||
0.0
|
||
}
|
||
}
|
||
|
||
/// Bewertet die Standsgruppe basierend auf Titeln der beiden Liebhaber.
|
||
/// Rückgabe: 0 = niedrig (noncivil/civil), 1 = mittel (townlord/etc.), 2+ = hoch (knight/etc.).
|
||
fn lover_status_category(title1: &str, title2: &str) -> u8 {
|
||
let t1 = title_group(title1);
|
||
let t2 = title_group(title2);
|
||
t1.max(t2) // 0 = niedrig, 1 = mittel, 2 = hoch, 3 = höchst
|
||
}
|
||
|
||
/// Berechnet tägl. Reputations-Delta basierend auf Stand, Sichtbarkeit und Status der Liebschaft.
|
||
/// Regel:
|
||
/// - Stand 3 (Höchst): +0.75/Tag (sehr positiv)
|
||
/// - Stand 2 (Hoch): +0.5/Tag (positiv)
|
||
/// - Stand 1 (Mittel): +0.3 wenn heimlich, -0.4 wenn öffentlich
|
||
/// - Stand 0 (Niedrig): 0 wenn heimlich, -0.5 wenn öffentlich
|
||
fn calculate_lover_reputation_delta(l: &LoverData) -> f64 {
|
||
let category = lover_status_category(&l.title1_tr, &l.title2_tr);
|
||
let is_public = l.visibility >= 60; // öffentlich wenn hohe Sichtbarkeit
|
||
|
||
match category {
|
||
3 => 0.75, // Höchst: sehr positiv
|
||
2 => 0.5, // Hoch: positiv
|
||
1 => {
|
||
// Mittel: heimlich +0,3, öffentlich -0,4
|
||
if is_public {
|
||
-0.4
|
||
} else {
|
||
0.3
|
||
}
|
||
}
|
||
_ => {
|
||
// Niedrig: heimlich/diskret 0, öffentlich -0,5
|
||
if is_public {
|
||
-0.5
|
||
} else {
|
||
0.0
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Berechnet tägl. Ehezufriedenheits-Delta für Ehe-Partner, wenn dieser eine Liebschaft hat.
|
||
/// Regel:
|
||
/// - Stand 3 (Höchst): +2/Tag (sehr gesellschaftlich akzeptiert)
|
||
/// - Stand 2 (Hoch): +1/Tag (gesellschaftlich akzeptiert)
|
||
/// - Stand 1 (Mittel): 0 wenn heimlich, -2 wenn öffentlich
|
||
/// - Stand 0 (Niedrig): 0 wenn heimlich, -1 wenn öffentlich
|
||
fn calculate_lover_marriage_satisfaction_delta(l: &LoverData) -> i32 {
|
||
let category = lover_status_category(&l.title1_tr, &l.title2_tr);
|
||
let is_public = l.visibility >= 60;
|
||
|
||
match category {
|
||
3 => 2, // Höchst: +2 pro Tag (sehr positiv)
|
||
2 => 1, // Hoch: +1 pro Tag
|
||
1 => {
|
||
// Mittel: +0 wenn heimlich, -2 wenn öffentlich (Skandal)
|
||
if is_public {
|
||
-2
|
||
} else {
|
||
0
|
||
}
|
||
}
|
||
_ => {
|
||
// Niedrig: 0 wenn heimlich, -1 wenn öffentlich
|
||
if is_public {
|
||
-1
|
||
} else {
|
||
0
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn breakup_risk_percent(l: &LoverData) -> f64 {
|
||
if l.affection >= 45 {
|
||
return 0.0;
|
||
}
|
||
|
||
let affection_risk = (45 - l.affection).max(0) as f64 * 0.5;
|
||
let underfund_risk = (l.months_underfunded.max(0) as f64 * 1.5).min(10.0);
|
||
let visibility_risk = if l.visibility >= 70 { 2.0 } else { 0.0 };
|
||
let discretion_risk = if l.discretion <= 30 { 3.0 } else { 0.0 };
|
||
|
||
(affection_risk + underfund_risk + visibility_risk + discretion_risk).clamp(0.0, 30.0)
|
||
}
|
||
|
||
/// Spec Skandalrisiko: stufenweise Zusatz (exklusiv).
|
||
fn scandal_age_extra_pct(min_age_years: i32) -> f64 {
|
||
if min_age_years <= 13 {
|
||
6.0
|
||
} else if min_age_years <= 15 {
|
||
3.0
|
||
} else if min_age_years <= 17 {
|
||
1.0
|
||
} else {
|
||
0.0
|
||
}
|
||
}
|
||
|
||
fn lover_marriage_daily_delta(
|
||
mg: u8,
|
||
l: &LoverData,
|
||
m_sat: i32,
|
||
mistress_n: usize,
|
||
total_lovers: usize,
|
||
) -> i32 {
|
||
let role = l.lover_role.as_str();
|
||
match mg {
|
||
0 => match role {
|
||
"secret_affair" => -1,
|
||
"lover" => -2,
|
||
"mistress_or_favorite" => -3,
|
||
_ => -2,
|
||
},
|
||
1 => match role {
|
||
"secret_affair" => -1,
|
||
"lover" => -1,
|
||
"mistress_or_favorite" => -2,
|
||
_ => -1,
|
||
},
|
||
2 => match role {
|
||
"secret_affair" => -1,
|
||
"lover" => -1,
|
||
"mistress_or_favorite" => -1,
|
||
_ => -1,
|
||
},
|
||
3 => {
|
||
if role == "mistress_or_favorite" {
|
||
if l.visibility <= 35
|
||
&& l.maintenance_level >= 65
|
||
&& m_sat >= 45
|
||
&& mistress_n <= 1
|
||
&& total_lovers <= 1
|
||
{
|
||
return 1;
|
||
}
|
||
if l.visibility > 50 || l.maintenance_level < 40 {
|
||
return -1;
|
||
}
|
||
return 0;
|
||
}
|
||
if role == "secret_affair" {
|
||
return 0;
|
||
}
|
||
if role == "lover" {
|
||
return 0;
|
||
}
|
||
0
|
||
}
|
||
_ => -1,
|
||
}
|
||
}
|
||
|
||
fn mistress_count_for_pair(lovers: &[LoverData], c1: i32, c2: i32) -> usize {
|
||
lovers
|
||
.iter()
|
||
.filter(|l| {
|
||
(l.c1 == c1 || l.c2 == c1 || l.c1 == c2 || l.c2 == c2)
|
||
&& l.lover_role == "mistress_or_favorite"
|
||
})
|
||
.count()
|
||
}
|
||
|
||
fn fetch_reputation(conn: &mut crate::db::DbConnection, cid: i32) -> Result<f64, DbError> {
|
||
conn.prepare("fr", "SELECT COALESCE(reputation, 50)::float8 AS r FROM falukant_data.character WHERE id = $1")?;
|
||
let rows = conn.execute("fr", &[&cid])?;
|
||
Ok(rows
|
||
.first()
|
||
.and_then(|r| r.get("r"))
|
||
.and_then(|v| v.parse::<f64>().ok())
|
||
.unwrap_or(50.0))
|
||
}
|
||
|
||
fn marriage_id_for_character(
|
||
conn: &mut crate::db::DbConnection,
|
||
cid: i32,
|
||
) -> Result<Option<i32>, DbError> {
|
||
conn.prepare(
|
||
"mid",
|
||
r#"SELECT r.id FROM falukant_data.relationship r
|
||
JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id
|
||
AND rt.tr IN ('married', 'engaged', 'wooing')
|
||
WHERE r.character1_id = $1 OR r.character2_id = $1 LIMIT 1"#,
|
||
)?;
|
||
let rows = conn.execute("mid", &[&cid])?;
|
||
Ok(rows
|
||
.first()
|
||
.and_then(|r| r.get("id"))
|
||
.and_then(|v| v.parse::<i32>().ok()))
|
||
}
|
||
|
||
impl Worker for FalukantFamilyWorker {
|
||
fn start_worker_thread(&mut self) {
|
||
let pool = self.base.pool.clone();
|
||
let broker = self.base.broker.clone();
|
||
|
||
self.base
|
||
.start_worker_with_loop(move |state: Arc<WorkerState>| {
|
||
let mut worker = FalukantFamilyWorker::new(pool.clone(), broker.clone());
|
||
while state.running_worker.load(Ordering::Relaxed) {
|
||
worker.run_iteration(&state);
|
||
}
|
||
});
|
||
}
|
||
|
||
fn stop_worker_thread(&mut self) {
|
||
self.base.stop_worker();
|
||
}
|
||
|
||
fn enable_watchdog(&mut self) {
|
||
self.base.start_watchdog();
|
||
}
|
||
}
|