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:
438
src/worker/falukant_certificate.rs
Normal file
438
src/worker/falukant_certificate.rs
Normal file
@@ -0,0 +1,438 @@
|
||||
//! Produktionszertifikat: tägliche Neuberechnung von `falukant_user.certificate`.
|
||||
//! Spec: docs/FALUKANT_PRODUCTION_CERTIFICATE.md
|
||||
|
||||
use crate::db::{ConnectionPool, DbError, Row};
|
||||
use crate::message_broker::MessageBroker;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::base::{BaseWorker, Worker, WorkerState};
|
||||
use crate::worker::sql::{
|
||||
QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS, QUERY_UPDATE_FALUKANT_USER_CERTIFICATE,
|
||||
};
|
||||
|
||||
const DAILY_INTERVAL: Duration = Duration::from_secs(24 * 3600);
|
||||
|
||||
/// Wenn `money` darunter liegt, gilt der Spieler als bankrott → Zertifikat auf Stufe 1.
|
||||
const BANKRUPTCY_MONEY_THRESHOLD: f64 = -5000.0;
|
||||
|
||||
pub struct FalukantCertificateWorker {
|
||||
base: BaseWorker,
|
||||
}
|
||||
|
||||
impl FalukantCertificateWorker {
|
||||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||||
Self {
|
||||
base: BaseWorker::new("FalukantCertificateWorker", pool, broker),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
|
||||
let mut last: Option<std::time::Instant> = None;
|
||||
while state.running_worker.load(Ordering::Relaxed) {
|
||||
let now = std::time::Instant::now();
|
||||
let run = match last {
|
||||
None => true,
|
||||
Some(t) => now.saturating_duration_since(t) >= DAILY_INTERVAL,
|
||||
};
|
||||
if run {
|
||||
if let Err(e) = Self::process_daily(&pool, &broker) {
|
||||
eprintln!("[FalukantCertificateWorker] process_daily: {e}");
|
||||
}
|
||||
last = Some(now);
|
||||
}
|
||||
for _ in 0..60 {
|
||||
if !state.running_worker.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_daily(pool: &ConnectionPool, broker: &MessageBroker) -> Result<(), DbError> {
|
||||
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 current = parse_i32(&row, "certificate", 1).clamp(1, 127);
|
||||
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 erlaubt (Spec)
|
||||
if money <= BANKRUPTCY_MONEY_THRESHOLD && current > 1 {
|
||||
conn.execute("cert_upd", &[&1_i32, &fu_id])?;
|
||||
publish_certificate_event(broker, fu_id, current, 1);
|
||||
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.35
|
||||
+ production_points as f64 * 0.20
|
||||
+ office_points as f64 * 0.15
|
||||
+ nobility_points as f64 * 0.10
|
||||
+ reputation_points as f64 * 0.10
|
||||
+ house_points as f64 * 0.10;
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
let new_certificate = if effective_target > current {
|
||||
(current + 1).min(effective_target)
|
||||
} else {
|
||||
current
|
||||
};
|
||||
|
||||
if new_certificate != current {
|
||||
conn.execute("cert_upd", &[&new_certificate, &fu_id])?;
|
||||
publish_certificate_event(broker, fu_id, current, new_certificate);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn publish_certificate_event(broker: &MessageBroker, user_id: i32, old_c: i32, new_c: i32) {
|
||||
let msg = format!(
|
||||
r#"{{"event":"falukantUpdateProductionCertificate","user_id":{},"reason":"daily_recalculation","old_certificate":{},"new_certificate":{}}}"#,
|
||||
user_id, 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::<i32>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn parse_i64(row: &Row, key: &str, default: i64) -> i64 {
|
||||
row.get(key)
|
||||
.and_then(|v| v.parse::<i64>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn parse_f64(row: &Row, key: &str, default: f64) -> f64 {
|
||||
row.get(key)
|
||||
.and_then(|v| v.parse::<f64>().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
|
||||
}
|
||||
}
|
||||
|
||||
/// Kirchliche Ämter: `hierarchy_level` auf 0–5 begrenzen (Daemon).
|
||||
fn church_rank_from_hierarchy(h: i32) -> i32 {
|
||||
if h <= 0 {
|
||||
0
|
||||
} else {
|
||||
h.min(5)
|
||||
}
|
||||
}
|
||||
|
||||
/// Politische Amtsnamen → Rang 1–5 (konfigurierbar im Daemon).
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_target_from_score(score: f64) -> i32 {
|
||||
if score >= 4.0 {
|
||||
5
|
||||
} else if score >= 3.0 {
|
||||
4
|
||||
} else if score >= 2.1 {
|
||||
3
|
||||
} else if score >= 1.2 {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn status_one(
|
||||
office_points: i32,
|
||||
nobility_points: i32,
|
||||
reputation_points: i32,
|
||||
house_points: i32,
|
||||
) -> bool {
|
||||
office_points >= 1
|
||||
|| nobility_points >= 1
|
||||
|| reputation_points >= 2
|
||||
|| house_points >= 1
|
||||
}
|
||||
|
||||
fn status_count_cert4(
|
||||
office_points: i32,
|
||||
nobility_points: i32,
|
||||
reputation_points: i32,
|
||||
house_points: i32,
|
||||
) -> i32 {
|
||||
let mut c = 0;
|
||||
if office_points >= 1 {
|
||||
c += 1;
|
||||
}
|
||||
if nobility_points >= 1 {
|
||||
c += 1;
|
||||
}
|
||||
if reputation_points >= 2 {
|
||||
c += 1;
|
||||
}
|
||||
if house_points >= 1 {
|
||||
c += 1;
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
fn cert5_extra_two(
|
||||
office_points: i32,
|
||||
nobility_points: i32,
|
||||
house_points: i32,
|
||||
) -> i32 {
|
||||
let mut c = 0;
|
||||
if office_points >= 2 {
|
||||
c += 1;
|
||||
}
|
||||
if nobility_points >= 2 {
|
||||
c += 1;
|
||||
}
|
||||
if house_points >= 2 {
|
||||
c += 1;
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
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 >= 25.0 && completed >= 5,
|
||||
3 => {
|
||||
avg_knowledge >= 40.0
|
||||
&& completed >= 20
|
||||
&& status_one(
|
||||
office_points,
|
||||
nobility_points,
|
||||
reputation_points,
|
||||
house_points,
|
||||
)
|
||||
}
|
||||
4 => {
|
||||
avg_knowledge >= 55.0
|
||||
&& completed >= 60
|
||||
&& status_count_cert4(
|
||||
office_points,
|
||||
nobility_points,
|
||||
reputation_points,
|
||||
house_points,
|
||||
) >= 2
|
||||
}
|
||||
5 => {
|
||||
avg_knowledge >= 70.0
|
||||
&& completed >= 150
|
||||
&& reputation_points >= 3
|
||||
&& cert5_extra_two(office_points, nobility_points, house_points) >= 2
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Höchste Stufe ≤ `raw_target`, die alle Mindestanforderungen erfüllt.
|
||||
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
|
||||
}
|
||||
|
||||
impl Worker for FalukantCertificateWorker {
|
||||
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>| {
|
||||
FalukantCertificateWorker::run_loop(pool.clone(), broker.clone(), state);
|
||||
});
|
||||
}
|
||||
|
||||
fn stop_worker_thread(&mut self) {
|
||||
self.base.stop_worker();
|
||||
}
|
||||
|
||||
fn enable_watchdog(&mut self) {
|
||||
self.base.start_watchdog();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user