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

428 lines
12 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.
//! 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::<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
}
}
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
}