diff --git a/docs/FALUKANT_DAEMON_AENDERUNGSNOTIZ_ZEITMASSSTAB.md b/docs/FALUKANT_DAEMON_AENDERUNGSNOTIZ_ZEITMASSSTAB.md new file mode 100644 index 0000000..d70719a --- /dev/null +++ b/docs/FALUKANT_DAEMON_AENDERUNGSNOTIZ_ZEITMASSSTAB.md @@ -0,0 +1,29 @@ +# Änderungsnotiz (externer Daemon / Übergabe) + +Kurznotiz zur **Abstimmung von Spielzeit, Monatsläufen und Wirtschaftswerten** — für Teams, die den Daemon einbinden oder die Spec pflegen. + +## Zeitmaßstab + +- **Falukant** nutzt einen stark komprimierten Zeitmaßstab: **1 Spieltag = 1 Spieljahr**. +- Der **Monatslauf** des Daemons entspricht in dieser Logik **ungefähr einem 2-Stunden-Schritt** (ein „Monatstick“ in der Spielzeit, nicht ein Kalendermonat in der Realität). + +## Dienerschaft & Wirtschaft + +- **Dienerkosten** dürfen **nicht** wie ein realistischer **Vollmonatslohn** skaliert oder interpretiert werden (kein 1:1-Vergleich mit realem Monatsgehalt). +- Der in Spec/Datenbank **definierte Monatswert** ist ein **abstrahierter Unterhalts- und Bindungsbetrag pro Monatstick** — er beschreibt die **ökonomische Last** im Spielmodell, nicht einen historischen Lohn. + +## Datenbank + +- Die Datenbank ist für diese Konzeption **bereits vorbereitet**; für diese inhaltliche Festlegung sind **keine zusätzlichen DB-Anpassungen** nötig. + +## Umsetzung im Daemon (YpDaemon) + +- **Gemeinsamer 2-h-Tick:** Dienerschaft (`falukant_servants::run_monthly`) und Liebschafts-Unterhalt (`process_lover_installments`) laufen im selben Intervall (`GAME_MONTH_SLICE_INTERVAL`). +- **Dienerschaft:** Abfrage über `servants_last_monthly_at` (alle 2 h fällig); Abbuchung = **1/12** des aus Stammdaten berechneten abstrakten Monatsbudgets. +- **Liebschaft:** Abbuchung = **1/12** des Monatsunterhalts; Idempotenz `lover_last_installment_at` (Migration `006`). +- **`process_monthly` (selten, ~30 Tage Wandzeit):** nur noch Liebschafts-Monatsmarkierung + Geburten — **keine** Dienerschaft mehr. + +## Siehe auch + +- Technisches Handoff: [`FALUKANT_DAEMON_HANDOFF.md`](./FALUKANT_DAEMON_HANDOFF.md) +- Dienerschaft im Daemon: [`FALUKANT_SERVANTS_DAEMON.md`](./FALUKANT_SERVANTS_DAEMON.md) diff --git a/docs/FALUKANT_DAEMON_HANDOFF.md b/docs/FALUKANT_DAEMON_HANDOFF.md index e82076c..b2b83c7 100644 --- a/docs/FALUKANT_DAEMON_HANDOFF.md +++ b/docs/FALUKANT_DAEMON_HANDOFF.md @@ -4,6 +4,8 @@ Technische Abstimmung mit dem Übergabedokument im Backend-Projekt (`FALUKANT_LO **Ehe & Hausfrieden (Phase A):** [`FALUKANT_MARRIAGE_HOUSEPEACE_DAEMON_HANDOFF.md`](./FALUKANT_MARRIAGE_HOUSEPEACE_DAEMON_HANDOFF.md) +**Zeitmaßstab & Monatstick (Übergabe extern):** [`FALUKANT_DAEMON_AENDERUNGSNOTIZ_ZEITMASSSTAB.md`](./FALUKANT_DAEMON_AENDERUNGSNOTIZ_ZEITMASSSTAB.md) + ## Abweichungen / Zuordnung | Handoff / Backend | YpDaemon | @@ -12,12 +14,13 @@ Technische Abstimmung mit dem Übergabedokument im Backend-Projekt (`FALUKANT_LO | `months_underfunded` | Spalte `months_underfunded` (Migration 001; Legacy: `002` benennt `consecutive_underpayment_months` um) | | Idempotenz `last_daily_processed_at` / `last_monthly_processed_at` | Gesetzt von `FalukantFamilyWorker` pro Liebschaft | | Idempotenz `lover_last_installment_at` (Migration `006`) | Gesetzt alle **2 h** pro Liebschaft nach Unterhalts-Tick (1/12 des Monatsbetrags) | +| Idempotenz `servants_last_monthly_at` (Migration `004`) | Gesetzt alle **2 h** pro Haushalt nach Dienerschafts-Tick (1/12 des abstrakten Monatsbudgets), semantisch „Monatstick“ | ## Ticks - **Daily:** nur Zeilen mit `(last_daily_processed_at IS NULL OR last_daily_processed_at::date < CURRENT_DATE)`; danach `last_daily_processed_at = NOW()`. -- **Monthly:** nur Zeilen mit `(last_monthly_processed_at IS NULL OR date_trunc('month', last_monthly_processed_at) < date_trunc('month', CURRENT_TIMESTAMP))`; danach `last_monthly_processed_at = NOW()` (**ohne** Liebschafts-Geld — nur Monatsstand/Schwangerschafts-Logik; ggf. **Dienerschaft** `run_monthly` mit eigenen Kosten). -- **Liebschafts-Unterhalt (Spielzeit):** alle **2 h** (`lover_last_installment_at`), Betrag = **1/12** des bisherigen Monatsunterhalts (12 „Spielmonate“ pro **Spieltag** = 1 Spieljahr). Action in `money_history`: weiterhin `lover maintenance`. +- **Monthly (Kalender / Liebschaft):** nur Zeilen mit `(last_monthly_processed_at IS NULL OR date_trunc('month', last_monthly_processed_at) < date_trunc('month', CURRENT_TIMESTAMP))`; danach `last_monthly_processed_at = NOW()` — **ohne** Liebschafts-Geld; Monatsstand/Schwangerschafts-Logik + `process_lover_births`. +- **Monatstick (~2 h, Spielzeit):** gemeinsamer Worker-Tick für **Dienerschaft** (`servants_last_monthly_at`, Abfrage alle 2 h) und **Liebschaft** (`lover_last_installment_at`). Beträge = **1/12** des abstrakten „Monats“-Budgets pro Rolle (12 Monatsticke pro **Spieltag** = 1 Spieljahr). `money_history`: `servants_monthly` bzw. `lover maintenance`. Siehe [`FALUKANT_DAEMON_AENDERUNGSNOTIZ_ZEITMASSSTAB.md`](./FALUKANT_DAEMON_AENDERUNGSNOTIZ_ZEITMASSSTAB.md). **Hinweis:** Der Worker nutzt weiterhin **Wandzeit** (24 h / 30 Tage / **2 h** Unterhalt) als Intervall; die Idempotenz über die Zeitstempel verhindert Doppelverarbeitung bei Neustarts am selben Tag/Monat bzw. im selben 2-h-Fenster. diff --git a/docs/FALUKANT_SERVANTS_DAEMON.md b/docs/FALUKANT_SERVANTS_DAEMON.md index cf9a94e..98c2558 100644 --- a/docs/FALUKANT_SERVANTS_DAEMON.md +++ b/docs/FALUKANT_SERVANTS_DAEMON.md @@ -2,6 +2,10 @@ Umsetzung gemäß Projektspezifikation (Daily/Monthly, Handoff). +**Wichtig (Spielzeit / Kosten):** Falukant arbeitet mit **1 Spieltag = 1 Spieljahr**; Monatsläufe entsprechen **ungefähr einem 2-h-Schritt**. Dienerkosten sind **kein** realistischer Vollmonatslohn, sondern ein **abstrakter Monatstick** — siehe [`FALUKANT_DAEMON_AENDERUNGSNOTIZ_ZEITMASSSTAB.md`](./FALUKANT_DAEMON_AENDERUNGSNOTIZ_ZEITMASSSTAB.md). + +**Umsetzung:** `run_monthly` wird alle **2 h** ausgeführt (gemeinsam mit Liebschafts-Raten im `FalukantFamilyWorker`). SQL: `servants_last_monthly_at` älter als 2 h oder `NULL`. Abgebucht wird **1/12** des berechneten Monatsbudgets pro Tick (`servants_monthly` in `money_history`). Spaltenname `servants_last_monthly_at` bleibt historisch; semantisch = letzter Monatstick. + ## Voraussetzungen 1. **Backend:** Stammdaten in `falukant_data.user_house` (`servant_count`, `servant_quality`, `servant_pay_level`, `household_order`) — z. B. Migration aus YourPart3. @@ -11,7 +15,7 @@ Umsetzung gemäß Projektspezifikation (Daily/Monthly, Handoff). - Logik: `src/worker/falukant_servants.rs` - SQL: `src/worker/sql.rs` (Abschnitt Dienerschaft) -- Ausführung: **`FalukantFamilyWorker`** ruft bei gesetztem Servant-Schema **vor** Liebschafts-Daily/Monthly `run_daily` / `run_monthly` auf (keine Race mit Liebschafts-Ticks). +- Ausführung: **`run_daily`** beim täglichen Familien-Tick; **`run_monthly`** im **2-h-Monatstick** zusammen mit Liebschafts-Installments (nicht mehr im seltenen Kalender-`process_monthly`). ## WebSocket diff --git a/docs/FALUKANT_UI_WEBSOCKET.md b/docs/FALUKANT_UI_WEBSOCKET.md index 834f4e4..6048eef 100644 --- a/docs/FALUKANT_UI_WEBSOCKET.md +++ b/docs/FALUKANT_UI_WEBSOCKET.md @@ -35,7 +35,7 @@ Dieses Dokument beschreibt die **Nachrichten**, die der **YpDaemon** (`FalukantF | `reason` | Bedeutung (Daemon) | Empfehlung UI | |----------|---------------------|---------------| | `daily` | Daily-Tick: Liebschafts-/Ehe-/Ansehens-Logik; u. a. `marriage_public_stability`, `household_tension_score` | Family-API + ggf. Charakter/Ansehen/Haus neu laden | -| `monthly` | Monthly-Tick: Monatsmarkierung Liebschaft (`last_monthly_processed_at`), ggf. Dienerschaft-Monatskosten; **kein** vollständiger Liebschafts-Monatsbetrag mehr | **Geld** (falls Dienerschaft zahlt) + Family-State neu laden | +| `monthly` | Kalender-`monthly` (Liebschafts-Monatsmarkierung / selten) **oder** Dienerschaft zahlt Monatstick: **Geld** (`servants_monthly`) | **Geld** + Family-State neu laden | | `lover_installment` | Alle **2 h**: **1/12** Liebschafts-Unterhalt bzw. Unterversorgung (`money_history`: `lover maintenance`) | **Geld** + Family-State neu laden | | `scandal` | Skandal-Ereignis (zusätzlich zu `daily` möglich) | Kurzer Hinweis / Eintrag „Skandal“; Family + Ruf | | `lover_birth` | Uneheliches Kind angelegt | Wie `children_update`, plus Eltern-Story | diff --git a/src/worker/falukant_family.rs b/src/worker/falukant_family.rs index 875a685..b4d2a17 100644 --- a/src/worker/falukant_family.rs +++ b/src/worker/falukant_family.rs @@ -34,8 +34,8 @@ use crate::message_broker::MessageBroker; const DAILY_INTERVAL: Duration = Duration::from_secs(24 * 3600); const MONTHLY_INTERVAL: Duration = Duration::from_secs(30 * 24 * 3600); -/// 12 Teilzahlungen pro Spieltag (24 h = 1 Spieljahr); 2 h = 1 Spielmonat. -const LOVER_INSTALLMENT_INTERVAL: Duration = Duration::from_secs(2 * 3600); +/// 12 Monatsticke pro Spieltag (24 h = 1 Spieljahr); 2 h = 1 Spielmonat (Liebschaft + Dienerschaft). +const GAME_MONTH_SLICE_INTERVAL: Duration = Duration::from_secs(2 * 3600); pub struct FalukantFamilyWorker { base: BaseWorker, @@ -43,7 +43,8 @@ pub struct FalukantFamilyWorker { dist: Uniform, last_daily: Option, last_monthly: Option, - last_lover_installment: Option, + /// Gemeinsamer Tick für Liebschafts-Raten + Dienerschaft (Monatstick ≈ 2 h). + last_game_month_slice: Option, schema_ready: bool, /// Migration `004_falukant_servants_daemon.sql` (Dienerschaft-Ticks). servants_schema_ready: bool, @@ -59,7 +60,7 @@ impl FalukantFamilyWorker { dist: Uniform::from(0.0..1.0), last_daily: None, last_monthly: None, - last_lover_installment: None, + last_game_month_slice: None, schema_ready: false, servants_schema_ready: false, lover_installment_schema_ready: false, @@ -93,12 +94,21 @@ impl FalukantFamilyWorker { self.last_monthly = Some(now); } - if self.lover_installment_schema_ready { - if Self::should_run(self.last_lover_installment, now, LOVER_INSTALLMENT_INTERVAL) { - if let Err(e) = self.process_lover_installments() { - eprintln!("[FalukantFamilyWorker] process_lover_installments: {e}"); + if self.servants_schema_ready || self.lover_installment_schema_ready { + if Self::should_run(self.last_game_month_slice, now, GAME_MONTH_SLICE_INTERVAL) { + if self.servants_schema_ready { + if let Err(e) = + super::falukant_servants::run_monthly(&self.base, &self.base.broker) + { + eprintln!("[FalukantFamilyWorker] falukant_servants::run_monthly: {e}"); + } } - self.last_lover_installment = Some(now); + if self.lover_installment_schema_ready { + if let Err(e) = self.process_lover_installments() { + eprintln!("[FalukantFamilyWorker] process_lover_installments: {e}"); + } + } + self.last_game_month_slice = Some(now); } } @@ -651,10 +661,6 @@ impl FalukantFamilyWorker { } fn process_monthly(&mut self) -> Result<(), DbError> { - if self.servants_schema_ready { - super::falukant_servants::run_monthly(&self.base, &self.base.broker)?; - } - let mut conn = self .base .pool diff --git a/src/worker/falukant_servants.rs b/src/worker/falukant_servants.rs index bede3c0..d9983cd 100644 --- a/src/worker/falukant_servants.rs +++ b/src/worker/falukant_servants.rs @@ -1,5 +1,10 @@ -//! Dienerschaft: Daily/Monthly nach Spec (docs: Handoff Dienerschaft). +//! Dienerschaft: Daily + „Monats“-Kosten nach Spielzeitmodell. //! Voraussetzung: `migrations/004_falukant_servants_daemon.sql`. +//! +//! **Zeitmaßstab:** 1 Spieltag = 1 Spieljahr; ein „Monatstick“ ≈ **2 h** Wandzeit. +//! Der berechnete `monthly_cost` ist ein **abstrakter** Unterhaltsbetrag pro **Spielmonat** +//! (kein realistischer Vollmonatslohn). Pro Tick wird nur **1/12** dieses Betrags abgebucht +//! (12 Monatsticke pro Spieltag), analog zur Liebschafts-Logik. use crate::db::DbError; use crate::message_broker::MessageBroker; @@ -260,7 +265,7 @@ pub fn run_daily(base: &BaseWorker, broker: &MessageBroker) -> Result<(), DbErro Ok(()) } -/// Monthly: Kosten, Unterversorgung, Boni. +/// Monatstick (~2 h): Teilzahlung (1/12 des abstrakten Monatsbudgets), Unterversorgung, Boni. pub fn run_monthly(base: &BaseWorker, broker: &MessageBroker) -> Result<(), DbError> { let mut conn = base .pool @@ -346,9 +351,12 @@ pub fn run_monthly(base: &BaseWorker, broker: &MessageBroker) -> Result<(), DbEr "high" => 1.3, _ => 1.0, }; - let monthly_cost = + // Abstraktes Monatsbudget (ein Spielmonat); Abrechnung erfolgt zwölftelweise pro Tick. + let abstract_monthly_cost = (servant_count as f64 * base_per_servant * quality_factor * pay_factor * 100.0).round() / 100.0; + let installment = + ((abstract_monthly_cost / 12.0) * 100.0).round() / 100.0; let lover_rows = conn.execute("cnt_lov", &[&character_id])?; let lover_cnt = lover_rows @@ -357,13 +365,16 @@ pub fn run_monthly(base: &BaseWorker, broker: &MessageBroker) -> Result<(), DbEr .and_then(|v| v.parse::().ok()) .unwrap_or(0); - let notify = if servant_count <= 0 || monthly_cost <= 0.0 { + let notify = if servant_count <= 0 || abstract_monthly_cost <= 0.0 { conn.execute("upd_uh_mon", &[&false, &user_house_id])?; false - } else if user_money >= monthly_cost { + } else if installment <= 0.0 { + conn.execute("upd_uh_mon", &[&false, &user_house_id])?; + false + } else if user_money >= installment { base.change_falukant_user_money( falukant_user_id, - -monthly_cost, + -installment, "servants_monthly", )?; conn.execute("upd_uh_mon", &[&false, &user_house_id])?; diff --git a/src/worker/sql.rs b/src/worker/sql.rs index a772ea7..6373055 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -2103,8 +2103,9 @@ pub const QUERY_GET_SERVANT_MONTHLY_ROWS: &str = r#" JOIN falukant_data.character c ON c.user_id = fu.id AND c.health > 0 LEFT JOIN falukant_type.title t ON t.id = c.title_of_nobility LEFT JOIN falukant_type.house ht ON ht.id = uh.house_type_id + -- Monatstick (Spielzeit): alle ~2 h, nicht Kalendermonat (siehe docs/FALUKANT_DAEMON_AENDERUNGSNOTIZ_ZEITMASSSTAB.md) WHERE (uh.servants_last_monthly_at IS NULL - OR date_trunc('month', uh.servants_last_monthly_at) < date_trunc('month', CURRENT_TIMESTAMP)) + OR uh.servants_last_monthly_at < NOW() - INTERVAL '2 hours') AND NOT EXISTS ( SELECT 1 FROM falukant_type.house h WHERE h.id = uh.house_type_id AND h.label_tr = 'under_bridge'