diff --git a/docs/FALUKANT_DIRECTOR_PRODUCTION_COST.md b/docs/FALUKANT_DIRECTOR_PRODUCTION_COST.md new file mode 100644 index 0000000..7d27d6c --- /dev/null +++ b/docs/FALUKANT_DIRECTOR_PRODUCTION_COST.md @@ -0,0 +1,40 @@ +# Director: Stückkosten Produktion (Balancing) + +## Problem + +Die DB wählt das „beste“ Produkt über `QUERY_GET_BEST_PRODUCTION` (Worth-Formel mit `- 6 * ftp.category`), während der Daemon früher **nur** `certificate * 6` pro Stück abgebucht hat – **ohne** die Produktklasse. Dadurch konnten Stückkosten und Worth-Ranking auseinanderlaufen. + +## Aktuelle Formel (Rust, `DirectorWorker::calc_one_piece_cost`) + +``` +raw = certificate × PRODUCTION_COST_PER_CERT_LEVEL + + product_category × PRODUCTION_COST_PER_PRODUCT_CATEGORY +effektiv = raw × (1 − headroom_discount) +``` + +- `product_category` kommt aus `QUERY_GET_BEST_PRODUCTION` (`ftp.category`). +- **Headroom** = `max(0, certificate − product_category)` + Wenn du **nicht** am Klassenlimit produzierst (Zertifikat höher als nötig für das Produkt), gibt es einen kleinen Rabatt (Effizienz / Reserve). + +Konstanten in `src/worker/director.rs` (anpassen zum Feintuning): + +| Konstante | Standard | Bedeutung | +|-----------|----------|-----------| +| `PRODUCTION_COST_PER_CERT_LEVEL` | `6.0` | wie früher `×6` pro Zertifikatsstufe | +| `PRODUCTION_COST_PER_PRODUCT_CATEGORY` | `1.0` | Material pro Produktklasse | +| `PRODUCTION_HEADROOM_DISCOUNT_PER_STEP` | `0.035` | Rabatt pro Headroom-Stufe | +| `PRODUCTION_HEADROOM_DISCOUNT_CAP` | `0.14` | maximaler Gesamtrabatt | + +## Spielerfortschritt + +- **Höheres Zertifikat** allein erhöht die Basis-Stückkosten linear – entspricht **besserer** Produktpalette (`ftp.category <= certificate` in SQL). +- **Höhere Produktklasse** erhöht `raw` über `product_category` (bessere Ware = mehr Material). +- **Zertifikat über der Produktklasse** (Headroom) senkt die effektiven Kosten – Belohnung, wenn du nicht immer nur am Limit produzierst. + +## Worth in SQL + +Die Worth-Zeile in `QUERY_GET_BEST_PRODUCTION` sollte bei größeren Formeländerungen **mit** angepasst werden, damit der Director weiterhin sinnvoll sortiert. + +## Parallelproduktionen + +`MAX_PARALLEL_PRODUCTIONS` (aktuell 2) bestimmt, wie viele Linien pro Tick Geld binden – unabhängig von der Stückkostenformel; bei Liquiditätsproblemen ggf. auf `1` setzen. diff --git a/docs/FALUKANT_PRODUCTION_CERTIFICATE.md b/docs/FALUKANT_PRODUCTION_CERTIFICATE.md index bb969f5..d61bd62 100644 --- a/docs/FALUKANT_PRODUCTION_CERTIFICATE.md +++ b/docs/FALUKANT_PRODUCTION_CERTIFICATE.md @@ -1,18 +1,20 @@ # Falukant: Produktionszertifikate (Daemon) -Der **`FalukantCertificateWorker`** berechnet einmal täglich die Zielstufe und schreibt `falukant_user.certificate` fort (max. **+1** pro Tag, keine normale Herabstufung). +Die Zertifikatslogik läuft **ausschließlich im Daily-Tick** von `FalukantFamilyWorker` (`process_daily`, 24h), nicht in einem eigenen Worker-Thread. Sie schreibt `falukant_user.certificate` fort (max. **+1** pro Tag, keine normale Herabstufung). + +Implementierung: `src/worker/falukant_certificate.rs` (`run_daily`). ## SQL - `QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS` – Eingangsdaten je Falukant-User (Spielercharakter, Wissen, Produktionen, Ämter, Haus …) - `QUERY_UPDATE_FALUKANT_USER_CERTIFICATE` – Update der Stufe -## Logik (Kurz) +## Logik (Kurz, Spec §4) -- `certificateScore` aus gewichteten Punktwerten (Wissen, Produktion, Amt, Adel, Ruf, Haus) -- `raw_target` aus Score-Schwellen (1.2 / 2.1 / 3.0 / 4.0) -- `effective_target` mit Mindestanforderungen je Stufe (Spec §4.5) -- Aufstieg nur wenn `effective_target > current` → **`current + 1`** (gegen `effective_target` begrenzt) +- **certificateScore** (Gewichte): Wissen 0,45 · Produktion 0,30 · Amt 0,08 · Adel 0,05 · Ruf 0,07 · Haus 0,05 +- **raw_target** aus Score-Schwellen: **<0,9** → 1, **≥0,9** → 2, **≥1,8** → 3, **≥2,8** → 4, **≥3,8** → 5 +- **effective_target** mit Mindestanforderungen je Stufe (Spec §4.5) +- Aufstieg nur wenn `effective_target > current` → **`current + 1`** (gegen `effective_target` und 5 begrenzt) - **Bankrott** (`money <= -5000`): Zertifikat auf **1**, mit Event ## Politische Ämter @@ -25,7 +27,7 @@ Rang aus **`political_office_type.name`** (Substring-Heuristik im Daemon, ohne D ## Abgeschlossene Produktionen -**`COUNT(*)`** aus `falukant_log.production` mit `producer_id = falukant_user.id` (Zeilen = aggregierte Log-Einträge). +**`COUNT(*)`** aus `falukant_log.production` mit `producer_id = falukant_user.id`. ## Events (WebSocket) @@ -34,6 +36,8 @@ Bei Änderung der Stufe: 1. `falukantUpdateProductionCertificate` mit `reason: "daily_recalculation"`, `old_certificate`, `new_certificate` 2. `falukantUpdateStatus` +`user_id` in den Events: **`app_user_id`** aus der Query (`COALESCE(fu.user_id, fu.id)`), sonst Fallback `falukant_user_id`. + ## Nicht umgesetzt (optional / später) - **Tod ohne Erben** / Zertifikats-Reset diff --git a/src/main.rs b/src/main.rs index 466a3f3..d4a23ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use worker::{ CharacterCreationWorker, ConnectionPool, DirectorWorker, EventsWorker, HouseWorker, PoliticsWorker, ProduceWorker, StockageManager, TransportWorker, UndergroundWorker, UserCharacterWorker, ValueRecalculationWorker, WeatherWorker, Worker, - FalukantFamilyWorker, FalukantCertificateWorker, + FalukantFamilyWorker, }; static KEEP_RUNNING: AtomicBool = AtomicBool::new(true); @@ -142,10 +142,6 @@ fn create_workers(pool: ConnectionPool, broker: MessageBroker) -> Vec Result<(), DbError> { + let pool = &base.pool; + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; -impl FalukantCertificateWorker { - pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self { - Self { - base: BaseWorker::new("FalukantCertificateWorker", pool, broker), + 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); + 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, + ); + + 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); } } - fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc) { - let mut last: Option = 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(()) - } + Ok(()) } fn publish_certificate_event(broker: &MessageBroker, user_id: i32, old_c: i32, new_c: i32) { @@ -189,7 +155,6 @@ fn production_points_from_count(n: i64) -> i32 { } } -/// Kirchliche Ämter: `hierarchy_level` auf 0–5 begrenzen (Daemon). fn church_rank_from_hierarchy(h: i32) -> i32 { if h <= 0 { 0 @@ -198,7 +163,7 @@ fn church_rank_from_hierarchy(h: i32) -> i32 { } } -/// Politische Amtsnamen → Rang 1–5 (konfigurierbar im Daemon). +/// 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") @@ -282,72 +247,36 @@ fn house_points_from_position(position: i32) -> i32 { } } +/// Spec §4.6 Schwellen für die Zielstufe (vor Mindestanforderungen). fn raw_target_from_score(score: f64) -> i32 { - if score >= 4.0 { + if score >= 3.8 { 5 - } else if score >= 3.0 { + } else if score >= 2.8 { 4 - } else if score >= 2.1 { + } else if score >= 1.8 { 3 - } else if score >= 1.2 { + } else if score >= 0.9 { 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 { +fn cert5_two_of_social(office_points: i32, nobility_points: i32, house_points: i32) -> bool { let mut c = 0; - if office_points >= 1 { + if office_points >= 2 { 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 + c >= 2 } +/// Mindestanforderungen je Stufe (Spec §4.5). fn meets_min_for_level( level: i32, avg_knowledge: f64, @@ -359,38 +288,27 @@ fn meets_min_for_level( ) -> 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, - ) - } + 2 => avg_knowledge >= 15.0 && completed >= 4, + 3 => avg_knowledge >= 28.0 && completed >= 15, 4 => { - avg_knowledge >= 55.0 - && completed >= 60 - && status_count_cert4( - office_points, - nobility_points, - reputation_points, - house_points, - ) >= 2 + avg_knowledge >= 45.0 + && completed >= 45 + && (office_points >= 1 + || nobility_points >= 1 + || reputation_points >= 2 + || house_points >= 2) } 5 => { - avg_knowledge >= 70.0 - && completed >= 150 - && reputation_points >= 3 - && cert5_extra_two(office_points, nobility_points, house_points) >= 2 + 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. +/// Höchste Stufe ≤ `raw_target`, die alle Mindestanforderungen erfüllt (Spec §4.6). fn effective_certificate_target( raw_target: i32, avg_knowledge: f64, @@ -416,23 +334,3 @@ fn effective_certificate_target( } 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| { - 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(); - } -} diff --git a/src/worker/falukant_family.rs b/src/worker/falukant_family.rs index 9068bf4..2cd18e7 100644 --- a/src/worker/falukant_family.rs +++ b/src/worker/falukant_family.rs @@ -158,6 +158,9 @@ impl FalukantFamilyWorker { if let Err(e) = super::falukant_debtors::run_daily(&self.base, &self.base.broker) { eprintln!("[FalukantFamilyWorker] falukant_debtors::run_daily: {e}"); } + if let Err(e) = super::falukant_certificate::run_daily(&self.base, &self.base.broker) { + eprintln!("[FalukantFamilyWorker] falukant_certificate::run_daily: {e}"); + } let mut conn = self .base diff --git a/src/worker/mod.rs b/src/worker/mod.rs index 773facd..b526c1d 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -33,5 +33,4 @@ pub use transport::TransportWorker; pub use weather::WeatherWorker; pub use events::EventsWorker; pub use falukant_family::FalukantFamilyWorker; -pub use falukant_certificate::FalukantCertificateWorker; diff --git a/src/worker/sql.rs b/src/worker/sql.rs index 26fc19d..0cab899 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -3135,6 +3135,7 @@ pub const QUERY_INSERT_CHILD_RELATION_LOVER: &str = r#" pub const QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS: &str = r#" SELECT DISTINCT ON (fu.id) fu.id AS falukant_user_id, + COALESCE(fu.user_id, fu.id)::int AS app_user_id, COALESCE(fu.certificate, 1)::int AS certificate, COALESCE(fu.money, 0)::float8 AS money, c.id AS character_id, @@ -3179,7 +3180,8 @@ pub const QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS: &str = r#" pub const QUERY_UPDATE_FALUKANT_USER_CERTIFICATE: &str = r#" UPDATE falukant_data.falukant_user - SET certificate = $1::int + SET certificate = $1::int, + updated_at = NOW() WHERE id = $2::int; "#;