//! Produktionszertifikat: regelmäßige Neuberechnung von `falukant_user.certificate` im **FalukantFamilyWorker** //! (Standard: **1× pro Minute**, `CERTIFICATE_RECALC_INTERVAL` in `falukant_family.rs`; unabhängig von der Familien-Schema-Migration). //! Spec: `docs/FALUKANT_PRODUCTION_CERTIFICATE.md` und //! „Falukant: Produktionszertifikate – Fach- und Integrationsspezifikation“. use crate::db::{DbError, Row}; use crate::message_broker::MessageBroker; use super::base::BaseWorker; use crate::worker::sql::{ QUERY_GET_FALUKANT_USER_CERT_AND_EVENT, QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS, QUERY_UPDATE_FALUKANT_USER_CERTIFICATE, }; /// Wenn `money` darunter liegt, gilt der Spieler als bankrott → Zertifikat auf Stufe 1 (Spec §4.7). const BANKRUPTCY_MONEY_THRESHOLD: f64 = -5000.0; /// Periodischer Tick (`FalukantFamilyWorker::run_iteration`, Standard: 1×/Minute). pub fn run_daily(base: &BaseWorker, broker: &MessageBroker) -> Result<(), DbError> { let pool = &base.pool; let mut conn = pool .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; conn.prepare("cert_rows", QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS)?; conn.prepare("cert_upd", QUERY_UPDATE_FALUKANT_USER_CERTIFICATE)?; let rows = conn.execute("cert_rows", &[])?; for row in rows { let fu_id = parse_i32(&row, "falukant_user_id", -1); if fu_id < 0 { continue; } let app_uid = parse_i32(&row, "app_user_id", -1); let event_uid = if app_uid > 0 { app_uid } else { fu_id }; let current = parse_i32(&row, "certificate", 1).clamp(1, 5); let money = parse_f64(&row, "money", 0.0); let avg_knowledge = parse_f64(&row, "avg_knowledge", 0.0); let completed = parse_i64(&row, "completed_production_count", 0); let max_church_hierarchy = parse_i32(&row, "max_church_hierarchy", 0); let pol_names = row .get("political_office_names") .cloned() .unwrap_or_default(); let reputation = parse_f64(&row, "reputation", 50.0); let title_level = parse_i32(&row, "title_level", 0); let house_position = parse_i32(&row, "house_position", 0); // Bankrott: Herabsetzung (Spec §4.7) if money <= BANKRUPTCY_MONEY_THRESHOLD && current > 1 { conn.execute("cert_upd", &[&1_i32, &fu_id])?; publish_certificate_event(broker, event_uid, current, 1, "bankruptcy"); continue; } let knowledge_points = knowledge_points_from_avg(avg_knowledge); let production_points = production_points_from_count(completed); let political_rank = max_political_rank_from_names(&pol_names); let church_rank = church_rank_from_hierarchy(max_church_hierarchy); let highest_office_rank = political_rank.max(church_rank).min(5); let office_points = highest_office_rank.min(5); let nobility_points = (title_level - 1).clamp(0, 5); let reputation_points = reputation_points_from_rep(reputation); let house_points = house_points_from_position(house_position); let certificate_score = knowledge_points as f64 * 0.45 + production_points as f64 * 0.30 + office_points as f64 * 0.08 + nobility_points as f64 * 0.05 + reputation_points as f64 * 0.07 + house_points as f64 * 0.05; let raw_target = raw_target_from_score(certificate_score); let effective_target = effective_certificate_target( raw_target, avg_knowledge, completed, office_points, nobility_points, reputation_points, house_points, ); // Hilfe bei „UI sagt bereit, Stufe steigt nicht“: oft Mindestwerte erfüllt, aber // `raw_target` (gewichtete Wertung) zu niedrig — siehe `docs/FALUKANT_PRODUCTION_CERTIFICATE.md`. if current < 5 { let next = current + 1; if meets_min_for_level( next, avg_knowledge, completed, office_points, nobility_points, reputation_points, house_points, ) && effective_target < next && cert_verbose() { eprintln!( "[falukant_certificate] fu_id={} bleibt auf Stufe {}: Mindestanforderungen für Stufe {} sind erfüllt, aber die gewichtete Wertung {:.3} ergibt raw_target={} (effective_target={}). avg_knowledge={:.2} completed={} kp={} pp={} office={} nob={} rep_p={} house_p={}", fu_id, current, next, certificate_score, raw_target, effective_target, avg_knowledge, completed, knowledge_points, production_points, office_points, nobility_points, reputation_points, house_points, ); } } let new_certificate = if effective_target > current { (current + 1).min(effective_target).min(5) } else { current }; if new_certificate != current { conn.execute("cert_upd", &[&new_certificate, &fu_id])?; publish_certificate_event( broker, event_uid, current, new_certificate, "daily_recalculation", ); } } Ok(()) } fn cert_verbose() -> bool { matches!( std::env::var("YPDAEMON_CERT_VERBOSE").map(|v| v == "1" || v.eq_ignore_ascii_case("true")), Ok(true) ) } /// Nach Tod ohne gültigen Erben: Zertifikat auf Stufe 1 (Spec: Erbfolge bricht ab). pub fn reset_certificate_on_succession_no_heir( base: &BaseWorker, broker: &MessageBroker, falukant_user_id: i32, ) -> Result<(), DbError> { if falukant_user_id < 1 { return Ok(()); } let pool = &base.pool; let mut conn = pool .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; conn.prepare("cert_get_ev", QUERY_GET_FALUKANT_USER_CERT_AND_EVENT)?; let rows = conn.execute("cert_get_ev", &[&falukant_user_id])?; let row = match rows.first() { Some(r) => r, None => return Ok(()), }; let current = parse_i32(row, "certificate", 1).clamp(1, 5); let event_uid = parse_i32(row, "event_user_id", falukant_user_id); if current <= 1 { return Ok(()); } conn.prepare("cert_upd", QUERY_UPDATE_FALUKANT_USER_CERTIFICATE)?; conn.execute("cert_upd", &[&1_i32, &falukant_user_id])?; publish_certificate_event(broker, event_uid, current, 1, "succession_no_heir"); Ok(()) } fn publish_certificate_event( broker: &MessageBroker, user_id: i32, old_c: i32, new_c: i32, reason: &str, ) { let msg = format!( r#"{{"event":"falukantUpdateProductionCertificate","user_id":{},"reason":"{}","old_certificate":{},"new_certificate":{}}}"#, user_id, reason, old_c, new_c ); broker.publish(msg); let status = format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id); broker.publish(status); } fn parse_i32(row: &Row, key: &str, default: i32) -> i32 { row.get(key) .and_then(|v| v.parse::().ok()) .unwrap_or(default) } fn parse_i64(row: &Row, key: &str, default: i64) -> i64 { row.get(key) .and_then(|v| v.parse::().ok()) .unwrap_or(default) } fn parse_f64(row: &Row, key: &str, default: f64) -> f64 { row.get(key) .and_then(|v| v.parse::().ok()) .unwrap_or(default) } fn knowledge_points_from_avg(avg: f64) -> i32 { if avg >= 80.0 { 5 } else if avg >= 65.0 { 4 } else if avg >= 50.0 { 3 } else if avg >= 35.0 { 2 } else if avg >= 20.0 { 1 } else { 0 } } fn production_points_from_count(n: i64) -> i32 { if n >= 200 { 5 } else if n >= 100 { 4 } else if n >= 50 { 3 } else if n >= 20 { 2 } else if n >= 5 { 1 } else { 0 } } fn church_rank_from_hierarchy(h: i32) -> i32 { if h <= 0 { 0 } else { h.min(5) } } /// Politische Amtsnamen → Rang 1–5 (Heuristik im Daemon, ohne DB-Änderung). fn political_name_to_rank(name: &str) -> i32 { let n = name.to_lowercase(); if n.contains("reich") || n.contains("könig") || n.contains("konig") || n.contains("king") || n.contains("kanzler") || n.contains("chancellor") { return 5; } if n.contains("minister") || n.contains("ministerpräsident") || n.contains("herzog") || n.contains("duke") || n.contains("landeshaupt") { return 4; } if n.contains("regierungs") || n.contains("oberbürger") || n.contains("oberbuerg") || n.contains("präsident") || n.contains("president") { return 3; } if n.contains("bürgermeister") || n.contains("buergermeister") || n.contains("mayor") || n.contains("landrat") || n.contains("regional") { return 2; } if !n.is_empty() { return 1; } 0 } /// Rang 1–5 für eine Amtsbezeichnung (Politik-Tagesgehalt, Zertifikat-Heuristik). Leer → 0. pub fn political_office_name_rank(name: &str) -> i32 { political_name_to_rank(name) } fn max_political_rank_from_names(agg: &str) -> i32 { if agg.is_empty() { return 0; } agg.split('|') .map(|s| political_name_to_rank(s.trim())) .max() .unwrap_or(0) } fn reputation_points_from_rep(rep: f64) -> i32 { if rep >= 90.0 { 5 } else if rep >= 75.0 { 4 } else if rep >= 60.0 { 3 } else if rep >= 40.0 { 2 } else if rep >= 20.0 { 1 } else { 0 } } fn house_points_from_position(position: i32) -> i32 { if position >= 10 { 5 } else if position >= 8 { 4 } else if position >= 6 { 3 } else if position >= 4 { 2 } else if position >= 2 { 1 } else { 0 } } /// Spec §4.6 Schwellen für die Zielstufe (vor Mindestanforderungen). fn raw_target_from_score(score: f64) -> i32 { if score >= 3.8 { 5 } else if score >= 2.8 { 4 } else if score >= 1.8 { 3 } else if score >= 0.9 { 2 } else { 1 } } fn cert5_two_of_social(office_points: i32, nobility_points: i32, house_points: i32) -> bool { let mut c = 0; if office_points >= 2 { c += 1; } if nobility_points >= 1 { c += 1; } if house_points >= 2 { c += 1; } c >= 2 } /// Mindestanforderungen je Stufe (Spec §4.5). fn meets_min_for_level( level: i32, avg_knowledge: f64, completed: i64, office_points: i32, nobility_points: i32, reputation_points: i32, house_points: i32, ) -> bool { match level { 1 => true, 2 => avg_knowledge >= 15.0 && completed >= 4, 3 => avg_knowledge >= 28.0 && completed >= 15, 4 => { avg_knowledge >= 45.0 && completed >= 45 && (office_points >= 1 || nobility_points >= 1 || reputation_points >= 2 || house_points >= 2) } 5 => { avg_knowledge >= 60.0 && completed >= 110 && reputation_points >= 2 && cert5_two_of_social(office_points, nobility_points, house_points) } _ => false, } } /// Höchste Stufe ≤ `raw_target`, die alle Mindestanforderungen erfüllt (Spec §4.6). fn effective_certificate_target( raw_target: i32, avg_knowledge: f64, completed: i64, office_points: i32, nobility_points: i32, reputation_points: i32, house_points: i32, ) -> i32 { let cap = raw_target.clamp(1, 5); for lvl in (1..=cap).rev() { if meets_min_for_level( lvl, avg_knowledge, completed, office_points, nobility_points, reputation_points, house_points, ) { return lvl; } } 1 }