Enhance Falukant family and production dynamics: Updated FalukantFamilyWorker to include public stability and household tension calculations, integrating new SQL queries for managing marriage states and household attributes. Added FalukantCertificateWorker for production certificate management, enhancing overall family interaction and production tracking.

This commit is contained in:
Torsten Schulz (local)
2026-03-23 09:02:51 +01:00
parent d921dc2f7e
commit fe0361971d
10 changed files with 997 additions and 12 deletions

View File

@@ -1,6 +1,7 @@
//! 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`).
//! optional `004` + Backend-Stammdaten für Dienerschaft (`falukant_servants`),
//! `005` Ehe öffentliche Stabilität + Hausfrieden (`household_tension_score`).
//!
//! WebSocket: `falukantUpdateFamily` (reason) + `falukantUpdateStatus` für betroffene Nutzer.
@@ -15,14 +16,16 @@ use rand::SeedableRng;
use super::base::{BaseWorker, Worker, WorkerState};
use super::sql::{
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_INSERT_CHILD, QUERY_INSERT_CHILD_RELATION_LOVER,
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_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;
@@ -137,6 +140,7 @@ impl FalukantFamilyWorker {
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(),
@@ -147,6 +151,10 @@ impl FalukantFamilyWorker {
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)
@@ -166,6 +174,8 @@ impl FalukantFamilyWorker {
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"),
@@ -267,6 +277,7 @@ impl FalukantFamilyWorker {
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;
@@ -346,12 +357,58 @@ impl FalukantFamilyWorker {
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, &m.id],
&[&sat, &dh, &dl, &gift_days, &feast, &nl_counter, &pst, &m.id],
)?;
if sat != sat0
|| pst != pst0
|| dh != dh0
|| dl != dl0
|| gift_days != g0
@@ -363,6 +420,71 @@ impl FalukantFamilyWorker {
}
}
// 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);
@@ -497,6 +619,7 @@ impl FalukantFamilyWorker {
push_user_id(&mut notify, char_user.get(cid).copied().flatten());
}
notify.extend(marriage_socket_users);
notify.extend(tension_socket_users);
self.publish_falukant_update_family_batch(&notify, "daily");
Ok(())
@@ -766,6 +889,8 @@ struct MarriageData {
m1: i32,
m2: i32,
satisfaction: i32,
/// Öffentliche Ehe-Stabilität (Daemon-Drift).
public_stability: i32,
drift_high: i32,
drift_low: i32,
title1_tr: String,
@@ -780,6 +905,10 @@ struct MarriageData {
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 {
@@ -793,6 +922,8 @@ struct LoverData {
maintenance_level: i32,
status_fit: i32,
scandal_extra: i32,
months_underfunded: i32,
acknowledged: bool,
title1_tr: String,
title2_tr: String,
user1_id: Option<i32>,
@@ -804,6 +935,27 @@ struct LoverData {
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);
@@ -820,10 +972,50 @@ 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,