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:
@@ -2,6 +2,8 @@
|
||||
|
||||
Technische Abstimmung mit dem Übergabedokument im Backend-Projekt (`FALUKANT_LOVERS_DAEMON_SPEC.md` / `FALUKANT_LOVERS_TECHNICAL_CONCEPT.md`).
|
||||
|
||||
**Ehe & Hausfrieden (Phase A):** [`FALUKANT_MARRIAGE_HOUSEPEACE_DAEMON_HANDOFF.md`](./FALUKANT_MARRIAGE_HOUSEPEACE_DAEMON_HANDOFF.md)
|
||||
|
||||
## Abweichungen / Zuordnung
|
||||
|
||||
| Handoff / Backend | YpDaemon |
|
||||
@@ -55,6 +57,7 @@ Ehe-Malus „≤ 15“ gilt pro Ehe, wenn **irgendeine** berührende Liebschaft
|
||||
2. Optional: `migrations/002_falukant_family_rename_legacy_columns.sql` bei Altbestand
|
||||
3. `migrations/003_falukant_family_marriage_buffs.sql` — Ehe-Buffs (`marriage_gift_buff_days_remaining`, `marriage_pending_feast_bonus`, `marriage_house_supply`, `marriage_no_lover_bonus_counter`); Daily-Tick schreibt Zufriedenheit + Zähler via `QUERY_UPDATE_MARRIAGE_STATE_AND_BUFFS`.
|
||||
4. `migrations/004_falukant_servants_daemon.sql` — Dienerschaft: Tick-Idempotenz + `servant_discretion_modifier` (Stammdaten-Dienerfelder kommen aus dem Backend). Siehe [`FALUKANT_SERVANTS_DAEMON.md`](./FALUKANT_SERVANTS_DAEMON.md).
|
||||
5. `migrations/005_falukant_marriage_housepeace.sql` — `relationship.marriage_public_stability`, `user_house.household_tension_score`. Siehe [`FALUKANT_MARRIAGE_HOUSEPEACE_DAEMON_HANDOFF.md`](./FALUKANT_MARRIAGE_HOUSEPEACE_DAEMON_HANDOFF.md).
|
||||
|
||||
### Ehe-Buffs (Daemon)
|
||||
|
||||
|
||||
156
docs/FALUKANT_MARRIAGE_HOUSEPEACE_DAEMON_HANDOFF.md
Normal file
156
docs/FALUKANT_MARRIAGE_HOUSEPEACE_DAEMON_HANDOFF.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Falukant: Daemon-Handoff für Ehe und Hausfrieden
|
||||
|
||||
Dieses Dokument beschreibt den Stand nach Phase A und die Rolle des externen Daemons (Übergabe Backend ↔ YpDaemon).
|
||||
|
||||
## 1. Was im Projekt jetzt vorhanden ist
|
||||
|
||||
Backend-/API-seitig vorhanden:
|
||||
|
||||
- `relationship_state.marriage_satisfaction`
|
||||
- `relationship_state.marriage_public_stability`
|
||||
- aktive Liebschaften mit:
|
||||
- `visibility`
|
||||
- `discretion`
|
||||
- `maintenance_level`
|
||||
- `status_fit`
|
||||
- `months_underfunded`
|
||||
- `acknowledged`
|
||||
- `user_house` mit:
|
||||
- `servant_count`
|
||||
- `servant_quality`
|
||||
- `servant_pay_level`
|
||||
- `household_order`
|
||||
- Family-API liefert jetzt zusätzlich:
|
||||
- `householdTension`
|
||||
- `householdTensionScore`
|
||||
- `householdTensionReasons`
|
||||
|
||||
Direkte Spieleraktionen vorhanden:
|
||||
|
||||
- `POST /api/falukant/family/marriage/spend-time`
|
||||
- `POST /api/falukant/family/marriage/gift`
|
||||
- `POST /api/falukant/family/marriage/reconcile`
|
||||
- `POST /api/falukant/houses/order`
|
||||
|
||||
## 2. Daily-Input für den externen Daemon
|
||||
|
||||
Pro betroffenem Falukant-User:
|
||||
|
||||
- `falukant_user.id`
|
||||
- `user.id` / `user.hashed_id`
|
||||
- aktive Ehe-`relationship` mit `relationship_state`
|
||||
- aktive Liebschaften mit `relationship_state`
|
||||
- Kinder mit:
|
||||
- `birth_context`
|
||||
- `legitimacy`
|
||||
- `public_known`
|
||||
- Haus mit:
|
||||
- `servant_count`
|
||||
- `servant_quality`
|
||||
- `servant_pay_level`
|
||||
- `household_order`
|
||||
- Charakter mit:
|
||||
- `reputation`
|
||||
- `title_of_nobility`
|
||||
|
||||
## 3. Was der Daemon täglich berechnen soll
|
||||
|
||||
### Ehe
|
||||
|
||||
- Drift von `marriage_satisfaction`
|
||||
- Drift von `marriage_public_stability`
|
||||
- Einfluss aus:
|
||||
- sichtbaren Liebschaften
|
||||
- unterfinanzierten Liebschaften
|
||||
- Standesunterschieden
|
||||
- Dienerschaft / Haushaltsordnung
|
||||
- zu jungen Liebschaften
|
||||
|
||||
### Hausfrieden
|
||||
|
||||
Der Daemon soll intern einen numerischen Spannungswert pflegen oder berechnen:
|
||||
|
||||
- `householdTensionScore` `0..100`
|
||||
|
||||
Einflussfaktoren:
|
||||
|
||||
- sichtbare Liebschaften
|
||||
- anerkannte Liebschaften
|
||||
- unterfinanzierte Liebschaften
|
||||
- Kinder aus Liebschaften
|
||||
- Haushaltsordnung
|
||||
- Dienerschaft
|
||||
- schwache Ehe
|
||||
|
||||
UI-Ableitung:
|
||||
|
||||
- `0..24` => `low`
|
||||
- `25..59` => `medium`
|
||||
- `60..100` => `high`
|
||||
|
||||
## 4. Was der Daemon zurückschreiben soll
|
||||
|
||||
Pflicht:
|
||||
|
||||
- `relationship_state.marriage_satisfaction`
|
||||
- `relationship_state.marriage_public_stability`
|
||||
- lover-state-Felder bei Änderungen:
|
||||
- `visibility`
|
||||
- `discretion`
|
||||
- `months_underfunded`
|
||||
- optional `notes_json` / `flags_json`
|
||||
- falls eigener Persistenzwert eingeführt wird:
|
||||
- `household_tension_score`
|
||||
|
||||
Wenn kein eigener Persistenzwert eingeführt wird:
|
||||
|
||||
- der Daemon darf den Spannungswert auch nur berechnen
|
||||
- die API kann ihn weiterhin aus Ehe, Liebschaften, Kindern und Haus ableiten
|
||||
|
||||
## 5. Socket-/Refresh-Verhalten
|
||||
|
||||
Wenn Daily-/Monthly-Verarbeitung Ehe oder Hausfrieden betrifft:
|
||||
|
||||
- `falukantUpdateFamily` mit `reason: "daily"` oder `reason: "monthly"`
|
||||
- danach `falukantUpdateStatus`
|
||||
|
||||
Wenn ein Sonderereignis entsteht:
|
||||
|
||||
- `reason: "scandal"` zusätzlich
|
||||
|
||||
## 6. Wichtige Phase-A-Regel
|
||||
|
||||
Die neuen Direktaktionen geben nur Sofortimpulse:
|
||||
|
||||
- `spend-time`
|
||||
- `gift`
|
||||
- `reconcile`
|
||||
- `house/order`
|
||||
|
||||
Der Daemon ist weiterhin verantwortlich für:
|
||||
|
||||
- Rückdrift
|
||||
- Gegenkräfte
|
||||
- Langzeiteffekte
|
||||
- Balancing
|
||||
|
||||
Kurz:
|
||||
|
||||
- UI/Backend setzen kleine direkte Impulse
|
||||
- der Daemon bestimmt die dauerhafte Entwicklung
|
||||
|
||||
---
|
||||
|
||||
## Anhang: Abgleich YpDaemon (dieses Repo)
|
||||
|
||||
| Thema | Stand in YpDaemon |
|
||||
|-------|---------------------|
|
||||
| Ehe-Zufriedenheit, Buffs, Drift (`marriage_drift_*`) | `FalukantFamilyWorker` + Migrationen `001`/`003`, siehe [`FALUKANT_DAEMON_HANDOFF.md`](./FALUKANT_DAEMON_HANDOFF.md) |
|
||||
| **`marriage_public_stability`** (Daily-Drift) | Migration **`005`**, `QUERY_GET_MARRIAGE_ROWS` / `QUERY_UPDATE_MARRIAGE_STATE_AND_BUFFS`, Logik in `falukant_family.rs` (Einfluss: sichtbare/unterfinanzierte/anerkannte Liebschaften, Stand, Alter, Haushalt/Diener, schwache Ehe) |
|
||||
| **`household_tension_score`** (0..100) | Migration **`005`**, berechnet im Daily-Tick, persistiert in `user_house`; UI-Band **low/medium/high** weiterhin aus Score ableitbar (0–24 / 25–59 / 60–100) |
|
||||
| Liebschaften (visibility, discretion, …, **`acknowledged`**, **`months_underfunded`**) | Daily-Query erweitert; Tension-Aggregation nutzt diese Felder |
|
||||
| Dienerschaft / `household_order` | `falukant_servants.rs` + Migration `004`; zusätzlich in Ehe-Stabilität und Haus-Spannung |
|
||||
| WebSocket | Bei Änderung von Ehe oder Spannung: `falukantUpdateFamily` mit `reason: "daily"` (wie bisher) |
|
||||
| **HTTP-Routen** (`spend-time`, `gift`, …) | Liegen im **Backend** (nicht im Daemon-Repo) |
|
||||
|
||||
Verwandte Doku: [`FALUKANT_UI_WEBSOCKET.md`](./FALUKANT_UI_WEBSOCKET.md), [`FALUKANT_SERVANTS_DAEMON.md`](./FALUKANT_SERVANTS_DAEMON.md).
|
||||
41
docs/FALUKANT_PRODUCTION_CERTIFICATE.md
Normal file
41
docs/FALUKANT_PRODUCTION_CERTIFICATE.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Falukant: Produktionszertifikate (Daemon)
|
||||
|
||||
Der **`FalukantCertificateWorker`** berechnet einmal täglich die Zielstufe und schreibt `falukant_user.certificate` fort (max. **+1** pro Tag, keine normale Herabstufung).
|
||||
|
||||
## 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)
|
||||
|
||||
- `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)
|
||||
- **Bankrott** (`money <= -5000`): Zertifikat auf **1**, mit Event
|
||||
|
||||
## Politische Ämter
|
||||
|
||||
Rang aus **`political_office_type.name`** (Substring-Heuristik im Daemon, ohne DB-Änderung). Anpassung über `political_name_to_rank` in `falukant_certificate.rs`.
|
||||
|
||||
## Kirchliche Ämter
|
||||
|
||||
`officePoints` aus **`max(hierarchy_level)`** der aktiven `church_office`-Zeilen (gekappt 0–5).
|
||||
|
||||
## Abgeschlossene Produktionen
|
||||
|
||||
**`COUNT(*)`** aus `falukant_log.production` mit `producer_id = falukant_user.id` (Zeilen = aggregierte Log-Einträge).
|
||||
|
||||
## Events (WebSocket)
|
||||
|
||||
Bei Änderung der Stufe:
|
||||
|
||||
1. `falukantUpdateProductionCertificate` mit `reason: "daily_recalculation"`, `old_certificate`, `new_certificate`
|
||||
2. `falukantUpdateStatus`
|
||||
|
||||
## Nicht umgesetzt (optional / später)
|
||||
|
||||
- **Tod ohne Erben** / Zertifikats-Reset
|
||||
- Feinere **Bankrott**-Definition
|
||||
- **`political_office_history`** (nicht im Repo)
|
||||
@@ -12,6 +12,7 @@ Dieses Dokument beschreibt die **Nachrichten**, die der **YpDaemon** (`FalukantF
|
||||
|---------|----------------|----------------------|
|
||||
| `falukantUpdateFamily` | `user_id`, `reason` | Gezielter Refresh Familie/Liebe/Geld je nach `reason` |
|
||||
| `falukantUpdateStatus` | `user_id` | Allgemeiner Status-/Spielstand-Refresh (wie bisher) |
|
||||
| `falukantUpdateProductionCertificate` | `user_id`, `reason`, `old_certificate`, `new_certificate` | Produkte / Produktions-UI / Zertifikat neu laden (nach Daily-Recalc oder Bankrott) |
|
||||
| `children_update` | `user_id` | Kinderliste / FamilyView aktualisieren |
|
||||
| `falukant_family_scandal_hint` | `relationship_id` | Optional: Toast, Log – **kein** `user_id` (siehe unten) |
|
||||
|
||||
@@ -33,7 +34,7 @@ Dieses Dokument beschreibt die **Nachrichten**, die der **YpDaemon** (`FalukantF
|
||||
|
||||
| `reason` | Bedeutung (Daemon) | Empfehlung UI |
|
||||
|----------|---------------------|---------------|
|
||||
| `daily` | Daily-Tick: Liebschafts-/Ehe-/Ansehens-Logik für den Tag | Family-API + ggf. Charakter/Ansehen neu laden |
|
||||
| `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: Kosten, Unterversorgung, Monatsstand | **Geld** (`falukant_user.money`) + 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 |
|
||||
@@ -49,7 +50,22 @@ Dieses Dokument beschreibt die **Nachrichten**, die der **YpDaemon** (`FalukantF
|
||||
|
||||
Kommt **typischerweise direkt nach** `falukantUpdateFamily` mit derselben `user_id` (gemeinsamer Refresh).
|
||||
|
||||
### 2.3 `children_update`
|
||||
### 2.3 `falukantUpdateProductionCertificate`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateProductionCertificate",
|
||||
"user_id": 123,
|
||||
"reason": "daily_recalculation",
|
||||
"old_certificate": 2,
|
||||
"new_certificate": 3
|
||||
}
|
||||
```
|
||||
|
||||
- **`reason`:** in der ersten Version fest `daily_recalculation` (inkl. Bankrott-Herabstufung, falls so umgesetzt).
|
||||
- Danach sendet der Daemon **`falukantUpdateStatus`** mit derselben `user_id` (wie bei anderen Falukant-Events).
|
||||
|
||||
### 2.4 `children_update`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -60,7 +76,7 @@ Kommt **typischerweise direkt nach** `falukantUpdateFamily` mit derselben `user_
|
||||
|
||||
Tritt bei **Geburt aus Liebschaft** auf; oft zusammen mit `falukantUpdateFamily` (`reason: lover_birth`) und `falukantUpdateStatus`.
|
||||
|
||||
### 2.4 `falukant_family_scandal_hint`
|
||||
### 2.5 `falukant_family_scandal_hint`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -84,6 +100,10 @@ onMessage(json):
|
||||
refreshPlayerStatus() // bestehend
|
||||
return
|
||||
|
||||
case "falukantUpdateProductionCertificate":
|
||||
refreshProductsAndProductionUi() // Zertifikat / erlaubte Produkte
|
||||
return
|
||||
|
||||
case "children_update":
|
||||
refreshChildrenAndFamilyView()
|
||||
return
|
||||
|
||||
16
migrations/005_falukant_marriage_housepeace.sql
Normal file
16
migrations/005_falukant_marriage_housepeace.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Ehe: öffentliche Stabilität (Daemon-Drift). Hausfrieden: persistenter Spannungswert 0..100.
|
||||
-- Siehe docs/FALUKANT_MARRIAGE_HOUSEPEACE_DAEMON_HANDOFF.md
|
||||
|
||||
ALTER TABLE falukant_data.relationship
|
||||
ADD COLUMN IF NOT EXISTS marriage_public_stability smallint NOT NULL DEFAULT 55
|
||||
CHECK (marriage_public_stability >= 0 AND marriage_public_stability <= 100);
|
||||
|
||||
COMMENT ON COLUMN falukant_data.relationship.marriage_public_stability IS
|
||||
'Öffentliche Ehe-Stabilität 0..100; Daily-Drift durch Daemon (Liebschaften, Haus, Stand)';
|
||||
|
||||
ALTER TABLE falukant_data.user_house
|
||||
ADD COLUMN IF NOT EXISTS household_tension_score smallint NOT NULL DEFAULT 0
|
||||
CHECK (household_tension_score >= 0 AND household_tension_score <= 100);
|
||||
|
||||
COMMENT ON COLUMN falukant_data.user_house.household_tension_score IS
|
||||
'Hausfrieden-Spannung 0..100 (Daemon); UI: low 0–24, medium 25–59, high 60–100';
|
||||
@@ -16,7 +16,7 @@ use worker::{
|
||||
CharacterCreationWorker, ConnectionPool, DirectorWorker, EventsWorker, HouseWorker,
|
||||
PoliticsWorker, ProduceWorker, StockageManager, TransportWorker, UndergroundWorker,
|
||||
UserCharacterWorker, ValueRecalculationWorker, WeatherWorker, Worker,
|
||||
FalukantFamilyWorker,
|
||||
FalukantFamilyWorker, FalukantCertificateWorker,
|
||||
};
|
||||
|
||||
static KEEP_RUNNING: AtomicBool = AtomicBool::new(true);
|
||||
@@ -142,6 +142,10 @@ fn create_workers(pool: ConnectionPool, broker: MessageBroker) -> Vec<Box<dyn Wo
|
||||
pool.clone(),
|
||||
broker.clone(),
|
||||
)),
|
||||
Box::new(FalukantCertificateWorker::new(
|
||||
pool.clone(),
|
||||
broker.clone(),
|
||||
)),
|
||||
Box::new(HouseWorker::new(pool.clone(), broker.clone())),
|
||||
Box::new(PoliticsWorker::new(pool.clone(), broker.clone())),
|
||||
Box::new(TransportWorker::new(pool.clone(), broker.clone())),
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Liebhaber, Ehezufriedenheit, Ansehen, Monatskosten (Handoff: docs/FALUKANT_DAEMON_HANDOFF.md).
|
||||
//! Benötigt `migrations/001_falukant_family_lovers.sql` (ggf. `002` bei Altbestand), `003` für Ehe-Buffs,
|
||||
//! optional `004` + Backend-Stammdaten für Dienerschaft (`falukant_servants`).
|
||||
//! optional `004` + Backend-Stammdaten für Dienerschaft (`falukant_servants`),
|
||||
//! `005` Ehe öffentliche Stabilität + Hausfrieden (`household_tension_score`).
|
||||
//!
|
||||
//! WebSocket: `falukantUpdateFamily` (reason) + `falukantUpdateStatus` für betroffene Nutzer.
|
||||
|
||||
@@ -15,14 +16,16 @@ use rand::SeedableRng;
|
||||
|
||||
use super::base::{BaseWorker, Worker, WorkerState};
|
||||
use super::sql::{
|
||||
QUERY_FAMILY_SCHEMA_READY, QUERY_GET_ACTIVE_LOVER_ROWS_FOR_DAILY,
|
||||
QUERY_GET_ACTIVE_LOVER_ROWS_FOR_MONTHLY, QUERY_GET_LOVER_PREGNANCY_CANDIDATES,
|
||||
QUERY_GET_MARRIAGE_ROWS, QUERY_INSERT_CHILD, QUERY_INSERT_CHILD_RELATION_LOVER,
|
||||
QUERY_COUNT_LOVER_CHILDREN_FOR_USER, QUERY_FAMILY_SCHEMA_READY,
|
||||
QUERY_GET_ACTIVE_LOVER_ROWS_FOR_DAILY, QUERY_GET_ACTIVE_LOVER_ROWS_FOR_MONTHLY,
|
||||
QUERY_GET_LOVER_PREGNANCY_CANDIDATES, QUERY_GET_MARRIAGE_ROWS,
|
||||
QUERY_GET_USER_HOUSE_ROW_BY_USER, QUERY_INSERT_CHILD, QUERY_INSERT_CHILD_RELATION_LOVER,
|
||||
QUERY_LOVER_BIRTH_PENALTY_MARRIAGE, QUERY_LOVER_BIRTH_PENALTY_REPUTATION,
|
||||
QUERY_MARK_LOVER_DAILY_DONE, QUERY_MARK_LOVER_MONTHLY_DONE,
|
||||
QUERY_MARRIAGE_SUBTRACT_SATISFACTION, QUERY_RESET_LOVER_UNDERPAY_COUNTERS,
|
||||
QUERY_UPDATE_CHARACTER_REPUTATION, QUERY_UPDATE_LOVER_UNDERPAY_STATE,
|
||||
QUERY_UPDATE_LOVER_VISIBILITY_DISCRETION, QUERY_UPDATE_MARRIAGE_STATE_AND_BUFFS,
|
||||
QUERY_UPDATE_USER_HOUSE_TENSION_BY_USER,
|
||||
};
|
||||
use crate::db::{ConnectionPool, DbError};
|
||||
use crate::message_broker::MessageBroker;
|
||||
@@ -137,6 +140,7 @@ impl FalukantFamilyWorker {
|
||||
m1: parse_i32(&r, "m1", -1),
|
||||
m2: parse_i32(&r, "m2", -1),
|
||||
satisfaction: parse_i32(&r, "marriage_satisfaction", 55),
|
||||
public_stability: parse_i32(&r, "marriage_public_stability", 55),
|
||||
drift_high: parse_i32(&r, "marriage_drift_high", 0),
|
||||
drift_low: parse_i32(&r, "marriage_drift_low", 0),
|
||||
title1_tr: r.get("title1_tr").cloned().unwrap_or_default(),
|
||||
@@ -147,6 +151,10 @@ impl FalukantFamilyWorker {
|
||||
feast_pending: parse_i32(&r, "marriage_pending_feast_bonus", 0),
|
||||
house_supply: parse_i32(&r, "marriage_house_supply", 50),
|
||||
no_lover_counter: parse_i32(&r, "marriage_no_lover_bonus_counter", 0),
|
||||
household_order_1: parse_i32(&r, "household_order_1", 55),
|
||||
household_order_2: parse_i32(&r, "household_order_2", 55),
|
||||
servant_quality_1: parse_i32(&r, "servant_quality_1", 50),
|
||||
servant_quality_2: parse_i32(&r, "servant_quality_2", 50),
|
||||
})
|
||||
})
|
||||
.filter(|m| m.id > 0)
|
||||
@@ -166,6 +174,8 @@ impl FalukantFamilyWorker {
|
||||
maintenance_level: parse_i32(&r, "maintenance_level", 50),
|
||||
status_fit: parse_i32(&r, "status_fit", 0),
|
||||
scandal_extra: parse_i32(&r, "scandal_extra_daily_pct", 0),
|
||||
months_underfunded: parse_i32(&r, "months_underfunded", 0),
|
||||
acknowledged: parse_bool_row(&r, "acknowledged"),
|
||||
title1_tr: r.get("title1_tr").cloned().unwrap_or_default(),
|
||||
title2_tr: r.get("title2_tr").cloned().unwrap_or_default(),
|
||||
user1_id: parse_opt_i32(&r, "user1_id"),
|
||||
@@ -267,6 +277,7 @@ impl FalukantFamilyWorker {
|
||||
|
||||
let touching_empty = touching.is_empty();
|
||||
let sat0 = m.satisfaction;
|
||||
let pst0 = m.public_stability;
|
||||
let dh0 = m.drift_high;
|
||||
let dl0 = m.drift_low;
|
||||
let g0 = m.gift_days;
|
||||
@@ -346,12 +357,58 @@ impl FalukantFamilyWorker {
|
||||
m.feast_pending = feast;
|
||||
m.no_lover_counter = nl_counter;
|
||||
|
||||
let mut pst = m.public_stability;
|
||||
if !touching_empty {
|
||||
let max_vis = touching.iter().map(|l| l.visibility).max().unwrap_or(0);
|
||||
pst -= (max_vis / 25).min(4);
|
||||
if touching.iter().any(|l| l.visibility >= 60) {
|
||||
pst -= 2;
|
||||
}
|
||||
if touching.iter().any(|l| l.months_underfunded >= 1) {
|
||||
pst -= 1;
|
||||
}
|
||||
if touching.iter().any(|l| l.months_underfunded >= 3) {
|
||||
pst -= 1;
|
||||
}
|
||||
if touching.iter().any(|l| l.acknowledged) {
|
||||
pst -= 2;
|
||||
}
|
||||
let g1 = title_group(&m.title1_tr);
|
||||
let g2 = title_group(&m.title2_tr);
|
||||
if (g1 as i32 - g2 as i32).abs() >= 2 {
|
||||
pst -= 1;
|
||||
}
|
||||
if touching.iter().any(|l| l.min_age_years <= 15) {
|
||||
pst -= 2;
|
||||
}
|
||||
let ho_avg = (m.household_order_1 + m.household_order_2) / 2;
|
||||
if ho_avg < 35 {
|
||||
pst -= 2;
|
||||
} else if ho_avg < 50 {
|
||||
pst -= 1;
|
||||
}
|
||||
let sq_avg = (m.servant_quality_1 + m.servant_quality_2) / 2;
|
||||
if sq_avg < 40 {
|
||||
pst -= 1;
|
||||
}
|
||||
if sat < 40 {
|
||||
pst -= 1;
|
||||
}
|
||||
} else if pst < 55 {
|
||||
pst += 1;
|
||||
} else if pst > 55 {
|
||||
pst -= 1;
|
||||
}
|
||||
pst = clamp_i32(pst, 0, 100);
|
||||
m.public_stability = pst;
|
||||
|
||||
conn.execute(
|
||||
"upd_marriage_full",
|
||||
&[&sat, &dh, &dl, &gift_days, &feast, &nl_counter, &m.id],
|
||||
&[&sat, &dh, &dl, &gift_days, &feast, &nl_counter, &pst, &m.id],
|
||||
)?;
|
||||
|
||||
if sat != sat0
|
||||
|| pst != pst0
|
||||
|| dh != dh0
|
||||
|| dl != dl0
|
||||
|| gift_days != g0
|
||||
@@ -363,6 +420,71 @@ impl FalukantFamilyWorker {
|
||||
}
|
||||
}
|
||||
|
||||
// Hausfrieden: Spannungswert 0..100 persistieren (Handoff Ehe & Hausfrieden)
|
||||
let mut tension_users: HashSet<i32> = HashSet::new();
|
||||
for l in &lovers {
|
||||
push_user_id(&mut tension_users, l.user1_id);
|
||||
push_user_id(&mut tension_users, l.user2_id);
|
||||
}
|
||||
for m in &marriages {
|
||||
push_user_id(&mut tension_users, m.user1_id);
|
||||
push_user_id(&mut tension_users, m.user2_id);
|
||||
}
|
||||
|
||||
let mut tension_agg: HashMap<i32, UserTensionAgg> = HashMap::new();
|
||||
for uid in &tension_users {
|
||||
tension_agg.insert(*uid, UserTensionAgg::default());
|
||||
}
|
||||
for l in &lovers {
|
||||
for uid in [l.user1_id, l.user2_id].into_iter().flatten() {
|
||||
if let Some(a) = tension_agg.get_mut(&uid) {
|
||||
a.max_lover_visibility = a.max_lover_visibility.max(l.visibility);
|
||||
if l.acknowledged {
|
||||
a.n_acknowledged += 1;
|
||||
}
|
||||
a.max_months_underfunded = a.max_months_underfunded.max(l.months_underfunded);
|
||||
}
|
||||
}
|
||||
}
|
||||
for m in &marriages {
|
||||
for uid in [m.user1_id, m.user2_id].into_iter().flatten() {
|
||||
if let Some(a) = tension_agg.get_mut(&uid) {
|
||||
a.min_marriage_sat = a.min_marriage_sat.min(m.satisfaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conn.prepare("upd_tension", QUERY_UPDATE_USER_HOUSE_TENSION_BY_USER)?;
|
||||
conn.prepare("child_cnt_lover", QUERY_COUNT_LOVER_CHILDREN_FOR_USER)?;
|
||||
conn.prepare("uh_row_u", QUERY_GET_USER_HOUSE_ROW_BY_USER)?;
|
||||
|
||||
let mut tension_socket_users: HashSet<i32> = HashSet::new();
|
||||
for (&uid, agg) in &tension_agg {
|
||||
let child_rows = conn.execute("child_cnt_lover", &[&uid])?;
|
||||
let lover_children = child_rows
|
||||
.first()
|
||||
.map(|r| parse_i32(r, "cnt", 0))
|
||||
.unwrap_or(0);
|
||||
|
||||
let house_rows = conn.execute("uh_row_u", &[&uid])?;
|
||||
let Some(hr) = house_rows.first() else {
|
||||
continue;
|
||||
};
|
||||
let ho = parse_i32(hr, "household_order", 55);
|
||||
let sq = parse_i32(hr, "servant_quality", 50);
|
||||
let pay = hr
|
||||
.get("servant_pay_level")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "normal".into());
|
||||
let prev_ts = parse_i32(hr, "prev_tension_score", 0);
|
||||
|
||||
let score = compute_household_tension_score(agg, lover_children, ho, sq, pay.as_str());
|
||||
if score != prev_ts {
|
||||
conn.execute("upd_tension", &[&score, &uid])?;
|
||||
tension_socket_users.insert(uid);
|
||||
}
|
||||
}
|
||||
|
||||
conn.prepare("upd_rep", QUERY_UPDATE_CHARACTER_REPUTATION)?;
|
||||
for l in &lovers {
|
||||
let g = pair_rank_group(&l.title1_tr, &l.title2_tr);
|
||||
@@ -497,6 +619,7 @@ impl FalukantFamilyWorker {
|
||||
push_user_id(&mut notify, char_user.get(cid).copied().flatten());
|
||||
}
|
||||
notify.extend(marriage_socket_users);
|
||||
notify.extend(tension_socket_users);
|
||||
self.publish_falukant_update_family_batch(¬ify, "daily");
|
||||
|
||||
Ok(())
|
||||
@@ -766,6 +889,8 @@ struct MarriageData {
|
||||
m1: i32,
|
||||
m2: i32,
|
||||
satisfaction: i32,
|
||||
/// Öffentliche Ehe-Stabilität (Daemon-Drift).
|
||||
public_stability: i32,
|
||||
drift_high: i32,
|
||||
drift_low: i32,
|
||||
title1_tr: String,
|
||||
@@ -780,6 +905,10 @@ struct MarriageData {
|
||||
house_supply: i32,
|
||||
/// Tageszähler für Haus-Bonus (0..4).
|
||||
no_lover_counter: i32,
|
||||
household_order_1: i32,
|
||||
household_order_2: i32,
|
||||
servant_quality_1: i32,
|
||||
servant_quality_2: i32,
|
||||
}
|
||||
|
||||
struct LoverData {
|
||||
@@ -793,6 +922,8 @@ struct LoverData {
|
||||
maintenance_level: i32,
|
||||
status_fit: i32,
|
||||
scandal_extra: i32,
|
||||
months_underfunded: i32,
|
||||
acknowledged: bool,
|
||||
title1_tr: String,
|
||||
title2_tr: String,
|
||||
user1_id: Option<i32>,
|
||||
@@ -804,6 +935,27 @@ struct LoverData {
|
||||
servant_disc_u2: i32,
|
||||
}
|
||||
|
||||
/// Aggregation für `household_tension_score` (0..100) pro Falukant-User.
|
||||
#[derive(Clone)]
|
||||
struct UserTensionAgg {
|
||||
max_lover_visibility: i32,
|
||||
n_acknowledged: i32,
|
||||
max_months_underfunded: i32,
|
||||
/// Mindest-Ehezufriedenheit aller Ehen dieses Users (100 = keine belastende Ehe).
|
||||
min_marriage_sat: i32,
|
||||
}
|
||||
|
||||
impl Default for UserTensionAgg {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_lover_visibility: 0,
|
||||
n_acknowledged: 0,
|
||||
max_months_underfunded: 0,
|
||||
min_marriage_sat: 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_user_id(set: &mut HashSet<i32>, uid: Option<i32>) {
|
||||
if let Some(id) = uid.filter(|x| *x > 0) {
|
||||
set.insert(id);
|
||||
@@ -820,10 +972,50 @@ fn parse_opt_i32(row: &crate::db::Row, key: &str) -> Option<i32> {
|
||||
row.get(key).and_then(|v| v.parse::<i32>().ok())
|
||||
}
|
||||
|
||||
fn parse_bool_row(row: &crate::db::Row, key: &str) -> bool {
|
||||
row.get(key)
|
||||
.map(|v| {
|
||||
matches!(
|
||||
v.as_str(),
|
||||
"t" | "true" | "True" | "1" | "TRUE"
|
||||
)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn clamp_i32(v: i32, lo: i32, hi: i32) -> i32 {
|
||||
v.max(lo).min(hi)
|
||||
}
|
||||
|
||||
/// Hausfrieden 0..100 (persistiert als `user_house.household_tension_score`).
|
||||
fn compute_household_tension_score(
|
||||
agg: &UserTensionAgg,
|
||||
lover_children: i32,
|
||||
household_order: i32,
|
||||
servant_quality: i32,
|
||||
servant_pay: &str,
|
||||
) -> i32 {
|
||||
let mut s: f64 = 0.0;
|
||||
s += agg.max_lover_visibility as f64 * 0.35;
|
||||
s += agg.n_acknowledged as f64 * 12.0;
|
||||
s += agg.max_months_underfunded as f64 * 8.0;
|
||||
s += lover_children as f64 * 10.0;
|
||||
s += (100 - household_order.clamp(0, 100)) as f64 * 0.22;
|
||||
if servant_pay == "low" {
|
||||
s += 6.0;
|
||||
}
|
||||
s += (50 - servant_quality.min(50)).max(0) as f64 * 0.35;
|
||||
if agg.min_marriage_sat < 100 {
|
||||
if agg.min_marriage_sat < 40 {
|
||||
s += 18.0;
|
||||
} else if agg.min_marriage_sat < 55 {
|
||||
s += 8.0;
|
||||
}
|
||||
}
|
||||
s = s.clamp(0.0, 100.0);
|
||||
s.round() as i32
|
||||
}
|
||||
|
||||
fn title_group(tr: &str) -> u8 {
|
||||
match tr {
|
||||
"noncivil" | "civil" | "sir" => 0,
|
||||
|
||||
@@ -12,6 +12,7 @@ mod transport;
|
||||
mod weather;
|
||||
mod events;
|
||||
mod falukant_family;
|
||||
mod falukant_certificate;
|
||||
mod falukant_servants;
|
||||
mod sql;
|
||||
|
||||
@@ -30,4 +31,5 @@ pub use transport::TransportWorker;
|
||||
pub use weather::WeatherWorker;
|
||||
pub use events::EventsWorker;
|
||||
pub use falukant_family::FalukantFamilyWorker;
|
||||
pub use falukant_certificate::FalukantCertificateWorker;
|
||||
|
||||
|
||||
@@ -2162,6 +2162,7 @@ pub const QUERY_GET_ACTIVE_LOVER_ROWS_FOR_DAILY: &str = r#"
|
||||
rs.monthly_base_cost,
|
||||
rs.scandal_extra_daily_pct,
|
||||
rs.months_underfunded,
|
||||
COALESCE(rs.acknowledged, false) AS acknowledged,
|
||||
c1.gender AS g1,
|
||||
c2.gender AS g2,
|
||||
COALESCE(t1.tr, '') AS title1_tr,
|
||||
@@ -2267,6 +2268,7 @@ pub const QUERY_GET_MARRIAGE_ROWS: &str = r#"
|
||||
r.character1_id AS m1,
|
||||
r.character2_id AS m2,
|
||||
r.marriage_satisfaction,
|
||||
COALESCE(r.marriage_public_stability, 55)::int AS marriage_public_stability,
|
||||
r.marriage_drift_high,
|
||||
r.marriage_drift_low,
|
||||
COALESCE(t1.tr, '') AS title1_tr,
|
||||
@@ -2276,7 +2278,23 @@ pub const QUERY_GET_MARRIAGE_ROWS: &str = r#"
|
||||
COALESCE(r.marriage_gift_buff_days_remaining, 0)::int AS marriage_gift_buff_days_remaining,
|
||||
COALESCE(r.marriage_pending_feast_bonus, 0)::int AS marriage_pending_feast_bonus,
|
||||
COALESCE(r.marriage_house_supply, 50)::int AS marriage_house_supply,
|
||||
COALESCE(r.marriage_no_lover_bonus_counter, 0)::int AS marriage_no_lover_bonus_counter
|
||||
COALESCE(r.marriage_no_lover_bonus_counter, 0)::int AS marriage_no_lover_bonus_counter,
|
||||
COALESCE((
|
||||
SELECT uh.household_order FROM falukant_data.user_house uh
|
||||
WHERE uh.user_id = c1.user_id ORDER BY uh.id LIMIT 1
|
||||
), 55)::int AS household_order_1,
|
||||
COALESCE((
|
||||
SELECT uh.household_order FROM falukant_data.user_house uh
|
||||
WHERE uh.user_id = c2.user_id ORDER BY uh.id LIMIT 1
|
||||
), 55)::int AS household_order_2,
|
||||
COALESCE((
|
||||
SELECT uh.servant_quality FROM falukant_data.user_house uh
|
||||
WHERE uh.user_id = c1.user_id ORDER BY uh.id LIMIT 1
|
||||
), 50)::int AS servant_quality_1,
|
||||
COALESCE((
|
||||
SELECT uh.servant_quality FROM falukant_data.user_house uh
|
||||
WHERE uh.user_id = c2.user_id ORDER BY uh.id LIMIT 1
|
||||
), 50)::int AS servant_quality_2
|
||||
FROM falukant_data.relationship r
|
||||
JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id
|
||||
AND rt.tr IN ('married', 'engaged', 'wooing')
|
||||
@@ -2298,6 +2316,7 @@ pub const QUERY_UPDATE_MARRIAGE_STATE: &str = r#"
|
||||
"#;
|
||||
|
||||
/// Inkl. Geschenk-/Fest-/Haus-Zähler (Migration `003_falukant_family_marriage_buffs.sql`).
|
||||
/// `marriage_public_stability`: Migration `005_falukant_marriage_housepeace.sql`.
|
||||
pub const QUERY_UPDATE_MARRIAGE_STATE_AND_BUFFS: &str = r#"
|
||||
UPDATE falukant_data.relationship
|
||||
SET marriage_satisfaction = $1::smallint,
|
||||
@@ -2305,8 +2324,48 @@ pub const QUERY_UPDATE_MARRIAGE_STATE_AND_BUFFS: &str = r#"
|
||||
marriage_drift_low = $3::smallint,
|
||||
marriage_gift_buff_days_remaining = $4::smallint,
|
||||
marriage_pending_feast_bonus = $5::smallint,
|
||||
marriage_no_lover_bonus_counter = $6::smallint
|
||||
WHERE id = $7::int;
|
||||
marriage_no_lover_bonus_counter = $6::smallint,
|
||||
marriage_public_stability = $7::smallint
|
||||
WHERE id = $8::int;
|
||||
"#;
|
||||
|
||||
/// Persistiert berechneten Hausfrieden (0..100), Migration `005_falukant_marriage_housepeace.sql`.
|
||||
pub const QUERY_UPDATE_USER_HOUSE_TENSION_BY_USER: &str = r#"
|
||||
UPDATE falukant_data.user_house
|
||||
SET household_tension_score = $1::smallint
|
||||
WHERE user_id = $2::int;
|
||||
"#;
|
||||
|
||||
/// Kinder aus Liebschaften, die einen Charakter dieses Falukant-Users betreffen.
|
||||
pub const QUERY_COUNT_LOVER_CHILDREN_FOR_USER: &str = r#"
|
||||
SELECT COUNT(*)::int AS cnt
|
||||
FROM falukant_data.child_relation cr
|
||||
WHERE cr.birth_context = 'lover'
|
||||
AND (
|
||||
EXISTS (
|
||||
SELECT 1 FROM falukant_data.character c
|
||||
WHERE c.id = cr.father_character_id AND c.user_id = $1::int
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM falukant_data.character c
|
||||
WHERE c.id = cr.mother_character_id AND c.user_id = $1::int
|
||||
)
|
||||
);
|
||||
"#;
|
||||
|
||||
/// Haushalt für Spannungsberechnung (ein Eintrag pro Zeile; typisch eine Zeile pro User).
|
||||
pub const QUERY_GET_USER_HOUSE_ROW_BY_USER: &str = r#"
|
||||
SELECT uh.id AS user_house_id,
|
||||
uh.user_id AS falukant_user_id,
|
||||
COALESCE(uh.household_order, 55)::int AS household_order,
|
||||
COALESCE(uh.servant_count, 0)::int AS servant_count,
|
||||
COALESCE(uh.servant_quality, 50)::int AS servant_quality,
|
||||
COALESCE(NULLIF(TRIM(uh.servant_pay_level), ''), 'normal') AS servant_pay_level,
|
||||
COALESCE(uh.household_tension_score, 0)::int AS prev_tension_score
|
||||
FROM falukant_data.user_house uh
|
||||
WHERE uh.user_id = $1::int
|
||||
ORDER BY uh.id
|
||||
LIMIT 1;
|
||||
"#;
|
||||
|
||||
pub const QUERY_MARRIAGE_SUBTRACT_SATISFACTION: &str = r#"
|
||||
@@ -2438,3 +2497,57 @@ pub const QUERY_INSERT_CHILD_RELATION_LOVER: &str = r#"
|
||||
);
|
||||
"#;
|
||||
|
||||
// --- Produktionszertifikat (Daemon Daily, Spec: Produktionszertifikate) ---
|
||||
|
||||
/// Ein Spielercharakter pro Falukant-User (niedrigste character.id bei mehreren lebenden).
|
||||
pub const QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS: &str = r#"
|
||||
SELECT DISTINCT ON (fu.id)
|
||||
fu.id AS falukant_user_id,
|
||||
COALESCE(fu.certificate, 1)::int AS certificate,
|
||||
COALESCE(fu.money, 0)::float8 AS money,
|
||||
c.id AS character_id,
|
||||
COALESCE(c.reputation, 50)::float8 AS reputation,
|
||||
COALESCE(t.level, 0)::int AS title_level,
|
||||
COALESCE((
|
||||
SELECT AVG(k.knowledge)::float8
|
||||
FROM falukant_data.knowledge k
|
||||
WHERE k.character_id = c.id
|
||||
), 0.0) AS avg_knowledge,
|
||||
COALESCE((
|
||||
SELECT COUNT(*)::bigint
|
||||
FROM falukant_log.production pl
|
||||
WHERE pl.producer_id = fu.id
|
||||
), 0) AS completed_production_count,
|
||||
COALESCE((
|
||||
SELECT MAX(cot.hierarchy_level)::int
|
||||
FROM falukant_data.church_office co
|
||||
JOIN falukant_type.church_office_type cot ON cot.id = co.office_type_id
|
||||
WHERE co.character_id = c.id
|
||||
), 0) AS max_church_hierarchy,
|
||||
COALESCE((
|
||||
SELECT STRING_AGG(DISTINCT pot.name, '|')
|
||||
FROM falukant_data.political_office po
|
||||
JOIN falukant_type.political_office_type pot ON pot.id = po.office_type_id
|
||||
WHERE po.character_id = c.id
|
||||
AND (po.created_at + (pot.term_length * INTERVAL '1 day')) > NOW()
|
||||
), '') AS political_office_names,
|
||||
COALESCE((
|
||||
SELECT h.position::int
|
||||
FROM falukant_data.user_house uh
|
||||
JOIN falukant_type.house h ON h.id = uh.house_type_id
|
||||
WHERE uh.user_id = fu.id
|
||||
ORDER BY uh.id
|
||||
LIMIT 1
|
||||
), 0) AS house_position
|
||||
FROM falukant_data.falukant_user fu
|
||||
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
|
||||
ORDER BY fu.id, c.id;
|
||||
"#;
|
||||
|
||||
pub const QUERY_UPDATE_FALUKANT_USER_CERTIFICATE: &str = r#"
|
||||
UPDATE falukant_data.falukant_user
|
||||
SET certificate = $1::int
|
||||
WHERE id = $2::int;
|
||||
"#;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user