Enhance Falukant family dynamics: Updated the FalukantFamilyWorker to incorporate marriage buffs and detailed age rules for relationships. Added new WebSocket events for real-time updates and expanded SQL queries to support marriage state and buff management, improving overall family interaction and satisfaction tracking.
This commit is contained in:
@@ -17,7 +17,52 @@ Technische Abstimmung mit dem Übergabedokument im Backend-Projekt (`FALUKANT_LO
|
||||
|
||||
**Hinweis:** Der Worker nutzt weiterhin **Wandzeit** (24 h / 30 Tage) als Intervall; die Idempotenz über die Zeitstempel verhindert Doppelverarbeitung bei Neustarts am selben Tag/Monat.
|
||||
|
||||
## WebSocket-Events (UI)
|
||||
|
||||
Pro betroffener `falukant_user.id` werden über den **MessageBroker** (Broadcast an alle WS-Clients) gesendet:
|
||||
|
||||
| Event | Payload (Beispiel) | Wann |
|
||||
|-------|-------------------|------|
|
||||
| `falukantUpdateFamily` | `{"event":"falukantUpdateFamily","user_id":N,"reason":"…"}` | Familie/Liebe relevant |
|
||||
| `falukantUpdateStatus` | `{"event":"falukantUpdateStatus","user_id":N}` | Immer gleich mit `falukantUpdateFamily` (Refresh) |
|
||||
| `children_update` | `{"event":"children_update","user_id":N}` | Kind aus Liebschaft |
|
||||
| `falukant_family_scandal_hint` | `{"event":"falukant_family_scandal_hint","relationship_id":…}` | Skandal (ohne `user_id`) |
|
||||
|
||||
**`reason`** bei `falukantUpdateFamily`: `daily`, `monthly`, `scandal`, `lover_birth`.
|
||||
|
||||
Die UI kann auf `falukantUpdateFamily` filtern und nach `reason` unterscheiden; `falukantUpdateStatus` wie bisher für allgemeinen Daten-Refresh nutzen.
|
||||
|
||||
**Detaillierte UI-Anleitung (Payloads, Handler, Checkliste):** [`FALUKANT_UI_WEBSOCKET.md`](./FALUKANT_UI_WEBSOCKET.md)
|
||||
|
||||
## Altersregeln (Spec-Erweiterung, im Daemon umgesetzt)
|
||||
|
||||
`min_age_years` = jüngeres Alter beider Partner in **ganzen Jahren** (`LEAST(…/365, …/365)` aus `birthdate`).
|
||||
|
||||
| Bereich | Ansehen (zusätzl. zur Basisformel, Spec 5a) | Ehezufriedenheit (Spec §3 neg.) | Skandalrisiko (+ %, exkl. Stufen) |
|
||||
|---------|---------------------------------------------|----------------------------------|-----------------------------------|
|
||||
| ≤ 13 | −1,5 / Tag | −1 / Tag bei aktiver Berührung | +6 % |
|
||||
| ≤ 15 | −0,8 / Tag | −1 / Tag (zusätzlich zu anderen Malus) | +3 % |
|
||||
| ≤ 17 | −0,3 / Tag | — | +1 % |
|
||||
| ≥ 18 | 0 | — | 0 |
|
||||
|
||||
Zusätzlich: wenn `min_age_years ≤ 15` **und** `visibility ≥ 50`: weiterer Ansehens-Malus **−0,5** / Tag.
|
||||
|
||||
Ehe-Malus „≤ 15“ gilt pro Ehe, wenn **irgendeine** berührende Liebschaft dieses Altersprofils hat.
|
||||
|
||||
## Migrationen
|
||||
|
||||
1. `migrations/001_falukant_family_lovers.sql`
|
||||
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`.
|
||||
|
||||
### Ehe-Buffs (Daemon)
|
||||
|
||||
- **Geschenk:** Backend setzt `marriage_gift_buff_days_remaining` (z. B. 5); Daemon **+1** Zufriedenheit/Tag, Zähler −1.
|
||||
- **Fest:** `marriage_pending_feast_bonus` einmalig (typ. +2…+5, max. 5 pro Tick) beim Daily verbrauchen.
|
||||
- **Haus:** `marriage_house_supply ≥ 65` und **keine** berührende Liebschaft: alle **4** Tage **+1** Zufriedenheit (`marriage_no_lover_bonus_counter`).
|
||||
|
||||
### Ansehen (Daily)
|
||||
|
||||
- **Ranggruppe 3:** Reputations-Multiplikator **0,7** nur bei „geordneter“ Liebschaft (`order_ok`: u. a. Unterhalt ≥ 65, Diskretion ≥ 60, Sichtbarkeit ≤ 35, höchstens eine Mätresse im Umfeld des Paares); sonst **1,0**; bei Skandal **1,5**.
|
||||
- **Zwei sichtbare Liebschaften** (`visibility ≥ 60`, mindestens zwei Beziehungen pro Charakter): zusätzlich **−4** Ansehen (einmal pro Person/Tag).
|
||||
- **Zufall:** selten **Gerücht −3** oder **Tadel −5** (niedrige Wahrscheinlichkeit) für Charaktere mit Liebschaftsbezug.
|
||||
|
||||
160
docs/FALUKANT_UI_WEBSOCKET.md
Normal file
160
docs/FALUKANT_UI_WEBSOCKET.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Falukant: UI-Anpassung – WebSocket & Familie / Liebschaften
|
||||
|
||||
Dieses Dokument beschreibt die **Nachrichten**, die der **YpDaemon** (`FalukantFamilyWorker`) über den WebSocket-Broadcast sendet, damit die **UI gezielt** reagieren kann (Refresh, Toasts, Family-Ansicht).
|
||||
|
||||
> **Transport:** Alle Clients erhalten denselben Broadcast. Die UI sollte Nachrichten **nach `user_id` filtern** (nur Events anzeigen/verarbeiten, die zur eingeloggten Session passen).
|
||||
|
||||
---
|
||||
|
||||
## 1. Übersicht der Events
|
||||
|
||||
| `event` | Pflichtfelder | Typische UI-Reaktion |
|
||||
|---------|----------------|----------------------|
|
||||
| `falukantUpdateFamily` | `user_id`, `reason` | Gezielter Refresh Familie/Liebe/Geld je nach `reason` |
|
||||
| `falukantUpdateStatus` | `user_id` | Allgemeiner Status-/Spielstand-Refresh (wie bisher) |
|
||||
| `children_update` | `user_id` | Kinderliste / FamilyView aktualisieren |
|
||||
| `falukant_family_scandal_hint` | `relationship_id` | Optional: Toast, Log – **kein** `user_id` (siehe unten) |
|
||||
|
||||
---
|
||||
|
||||
## 2. JSON-Payloads (exakt)
|
||||
|
||||
### 2.1 `falukantUpdateFamily`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateFamily",
|
||||
"user_id": 123,
|
||||
"reason": "daily"
|
||||
}
|
||||
```
|
||||
|
||||
**`reason`** ist immer einer der folgenden **festen** Strings:
|
||||
|
||||
| `reason` | Bedeutung (Daemon) | Empfehlung UI |
|
||||
|----------|---------------------|---------------|
|
||||
| `daily` | Daily-Tick: Liebschafts-/Ehe-/Ansehens-Logik für den Tag | Family-API + ggf. Charakter/Ansehen 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 |
|
||||
|
||||
### 2.2 `falukantUpdateStatus`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukantUpdateStatus",
|
||||
"user_id": 123
|
||||
}
|
||||
```
|
||||
|
||||
Kommt **typischerweise direkt nach** `falukantUpdateFamily` mit derselben `user_id` (gemeinsamer Refresh).
|
||||
|
||||
### 2.3 `children_update`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "children_update",
|
||||
"user_id": 123
|
||||
}
|
||||
```
|
||||
|
||||
Tritt bei **Geburt aus Liebschaft** auf; oft zusammen mit `falukantUpdateFamily` (`reason: lover_birth`) und `falukantUpdateStatus`.
|
||||
|
||||
### 2.4 `falukant_family_scandal_hint`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "falukant_family_scandal_hint",
|
||||
"relationship_id": 456
|
||||
}
|
||||
```
|
||||
|
||||
- **Kein** `user_id` – Betroffene erkennst du nur über die **Beziehung** (Backend: `relationship.id` → Charaktere laden) oder du ignorierst das Event und verlässt dich auf `falukantUpdateFamily` mit `reason: scandal` für deine Nutzer.
|
||||
|
||||
---
|
||||
|
||||
## 3. Empfohlene Handler-Logik (Pseudo)
|
||||
|
||||
```text
|
||||
onMessage(json):
|
||||
if json.user_id != currentUserId: return // Broadcast filtern
|
||||
|
||||
switch json.event:
|
||||
case "falukantUpdateStatus":
|
||||
refreshPlayerStatus() // bestehend
|
||||
return
|
||||
|
||||
case "children_update":
|
||||
refreshChildrenAndFamilyView()
|
||||
return
|
||||
|
||||
case "falukantUpdateFamily":
|
||||
switch json.reason:
|
||||
case "daily":
|
||||
refreshFamilyAndRelationships()
|
||||
refreshCharactersReputationIfNeeded()
|
||||
break
|
||||
case "monthly":
|
||||
refreshMoney()
|
||||
refreshFamilyAndRelationships()
|
||||
break
|
||||
case "scandal":
|
||||
showScandalToastOptional()
|
||||
refreshFamilyAndRelationships()
|
||||
break
|
||||
case "lover_birth":
|
||||
refreshChildrenAndFamilyView()
|
||||
break
|
||||
return
|
||||
|
||||
case "falukant_family_scandal_hint":
|
||||
// optional: relationship_id → Detail-Modal / Log
|
||||
return
|
||||
```
|
||||
|
||||
**Hinweis:** Am selben Tag kann ein Nutzer **`scandal`** und danach **`daily`** erhalten – UI kann **deduplizieren** (z. B. nur ein voller Refresh) oder beide verarbeiten (idempotente API-Calls).
|
||||
|
||||
---
|
||||
|
||||
## 4. Welche Backend-Daten neu laden?
|
||||
|
||||
| Situation | Sinnvolle Endpunkte / Daten (konzeptionell) |
|
||||
|-----------|---------------------------------------------|
|
||||
| Jede `falukantUpdateFamily` | Family-/Relationship-API mit `relationship_state`, Ehe (`married`/`engaged`/`wooing`) |
|
||||
| `reason: monthly` | **Geld** des Users, ggf. Kredit/Log |
|
||||
| `reason: daily` / `scandal` | Ansehen (`character.reputation`), Sichtbarkeit/Diskretion der Liebschaften |
|
||||
| `children_update` / `lover_birth` | `child_relation` inkl. `legitimacy`, `birth_context`, `public_known` |
|
||||
|
||||
Konkrete Routen stehen im **YourPart3**-Backend; das Frontend sollte eine zentrale Funktion `refreshFamilyContext(userId)` kapseln.
|
||||
|
||||
---
|
||||
|
||||
## 5. Sonderfälle
|
||||
|
||||
| Fall | Verhalten |
|
||||
|------|-----------|
|
||||
| Charakter ohne `user_id` (NPC) | **Keine** Socket-Events für diesen Charakter – nur Spieler mit `falukant_user` erhalten `user_id`-Events. |
|
||||
| Mehrere Events hintereinander | Normal; Requests sollten **idempotent** sein (mehrfaches Laden ok). |
|
||||
| Nur `falukantUpdateStatus` ohne Family | Kann von **anderen** Workern kommen – nicht nur Familie. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Bezug zum Code (YpDaemon)
|
||||
|
||||
- Worker: `src/worker/falukant_family.rs`
|
||||
- SQL-Konstanten: `src/worker/sql.rs` (Abschnitt Falukant Familie)
|
||||
- Schema: `migrations/001_falukant_family_lovers.sql`
|
||||
- Daemon-Handoff (technisch): `docs/FALUKANT_DAEMON_HANDOFF.md`
|
||||
|
||||
---
|
||||
|
||||
## 7. Checkliste UI-Integration
|
||||
|
||||
- [ ] WebSocket-Handler: `user_id` mit Session abgleichen
|
||||
- [ ] Auf `falukantUpdateFamily` reagieren und **`reason`** auswerten
|
||||
- [ ] `falukantUpdateStatus` weiter nutzen (globaler Refresh)
|
||||
- [ ] `children_update` + `lover_birth`: Kinder-Ansicht
|
||||
- [ ] Optional: `falukant_family_scandal_hint` mit `relationship_id`
|
||||
- [ ] Optional: Deduplizierung bei `scandal` + `daily` am selben Tag
|
||||
|
||||
Damit kannst du die Oberfläche **gezielt** an die Daemon-Events anbinden, ohne jedes Mal den vollen Spielstand blind zu aktualisieren.
|
||||
17
migrations/003_falukant_family_marriage_buffs.sql
Normal file
17
migrations/003_falukant_family_marriage_buffs.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Ehe-Buffs (Spec: Geschenke, Fest, Hausversorgung ohne Liebschaft) + Backend-Steuerung
|
||||
ALTER TABLE falukant_data.relationship
|
||||
ADD COLUMN IF NOT EXISTS marriage_gift_buff_days_remaining smallint NOT NULL DEFAULT 0
|
||||
CHECK (marriage_gift_buff_days_remaining >= 0 AND marriage_gift_buff_days_remaining <= 30),
|
||||
ADD COLUMN IF NOT EXISTS marriage_pending_feast_bonus smallint NOT NULL DEFAULT 0
|
||||
CHECK (marriage_pending_feast_bonus >= 0 AND marriage_pending_feast_bonus <= 20),
|
||||
ADD COLUMN IF NOT EXISTS marriage_house_supply smallint NOT NULL DEFAULT 50
|
||||
CHECK (marriage_house_supply >= 0 AND marriage_house_supply <= 100),
|
||||
ADD COLUMN IF NOT EXISTS marriage_no_lover_bonus_counter smallint NOT NULL DEFAULT 0
|
||||
CHECK (marriage_no_lover_bonus_counter >= 0 AND marriage_no_lover_bonus_counter < 4);
|
||||
|
||||
COMMENT ON COLUMN falukant_data.relationship.marriage_gift_buff_days_remaining IS
|
||||
'Backend setzt z.B. 5 nach Geschenk: Daemon +1 Ehezufriedenheit/Tag, Zähler runter';
|
||||
COMMENT ON COLUMN falukant_data.relationship.marriage_pending_feast_bonus IS
|
||||
'Einmal +2..+5 beim nächsten Daily-Tick (Backend), danach 0';
|
||||
COMMENT ON COLUMN falukant_data.relationship.marriage_house_supply IS
|
||||
'Hausversorgung 0..100; ab ~65: +1 Ehezufriedenheit alle 4 Tage ohne aktive Liebschaft';
|
||||
@@ -1,6 +1,9 @@
|
||||
//! Liebhaber, Ehezufriedenheit, Ansehen, Monatskosten (Handoff: docs/FALUKANT_DAEMON_HANDOFF.md).
|
||||
//! Benötigt `migrations/001_falukant_family_lovers.sql` (ggf. `002` bei Altbestand).
|
||||
//! Benötigt `migrations/001_falukant_family_lovers.sql` (ggf. `002` bei Altbestand), `003` für Ehe-Buffs.
|
||||
//!
|
||||
//! WebSocket: `falukantUpdateFamily` (reason) + `falukantUpdateStatus` für betroffene Nutzer.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -18,7 +21,7 @@ use super::sql::{
|
||||
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,
|
||||
QUERY_UPDATE_LOVER_VISIBILITY_DISCRETION, QUERY_UPDATE_MARRIAGE_STATE_AND_BUFFS,
|
||||
};
|
||||
use crate::db::{ConnectionPool, DbError};
|
||||
use crate::message_broker::MessageBroker;
|
||||
@@ -115,7 +118,7 @@ impl FalukantFamilyWorker {
|
||||
conn.prepare("get_marriages", QUERY_GET_MARRIAGE_ROWS)?;
|
||||
let marriage_rows = conn.execute("get_marriages", &[])?;
|
||||
|
||||
let marriages: Vec<MarriageData> = marriage_rows
|
||||
let mut marriages: Vec<MarriageData> = marriage_rows
|
||||
.into_iter()
|
||||
.filter_map(|r| {
|
||||
Some(MarriageData {
|
||||
@@ -127,6 +130,12 @@ impl FalukantFamilyWorker {
|
||||
drift_low: parse_i32(&r, "marriage_drift_low", 0),
|
||||
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"),
|
||||
user2_id: parse_opt_i32(&r, "user2_id"),
|
||||
gift_days: parse_i32(&r, "marriage_gift_buff_days_remaining", 0),
|
||||
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),
|
||||
})
|
||||
})
|
||||
.filter(|m| m.id > 0)
|
||||
@@ -148,16 +157,31 @@ impl FalukantFamilyWorker {
|
||||
scandal_extra: parse_i32(&r, "scandal_extra_daily_pct", 0),
|
||||
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"),
|
||||
user2_id: parse_opt_i32(&r, "user2_id"),
|
||||
min_age_years: parse_i32(&r, "min_age_years", 99),
|
||||
})
|
||||
})
|
||||
.filter(|l| l.rel_id > 0)
|
||||
.collect();
|
||||
|
||||
let mut char_user: HashMap<i32, Option<i32>> = HashMap::new();
|
||||
for l in &lovers {
|
||||
char_user.insert(l.c1, l.user1_id);
|
||||
char_user.insert(l.c2, l.user2_id);
|
||||
}
|
||||
|
||||
let mut char_lover_count: std::collections::HashMap<i32, usize> =
|
||||
std::collections::HashMap::new();
|
||||
let mut char_visible_lover_rel_count: std::collections::HashMap<i32, usize> =
|
||||
std::collections::HashMap::new();
|
||||
for l in &lovers {
|
||||
*char_lover_count.entry(l.c1).or_insert(0) += 1;
|
||||
*char_lover_count.entry(l.c2).or_insert(0) += 1;
|
||||
if l.visibility >= 60 {
|
||||
*char_visible_lover_rel_count.entry(l.c1).or_insert(0) += 1;
|
||||
*char_visible_lover_rel_count.entry(l.c2).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
conn.prepare("upd_vd", QUERY_UPDATE_LOVER_VISIBILITY_DISCRETION)?;
|
||||
@@ -212,7 +236,11 @@ impl FalukantFamilyWorker {
|
||||
conn.execute("mark_daily", &[&l.rel_id])?;
|
||||
}
|
||||
|
||||
for m in &marriages {
|
||||
let mut marriage_socket_users: HashSet<i32> = HashSet::new();
|
||||
|
||||
conn.prepare("upd_marriage_full", QUERY_UPDATE_MARRIAGE_STATE_AND_BUFFS)?;
|
||||
|
||||
for m in marriages.iter_mut() {
|
||||
let touching: Vec<&LoverData> = lovers
|
||||
.iter()
|
||||
.filter(|l| {
|
||||
@@ -222,10 +250,23 @@ impl FalukantFamilyWorker {
|
||||
})
|
||||
.collect();
|
||||
|
||||
if touching.is_empty() {
|
||||
let mut sat = m.satisfaction;
|
||||
let mut dh = m.drift_high;
|
||||
let mut dl = m.drift_low;
|
||||
let touching_empty = touching.is_empty();
|
||||
let sat0 = m.satisfaction;
|
||||
let dh0 = m.drift_high;
|
||||
let dl0 = m.drift_low;
|
||||
let g0 = m.gift_days;
|
||||
let f0 = m.feast_pending;
|
||||
let n0 = m.no_lover_counter;
|
||||
|
||||
let mut sat = m.satisfaction;
|
||||
let mut dh = m.drift_high;
|
||||
let mut dl = m.drift_low;
|
||||
let mut gift_days = m.gift_days;
|
||||
let mut feast = m.feast_pending;
|
||||
let mut nl_counter = m.no_lover_counter;
|
||||
let house_supply = m.house_supply;
|
||||
|
||||
if touching_empty {
|
||||
if sat > 55 {
|
||||
dh += 1;
|
||||
if dh >= 3 {
|
||||
@@ -239,56 +280,83 @@ impl FalukantFamilyWorker {
|
||||
dl = 0;
|
||||
}
|
||||
}
|
||||
conn.prepare("upd_marriage", QUERY_UPDATE_MARRIAGE_STATE)?;
|
||||
conn.execute(
|
||||
"upd_marriage",
|
||||
&[&sat, &dh, &dl, &m.id],
|
||||
)?;
|
||||
continue;
|
||||
} else {
|
||||
let mg = marriage_rank_group(&m.title1_tr, &m.title2_tr);
|
||||
let mistress_n = touching
|
||||
.iter()
|
||||
.filter(|l| l.lover_role == "mistress_or_favorite")
|
||||
.count();
|
||||
let total_lovers_here = touching.len();
|
||||
let mut delta = 0i32;
|
||||
for l in &touching {
|
||||
delta += lover_marriage_daily_delta(
|
||||
mg,
|
||||
l,
|
||||
m.satisfaction,
|
||||
mistress_n,
|
||||
total_lovers_here,
|
||||
);
|
||||
}
|
||||
if touching.iter().any(|l| l.visibility >= 60) {
|
||||
delta -= 2;
|
||||
}
|
||||
if total_lovers_here >= 2 {
|
||||
delta -= 2;
|
||||
}
|
||||
if touching
|
||||
.iter()
|
||||
.any(|l| l.lover_role == "mistress_or_favorite" && l.maintenance_level < 35)
|
||||
{
|
||||
delta -= 1;
|
||||
}
|
||||
if touching.iter().any(|l| l.min_age_years <= 15) {
|
||||
delta -= 1;
|
||||
}
|
||||
sat = clamp_i32(sat + delta, 0, 100);
|
||||
}
|
||||
|
||||
let mg = marriage_rank_group(&m.title1_tr, &m.title2_tr);
|
||||
let mistress_n = touching
|
||||
.iter()
|
||||
.filter(|l| l.lover_role == "mistress_or_favorite")
|
||||
.count();
|
||||
let total_lovers_here = touching.len();
|
||||
let mut delta = 0i32;
|
||||
for l in &touching {
|
||||
delta += lover_marriage_daily_delta(
|
||||
mg,
|
||||
l,
|
||||
m.satisfaction,
|
||||
mistress_n,
|
||||
total_lovers_here,
|
||||
);
|
||||
}
|
||||
if touching.iter().any(|l| l.visibility >= 60) {
|
||||
delta -= 2;
|
||||
}
|
||||
if total_lovers_here >= 2 {
|
||||
delta -= 2;
|
||||
}
|
||||
if touching
|
||||
.iter()
|
||||
.any(|l| l.lover_role == "mistress_or_favorite" && l.maintenance_level < 35)
|
||||
{
|
||||
delta -= 1;
|
||||
}
|
||||
Self::apply_marriage_positive_buffs(
|
||||
&mut sat,
|
||||
&mut gift_days,
|
||||
&mut feast,
|
||||
&mut nl_counter,
|
||||
touching_empty,
|
||||
house_supply,
|
||||
);
|
||||
|
||||
m.satisfaction = sat;
|
||||
m.drift_high = dh;
|
||||
m.drift_low = dl;
|
||||
m.gift_days = gift_days;
|
||||
m.feast_pending = feast;
|
||||
m.no_lover_counter = nl_counter;
|
||||
|
||||
let sat = clamp_i32(m.satisfaction + delta, 0, 100);
|
||||
conn.prepare("upd_marriage", QUERY_UPDATE_MARRIAGE_STATE)?;
|
||||
conn.execute(
|
||||
"upd_marriage",
|
||||
&[&sat, &m.drift_high, &m.drift_low, &m.id],
|
||||
"upd_marriage_full",
|
||||
&[&sat, &dh, &dl, &gift_days, &feast, &nl_counter, &m.id],
|
||||
)?;
|
||||
|
||||
if sat != sat0
|
||||
|| dh != dh0
|
||||
|| dl != dl0
|
||||
|| gift_days != g0
|
||||
|| feast != f0
|
||||
|| nl_counter != n0
|
||||
{
|
||||
push_user_id(&mut marriage_socket_users, m.user1_id);
|
||||
push_user_id(&mut marriage_socket_users, m.user2_id);
|
||||
}
|
||||
}
|
||||
|
||||
conn.prepare("upd_rep", QUERY_UPDATE_CHARACTER_REPUTATION)?;
|
||||
for l in &lovers {
|
||||
let g = pair_rank_group(&l.title1_tr, &l.title2_tr);
|
||||
let scandal = l.visibility > 70;
|
||||
let rm = rank_rep_modifier(g, scandal);
|
||||
let order_ok = l.maintenance_level >= 65
|
||||
&& l.discretion >= 60
|
||||
&& l.visibility <= 35
|
||||
&& mistress_count_for_pair(&lovers, l.c1, l.c2) <= 1;
|
||||
let rm = rank_rep_modifier(g, scandal, order_ok);
|
||||
let vf = 0.4 + (l.visibility as f64 / 100.0) * 1.6;
|
||||
let base = match l.lover_role.as_str() {
|
||||
"secret_affair" => -0.2,
|
||||
@@ -298,10 +366,6 @@ impl FalukantFamilyWorker {
|
||||
};
|
||||
let mut delta = base * vf * rm;
|
||||
|
||||
let order_ok = l.maintenance_level >= 65
|
||||
&& l.discretion >= 60
|
||||
&& l.visibility <= 35
|
||||
&& mistress_count_for_pair(&lovers, l.c1, l.c2) <= 1;
|
||||
if l.lover_role == "mistress_or_favorite" && order_ok {
|
||||
if g == 2 {
|
||||
delta = 0.1;
|
||||
@@ -312,9 +376,13 @@ impl FalukantFamilyWorker {
|
||||
|
||||
delta = delta.clamp(-3.0, 1.0);
|
||||
|
||||
let age_rep = age_reputation_delta(l.min_age_years);
|
||||
let vis_young = visibility_young_penalty(l.min_age_years, l.visibility);
|
||||
let final_delta = delta + age_rep + vis_young;
|
||||
|
||||
for cid in [l.c1, l.c2] {
|
||||
let cur = fetch_reputation(&mut conn, cid)?;
|
||||
let new_rep = (cur + delta).clamp(0.0, 100.0);
|
||||
let new_rep = (cur + final_delta).clamp(0.0, 100.0);
|
||||
let s = format!("{:.2}", new_rep);
|
||||
conn.execute("upd_rep", &[&s, &cid])?;
|
||||
}
|
||||
@@ -337,7 +405,8 @@ impl FalukantFamilyWorker {
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
+ l.scandal_extra as f64;
|
||||
+ l.scandal_extra as f64
|
||||
+ scandal_age_extra_pct(l.min_age_years);
|
||||
if l.discretion >= 75 {
|
||||
p -= 2.0;
|
||||
}
|
||||
@@ -354,9 +423,67 @@ impl FalukantFamilyWorker {
|
||||
r#"{{"event":"falukant_family_scandal_hint","relationship_id":{}}}"#,
|
||||
l.rel_id
|
||||
));
|
||||
if let Some(uid) = l.user1_id.filter(|x| *x > 0) {
|
||||
self.publish_falukant_update_family_and_status(uid, "scandal");
|
||||
}
|
||||
if let Some(uid) = l.user2_id.filter(|x| *x > 0) {
|
||||
self.publish_falukant_update_family_and_status(uid, "scandal");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zwei sichtbare Liebschaften (visibility ≥ 60) pro Person: zusätzlich −4 Ansehen.
|
||||
for (&cid, &n_vis) in &char_visible_lover_rel_count {
|
||||
if n_vis < 2 {
|
||||
continue;
|
||||
}
|
||||
let cur = fetch_reputation(&mut conn, cid)?;
|
||||
let new_rep = (cur - 4.0).clamp(0.0, 100.0);
|
||||
let s = format!("{:.2}", new_rep);
|
||||
conn.execute("upd_rep", &[&s, &cid])?;
|
||||
}
|
||||
|
||||
// Seltene Einmal-Malus (Gerücht / Tadel) für beteiligte Charaktere.
|
||||
let mut unique_cids: HashSet<i32> = HashSet::new();
|
||||
for l in &lovers {
|
||||
unique_cids.insert(l.c1);
|
||||
unique_cids.insert(l.c2);
|
||||
}
|
||||
let mut rng_malus_cids: Vec<i32> = Vec::new();
|
||||
for cid in unique_cids.iter().copied() {
|
||||
let r = self.dist.sample(&mut self.rng) * 100.0;
|
||||
let malus = if r < 1.0 {
|
||||
-5.0
|
||||
} else if r < 3.0 {
|
||||
-3.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
if malus < 0.0 {
|
||||
let cur = fetch_reputation(&mut conn, cid)?;
|
||||
let new_rep = (cur + malus).clamp(0.0, 100.0);
|
||||
let s = format!("{:.2}", new_rep);
|
||||
conn.execute("upd_rep", &[&s, &cid])?;
|
||||
rng_malus_cids.push(cid);
|
||||
}
|
||||
}
|
||||
|
||||
let mut notify: HashSet<i32> = HashSet::new();
|
||||
for l in &lovers {
|
||||
push_user_id(&mut notify, l.user1_id);
|
||||
push_user_id(&mut notify, l.user2_id);
|
||||
}
|
||||
for (&cid, &n_vis) in &char_visible_lover_rel_count {
|
||||
if n_vis >= 2 {
|
||||
push_user_id(&mut notify, char_user.get(&cid).copied().flatten());
|
||||
}
|
||||
}
|
||||
for cid in &rng_malus_cids {
|
||||
push_user_id(&mut notify, char_user.get(cid).copied().flatten());
|
||||
}
|
||||
notify.extend(marriage_socket_users);
|
||||
self.publish_falukant_update_family_batch(¬ify, "daily");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -375,6 +502,8 @@ impl FalukantFamilyWorker {
|
||||
conn.prepare("mar_sub", QUERY_MARRIAGE_SUBTRACT_SATISFACTION)?;
|
||||
conn.prepare("mark_monthly", QUERY_MARK_LOVER_MONTHLY_DONE)?;
|
||||
|
||||
let mut monthly_notify: HashSet<i32> = HashSet::new();
|
||||
|
||||
for r in lover_rows {
|
||||
let rel_id = parse_i32(&r, "rel_id", -1);
|
||||
if rel_id < 0 {
|
||||
@@ -466,8 +595,13 @@ impl FalukantFamilyWorker {
|
||||
}
|
||||
|
||||
conn.execute("mark_monthly", &[&rel_id])?;
|
||||
|
||||
push_user_id(&mut monthly_notify, u1);
|
||||
push_user_id(&mut monthly_notify, u2);
|
||||
}
|
||||
|
||||
self.publish_falukant_update_family_batch(&monthly_notify, "monthly");
|
||||
|
||||
drop(conn);
|
||||
self.process_lover_births()?;
|
||||
Ok(())
|
||||
@@ -558,9 +692,53 @@ impl FalukantFamilyWorker {
|
||||
let children_update =
|
||||
format!(r#"{{"event":"children_update","user_id":{}}}"#, user_id);
|
||||
self.base.broker.publish(children_update);
|
||||
let update_status =
|
||||
format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
|
||||
self.base.broker.publish(update_status);
|
||||
self.publish_falukant_update_family_and_status(user_id, "lover_birth");
|
||||
}
|
||||
|
||||
/// `falukantUpdateFamily` (mit `reason`) + `falukantUpdateStatus` — für UI-Refresh.
|
||||
fn publish_falukant_update_family_and_status(&self, user_id: i32, reason: &str) {
|
||||
let family = format!(
|
||||
r#"{{"event":"falukantUpdateFamily","user_id":{},"reason":"{}"}}"#,
|
||||
user_id, reason
|
||||
);
|
||||
self.base.broker.publish(family);
|
||||
let status = format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
|
||||
self.base.broker.publish(status);
|
||||
}
|
||||
|
||||
fn publish_falukant_update_family_batch(&self, user_ids: &HashSet<i32>, reason: &str) {
|
||||
for &uid in user_ids {
|
||||
self.publish_falukant_update_family_and_status(uid, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fest (einmal), Geschenk (−1 Tag, +1/Tag), Haus hoch & keine Liebschaft: alle 4 Tage +1.
|
||||
fn apply_marriage_positive_buffs(
|
||||
sat: &mut i32,
|
||||
gift_days: &mut i32,
|
||||
feast: &mut i32,
|
||||
nl_counter: &mut i32,
|
||||
touching_empty: bool,
|
||||
house_supply: i32,
|
||||
) {
|
||||
if *feast > 0 {
|
||||
let bonus = (*feast).min(20).min(5);
|
||||
*sat = (*sat + bonus).min(100);
|
||||
*feast = 0;
|
||||
}
|
||||
if *gift_days > 0 {
|
||||
*sat = (*sat + 1).min(100);
|
||||
*gift_days -= 1;
|
||||
}
|
||||
if touching_empty && house_supply >= 65 {
|
||||
*nl_counter += 1;
|
||||
if *nl_counter >= 4 {
|
||||
*sat = (*sat + 1).min(100);
|
||||
*nl_counter = 0;
|
||||
}
|
||||
} else {
|
||||
*nl_counter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,6 +751,16 @@ struct MarriageData {
|
||||
drift_low: i32,
|
||||
title1_tr: String,
|
||||
title2_tr: String,
|
||||
user1_id: Option<i32>,
|
||||
user2_id: Option<i32>,
|
||||
/// Tage mit +1 Zufriedenheit (Geschenk), runtergezählt.
|
||||
gift_days: i32,
|
||||
/// Einmal-Bonus (z. B. Fest), wird beim nächsten Daily verbraucht.
|
||||
feast_pending: i32,
|
||||
/// Hausversorgung 0–100; ab ~65: Bonus alle 4 Tage ohne Liebschaft.
|
||||
house_supply: i32,
|
||||
/// Tageszähler für Haus-Bonus (0..4).
|
||||
no_lover_counter: i32,
|
||||
}
|
||||
|
||||
struct LoverData {
|
||||
@@ -588,6 +776,16 @@ struct LoverData {
|
||||
scandal_extra: i32,
|
||||
title1_tr: String,
|
||||
title2_tr: String,
|
||||
user1_id: Option<i32>,
|
||||
user2_id: Option<i32>,
|
||||
/// Jüngeres Alter beider Partner (Jahre, ganzzahlig); für Altersmalus / Skandal / Ehe.
|
||||
min_age_years: i32,
|
||||
}
|
||||
|
||||
fn push_user_id(set: &mut HashSet<i32>, uid: Option<i32>) {
|
||||
if let Some(id) = uid.filter(|x| *x > 0) {
|
||||
set.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_i32(row: &crate::db::Row, key: &str, default: i32) -> i32 {
|
||||
@@ -637,7 +835,8 @@ fn rank_cost_multiplier(g: u8) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
fn rank_rep_modifier(g: u8, scandal: bool) -> f64 {
|
||||
/// Gruppe 3: 0,7 nur bei „geordneter“ Liebschaft; sonst 1,0; Skandal 1,5.
|
||||
fn rank_rep_modifier(g: u8, scandal: bool, order_ok: bool) -> f64 {
|
||||
if g == 3 && scandal {
|
||||
return 1.5;
|
||||
}
|
||||
@@ -645,11 +844,46 @@ fn rank_rep_modifier(g: u8, scandal: bool) -> f64 {
|
||||
0 => 1.8,
|
||||
1 => 1.3,
|
||||
2 => 1.0,
|
||||
3 => 0.7,
|
||||
3 => if order_ok { 0.7 } else { 1.0 },
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Spec 5a: Altersmalus (jüngeres Alter der Beteiligten), zusätzlich zur Basis-Reputation.
|
||||
fn age_reputation_delta(min_age_years: i32) -> f64 {
|
||||
if min_age_years <= 13 {
|
||||
-1.5
|
||||
} else if min_age_years <= 15 {
|
||||
-0.8
|
||||
} else if min_age_years <= 17 {
|
||||
-0.3
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Spec 5a: minAge <= 15 und hohe Sichtbarkeit.
|
||||
fn visibility_young_penalty(min_age_years: i32, visibility: i32) -> f64 {
|
||||
if min_age_years <= 15 && visibility >= 50 {
|
||||
-0.5
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Spec Skandalrisiko: stufenweise Zusatz (exklusiv).
|
||||
fn scandal_age_extra_pct(min_age_years: i32) -> f64 {
|
||||
if min_age_years <= 13 {
|
||||
6.0
|
||||
} else if min_age_years <= 15 {
|
||||
3.0
|
||||
} else if min_age_years <= 17 {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
fn lover_marriage_daily_delta(
|
||||
mg: u8,
|
||||
l: &LoverData,
|
||||
|
||||
@@ -2041,7 +2041,11 @@ pub const QUERY_GET_ACTIVE_LOVER_ROWS_FOR_DAILY: &str = r#"
|
||||
COALESCE(c1.reputation, 50)::float8 AS rep1,
|
||||
COALESCE(c2.reputation, 50)::float8 AS rep2,
|
||||
fu1.id AS user1_id,
|
||||
fu2.id AS user2_id
|
||||
fu2.id AS user2_id,
|
||||
LEAST(
|
||||
((CURRENT_DATE - c1.birthdate::date) / 365),
|
||||
((CURRENT_DATE - c2.birthdate::date) / 365)
|
||||
)::int AS min_age_years
|
||||
FROM falukant_data.relationship r
|
||||
JOIN falukant_type.relationship rt
|
||||
ON rt.id = r.relationship_type_id AND rt.tr = 'lover'
|
||||
@@ -2080,7 +2084,11 @@ pub const QUERY_GET_ACTIVE_LOVER_ROWS_FOR_MONTHLY: &str = r#"
|
||||
COALESCE(c1.reputation, 50)::float8 AS rep1,
|
||||
COALESCE(c2.reputation, 50)::float8 AS rep2,
|
||||
fu1.id AS user1_id,
|
||||
fu2.id AS user2_id
|
||||
fu2.id AS user2_id,
|
||||
LEAST(
|
||||
((CURRENT_DATE - c1.birthdate::date) / 365),
|
||||
((CURRENT_DATE - c2.birthdate::date) / 365)
|
||||
)::int AS min_age_years
|
||||
FROM falukant_data.relationship r
|
||||
JOIN falukant_type.relationship rt
|
||||
ON rt.id = r.relationship_type_id AND rt.tr = 'lover'
|
||||
@@ -2124,16 +2132,25 @@ pub const QUERY_GET_MARRIAGE_ROWS: &str = r#"
|
||||
r.marriage_drift_high,
|
||||
r.marriage_drift_low,
|
||||
COALESCE(t1.tr, '') AS title1_tr,
|
||||
COALESCE(t2.tr, '') AS title2_tr
|
||||
COALESCE(t2.tr, '') AS title2_tr,
|
||||
fu1.id AS user1_id,
|
||||
fu2.id AS user2_id,
|
||||
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
|
||||
FROM falukant_data.relationship r
|
||||
JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id
|
||||
AND rt.tr IN ('married', 'engaged', 'wooing')
|
||||
JOIN falukant_data.character c1 ON c1.id = r.character1_id
|
||||
JOIN falukant_data.character c2 ON c2.id = r.character2_id
|
||||
LEFT JOIN falukant_type.title t1 ON t1.id = c1.title_of_nobility
|
||||
LEFT JOIN falukant_type.title t2 ON t2.id = c2.title_of_nobility;
|
||||
LEFT JOIN falukant_type.title t2 ON t2.id = c2.title_of_nobility
|
||||
LEFT JOIN falukant_data.falukant_user fu1 ON fu1.id = c1.user_id
|
||||
LEFT JOIN falukant_data.falukant_user fu2 ON fu2.id = c2.user_id;
|
||||
"#;
|
||||
|
||||
#[allow(dead_code)] // Einfaches Update ohne Buff-Spalten; Daemon nutzt QUERY_UPDATE_MARRIAGE_STATE_AND_BUFFS.
|
||||
pub const QUERY_UPDATE_MARRIAGE_STATE: &str = r#"
|
||||
UPDATE falukant_data.relationship
|
||||
SET marriage_satisfaction = $1::smallint,
|
||||
@@ -2142,6 +2159,18 @@ pub const QUERY_UPDATE_MARRIAGE_STATE: &str = r#"
|
||||
WHERE id = $4::int;
|
||||
"#;
|
||||
|
||||
/// Inkl. Geschenk-/Fest-/Haus-Zähler (Migration `003_falukant_family_marriage_buffs.sql`).
|
||||
pub const QUERY_UPDATE_MARRIAGE_STATE_AND_BUFFS: &str = r#"
|
||||
UPDATE falukant_data.relationship
|
||||
SET marriage_satisfaction = $1::smallint,
|
||||
marriage_drift_high = $2::smallint,
|
||||
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;
|
||||
"#;
|
||||
|
||||
pub const QUERY_MARRIAGE_SUBTRACT_SATISFACTION: &str = r#"
|
||||
UPDATE falukant_data.relationship r
|
||||
SET marriage_satisfaction = GREATEST(0, r.marriage_satisfaction - $2::int)
|
||||
|
||||
Reference in New Issue
Block a user