Files
yourpart-daemon/src/worker/falukant_family.rs

1001 lines
34 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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.
//!
//! 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_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_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,
};
use crate::db::{ConnectionPool, DbError};
use crate::message_broker::MessageBroker;
const DAILY_INTERVAL: Duration = Duration::from_secs(24 * 3600);
const MONTHLY_INTERVAL: Duration = Duration::from_secs(30 * 24 * 3600);
pub struct FalukantFamilyWorker {
base: BaseWorker,
rng: StdRng,
dist: Uniform<f64>,
last_daily: Option<Instant>,
last_monthly: Option<Instant>,
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,
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 {
if let Ok(true) = self.check_schema() {
self.schema_ready = true;
} else {
std::thread::sleep(Duration::from_secs(1));
return;
}
}
if Self::should_run(self.last_daily, now, DAILY_INTERVAL) {
if 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);
}
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", &[])?;
Ok(rows
.first()
.and_then(|r| r.get("ready"))
.map(|v| v == "true" || v == "t")
.unwrap_or(false))
}
fn process_daily(&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", 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()
.filter_map(|r| {
Some(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),
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),
})
})
.filter(|m| m.id > 0)
.collect();
let mut lovers: Vec<LoverData> = lover_rows
.into_iter()
.filter_map(|r| {
Some(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),
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),
})
})
.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)?;
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 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;
}
conn.prepare("mark_daily", QUERY_MARK_LOVER_DAILY_DONE)?;
for l in &lovers {
conn.execute("mark_daily", &[&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 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;
}
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;
conn.execute(
"upd_marriage_full",
&[&sat, &dh, &dl, &gift_days, &feast, &nl_counter, &m.id],
)?;
if sat != sat0
|| 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);
}
}
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 {
let _ = 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);
self.publish_falukant_update_family_batch(&notify, "daily");
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("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)?;
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 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 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 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 affection = parse_i32(&r, "affection", 50);
let visibility = parse_i32(&r, "visibility", 0);
let discretion = parse_i32(&r, "discretion", 50);
let mut consec = parse_i32(&r, "months_underfunded", 0);
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])?;
} 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;
}
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_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(())
}
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)?;
let rows = conn.execute("lover_preg", &[])?;
conn.prepare("insert_child", QUERY_INSERT_CHILD)?;
conn.prepare("insert_child_rel_lover", QUERY_INSERT_CHILD_RELATION_LOVER)?;
conn.prepare("pen_mar", QUERY_LOVER_BIRTH_PENALTY_MARRIAGE)?;
conn.prepare("pen_rep", QUERY_LOVER_BIRTH_PENALTY_REPUTATION)?;
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 title_of_nobility = parse_i32(&row, "title_of_nobility", 0);
let last_name = parse_i32(&row, "last_name", 0);
let region_id = parse_i32(&row, "region_id", 0);
let father_uid = parse_opt_i32(&row, "father_uid");
let mother_uid = parse_opt_i32(&row, "mother_uid");
let gender = if self.dist.sample(&mut self.rng) < 0.5 {
"male"
} else {
"female"
};
let inserted =
conn.execute("insert_child", &[&region_id, &gender, &last_name, &title_of_nobility])?;
let child_cid = inserted
.first()
.and_then(|r| r.get("child_cid"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(-1);
if child_cid < 0 {
continue;
}
conn.execute(
"insert_child_rel_lover",
&[&father_cid, &mother_cid, &child_cid],
)?;
conn.execute("pen_mar", &[&father_cid])?;
conn.execute("pen_mar", &[&mother_cid])?;
conn.execute("pen_rep", &[&father_cid])?;
conn.execute("pen_rep", &[&mother_cid])?;
if let Some(uid) = father_uid {
self.publish_children(uid);
}
if let Some(uid) = mother_uid {
self.publish_children(uid);
}
}
Ok(())
}
fn publish_children(&self, user_id: i32) {
let children_update =
format!(r#"{{"event":"children_update","user_id":{}}}"#, user_id);
self.base.broker.publish(children_update);
self.publish_falukant_update_family_and_status(user_id, "lover_birth");
}
/// `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,
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 0100; ab ~65: Bonus alle 4 Tage ohne Liebschaft.
house_supply: i32,
/// Tageszähler für Haus-Bonus (0..4).
no_lover_counter: 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,
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,
}
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 clamp_i32(v: i32, lo: i32, hi: i32) -> i32 {
v.max(lo).min(hi)
}
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
}
}
/// 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();
}
}