All checks were successful
Deploy yourpart (blue-green) / deploy (push) Successful in 2m56s
428 lines
12 KiB
Rust
428 lines
12 KiB
Rust
//! 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
|
||
}
|