2448 lines
94 KiB
Rust
2448 lines
94 KiB
Rust
use crate::db::{ConnectionPool, DbConnection, DbError};
|
||
use crate::message_broker::MessageBroker;
|
||
use rand::Rng;
|
||
use rand::seq::SliceRandom;
|
||
use serde_json::json;
|
||
use std::sync::atomic::Ordering;
|
||
use std::sync::Arc;
|
||
use std::sync::Mutex;
|
||
use std::time::{Duration, Instant};
|
||
|
||
use super::base::{BaseWorker, Worker, WorkerState};
|
||
use crate::worker::sql::{
|
||
QUERY_GET_RANDOM_USER,
|
||
QUERY_GET_RANDOM_INFANT,
|
||
QUERY_GET_RANDOM_CITY,
|
||
QUERY_GET_AFFECTED_USERS,
|
||
QUERY_GET_MONEY,
|
||
QUERY_UPDATE_MONEY,
|
||
QUERY_GET_REGION_STOCKS,
|
||
QUERY_GET_USER_STOCKS,
|
||
QUERY_UPDATE_STOCK_CAPACITY,
|
||
QUERY_UPDATE_STOCK_CAPACITY_REGIONAL,
|
||
QUERY_GET_REGION_HOUSES,
|
||
QUERY_UPDATE_HOUSE_QUALITY,
|
||
QUERY_CHANGE_WEATHER,
|
||
QUERY_GET_RANDOM_CHARACTER,
|
||
QUERY_UPDATE_HEALTH,
|
||
QUERY_GET_REGION_CHARACTERS,
|
||
QUERY_GET_INVENTORY_ITEMS,
|
||
QUERY_REDUCE_INVENTORY,
|
||
QUERY_DELETE_INVENTORY,
|
||
QUERY_DELETE_STOCK,
|
||
QUERY_GET_STOCK_INVENTORY,
|
||
QUERY_CAP_INVENTORY,
|
||
QUERY_GET_USER_INVENTORY_ITEMS,
|
||
QUERY_GET_STOCK_TYPE_ID,
|
||
QUERY_REDUCE_INVENTORY_PERSONAL,
|
||
QUERY_DELETE_INVENTORY_PERSONAL,
|
||
QUERY_DELETE_STOCK_PERSONAL,
|
||
QUERY_GET_STOCK_INVENTORY_PERSONAL,
|
||
QUERY_CAP_INVENTORY_PERSONAL,
|
||
QUERY_DELETE_DIRECTOR,
|
||
QUERY_DELETE_RELATIONSHIP,
|
||
QUERY_GET_USER_ID,
|
||
QUERY_DELETE_CHILD_RELATION,
|
||
QUERY_DELETE_CHARACTER,
|
||
QUERY_GET_HEIR,
|
||
QUERY_GET_RANDOM_HEIR_FROM_REGION,
|
||
QUERY_SET_CHARACTER_USER,
|
||
QUERY_CLEAR_CHARACTER_USER,
|
||
QUERY_GET_CURRENT_MONEY,
|
||
QUERY_GET_HOUSE_VALUE,
|
||
QUERY_GET_SETTLEMENT_VALUE,
|
||
QUERY_GET_INVENTORY_VALUE,
|
||
QUERY_GET_CREDIT_DEBT,
|
||
QUERY_COUNT_CHILDREN,
|
||
};
|
||
use crate::worker::{insert_notification, publish_update_status};
|
||
|
||
/// Typisierung von Ereignissen
|
||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||
pub enum EventType {
|
||
/// Persönliches Ereignis für einen einzelnen Spieler
|
||
Personal,
|
||
/// Regionales Ereignis, das eine ganze Region betrifft
|
||
Regional,
|
||
}
|
||
|
||
/// Mögliche Effekte, die ein Ereignis haben kann
|
||
#[derive(Debug, Clone)]
|
||
pub enum EventEffect {
|
||
/// Änderung des Geldes eines Spielers (in Prozent)
|
||
MoneyChange { probability: f64, min_percent: f64, max_percent: f64 },
|
||
/// Änderung der Produktionsqualität in einer Region (in Prozentpunkten)
|
||
ProductionQualityChange { probability: f64, min_change: i32, max_change: i32 },
|
||
/// Änderung der Verkaufspreise in einer Region (in Prozent)
|
||
PriceChange { probability: f64, min_percent: f64, max_percent: f64 },
|
||
/// Änderung der Wetterbedingungen in einer Region
|
||
WeatherChange { probability: f64 },
|
||
/// Änderung der Lagerkapazität eines Spielers (in Prozent)
|
||
StorageCapacityChange { probability: f64, min_percent: f64, max_percent: f64 },
|
||
/// Änderung der Transportgeschwindigkeit (in Prozent)
|
||
TransportSpeedChange { probability: f64, min_percent: f64, max_percent: f64 },
|
||
/// Änderung der Gesundheit eines Charakters (in Punkten, kann negativ sein)
|
||
CharacterHealthChange { probability: f64, min_change: i32, max_change: i32 },
|
||
/// Ein Charakter stirbt (kann persönlich oder regional sein)
|
||
CharacterDeath { probability: f64 },
|
||
/// Beschädigung von Lagern und Lagerbestand in einer Region
|
||
StorageDamage {
|
||
probability: f64,
|
||
/// Stock-Typ Label (z.B. "field" oder "wood")
|
||
stock_type_label: String,
|
||
/// Prozent des Lagerbestands, der betroffen ist (min-max)
|
||
inventory_damage_min_percent: f64,
|
||
inventory_damage_max_percent: f64,
|
||
/// Prozent der Lager, die zerstört werden (min-max)
|
||
storage_destruction_min_percent: f64,
|
||
storage_destruction_max_percent: f64,
|
||
},
|
||
/// Änderung der Hausqualität (in Punkten, kann negativ sein)
|
||
HouseQualityChange { probability: f64, min_change: i32, max_change: i32 },
|
||
}
|
||
|
||
/// Hilfsstruktur für Character-Informationen aus Effekten
|
||
struct CharacterInfo {
|
||
character_id: Option<i32>,
|
||
first_name: Option<String>,
|
||
last_name: Option<String>,
|
||
}
|
||
|
||
/// Definition eines zufälligen Ereignisses
|
||
#[derive(Debug, Clone)]
|
||
pub struct RandomEvent {
|
||
/// Eindeutiger Identifikations-String für das Ereignis
|
||
pub id: String,
|
||
/// Wahrscheinlichkeit pro Minute, dass dieses Ereignis auftritt (0.0 - 1.0)
|
||
pub probability_per_minute: f64,
|
||
/// Typ des Ereignisses (persönlich oder regional)
|
||
pub event_type: EventType,
|
||
/// Liste der möglichen Effekte dieses Ereignisses
|
||
pub effects: Vec<EventEffect>,
|
||
/// Titel/Beschreibung des Ereignisses für Benachrichtigungen
|
||
pub title: String,
|
||
/// Detaillierte Beschreibung des Ereignisses
|
||
pub description: String,
|
||
}
|
||
|
||
pub struct EventsWorker {
|
||
base: BaseWorker,
|
||
}
|
||
|
||
/// Informationen über Lagerzerstörung durch ein Ereignis
|
||
struct StorageDamageInfo {
|
||
inventory_damage_percent: f64,
|
||
storage_destruction_percent: f64,
|
||
affected_stocks: i32,
|
||
destroyed_stocks: i32,
|
||
}
|
||
|
||
/// Parameter für regionale Lager-Schäden
|
||
pub struct StorageDamageParams<'a> {
|
||
pub region_id: i32,
|
||
pub stock_type_label: &'a str,
|
||
pub inventory_damage_min_percent: f64,
|
||
pub inventory_damage_max_percent: f64,
|
||
pub storage_destruction_min_percent: f64,
|
||
pub storage_destruction_max_percent: f64,
|
||
}
|
||
|
||
/// Parameter für persönliche Lager-Schäden
|
||
pub struct PersonalStorageDamageParams<'a> {
|
||
pub user_id: i32,
|
||
pub stock_type_label: &'a str,
|
||
pub inventory_damage_min_percent: f64,
|
||
pub inventory_damage_max_percent: f64,
|
||
pub storage_destruction_min_percent: f64,
|
||
pub storage_destruction_max_percent: f64,
|
||
}
|
||
|
||
impl EventsWorker {
|
||
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
|
||
Self {
|
||
base: BaseWorker::new("EventsWorker", pool, broker),
|
||
}
|
||
}
|
||
|
||
/// Initialisiert die Liste aller möglichen Ereignisse
|
||
fn initialize_events() -> Vec<RandomEvent> {
|
||
vec![
|
||
RandomEvent {
|
||
id: "windfall".to_string(),
|
||
probability_per_minute: 0.0025, // 0.25% pro Minute (reduziert auf 25% der ursprünglichen Rate)
|
||
event_type: EventType::Personal,
|
||
title: "Unerwarteter Geldsegen".to_string(),
|
||
description: "Du findest eine vergessene Geldbörse auf der Straße.".to_string(),
|
||
effects: vec![
|
||
EventEffect::MoneyChange {
|
||
probability: 1.0,
|
||
min_percent: 5.0,
|
||
max_percent: 15.0,
|
||
},
|
||
],
|
||
},
|
||
RandomEvent {
|
||
id: "theft".to_string(),
|
||
probability_per_minute: 0.002, // 0.2% pro Minute (reduziert auf 25% der ursprünglichen Rate)
|
||
event_type: EventType::Personal,
|
||
title: "Diebstahl".to_string(),
|
||
description: "Ein Dieb hat einen Teil deines Geldes gestohlen.".to_string(),
|
||
effects: vec![
|
||
EventEffect::MoneyChange {
|
||
probability: 1.0,
|
||
min_percent: -10.0,
|
||
max_percent: -5.0,
|
||
},
|
||
],
|
||
},
|
||
RandomEvent {
|
||
id: "regional_storm".to_string(),
|
||
probability_per_minute: 0.00125, // 0.125% pro Minute (reduziert auf 25% der ursprünglichen Rate)
|
||
event_type: EventType::Regional,
|
||
title: "Sturm in der Region".to_string(),
|
||
description: "Ein schwerer Sturm hat die Region getroffen.".to_string(),
|
||
effects: vec![
|
||
EventEffect::WeatherChange {
|
||
probability: 1.0,
|
||
},
|
||
EventEffect::ProductionQualityChange {
|
||
probability: 0.8,
|
||
min_change: -10,
|
||
max_change: -5,
|
||
},
|
||
EventEffect::TransportSpeedChange {
|
||
probability: 0.6,
|
||
min_percent: -20.0,
|
||
max_percent: -10.0,
|
||
},
|
||
EventEffect::StorageDamage {
|
||
probability: 1.0,
|
||
stock_type_label: "field".to_string(),
|
||
inventory_damage_min_percent: 5.0,
|
||
inventory_damage_max_percent: 75.0,
|
||
storage_destruction_min_percent: 0.0,
|
||
storage_destruction_max_percent: 50.0,
|
||
},
|
||
EventEffect::StorageDamage {
|
||
probability: 1.0,
|
||
stock_type_label: "wood".to_string(),
|
||
inventory_damage_min_percent: 0.0,
|
||
inventory_damage_max_percent: 25.0,
|
||
storage_destruction_min_percent: 0.0,
|
||
storage_destruction_max_percent: 10.0,
|
||
},
|
||
// Verbleibende Lager können durch den Sturm beschädigt werden und Kapazität verlieren
|
||
EventEffect::StorageCapacityChange {
|
||
probability: 0.7, // 70% Chance, dass verbleibende Lager beschädigt werden
|
||
min_percent: -10.0,
|
||
max_percent: -3.0,
|
||
},
|
||
],
|
||
},
|
||
RandomEvent {
|
||
id: "regional_festival".to_string(),
|
||
probability_per_minute: 0.00075, // 0.075% pro Minute (reduziert auf 25% der ursprünglichen Rate)
|
||
event_type: EventType::Regional,
|
||
title: "Regionales Fest".to_string(),
|
||
description: "Ein großes Fest findet in der Region statt.".to_string(),
|
||
effects: vec![
|
||
EventEffect::PriceChange {
|
||
probability: 0.9,
|
||
min_percent: 5.0,
|
||
max_percent: 15.0,
|
||
},
|
||
EventEffect::ProductionQualityChange {
|
||
probability: 0.5,
|
||
min_change: 2,
|
||
max_change: 5,
|
||
},
|
||
],
|
||
},
|
||
RandomEvent {
|
||
id: "warehouse_fire".to_string(),
|
||
probability_per_minute: 0.0005, // 0.05% pro Minute (reduziert auf 25% der ursprünglichen Rate)
|
||
event_type: EventType::Personal,
|
||
title: "Lagerbrand".to_string(),
|
||
description: "Ein Feuer hat Teile deines Lagers beschädigt.".to_string(),
|
||
effects: vec![
|
||
// Feldlager: Lagerbestand kann zerstört werden, Lager können zerstört werden
|
||
EventEffect::StorageDamage {
|
||
probability: 1.0,
|
||
stock_type_label: "field".to_string(),
|
||
inventory_damage_min_percent: 0.0,
|
||
inventory_damage_max_percent: 100.0,
|
||
storage_destruction_min_percent: 0.0,
|
||
storage_destruction_max_percent: 50.0,
|
||
},
|
||
// Holzlager: Lagerbestand kann zerstört werden, Lager können zerstört werden
|
||
EventEffect::StorageDamage {
|
||
probability: 1.0,
|
||
stock_type_label: "wood".to_string(),
|
||
inventory_damage_min_percent: 0.0,
|
||
inventory_damage_max_percent: 100.0,
|
||
storage_destruction_min_percent: 0.0,
|
||
storage_destruction_max_percent: 50.0,
|
||
},
|
||
// Verbleibende Lager können durch das Feuer beschädigt werden und Kapazität verlieren
|
||
EventEffect::StorageCapacityChange {
|
||
probability: 0.8, // 80% Chance, dass verbleibende Lager beschädigt werden
|
||
min_percent: -15.0,
|
||
max_percent: -5.0,
|
||
},
|
||
],
|
||
},
|
||
RandomEvent {
|
||
id: "character_illness".to_string(),
|
||
probability_per_minute: 0.0025, // 0.25% pro Minute (reduziert auf 25% der ursprünglichen Rate)
|
||
event_type: EventType::Personal,
|
||
title: "Krankheit".to_string(),
|
||
description: "Ein Charakter ist erkrankt und hat an Gesundheit verloren.".to_string(),
|
||
effects: vec![
|
||
EventEffect::CharacterHealthChange {
|
||
probability: 1.0,
|
||
// Soll nur leicht reduzieren: maximal -12 (User-Wunsch)
|
||
min_change: -12,
|
||
max_change: -3,
|
||
},
|
||
],
|
||
},
|
||
RandomEvent {
|
||
id: "character_recovery".to_string(),
|
||
probability_per_minute: 0.002, // 0.2% pro Minute (reduziert auf 25% der ursprünglichen Rate)
|
||
event_type: EventType::Personal,
|
||
title: "Genesung".to_string(),
|
||
description: "Ein Charakter hat sich von einer Krankheit erholt.".to_string(),
|
||
effects: vec![
|
||
EventEffect::CharacterHealthChange {
|
||
probability: 1.0,
|
||
min_change: 5,
|
||
max_change: 15,
|
||
},
|
||
],
|
||
},
|
||
RandomEvent {
|
||
id: "character_rest".to_string(),
|
||
probability_per_minute: 0.0015, // 0.15% pro Minute (reduziert)
|
||
event_type: EventType::Personal,
|
||
title: "Erholung".to_string(),
|
||
description: "Ein Charakter hat sich gut ausgeruht und gewinnt Gesundheit zurück.".to_string(),
|
||
effects: vec![
|
||
EventEffect::CharacterHealthChange {
|
||
probability: 1.0,
|
||
min_change: 3,
|
||
max_change: 10,
|
||
},
|
||
],
|
||
},
|
||
RandomEvent {
|
||
id: "character_healer".to_string(),
|
||
probability_per_minute: 0.001, // 0.10% pro Minute (reduziert)
|
||
event_type: EventType::Personal,
|
||
title: "Heilerbesuch".to_string(),
|
||
description: "Ein Heiler behandelt einen Charakter. Die Gesundheit steigt.".to_string(),
|
||
effects: vec![
|
||
EventEffect::CharacterHealthChange {
|
||
probability: 1.0,
|
||
min_change: 8,
|
||
max_change: 20,
|
||
},
|
||
],
|
||
},
|
||
RandomEvent {
|
||
id: "character_accident".to_string(),
|
||
probability_per_minute: 0.00075, // 0.075% pro Minute (reduziert auf 25% der ursprünglichen Rate)
|
||
event_type: EventType::Personal,
|
||
title: "Unfall".to_string(),
|
||
description: "Ein schwerer Unfall hat einen Charakter schwer verletzt.".to_string(),
|
||
effects: vec![
|
||
EventEffect::CharacterHealthChange {
|
||
probability: 1.0,
|
||
min_change: -50,
|
||
max_change: -30,
|
||
},
|
||
EventEffect::CharacterDeath {
|
||
probability: 0.2, // 20% Chance auf Tod
|
||
},
|
||
],
|
||
},
|
||
RandomEvent {
|
||
id: "regional_epidemic".to_string(),
|
||
probability_per_minute: 0.0005, // 0.05% pro Minute (reduziert auf 25% der ursprünglichen Rate)
|
||
event_type: EventType::Regional,
|
||
title: "Epidemie".to_string(),
|
||
description: "Eine Seuche hat die Region erfasst.".to_string(),
|
||
effects: vec![
|
||
EventEffect::CharacterHealthChange {
|
||
probability: 0.8, // 80% der Charaktere betroffen
|
||
min_change: -15,
|
||
max_change: -5,
|
||
},
|
||
EventEffect::CharacterDeath {
|
||
probability: 0.1, // 10% Chance auf Tod pro Charakter
|
||
},
|
||
],
|
||
},
|
||
RandomEvent {
|
||
id: "sudden_infant_death".to_string(),
|
||
// Wahrscheinlichkeit pro Minute: 0.000125 (0.0125%) - reduziert auf 25% der ursprünglichen Rate
|
||
// Im Mittelalter starben etwa 30-40% der Kinder vor dem 2. Geburtstag
|
||
// Diese Wahrscheinlichkeit führt bei regelmäßiger Prüfung zu einer
|
||
// realistischen mittelalterlichen Säuglingssterblichkeit
|
||
probability_per_minute: 0.000125,
|
||
event_type: EventType::Personal,
|
||
title: "Plötzlicher Kindstod".to_string(),
|
||
description: "Ein Kleinkind ist plötzlich verstorben.".to_string(),
|
||
effects: vec![
|
||
EventEffect::CharacterDeath {
|
||
probability: 1.0, // Wenn das Ereignis auftritt, stirbt das Kind
|
||
},
|
||
],
|
||
},
|
||
RandomEvent {
|
||
id: "earthquake".to_string(),
|
||
probability_per_minute: 0.00025, // 0.025% pro Minute (reduziert auf 25% der ursprünglichen Rate)
|
||
event_type: EventType::Regional,
|
||
title: "Erdbeben".to_string(),
|
||
description: "Ein Erdbeben hat die Region erschüttert.".to_string(),
|
||
effects: vec![
|
||
EventEffect::CharacterHealthChange {
|
||
probability: 0.3, // 30% Chance auf Gesundheitsschäden (geringe Wahrscheinlichkeit)
|
||
min_change: -20,
|
||
max_change: -5,
|
||
},
|
||
EventEffect::CharacterDeath {
|
||
probability: 0.05, // 5% Chance auf Tod (sehr geringe Wahrscheinlichkeit)
|
||
},
|
||
EventEffect::StorageCapacityChange {
|
||
probability: 1.0, // Alle Lager werden beschädigt
|
||
min_percent: -20.0,
|
||
max_percent: -5.0,
|
||
},
|
||
EventEffect::HouseQualityChange {
|
||
probability: 1.0, // Alle Häuser werden beschädigt
|
||
min_change: -15,
|
||
max_change: -5,
|
||
},
|
||
],
|
||
},
|
||
]
|
||
}
|
||
|
||
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
|
||
let mut last_event_check = None;
|
||
let mut last_notification_cleanup: Option<Instant> = None;
|
||
let mut rng = rand::thread_rng();
|
||
let events = Self::initialize_events();
|
||
eprintln!("[EventsWorker] Worker-Thread gestartet");
|
||
|
||
loop {
|
||
if !state.running_worker.load(Ordering::Relaxed) {
|
||
break;
|
||
}
|
||
|
||
let now = Instant::now();
|
||
|
||
// Heartbeat im Run-Loop (unabhängig davon, ob Events triggern)
|
||
static LAST_RUNLOOP_HEARTBEAT: Mutex<Option<Instant>> = Mutex::new(None);
|
||
{
|
||
let mut last = LAST_RUNLOOP_HEARTBEAT.lock().unwrap();
|
||
let should_log = last
|
||
.map(|t| t.elapsed().as_secs() >= 3600)
|
||
.unwrap_or(true);
|
||
if should_log {
|
||
eprintln!("[EventsWorker] RunLoop Heartbeat: alive");
|
||
*last = Some(Instant::now());
|
||
}
|
||
}
|
||
|
||
// Ereignisse einmal pro Minute prüfen
|
||
if should_run_interval(last_event_check, now, Duration::from_secs(60)) {
|
||
if let Err(err) = Self::check_and_trigger_events_inner(
|
||
&pool,
|
||
&broker,
|
||
&state,
|
||
&mut rng,
|
||
&events,
|
||
) {
|
||
eprintln!("[EventsWorker] Fehler beim Prüfen von Ereignissen: {err}");
|
||
}
|
||
last_event_check = Some(now);
|
||
}
|
||
|
||
// Alte Notifications einmal täglich aufräumen (alle 24 Stunden)
|
||
if should_run_interval(last_notification_cleanup, now, Duration::from_secs(86400)) {
|
||
Self::cleanup_old_notifications(&pool);
|
||
last_notification_cleanup = Some(now);
|
||
}
|
||
|
||
// 10-Sekunden-Wartezeit in kurze Scheiben aufteilen
|
||
const SLICE_MS: u64 = 500;
|
||
let total_ms = 10_000;
|
||
let mut slept = 0;
|
||
while slept < total_ms {
|
||
if !state.running_worker.load(Ordering::Relaxed) {
|
||
break;
|
||
}
|
||
let remaining = total_ms - slept;
|
||
let slice = SLICE_MS.min(remaining);
|
||
std::thread::sleep(Duration::from_millis(slice));
|
||
slept += slice;
|
||
}
|
||
}
|
||
}
|
||
|
||
fn cleanup_old_notifications(pool: &ConnectionPool) {
|
||
use crate::worker::sql::QUERY_DELETE_OLD_NOTIFICATIONS;
|
||
|
||
let result = (|| -> Result<(), DbError> {
|
||
let mut conn = pool
|
||
.get()
|
||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||
|
||
conn.prepare("delete_old_notifications", QUERY_DELETE_OLD_NOTIFICATIONS)?;
|
||
conn.execute("delete_old_notifications", &[])?;
|
||
Ok(())
|
||
})();
|
||
|
||
match result {
|
||
Ok(()) => eprintln!("[EventsWorker] Alte Notifications (>30 Tage) aufgeräumt"),
|
||
Err(err) => eprintln!("[EventsWorker] Fehler beim Aufräumen alter Notifications: {err}"),
|
||
}
|
||
}
|
||
|
||
// Globaler Skalierungsfaktor für Ereignisfrequenz.
|
||
// Default: 1.0 (unverändert). Optional per ENV `EVENT_RATE_SCALE` konfigurierbar.
|
||
fn event_rate_scale() -> f64 {
|
||
std::env::var("EVENT_RATE_SCALE")
|
||
.ok()
|
||
.and_then(|v| v.parse::<f64>().ok())
|
||
.filter(|v| v.is_finite() && *v >= 0.0)
|
||
.unwrap_or(1.0)
|
||
}
|
||
|
||
fn check_and_trigger_events_inner(
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
_state: &Arc<WorkerState>,
|
||
rng: &mut impl Rng,
|
||
events: &[RandomEvent],
|
||
) -> Result<(), DbError> {
|
||
let rate_scale = Self::event_rate_scale();
|
||
// Einmalige/gedrosselte Debug-Ausgaben, damit sichtbar ist, dass der Worker läuft
|
||
// und welche Skalierung aktiv ist.
|
||
static LAST_HEARTBEAT: Mutex<Option<Instant>> = Mutex::new(None);
|
||
// optional counter; keep it underscore-prefixed to avoid unused warnings
|
||
let mut _triggered = 0usize;
|
||
|
||
{
|
||
let mut last = LAST_HEARTBEAT.lock().unwrap();
|
||
let should_log = last
|
||
.map(|t| t.elapsed().as_secs() >= 3600)
|
||
.unwrap_or(true);
|
||
if should_log {
|
||
if (rate_scale - 1.0).abs() > f64::EPSILON {
|
||
eprintln!(
|
||
"[EventsWorker] Heartbeat: EVENT_RATE_SCALE={} (ENV `EVENT_RATE_SCALE` gesetzt?)",
|
||
rate_scale
|
||
);
|
||
} else {
|
||
eprintln!("[EventsWorker] Heartbeat: EVENT_RATE_SCALE=1.0");
|
||
}
|
||
*last = Some(Instant::now());
|
||
}
|
||
}
|
||
|
||
// Prüfe jedes mögliche Ereignis
|
||
for event in events {
|
||
// Zufällige Prüfung basierend auf Wahrscheinlichkeit
|
||
let roll = rng.gen_range(0.0..=1.0);
|
||
let effective_prob = event.probability_per_minute * rate_scale;
|
||
if roll < effective_prob {
|
||
_triggered += 1;
|
||
eprintln!(
|
||
"[EventsWorker] Ereignis '{}' wurde ausgelöst (Wahrscheinlichkeit: {:.4}% -> skaliert {:.4}%)",
|
||
event.id,
|
||
event.probability_per_minute * 100.0,
|
||
effective_prob * 100.0
|
||
);
|
||
|
||
match event.event_type {
|
||
EventType::Personal => {
|
||
Self::trigger_personal_event(pool, broker, event, rng)?;
|
||
}
|
||
EventType::Regional => {
|
||
Self::trigger_regional_event(pool, broker, event, rng)?;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Wenn über längere Zeit kein Ereignis triggert, sieht man sonst keinerlei Logs.
|
||
// Das Heartbeat-Log oben zeigt Liveness; hier loggen wir bewusst nicht pro Minute.
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn trigger_personal_event(
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
event: &RandomEvent,
|
||
rng: &mut impl Rng,
|
||
) -> Result<(), DbError> {
|
||
if event.id == "sudden_infant_death" {
|
||
return Self::trigger_sudden_infant_death(pool, broker, event, rng);
|
||
}
|
||
|
||
let user_id = match Self::get_random_user_id(pool)? {
|
||
Some(id) => id,
|
||
None => {
|
||
eprintln!("[EventsWorker] Kein Spieler gefunden für persönliches Ereignis");
|
||
return Ok(());
|
||
}
|
||
};
|
||
|
||
let mut conn = pool
|
||
.get()
|
||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||
|
||
let effect_results = Self::apply_personal_effects(&mut conn, pool, broker, user_id, event, rng)?;
|
||
|
||
if effect_results.is_empty() {
|
||
eprintln!(
|
||
"[EventsWorker] Persönliches Ereignis '{}' für Spieler {} übersprungen (keine Effekte)",
|
||
event.id, user_id
|
||
);
|
||
return Ok(());
|
||
}
|
||
|
||
let char_info = Self::extract_character_info_from_effects(&effect_results);
|
||
Self::send_personal_event_notifications(pool, broker, user_id, event, &effect_results, &char_info)?;
|
||
|
||
eprintln!(
|
||
"[EventsWorker] Persönliches Ereignis '{}' für Spieler {} verarbeitet",
|
||
event.id, user_id
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn get_random_user_id(pool: &ConnectionPool) -> Result<Option<i32>, DbError> {
|
||
let mut conn = pool
|
||
.get()
|
||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||
|
||
conn.prepare("get_random_user", QUERY_GET_RANDOM_USER)?;
|
||
let rows = conn.execute("get_random_user", &[])?;
|
||
|
||
Ok(rows
|
||
.first()
|
||
.and_then(|r| r.get("id"))
|
||
.and_then(|v| v.parse::<i32>().ok()))
|
||
}
|
||
|
||
fn apply_personal_effects(
|
||
conn: &mut DbConnection,
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
user_id: i32,
|
||
event: &RandomEvent,
|
||
rng: &mut impl Rng,
|
||
) -> Result<Vec<serde_json::Value>, DbError> {
|
||
let mut effect_results = Vec::new();
|
||
|
||
for effect in &event.effects {
|
||
let effect_roll = rng.gen_range(0.0..=1.0);
|
||
if let Some(result) = Self::apply_single_personal_effect(conn, pool, broker, user_id, effect, effect_roll, rng)? {
|
||
effect_results.extend(result);
|
||
}
|
||
}
|
||
|
||
Ok(effect_results)
|
||
}
|
||
|
||
fn apply_single_personal_effect(
|
||
conn: &mut DbConnection,
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
user_id: i32,
|
||
effect: &EventEffect,
|
||
effect_roll: f64,
|
||
rng: &mut impl Rng,
|
||
) -> Result<Option<Vec<serde_json::Value>>, DbError> {
|
||
match effect {
|
||
EventEffect::MoneyChange { probability, min_percent, max_percent } => {
|
||
Self::handle_money_change_effect(conn, user_id, effect_roll, *probability, *min_percent, *max_percent, rng)
|
||
}
|
||
EventEffect::StorageCapacityChange { probability, min_percent, max_percent } => {
|
||
Self::handle_storage_capacity_effect(conn, user_id, effect_roll, *probability, *min_percent, *max_percent, rng)
|
||
}
|
||
EventEffect::CharacterHealthChange { probability, min_change, max_change } => {
|
||
Self::handle_character_health_effect(conn, pool, broker, user_id, effect_roll, *probability, *min_change, *max_change, rng)
|
||
}
|
||
EventEffect::CharacterDeath { probability } => {
|
||
Self::handle_character_death_effect(conn, pool, broker, user_id, effect_roll, *probability)
|
||
}
|
||
EventEffect::StorageDamage { probability, stock_type_label, inventory_damage_min_percent, inventory_damage_max_percent, storage_destruction_min_percent, storage_destruction_max_percent } => {
|
||
Self::handle_storage_damage_effect(conn, user_id, effect_roll, *probability, stock_type_label, *inventory_damage_min_percent, *inventory_damage_max_percent, *storage_destruction_min_percent, *storage_destruction_max_percent, rng)
|
||
}
|
||
_ => {
|
||
eprintln!("[EventsWorker] Effekt {:?} wird für persönliche Ereignisse noch nicht unterstützt", effect);
|
||
Ok(None)
|
||
}
|
||
}
|
||
}
|
||
|
||
fn handle_money_change_effect(
|
||
conn: &mut DbConnection,
|
||
user_id: i32,
|
||
effect_roll: f64,
|
||
probability: f64,
|
||
min_percent: f64,
|
||
max_percent: f64,
|
||
rng: &mut impl Rng,
|
||
) -> Result<Option<Vec<serde_json::Value>>, DbError> {
|
||
if effect_roll >= probability {
|
||
return Ok(None);
|
||
}
|
||
let percent_change = rng.gen_range(min_percent..=max_percent);
|
||
if let Ok(absolute_change) = Self::apply_money_change(conn, user_id, percent_change) {
|
||
Ok(Some(vec![json!({
|
||
"type": "money_change",
|
||
"percent": percent_change,
|
||
"absolute": absolute_change
|
||
})]))
|
||
} else {
|
||
Ok(None)
|
||
}
|
||
}
|
||
|
||
fn handle_storage_capacity_effect(
|
||
conn: &mut DbConnection,
|
||
user_id: i32,
|
||
effect_roll: f64,
|
||
probability: f64,
|
||
min_percent: f64,
|
||
max_percent: f64,
|
||
rng: &mut impl Rng,
|
||
) -> Result<Option<Vec<serde_json::Value>>, DbError> {
|
||
if effect_roll >= probability {
|
||
return Ok(None);
|
||
}
|
||
let percent_change = rng.gen_range(min_percent..=max_percent);
|
||
Self::apply_storage_capacity_change(conn, user_id, percent_change)?;
|
||
Ok(Some(vec![json!({
|
||
"type": "storage_capacity_change",
|
||
"percent": percent_change
|
||
})]))
|
||
}
|
||
|
||
fn handle_character_health_effect(
|
||
conn: &mut DbConnection,
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
user_id: i32,
|
||
effect_roll: f64,
|
||
probability: f64,
|
||
min_change: i32,
|
||
max_change: i32,
|
||
rng: &mut impl Rng,
|
||
) -> Result<Option<Vec<serde_json::Value>>, DbError> {
|
||
if effect_roll >= probability {
|
||
return Ok(None);
|
||
}
|
||
let Ok((character_id, health_change, died, first_name, last_name)) =
|
||
Self::apply_character_health_change(conn, pool, broker, user_id, min_change, max_change, rng)
|
||
else {
|
||
return Ok(None);
|
||
};
|
||
|
||
let mut results = vec![json!({
|
||
"type": "character_health_change",
|
||
"character_id": character_id,
|
||
"change": health_change,
|
||
"character_first_name": first_name,
|
||
"character_last_name": last_name
|
||
})];
|
||
|
||
if died {
|
||
results.push(json!({
|
||
"type": "character_death",
|
||
"character_id": character_id,
|
||
"character_first_name": first_name,
|
||
"character_last_name": last_name
|
||
}));
|
||
}
|
||
|
||
Ok(Some(results))
|
||
}
|
||
|
||
fn handle_character_death_effect(
|
||
conn: &mut DbConnection,
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
user_id: i32,
|
||
effect_roll: f64,
|
||
probability: f64,
|
||
) -> Result<Option<Vec<serde_json::Value>>, DbError> {
|
||
if effect_roll >= probability {
|
||
return Ok(None);
|
||
}
|
||
let Ok((character_id, first_name, last_name)) = Self::apply_character_death(conn, user_id, pool, broker) else {
|
||
return Ok(None);
|
||
};
|
||
Ok(Some(vec![json!({
|
||
"type": "character_death",
|
||
"character_id": character_id,
|
||
"character_first_name": first_name,
|
||
"character_last_name": last_name
|
||
})]))
|
||
}
|
||
|
||
fn handle_storage_damage_effect(
|
||
conn: &mut DbConnection,
|
||
user_id: i32,
|
||
effect_roll: f64,
|
||
probability: f64,
|
||
stock_type_label: &str,
|
||
inventory_damage_min_percent: f64,
|
||
inventory_damage_max_percent: f64,
|
||
storage_destruction_min_percent: f64,
|
||
storage_destruction_max_percent: f64,
|
||
rng: &mut impl Rng,
|
||
) -> Result<Option<Vec<serde_json::Value>>, DbError> {
|
||
if effect_roll >= probability {
|
||
return Ok(None);
|
||
}
|
||
let Ok(damage_info) = Self::apply_personal_storage_damage(
|
||
conn,
|
||
PersonalStorageDamageParams {
|
||
user_id,
|
||
stock_type_label,
|
||
inventory_damage_min_percent,
|
||
inventory_damage_max_percent,
|
||
storage_destruction_min_percent,
|
||
storage_destruction_max_percent,
|
||
},
|
||
rng,
|
||
) else {
|
||
return Ok(None);
|
||
};
|
||
Ok(Some(vec![json!({
|
||
"type": "storage_damage",
|
||
"stock_type": stock_type_label,
|
||
"inventory_damage_percent": damage_info.inventory_damage_percent,
|
||
"storage_destruction_percent": damage_info.storage_destruction_percent,
|
||
"affected_stocks": damage_info.affected_stocks,
|
||
"destroyed_stocks": damage_info.destroyed_stocks,
|
||
})]))
|
||
}
|
||
|
||
fn extract_character_info_from_effects(effect_results: &[serde_json::Value]) -> CharacterInfo {
|
||
let character_id = effect_results.iter().find_map(|eff| {
|
||
eff.get("character_id").and_then(|v| v.as_i64()).map(|n| n as i32)
|
||
});
|
||
|
||
let (first_name, last_name) = if let Some(cid) = character_id {
|
||
let eff = effect_results.iter().find(|e| {
|
||
e.get("character_id")
|
||
.and_then(|v| v.as_i64())
|
||
.map(|n| n as i32)
|
||
== Some(cid)
|
||
});
|
||
let fn_ = eff
|
||
.and_then(|e| e.get("character_first_name"))
|
||
.and_then(|v| v.as_str())
|
||
.map(|s| s.to_string());
|
||
let ln_ = eff
|
||
.and_then(|e| e.get("character_last_name"))
|
||
.and_then(|v| v.as_str())
|
||
.map(|s| s.to_string());
|
||
(fn_, ln_)
|
||
} else {
|
||
(None, None)
|
||
};
|
||
|
||
CharacterInfo { character_id, first_name, last_name }
|
||
}
|
||
|
||
fn send_personal_event_notifications(
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
user_id: i32,
|
||
event: &RandomEvent,
|
||
effect_results: &[serde_json::Value],
|
||
char_info: &CharacterInfo,
|
||
) -> Result<(), DbError> {
|
||
// DB-Benachrichtigung
|
||
let mut notification_json = serde_json::json!({
|
||
"tr": format!("random_event.{}", event.id),
|
||
"event_id": event.id,
|
||
"event_type": "personal",
|
||
"effects": effect_results
|
||
});
|
||
|
||
if let Some(cid) = char_info.character_id {
|
||
notification_json["character_id"] = serde_json::json!(cid);
|
||
}
|
||
if let Some(ref fn_) = char_info.first_name {
|
||
notification_json["character_first_name"] = serde_json::json!(fn_);
|
||
}
|
||
if let Some(ref ln_) = char_info.last_name {
|
||
notification_json["character_last_name"] = serde_json::json!(ln_);
|
||
}
|
||
|
||
Self::notify_user(pool, broker, user_id, ¬ification_json.to_string(), char_info.character_id)?;
|
||
|
||
// WebSocket-Benachrichtigung
|
||
let mut ws_notification = json!({
|
||
"event": "random_event",
|
||
"event_id": event.id,
|
||
"event_type": "personal",
|
||
"user_id": user_id,
|
||
"title": event.title,
|
||
"description": event.description,
|
||
"effects": effect_results
|
||
});
|
||
|
||
if let Some(cid) = char_info.character_id {
|
||
ws_notification["character_id"] = json!(cid);
|
||
}
|
||
|
||
broker.publish(ws_notification.to_string());
|
||
Ok(())
|
||
}
|
||
|
||
fn trigger_sudden_infant_death(
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
event: &RandomEvent,
|
||
rng: &mut impl Rng,
|
||
) -> Result<(), DbError> {
|
||
let mut conn = pool
|
||
.get()
|
||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||
|
||
// Finde ein zufälliges Kind unter 2 Jahren
|
||
// Maximalalter: 730 Tage (2 Jahre) - festgelegt in der WHERE-Klausel unten
|
||
conn.prepare("get_random_infant", QUERY_GET_RANDOM_INFANT)?;
|
||
let rows = conn.execute("get_random_infant", &[])?;
|
||
|
||
let character_id: Option<i32> = rows
|
||
.first()
|
||
.and_then(|r| r.get("character_id"))
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let character_id = match character_id {
|
||
Some(id) => id,
|
||
None => {
|
||
eprintln!("[EventsWorker] Kein Kind unter 2 Jahren gefunden für plötzlichen Kindstod");
|
||
return Ok(());
|
||
}
|
||
};
|
||
|
||
let user_id: Option<i32> = rows
|
||
.first()
|
||
.and_then(|r| r.get("user_id"))
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let user_id = match user_id {
|
||
Some(id) => id,
|
||
None => {
|
||
eprintln!("[EventsWorker] Kein user_id für Kind {} gefunden", character_id);
|
||
return Ok(());
|
||
}
|
||
};
|
||
|
||
let first_name = rows.first().and_then(|r| r.get("first_name")).cloned();
|
||
let last_name = rows.first().and_then(|r| r.get("last_name")).cloned();
|
||
|
||
// Wende Effekte an (in diesem Fall nur CharacterDeath)
|
||
let mut effect_results = Vec::new();
|
||
for effect in &event.effects {
|
||
let effect_roll = rng.gen_range(0.0..=1.0);
|
||
match effect {
|
||
EventEffect::CharacterDeath { probability } => {
|
||
if effect_roll < *probability
|
||
&& Self::handle_character_death(pool, broker, character_id).is_ok()
|
||
{
|
||
effect_results.push(json!({
|
||
"type": "character_death",
|
||
"character_id": character_id,
|
||
"character_first_name": first_name,
|
||
"character_last_name": last_name
|
||
}));
|
||
}
|
||
}
|
||
_ => {
|
||
eprintln!(
|
||
"[EventsWorker] Effekt {:?} wird für plötzlichen Kindstod nicht unterstützt",
|
||
effect
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Schreibe Benachrichtigung in die Datenbank mit Event-Details
|
||
let notification_json = serde_json::json!({
|
||
"tr": format!("random_event.{}", event.id),
|
||
"value": {
|
||
"event_id": event.id,
|
||
"event_type": "personal",
|
||
"title": event.title,
|
||
"description": event.description,
|
||
"character_id": character_id,
|
||
"character_first_name": first_name,
|
||
"character_last_name": last_name,
|
||
"effects": effect_results
|
||
}
|
||
});
|
||
// Falls das Kind inzwischen gelöscht wurde, halten wir Name + ID im Payload fest.
|
||
Self::notify_user(
|
||
pool,
|
||
broker,
|
||
user_id,
|
||
¬ification_json.to_string(),
|
||
Some(character_id),
|
||
)?;
|
||
|
||
// Sende Benachrichtigung über WebSocket
|
||
let notification = json!({
|
||
"event": "random_event",
|
||
"event_id": event.id,
|
||
"event_type": "personal",
|
||
"user_id": user_id,
|
||
"character_id": character_id,
|
||
"title": event.title,
|
||
"description": event.description,
|
||
"effects": effect_results
|
||
});
|
||
|
||
broker.publish(notification.to_string());
|
||
eprintln!(
|
||
"[EventsWorker] Plötzlicher Kindstod für Charakter {} (Spieler {}) verarbeitet",
|
||
character_id, user_id
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn trigger_regional_event(
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
event: &RandomEvent,
|
||
rng: &mut impl Rng,
|
||
) -> Result<(), DbError> {
|
||
let mut conn = pool
|
||
.get()
|
||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||
|
||
// Hole eine zufällige Stadt-Region
|
||
conn.prepare("get_random_city", QUERY_GET_RANDOM_CITY)?;
|
||
let rows = conn.execute("get_random_city", &[])?;
|
||
|
||
let region_id: Option<i32> = rows
|
||
.first()
|
||
.and_then(|r| r.get("region_id"))
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let region_id = match region_id {
|
||
Some(id) => id,
|
||
None => {
|
||
eprintln!("[EventsWorker] Keine Stadt-Region gefunden für regionales Ereignis");
|
||
return Ok(());
|
||
}
|
||
};
|
||
|
||
// Wende Effekte an
|
||
let mut effect_results = Vec::new();
|
||
for effect in &event.effects {
|
||
let effect_roll = rng.gen_range(0.0..=1.0);
|
||
match effect {
|
||
EventEffect::WeatherChange { probability } => {
|
||
if effect_roll < *probability {
|
||
Self::apply_weather_change(&mut conn, region_id)?;
|
||
effect_results.push(json!({
|
||
"type": "weather_change"
|
||
}));
|
||
}
|
||
}
|
||
EventEffect::ProductionQualityChange {
|
||
probability,
|
||
min_change,
|
||
max_change,
|
||
} => {
|
||
if effect_roll < *probability {
|
||
let change = rng.gen_range(*min_change..=*max_change);
|
||
Self::apply_production_quality_change(&mut conn, region_id, change)?;
|
||
effect_results.push(json!({
|
||
"type": "production_quality_change",
|
||
"change": change
|
||
}));
|
||
}
|
||
}
|
||
EventEffect::PriceChange {
|
||
probability,
|
||
min_percent,
|
||
max_percent,
|
||
} => {
|
||
if effect_roll < *probability {
|
||
let percent_change = rng.gen_range(*min_percent..=*max_percent);
|
||
Self::apply_price_change(&mut conn, region_id, percent_change)?;
|
||
effect_results.push(json!({
|
||
"type": "price_change",
|
||
"percent": percent_change
|
||
}));
|
||
}
|
||
}
|
||
EventEffect::TransportSpeedChange {
|
||
probability,
|
||
min_percent,
|
||
max_percent,
|
||
} => {
|
||
if effect_roll < *probability {
|
||
let percent_change = rng.gen_range(*min_percent..=*max_percent);
|
||
Self::apply_transport_speed_change(&mut conn, region_id, percent_change)?;
|
||
effect_results.push(json!({
|
||
"type": "transport_speed_change",
|
||
"percent": percent_change
|
||
}));
|
||
}
|
||
}
|
||
EventEffect::CharacterHealthChange {
|
||
probability,
|
||
min_change,
|
||
max_change,
|
||
} => {
|
||
// WICHTIG: Bei regionalen Ereignissen ist `probability` als Anteil der
|
||
// betroffenen Charaktere gemeint (z.B. 0.8 = 80% betroffen),
|
||
// nicht als "Chance, dass der Effekt global auf ALLE greift".
|
||
if let Ok((affected_characters, dead_characters)) =
|
||
Self::apply_regional_character_health_change(
|
||
&mut conn,
|
||
pool,
|
||
broker,
|
||
region_id,
|
||
*probability,
|
||
*min_change,
|
||
*max_change,
|
||
rng,
|
||
)
|
||
{
|
||
effect_results.push(json!({
|
||
"type": "character_health_change",
|
||
"affected_count": affected_characters.len(),
|
||
"characters": affected_characters
|
||
}));
|
||
if !dead_characters.is_empty() {
|
||
effect_results.push(json!({
|
||
"type": "character_death",
|
||
"dead_count": dead_characters.len(),
|
||
"characters": dead_characters
|
||
}));
|
||
}
|
||
}
|
||
}
|
||
EventEffect::CharacterDeath { probability } => {
|
||
// WICHTIG: Bei regionalen Ereignissen ist `probability` als
|
||
// "Chance pro Charakter zu sterben" gemeint (z.B. 0.1 = 10%),
|
||
// nicht als "10% Chance, dass alle Charaktere der Region sterben".
|
||
if let Ok(dead_characters) = Self::apply_regional_character_death(
|
||
&mut conn,
|
||
region_id,
|
||
*probability,
|
||
pool,
|
||
broker,
|
||
rng,
|
||
) {
|
||
effect_results.push(json!({
|
||
"type": "character_death",
|
||
"dead_count": dead_characters.len(),
|
||
"characters": dead_characters
|
||
}));
|
||
}
|
||
}
|
||
EventEffect::StorageDamage {
|
||
probability,
|
||
stock_type_label,
|
||
inventory_damage_min_percent,
|
||
inventory_damage_max_percent,
|
||
storage_destruction_min_percent,
|
||
storage_destruction_max_percent,
|
||
} => {
|
||
if effect_roll < *probability
|
||
&& let Ok(damage_info) = Self::apply_storage_damage(
|
||
&mut conn,
|
||
StorageDamageParams {
|
||
region_id,
|
||
stock_type_label,
|
||
inventory_damage_min_percent: *inventory_damage_min_percent,
|
||
inventory_damage_max_percent: *inventory_damage_max_percent,
|
||
storage_destruction_min_percent: *storage_destruction_min_percent,
|
||
storage_destruction_max_percent: *storage_destruction_max_percent,
|
||
},
|
||
rng,
|
||
)
|
||
{
|
||
effect_results.push(json!({
|
||
"type": "storage_damage",
|
||
"stock_type": stock_type_label,
|
||
"inventory_damage_percent": damage_info.inventory_damage_percent,
|
||
"storage_destruction_percent": damage_info.storage_destruction_percent,
|
||
"affected_stocks": damage_info.affected_stocks,
|
||
"destroyed_stocks": damage_info.destroyed_stocks,
|
||
}));
|
||
}
|
||
}
|
||
EventEffect::StorageCapacityChange {
|
||
probability,
|
||
min_percent,
|
||
max_percent,
|
||
} => {
|
||
if effect_roll < *probability
|
||
&& let Ok((affected_stocks, percent_change)) = Self::apply_regional_storage_capacity_change(
|
||
&mut conn,
|
||
region_id,
|
||
*min_percent,
|
||
*max_percent,
|
||
rng,
|
||
)
|
||
{
|
||
effect_results.push(json!({
|
||
"type": "storage_capacity_change",
|
||
"percent": percent_change,
|
||
"affected_stocks": affected_stocks,
|
||
}));
|
||
}
|
||
}
|
||
EventEffect::HouseQualityChange {
|
||
probability,
|
||
min_change,
|
||
max_change,
|
||
} => {
|
||
if effect_roll < *probability
|
||
&& let Ok((affected_houses, quality_change)) = Self::apply_regional_house_quality_change(
|
||
&mut conn,
|
||
region_id,
|
||
*min_change,
|
||
*max_change,
|
||
rng,
|
||
)
|
||
{
|
||
effect_results.push(json!({
|
||
"type": "house_quality_change",
|
||
"change": quality_change,
|
||
"affected_houses": affected_houses,
|
||
}));
|
||
}
|
||
}
|
||
_ => {
|
||
eprintln!(
|
||
"[EventsWorker] Effekt {:?} wird für regionale Ereignisse noch nicht unterstützt",
|
||
effect
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Finde alle betroffenen User in dieser Region (User mit Branches)
|
||
conn.prepare("get_affected_users", QUERY_GET_AFFECTED_USERS)?;
|
||
let user_rows = conn.execute("get_affected_users", &[®ion_id])?;
|
||
|
||
// Sende Benachrichtigung an jeden betroffenen User einzeln
|
||
let mut notified_users = 0;
|
||
for row in user_rows {
|
||
let user_id: Option<i32> = row
|
||
.get("user_id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
if let Some(uid) = user_id {
|
||
// Schreibe Benachrichtigung in die Datenbank mit Event-Details
|
||
let notification_json = serde_json::json!({
|
||
"tr": format!("random_event.{}", event.id),
|
||
"value": {
|
||
"event_id": event.id,
|
||
"event_type": "regional",
|
||
"title": event.title,
|
||
"description": event.description,
|
||
"region_id": region_id,
|
||
"effects": effect_results
|
||
}
|
||
});
|
||
if let Err(err) =
|
||
Self::notify_user(pool, broker, uid, ¬ification_json.to_string(), None)
|
||
{
|
||
eprintln!("[EventsWorker] Fehler beim Schreiben der Benachrichtigung für User {}: {}", uid, err);
|
||
}
|
||
|
||
// Sende Benachrichtigung über WebSocket
|
||
let notification = json!({
|
||
"event": "random_event",
|
||
"event_id": event.id,
|
||
"event_type": "regional",
|
||
"region_id": region_id,
|
||
"user_id": uid,
|
||
"title": event.title,
|
||
"description": event.description,
|
||
"effects": effect_results
|
||
});
|
||
|
||
broker.publish(notification.to_string());
|
||
notified_users += 1;
|
||
}
|
||
}
|
||
|
||
eprintln!(
|
||
"[EventsWorker] Regionales Ereignis '{}' für Region {} verarbeitet, {} User benachrichtigt",
|
||
event.id, region_id, notified_users
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// Hilfsfunktionen zum Anwenden von Effekten
|
||
|
||
fn apply_money_change(
|
||
conn: &mut DbConnection,
|
||
user_id: i32,
|
||
percent_change: f64,
|
||
) -> Result<f64, DbError> {
|
||
// Hole aktuelles Geld
|
||
conn.prepare("get_money", QUERY_GET_MONEY)?;
|
||
let rows = conn.execute("get_money", &[&user_id])?;
|
||
|
||
let current_money: Option<f64> = rows
|
||
.first()
|
||
.and_then(|r| r.get("money"))
|
||
.and_then(|v| v.parse::<f64>().ok());
|
||
|
||
let current_money = match current_money {
|
||
Some(m) => m,
|
||
None => {
|
||
eprintln!("[EventsWorker] Spieler {} nicht gefunden", user_id);
|
||
return Err(DbError::new("Spieler nicht gefunden".to_string()));
|
||
}
|
||
};
|
||
|
||
let change = current_money * (percent_change / 100.0);
|
||
// Action-String für money history: Unterscheide zwischen positiven (Geldsegen) und negativen (Diebstahl) Änderungen
|
||
let action = if percent_change > 0.0 {
|
||
"Zufallsereignis: Unerwarteter Geldsegen"
|
||
} else {
|
||
"Zufallsereignis: Diebstahl"
|
||
};
|
||
|
||
// Verwende parametrisierte Queries für Sicherheit gegen SQL-Injection
|
||
conn.prepare("update_money_event", QUERY_UPDATE_MONEY)?;
|
||
let _ = conn.execute("update_money_event", &[&user_id, &change, &action])?;
|
||
|
||
// Best-effort money_history insert for UI/history visibility.
|
||
let money_str = format!("{:.2}", change);
|
||
fn escape_sql_literal(s: &str) -> String { s.replace('\'', "''") }
|
||
let escaped_action = escape_sql_literal(&action);
|
||
let create_sql = r#"
|
||
CREATE TABLE IF NOT EXISTS falukant_log.money_history (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
user_id INTEGER NOT NULL,
|
||
change NUMERIC(10,2) NOT NULL,
|
||
action TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
"#;
|
||
let _ = conn.query(create_sql);
|
||
|
||
let history_sql = format!(
|
||
"INSERT INTO falukant_log.money_history (user_id, change, action, created_at) VALUES ({uid}, {money}::numeric, '{act}', NOW());",
|
||
uid = user_id,
|
||
money = money_str,
|
||
act = escaped_action
|
||
);
|
||
if let Err(err) = conn.query(&history_sql) {
|
||
eprintln!(
|
||
"[EventsWorker] Warning: inserting money_history failed for user {}: {}",
|
||
user_id, err
|
||
);
|
||
}
|
||
|
||
Ok(change)
|
||
}
|
||
|
||
fn apply_storage_capacity_change(
|
||
conn: &mut DbConnection,
|
||
user_id: i32,
|
||
percent_change: f64,
|
||
) -> Result<(), DbError> {
|
||
// Hole alle Stocks des Spielers
|
||
conn.prepare("get_user_stocks_capacity", QUERY_GET_USER_STOCKS)?;
|
||
let stock_rows = conn.execute("get_user_stocks_capacity", &[&user_id])?;
|
||
|
||
if stock_rows.is_empty() {
|
||
eprintln!(
|
||
"[EventsWorker] Keine Stocks für Spieler {} gefunden",
|
||
user_id
|
||
);
|
||
return Ok(());
|
||
}
|
||
|
||
// Reduziere die Kapazität aller Stocks
|
||
conn.prepare("update_stock_capacity", QUERY_UPDATE_STOCK_CAPACITY)?;
|
||
|
||
let mut affected_stocks = 0;
|
||
for row in stock_rows {
|
||
let stock_id: Option<i32> = row
|
||
.get("stock_id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let stock_id = match stock_id {
|
||
Some(id) => id,
|
||
None => continue,
|
||
};
|
||
|
||
let current_capacity: i32 = row
|
||
.get("current_capacity")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(0);
|
||
|
||
if current_capacity > 0 {
|
||
conn.execute("update_stock_capacity", &[&percent_change, &stock_id])?;
|
||
affected_stocks += 1;
|
||
}
|
||
}
|
||
|
||
eprintln!(
|
||
"[EventsWorker] Lagerkapazitätsänderung für Spieler {}: {:.2}% bei {} Stocks",
|
||
user_id, percent_change, affected_stocks
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn apply_regional_storage_capacity_change(
|
||
conn: &mut DbConnection,
|
||
region_id: i32,
|
||
min_percent: f64,
|
||
max_percent: f64,
|
||
rng: &mut impl Rng,
|
||
) -> Result<(i32, f64), DbError> {
|
||
// Hole alle Stocks in der Region
|
||
conn.prepare("get_region_stocks_capacity", QUERY_GET_REGION_STOCKS)?;
|
||
let stock_rows = conn.execute("get_region_stocks_capacity", &[®ion_id])?;
|
||
|
||
if stock_rows.is_empty() {
|
||
eprintln!(
|
||
"[EventsWorker] Keine Stocks in Region {} gefunden",
|
||
region_id
|
||
);
|
||
return Ok((0, 0.0));
|
||
}
|
||
|
||
// Berechne die prozentuale Änderung
|
||
let percent_change = rng.gen_range(min_percent..=max_percent);
|
||
|
||
// Reduziere die Kapazität aller Stocks
|
||
conn.prepare("update_stock_capacity_regional", QUERY_UPDATE_STOCK_CAPACITY_REGIONAL)?;
|
||
|
||
let mut affected_stocks = 0;
|
||
for row in stock_rows {
|
||
let stock_id: Option<i32> = row
|
||
.get("stock_id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let stock_id = match stock_id {
|
||
Some(id) => id,
|
||
None => continue,
|
||
};
|
||
|
||
let current_capacity: i32 = row
|
||
.get("current_capacity")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(0);
|
||
|
||
if current_capacity > 0 {
|
||
conn.execute("update_stock_capacity_regional", &[&percent_change, &stock_id])?;
|
||
affected_stocks += 1;
|
||
}
|
||
}
|
||
|
||
eprintln!(
|
||
"[EventsWorker] Regionale Lagerkapazitätsänderung für Region {}: {:.2}% bei {} Stocks",
|
||
region_id, percent_change, affected_stocks
|
||
);
|
||
|
||
Ok((affected_stocks, percent_change))
|
||
}
|
||
|
||
fn apply_regional_house_quality_change(
|
||
conn: &mut DbConnection,
|
||
region_id: i32,
|
||
min_change: i32,
|
||
max_change: i32,
|
||
rng: &mut impl Rng,
|
||
) -> Result<(i32, i32), DbError> {
|
||
// Hole alle Häuser in der Region
|
||
conn.prepare("get_region_houses", QUERY_GET_REGION_HOUSES)?;
|
||
let house_rows = conn.execute("get_region_houses", &[®ion_id])?;
|
||
|
||
if house_rows.is_empty() {
|
||
eprintln!(
|
||
"[EventsWorker] Keine Häuser in Region {} gefunden",
|
||
region_id
|
||
);
|
||
return Ok((0, 0));
|
||
}
|
||
|
||
// Berechne die Änderung
|
||
let quality_change = rng.gen_range(min_change..=max_change);
|
||
|
||
// Reduziere die Qualität aller Häuser
|
||
conn.prepare("update_house_quality", QUERY_UPDATE_HOUSE_QUALITY)?;
|
||
|
||
let mut affected_houses = 0;
|
||
for row in house_rows {
|
||
let house_id: Option<i32> = row
|
||
.get("house_id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let house_id = match house_id {
|
||
Some(id) => id,
|
||
None => continue,
|
||
};
|
||
|
||
conn.execute("update_house_quality", &[&quality_change, &house_id])?;
|
||
affected_houses += 1;
|
||
}
|
||
|
||
eprintln!(
|
||
"[EventsWorker] Regionale Hausqualitätsänderung für Region {}: {} Punkte bei {} Häusern",
|
||
region_id, quality_change, affected_houses
|
||
);
|
||
|
||
Ok((affected_houses, quality_change))
|
||
}
|
||
|
||
fn apply_weather_change(
|
||
conn: &mut DbConnection,
|
||
region_id: i32,
|
||
) -> Result<(), DbError> {
|
||
// Wähle ein zufälliges Wetter
|
||
conn.prepare("change_weather", QUERY_CHANGE_WEATHER)?;
|
||
conn.execute("change_weather", &[®ion_id])?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn apply_production_quality_change(
|
||
_conn: &mut DbConnection,
|
||
region_id: i32,
|
||
change: i32,
|
||
) -> Result<(), DbError> {
|
||
// TODO: Implementierung für temporäre Produktionsqualitätsänderung
|
||
// Dies könnte eine temporäre Modifikation sein, die nach einer bestimmten Zeit abläuft
|
||
eprintln!(
|
||
"[EventsWorker] Produktionsqualitätsänderung für Region {}: {} (noch nicht implementiert)",
|
||
region_id, change
|
||
);
|
||
Ok(())
|
||
}
|
||
|
||
fn apply_price_change(
|
||
_conn: &mut DbConnection,
|
||
region_id: i32,
|
||
percent_change: f64,
|
||
) -> Result<(), DbError> {
|
||
// TODO: Implementierung für temporäre Preisänderung
|
||
// Dies könnte eine temporäre Modifikation der worth_percent Werte sein
|
||
eprintln!(
|
||
"[EventsWorker] Preisänderung für Region {}: {:.2}% (noch nicht implementiert)",
|
||
region_id, percent_change
|
||
);
|
||
Ok(())
|
||
}
|
||
|
||
fn apply_transport_speed_change(
|
||
_conn: &mut DbConnection,
|
||
region_id: i32,
|
||
percent_change: f64,
|
||
) -> Result<(), DbError> {
|
||
// TODO: Implementierung für temporäre Transportgeschwindigkeitsänderung
|
||
eprintln!(
|
||
"[EventsWorker] Transportgeschwindigkeitsänderung für Region {}: {:.2}% (noch nicht implementiert)",
|
||
region_id, percent_change
|
||
);
|
||
Ok(())
|
||
}
|
||
|
||
fn apply_character_health_change(
|
||
conn: &mut DbConnection,
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
user_id: i32,
|
||
min_change: i32,
|
||
max_change: i32,
|
||
rng: &mut impl Rng,
|
||
) -> Result<(i32, i32, bool, Option<String>, Option<String>), DbError> {
|
||
// Hole einen zufälligen Charakter des Spielers
|
||
conn.prepare("get_random_character", QUERY_GET_RANDOM_CHARACTER)?;
|
||
let rows = conn.execute("get_random_character", &[&user_id])?;
|
||
|
||
let character_id: Option<i32> = rows
|
||
.first()
|
||
.and_then(|r| r.get("id"))
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let character_id = match character_id {
|
||
Some(id) => id,
|
||
None => {
|
||
eprintln!("[EventsWorker] Kein lebender Charakter für Spieler {} gefunden", user_id);
|
||
return Err(DbError::new("Kein Charakter gefunden".to_string()));
|
||
}
|
||
};
|
||
|
||
let current_health: i32 = rows
|
||
.first()
|
||
.and_then(|r| r.get("health"))
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(100);
|
||
|
||
// Namen-Snapshot (wichtig, falls der Charakter durch das Event stirbt und gelöscht wird)
|
||
let first_name = rows.first().and_then(|r| r.get("first_name")).cloned();
|
||
let last_name = rows.first().and_then(|r| r.get("last_name")).cloned();
|
||
|
||
let health_change = rng.gen_range(min_change..=max_change);
|
||
let new_health = (current_health + health_change).clamp(0, 100);
|
||
|
||
// Update Gesundheit
|
||
conn.prepare("update_health", QUERY_UPDATE_HEALTH)?;
|
||
conn.execute("update_health", &[&new_health, &character_id])?;
|
||
|
||
// Wenn die Gesundheit 0 erreicht, muss der Charakter „sterben“ (Cleanup + Löschen).
|
||
// Sonst bleibt er als health=0 „hängen“, weil spätere Queries meist health>0 filtern.
|
||
let died = new_health <= 0;
|
||
if died {
|
||
// Best-effort: Death-Handling kann fehlschlagen (FK/DB), dann loggt es selbst.
|
||
let _ = Self::handle_character_death(pool, broker, character_id);
|
||
}
|
||
|
||
Ok((character_id, health_change, died, first_name, last_name))
|
||
}
|
||
|
||
fn apply_character_death(
|
||
conn: &mut DbConnection,
|
||
user_id: i32,
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
) -> Result<(i32, Option<String>, Option<String>), DbError> {
|
||
// Hole einen zufälligen Charakter des Spielers
|
||
conn.prepare("get_random_character_death", QUERY_GET_RANDOM_CHARACTER)?;
|
||
let rows = conn.execute("get_random_character_death", &[&user_id])?;
|
||
|
||
let character_id: Option<i32> = rows
|
||
.first()
|
||
.and_then(|r| r.get("id"))
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let character_id = match character_id {
|
||
Some(id) => id,
|
||
None => {
|
||
eprintln!("[EventsWorker] Kein lebender Charakter für Spieler {} gefunden", user_id);
|
||
return Err(DbError::new("Kein Charakter gefunden".to_string()));
|
||
}
|
||
};
|
||
|
||
let first_name = rows.first().and_then(|r| r.get("first_name")).cloned();
|
||
let last_name = rows.first().and_then(|r| r.get("last_name")).cloned();
|
||
|
||
// Verwende die existierende Logik zum Löschen von Charakteren
|
||
// (ähnlich wie in CharacterCreationWorker)
|
||
Self::handle_character_death(pool, broker, character_id)?;
|
||
|
||
Ok((character_id, first_name, last_name))
|
||
}
|
||
|
||
fn apply_regional_character_health_change(
|
||
conn: &mut DbConnection,
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
region_id: i32,
|
||
affected_probability: f64,
|
||
min_change: i32,
|
||
max_change: i32,
|
||
rng: &mut impl Rng,
|
||
) -> Result<(Vec<(i32, i32)>, Vec<i32>), DbError> {
|
||
// Hole alle lebenden Charaktere in der Region
|
||
conn.prepare("get_region_characters", QUERY_GET_REGION_CHARACTERS)?;
|
||
let rows = conn.execute("get_region_characters", &[®ion_id])?;
|
||
|
||
let mut affected_characters = Vec::new();
|
||
let mut dead_characters = Vec::new();
|
||
|
||
for row in rows {
|
||
// Nur ein Teil der Charaktere soll betroffen sein (z.B. 80% bei Epidemie).
|
||
if rng.gen_range(0.0..=1.0) >= affected_probability {
|
||
continue;
|
||
}
|
||
|
||
let character_id: Option<i32> = row
|
||
.get("id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let character_id = match character_id {
|
||
Some(id) => id,
|
||
None => continue,
|
||
};
|
||
|
||
let current_health: i32 = row
|
||
.get("health")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(100);
|
||
|
||
let health_change = rng.gen_range(min_change..=max_change);
|
||
let new_health = (current_health + health_change).clamp(0, 100);
|
||
|
||
// Update Gesundheit
|
||
conn.prepare("update_health_regional", QUERY_UPDATE_HEALTH)?;
|
||
conn.execute("update_health_regional", &[&new_health, &character_id])?;
|
||
|
||
affected_characters.push((character_id, health_change));
|
||
|
||
if new_health <= 0 {
|
||
if Self::handle_character_death(pool, broker, character_id).is_ok() {
|
||
dead_characters.push(character_id);
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok((affected_characters, dead_characters))
|
||
}
|
||
|
||
fn apply_regional_character_death(
|
||
conn: &mut DbConnection,
|
||
region_id: i32,
|
||
death_probability: f64,
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
rng: &mut impl Rng,
|
||
) -> Result<Vec<i32>, DbError> {
|
||
// Hole alle lebenden Charaktere in der Region
|
||
conn.prepare("get_region_characters_death", QUERY_GET_REGION_CHARACTERS)?;
|
||
let rows = conn.execute("get_region_characters_death", &[®ion_id])?;
|
||
|
||
let mut dead_characters = Vec::new();
|
||
|
||
for row in rows {
|
||
// Chance pro Charakter (z.B. 10% bei Epidemie), nicht global.
|
||
if rng.gen_range(0.0..=1.0) >= death_probability {
|
||
continue;
|
||
}
|
||
|
||
let character_id: Option<i32> = row
|
||
.get("id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let character_id = match character_id {
|
||
Some(id) => id,
|
||
None => continue,
|
||
};
|
||
|
||
// Verwende die existierende Logik zum Löschen von Charakteren
|
||
if Self::handle_character_death(pool, broker, character_id).is_ok() {
|
||
dead_characters.push(character_id);
|
||
}
|
||
}
|
||
|
||
Ok(dead_characters)
|
||
}
|
||
|
||
fn handle_character_death(
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
character_id: i32,
|
||
) -> Result<(), DbError> {
|
||
// Diese Funktion verwendet die gleiche Logik wie CharacterCreationWorker
|
||
// Wir müssen die Queries aus character_creation.rs verwenden
|
||
let mut conn = pool
|
||
.get()
|
||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||
|
||
// 1) Director löschen und User benachrichtigen
|
||
conn.prepare("delete_director", QUERY_DELETE_DIRECTOR)?;
|
||
let dir_result = conn.execute("delete_director", &[&character_id])?;
|
||
for row in dir_result {
|
||
if let Some(user_id) = row
|
||
.get("employer_user_id")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
{
|
||
Self::notify_user(pool, broker, user_id, "director_death", None)?;
|
||
}
|
||
}
|
||
|
||
// 2) Relationships löschen und betroffene User benachrichtigen
|
||
conn.prepare("delete_relationship", QUERY_DELETE_RELATIONSHIP)?;
|
||
let rel_result = conn.execute("delete_relationship", &[&character_id])?;
|
||
|
||
// Logging: Anzahl gelöschter Relationships
|
||
let deleted_count = rel_result.len();
|
||
if deleted_count > 0 {
|
||
eprintln!(
|
||
"[EventsWorker] {} Relationship(s) gelöscht für character_id={}",
|
||
deleted_count, character_id
|
||
);
|
||
}
|
||
|
||
for row in rel_result {
|
||
let related_user_id = row
|
||
.get("related_user_id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
let related_character_id = row
|
||
.get("related_character_id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
let relationship_type_tr = row
|
||
.get("relationship_type_tr")
|
||
.and_then(|v| v.as_str().map(|s| s.to_string()));
|
||
|
||
// Logging: Relationship wurde gelöscht
|
||
eprintln!(
|
||
"[EventsWorker] Relationship gelöscht: character_id={}, related_character_id={:?}, related_user_id={:?}, relationship_type={:?}",
|
||
character_id,
|
||
related_character_id,
|
||
related_user_id,
|
||
relationship_type_tr
|
||
);
|
||
|
||
if let Some(uid) = related_user_id {
|
||
// Spezielle Notification für Verlobungen
|
||
if relationship_type_tr.as_deref() == Some("engaged") {
|
||
let notification_json = serde_json::json!({
|
||
"tr": "relationship.engaged_character_death",
|
||
"character_id": related_character_id
|
||
});
|
||
Self::notify_user(pool, broker, uid, ¬ification_json.to_string(), related_character_id)?;
|
||
} else {
|
||
Self::notify_user(pool, broker, uid, "relationship_death", None)?;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3) Erben-Logik für Spieler-Charaktere (VOR dem Löschen der Child-Relations!)
|
||
// Prüfe, ob der Charakter ein Spieler-Charakter ist
|
||
conn.prepare("get_user_id", QUERY_GET_USER_ID)?;
|
||
let user_rows = conn.execute("get_user_id", &[&character_id])?;
|
||
|
||
let user_id: Option<i32> = user_rows
|
||
.first()
|
||
.and_then(|r| r.get("user_id"))
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
if let Some(falukant_user_id) = user_id {
|
||
// Spieler-Charakter: Erben-Logik ausführen
|
||
// WICHTIG: Dies muss VOR dem Löschen der Child-Relations passieren!
|
||
Self::handle_inheritance(pool, broker, &mut conn, character_id, falukant_user_id)?;
|
||
}
|
||
|
||
// 4) Child-Relations löschen und Eltern benachrichtigen
|
||
conn.prepare("delete_child_relation", QUERY_DELETE_CHILD_RELATION)?;
|
||
let child_result = conn.execute("delete_child_relation", &[&character_id])?;
|
||
for row in child_result {
|
||
if let Some(father_user_id) = row
|
||
.get("father_user_id")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
{
|
||
Self::notify_user(pool, broker, father_user_id, "child_death", None)?;
|
||
}
|
||
if let Some(mother_user_id) = row
|
||
.get("mother_user_id")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
{
|
||
Self::notify_user(pool, broker, mother_user_id, "child_death", None)?;
|
||
}
|
||
}
|
||
|
||
// 5) Charakter löschen
|
||
conn.prepare("delete_character", QUERY_DELETE_CHARACTER)?;
|
||
conn.execute("delete_character", &[&character_id])?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn handle_inheritance(
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
conn: &mut DbConnection,
|
||
deceased_character_id: i32,
|
||
falukant_user_id: i32,
|
||
) -> Result<(), DbError> {
|
||
// 1) Finde den Erben (bevorzugt is_heir = TRUE)
|
||
conn.prepare("get_heir", QUERY_GET_HEIR)?;
|
||
let heir_rows = conn.execute("get_heir", &[&deceased_character_id])?;
|
||
|
||
let heir_id: Option<i32> = heir_rows
|
||
.first()
|
||
.and_then(|r| r.get("child_character_id"))
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
// Kein Kind als Erbe vorhanden? Dann fallback: zufälliger NPC-Character aus der Region,
|
||
// Alter 10–14 Tage.
|
||
let heir_id = match heir_id {
|
||
Some(id) if id > 0 => id,
|
||
_ => {
|
||
conn.prepare("random_heir_region", QUERY_GET_RANDOM_HEIR_FROM_REGION)?;
|
||
let rows = conn.execute("random_heir_region", &[&deceased_character_id])?;
|
||
rows.first()
|
||
.and_then(|r| r.get("child_character_id"))
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(0)
|
||
}
|
||
};
|
||
|
||
if heir_id <= 0 {
|
||
eprintln!(
|
||
"[EventsWorker] Kein Erbe für Charakter {} gefunden (weder Kind noch Fallback in Region). User {} hat danach keinen Character.",
|
||
deceased_character_id, falukant_user_id
|
||
);
|
||
return Ok(());
|
||
}
|
||
|
||
// 2) Wichtig: erst die alte User-Zuordnung am verstorbenen Charakter lösen.
|
||
// Falls es einen Unique-Constraint auf `character.user_id` gibt, würde das
|
||
// direkte Setzen am Erben sonst fehlschlagen.
|
||
conn.prepare("clear_character_user", QUERY_CLEAR_CHARACTER_USER)?;
|
||
if let Err(err) = conn.execute("clear_character_user", &[&deceased_character_id]) {
|
||
eprintln!(
|
||
"[EventsWorker] Erben-Logik: Konnte user_id vom verstorbenen Charakter {} nicht lösen: {}",
|
||
deceased_character_id, err
|
||
);
|
||
}
|
||
|
||
// 3) Setze den Erben als neuen Spieler-Charakter
|
||
conn.prepare("set_character_user", QUERY_SET_CHARACTER_USER)?;
|
||
if let Err(err) = conn.execute("set_character_user", &[&falukant_user_id, &heir_id]) {
|
||
eprintln!(
|
||
"[EventsWorker] Erben-Logik: Konnte user_id={} nicht auf Erben {} setzen (verstorbener Charakter {}): {}",
|
||
falukant_user_id, heir_id, deceased_character_id, err
|
||
);
|
||
// Abbrechen, damit wir nicht still ohne Character weiterlaufen.
|
||
return Err(err);
|
||
}
|
||
|
||
// 4) Berechne das neue Vermögen basierend auf dem gesamten Vermögen
|
||
// Hole alle Vermögenswerte (analog zu UserCharacterWorker::calculate_new_money)
|
||
conn.prepare("get_current_money", QUERY_GET_CURRENT_MONEY)?;
|
||
conn.prepare("get_house_value", QUERY_GET_HOUSE_VALUE)?;
|
||
conn.prepare("get_settlement_value", QUERY_GET_SETTLEMENT_VALUE)?;
|
||
conn.prepare("get_inventory_value", QUERY_GET_INVENTORY_VALUE)?;
|
||
conn.prepare("get_credit_debt", QUERY_GET_CREDIT_DEBT)?;
|
||
conn.prepare("count_children", QUERY_COUNT_CHILDREN)?;
|
||
|
||
let cash: f64 = conn
|
||
.execute("get_current_money", &[&falukant_user_id])?
|
||
.first()
|
||
.and_then(|r| r.get("money"))
|
||
.and_then(|v| v.parse::<f64>().ok())
|
||
.unwrap_or(0.0);
|
||
|
||
let houses: f64 = conn
|
||
.execute("get_house_value", &[&falukant_user_id])?
|
||
.first()
|
||
.and_then(|r| r.get("sum"))
|
||
.and_then(|v| v.parse::<f64>().ok())
|
||
.unwrap_or(0.0);
|
||
|
||
let settlements: f64 = conn
|
||
.execute("get_settlement_value", &[&falukant_user_id])?
|
||
.first()
|
||
.and_then(|r| r.get("sum"))
|
||
.and_then(|v| v.parse::<f64>().ok())
|
||
.unwrap_or(0.0);
|
||
|
||
let inventory: f64 = conn
|
||
.execute("get_inventory_value", &[&falukant_user_id])?
|
||
.first()
|
||
.and_then(|r| r.get("sum"))
|
||
.and_then(|v| v.parse::<f64>().ok())
|
||
.unwrap_or(0.0);
|
||
|
||
let debt: f64 = conn
|
||
.execute("get_credit_debt", &[&falukant_user_id])?
|
||
.first()
|
||
.and_then(|r| r.get("sum"))
|
||
.and_then(|v| v.parse::<f64>().ok())
|
||
.unwrap_or(0.0);
|
||
|
||
let child_count: i32 = conn
|
||
.execute("count_children", &[&deceased_character_id, &heir_id])?
|
||
.first()
|
||
.and_then(|r| r.get("cnt"))
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(0);
|
||
|
||
// Berechne das neue Vermögen (analog zu UserCharacterWorker::calculate_new_money)
|
||
let total_assets = cash + houses + settlements + inventory - debt;
|
||
let single = child_count <= 0; // Nur der Erbe bleibt
|
||
|
||
let heir_share = if single {
|
||
total_assets
|
||
} else {
|
||
total_assets * 0.8
|
||
};
|
||
|
||
let new_money = heir_share - (houses + settlements + inventory + debt);
|
||
let final_money = if new_money <= 1000.0 {
|
||
1000.0
|
||
} else {
|
||
new_money
|
||
};
|
||
|
||
// 4) Aktualisiere das Vermögen über die update_money Funktion
|
||
// Verwende die BaseWorker-Funktion für konsistente Geld-Updates
|
||
use crate::worker::base::BaseWorker;
|
||
let base = BaseWorker::new("EventsWorker", pool.clone(), broker.clone());
|
||
let money_change = final_money - cash;
|
||
base.change_falukant_user_money(
|
||
falukant_user_id,
|
||
money_change,
|
||
&format!("Erbe für Charakter {}", deceased_character_id),
|
||
)?;
|
||
|
||
eprintln!(
|
||
"[EventsWorker] Erbe {} übernimmt Vermögen von Charakter {} (User {}): {:.2} (von {:.2} Gesamtvermögen, {} weitere Kinder)",
|
||
heir_id, deceased_character_id, falukant_user_id, final_money, total_assets, child_count
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn notify_user(
|
||
pool: &ConnectionPool,
|
||
broker: &MessageBroker,
|
||
user_id: i32,
|
||
event_type: &str,
|
||
character_id: Option<i32>,
|
||
) -> Result<(), DbError> {
|
||
// DB-Notification (zentralisiert). Historisch wird hier als `tr` der event_type-String gespeichert.
|
||
insert_notification(pool, user_id, event_type, character_id)?;
|
||
|
||
// Frontend-Update (zentralisiert)
|
||
publish_update_status(broker, user_id);
|
||
|
||
// ursprüngliche Benachrichtigung
|
||
let message =
|
||
format!(r#"{{"event":"{event_type}","user_id":{}}}"#, user_id);
|
||
broker.publish(message);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn apply_storage_damage(
|
||
conn: &mut DbConnection,
|
||
params: StorageDamageParams,
|
||
rng: &mut impl Rng,
|
||
) -> Result<StorageDamageInfo, DbError> {
|
||
// 1. Finde Stock-Typ-ID basierend auf Label
|
||
conn.prepare("get_stock_type_id", QUERY_GET_STOCK_TYPE_ID)?;
|
||
let stock_type_rows = conn.execute("get_stock_type_id", &[¶ms.stock_type_label])?;
|
||
|
||
let stock_type_id: Option<i32> = stock_type_rows
|
||
.first()
|
||
.and_then(|r| r.get("id"))
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let stock_type_id = match stock_type_id {
|
||
Some(id) => id,
|
||
None => {
|
||
eprintln!(
|
||
"[EventsWorker] Stock-Typ '{}' nicht gefunden",
|
||
params.stock_type_label
|
||
);
|
||
return Err(DbError::new(format!(
|
||
"Stock-Typ '{}' nicht gefunden",
|
||
params.stock_type_label
|
||
)));
|
||
}
|
||
};
|
||
|
||
// 2. Hole alle Stocks dieses Typs in der Region mit ihren Branches
|
||
conn.prepare("get_region_stocks", QUERY_GET_REGION_STOCKS)?;
|
||
let stock_rows = conn.execute("get_region_stocks", &[¶ms.region_id, &stock_type_id])?;
|
||
|
||
if stock_rows.is_empty() {
|
||
eprintln!(
|
||
"[EventsWorker] Keine Stocks vom Typ '{}' in Region {} gefunden",
|
||
params.stock_type_label, params.region_id
|
||
);
|
||
return Ok(StorageDamageInfo {
|
||
inventory_damage_percent: 0.0,
|
||
storage_destruction_percent: 0.0,
|
||
affected_stocks: 0,
|
||
destroyed_stocks: 0,
|
||
});
|
||
}
|
||
|
||
// 3. Berechne Schäden
|
||
let inventory_damage_percent =
|
||
rng.gen_range(params.inventory_damage_min_percent..=params.inventory_damage_max_percent);
|
||
let storage_destruction_percent =
|
||
rng.gen_range(params.storage_destruction_min_percent..=params.storage_destruction_max_percent);
|
||
|
||
let total_stocks = stock_rows.len();
|
||
let stocks_to_destroy = ((total_stocks as f64 * storage_destruction_percent / 100.0)
|
||
.round() as usize)
|
||
.min(total_stocks);
|
||
|
||
// 4. Reduziere Lagerbestand in allen betroffenen Stocks
|
||
// Hole alle Inventar-Einträge für diese Stocks
|
||
conn.prepare("get_inventory_items", QUERY_GET_INVENTORY_ITEMS)?;
|
||
let inventory_rows = conn.execute("get_inventory_items", &[¶ms.region_id, &stock_type_id])?;
|
||
|
||
let mut affected_stocks = 0;
|
||
let mut processed_stocks = std::collections::HashSet::new();
|
||
|
||
for row in &inventory_rows {
|
||
let inventory_id: Option<i32> = row
|
||
.get("inventory_id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let inventory_id = match inventory_id {
|
||
Some(id) => id,
|
||
None => continue,
|
||
};
|
||
|
||
let stock_id: Option<i32> = row
|
||
.get("stock_id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let stock_id = match stock_id {
|
||
Some(id) => id,
|
||
None => continue,
|
||
};
|
||
|
||
let inventory_quantity: i32 = row
|
||
.get("inventory_quantity")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(0);
|
||
|
||
if inventory_quantity > 0 {
|
||
let damage = (inventory_quantity as f64 * inventory_damage_percent / 100.0).round() as i32;
|
||
let new_quantity = (inventory_quantity - damage).max(0);
|
||
|
||
// Reduziere Lagerbestand pro Inventar-Eintrag
|
||
conn.prepare("reduce_inventory", QUERY_REDUCE_INVENTORY)?;
|
||
conn.execute("reduce_inventory", &[&new_quantity, &inventory_id])?;
|
||
|
||
if !processed_stocks.contains(&stock_id) {
|
||
processed_stocks.insert(stock_id);
|
||
affected_stocks += 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5. Zerstöre zufällig ausgewählte Stocks
|
||
let mut destroyed_stocks = 0;
|
||
if stocks_to_destroy > 0 {
|
||
// Wähle zufällige Stocks zum Zerstören
|
||
let mut stock_ids_to_destroy: Vec<i32> = stock_rows
|
||
.iter()
|
||
.filter_map(|row| row.get("stock_id").and_then(|v| v.parse::<i32>().ok()))
|
||
.collect();
|
||
|
||
// Mische die Liste zufällig
|
||
stock_ids_to_destroy.shuffle(rng);
|
||
stock_ids_to_destroy.truncate(stocks_to_destroy);
|
||
|
||
for stock_id in &stock_ids_to_destroy {
|
||
// Lösche zuerst den Lagerbestand
|
||
conn.prepare("delete_inventory", QUERY_DELETE_INVENTORY)?;
|
||
conn.execute("delete_inventory", &[stock_id])?;
|
||
|
||
// Lösche dann das Stock selbst
|
||
conn.prepare("delete_stock", QUERY_DELETE_STOCK)?;
|
||
conn.execute("delete_stock", &[stock_id])?;
|
||
|
||
destroyed_stocks += 1;
|
||
}
|
||
}
|
||
|
||
// 6. Sicherstelle, dass Lagerbestand <= Lageranzahl für alle verbleibenden Stocks
|
||
// Hole alle verbleibenden Stocks mit ihrem Lagerbestand
|
||
let remaining_stock_rows = conn.execute("get_region_stocks", &[¶ms.region_id, &stock_type_id])?;
|
||
|
||
for row in remaining_stock_rows {
|
||
let stock_id: Option<i32> = row
|
||
.get("stock_id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let stock_id = match stock_id {
|
||
Some(id) => id,
|
||
None => continue,
|
||
};
|
||
|
||
let stock_capacity: i32 = row
|
||
.get("stock_capacity")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(0);
|
||
|
||
let total_inventory_quantity: i32 = row
|
||
.get("inventory_quantity")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(0);
|
||
|
||
// Wenn Gesamt-Lagerbestand > Lageranzahl, reduziere proportional
|
||
if total_inventory_quantity > stock_capacity && stock_capacity > 0 {
|
||
// Hole alle Inventar-Einträge für diesen Stock
|
||
conn.prepare("get_stock_inventory", QUERY_GET_STOCK_INVENTORY)?;
|
||
let inventory_items = conn.execute("get_stock_inventory", &[&stock_id])?;
|
||
|
||
// Reduziere proportional, sodass Gesamtmenge = stock_capacity
|
||
let reduction_factor = stock_capacity as f64 / total_inventory_quantity as f64;
|
||
|
||
for item_row in inventory_items {
|
||
let inventory_id: Option<i32> = item_row
|
||
.get("id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let inventory_id = match inventory_id {
|
||
Some(id) => id,
|
||
None => continue,
|
||
};
|
||
|
||
let item_quantity: i32 = item_row
|
||
.get("quantity")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(0);
|
||
|
||
let new_quantity = (item_quantity as f64 * reduction_factor).round() as i32;
|
||
|
||
conn.prepare("cap_inventory", QUERY_CAP_INVENTORY)?;
|
||
conn.execute("cap_inventory", &[&new_quantity, &inventory_id])?;
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(StorageDamageInfo {
|
||
inventory_damage_percent,
|
||
storage_destruction_percent,
|
||
affected_stocks,
|
||
destroyed_stocks,
|
||
})
|
||
}
|
||
|
||
fn apply_personal_storage_damage(
|
||
conn: &mut DbConnection,
|
||
params: PersonalStorageDamageParams,
|
||
rng: &mut impl Rng,
|
||
) -> Result<StorageDamageInfo, DbError> {
|
||
// 1. Finde Stock-Typ-ID basierend auf Label
|
||
conn.prepare("get_stock_type_id_personal", QUERY_GET_STOCK_TYPE_ID)?;
|
||
let stock_type_rows = conn.execute("get_stock_type_id_personal", &[¶ms.stock_type_label])?;
|
||
|
||
let stock_type_id: Option<i32> = stock_type_rows
|
||
.first()
|
||
.and_then(|r| r.get("id"))
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let stock_type_id = match stock_type_id {
|
||
Some(id) => id,
|
||
None => {
|
||
eprintln!(
|
||
"[EventsWorker] Stock-Typ '{}' nicht gefunden",
|
||
params.stock_type_label
|
||
);
|
||
return Err(DbError::new(format!(
|
||
"Stock-Typ '{}' nicht gefunden",
|
||
params.stock_type_label
|
||
)));
|
||
}
|
||
};
|
||
|
||
// 2. Hole alle Stocks dieses Typs für alle Branches des Spielers
|
||
conn.prepare("get_user_stocks", QUERY_GET_USER_STOCKS)?;
|
||
let stock_rows = conn.execute("get_user_stocks", &[¶ms.user_id, &stock_type_id])?;
|
||
|
||
if stock_rows.is_empty() {
|
||
eprintln!(
|
||
"[EventsWorker] Keine Stocks vom Typ '{}' für Spieler {} gefunden",
|
||
params.stock_type_label, params.user_id
|
||
);
|
||
return Ok(StorageDamageInfo {
|
||
inventory_damage_percent: 0.0,
|
||
storage_destruction_percent: 0.0,
|
||
affected_stocks: 0,
|
||
destroyed_stocks: 0,
|
||
});
|
||
}
|
||
|
||
// 3. Berechne Schäden
|
||
let inventory_damage_percent =
|
||
rng.gen_range(params.inventory_damage_min_percent..=params.inventory_damage_max_percent);
|
||
let storage_destruction_percent =
|
||
rng.gen_range(params.storage_destruction_min_percent..=params.storage_destruction_max_percent);
|
||
|
||
let total_stocks = stock_rows.len();
|
||
let stocks_to_destroy = ((total_stocks as f64 * storage_destruction_percent / 100.0)
|
||
.round() as usize)
|
||
.min(total_stocks);
|
||
|
||
// 4. Reduziere Lagerbestand in allen betroffenen Stocks
|
||
// Hole alle Inventar-Einträge für diese Stocks
|
||
conn.prepare("get_user_inventory_items", QUERY_GET_USER_INVENTORY_ITEMS)?;
|
||
let inventory_rows = conn.execute("get_user_inventory_items", &[¶ms.user_id, &stock_type_id])?;
|
||
|
||
let mut affected_stocks = 0;
|
||
let mut processed_stocks = std::collections::HashSet::new();
|
||
|
||
for row in &inventory_rows {
|
||
let inventory_id: Option<i32> = row
|
||
.get("inventory_id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let inventory_id = match inventory_id {
|
||
Some(id) => id,
|
||
None => continue,
|
||
};
|
||
|
||
let stock_id: Option<i32> = row
|
||
.get("stock_id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let stock_id = match stock_id {
|
||
Some(id) => id,
|
||
None => continue,
|
||
};
|
||
|
||
let inventory_quantity: i32 = row
|
||
.get("inventory_quantity")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(0);
|
||
|
||
if inventory_quantity > 0 {
|
||
let damage = (inventory_quantity as f64 * inventory_damage_percent / 100.0).round() as i32;
|
||
let new_quantity = (inventory_quantity - damage).max(0);
|
||
|
||
// Reduziere Lagerbestand pro Inventar-Eintrag
|
||
conn.prepare("reduce_inventory_personal", QUERY_REDUCE_INVENTORY_PERSONAL)?;
|
||
conn.execute("reduce_inventory_personal", &[&new_quantity, &inventory_id])?;
|
||
|
||
if !processed_stocks.contains(&stock_id) {
|
||
processed_stocks.insert(stock_id);
|
||
affected_stocks += 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5. Zerstöre zufällig ausgewählte Stocks (nur für field und wood)
|
||
let mut destroyed_stocks = 0;
|
||
if stocks_to_destroy > 0 && (params.stock_type_label == "field" || params.stock_type_label == "wood") {
|
||
// Wähle zufällige Stocks zum Zerstören
|
||
let mut stock_ids_to_destroy: Vec<i32> = stock_rows
|
||
.iter()
|
||
.filter_map(|row| row.get("stock_id").and_then(|v| v.parse::<i32>().ok()))
|
||
.collect();
|
||
|
||
// Mische die Liste zufällig
|
||
stock_ids_to_destroy.shuffle(rng);
|
||
stock_ids_to_destroy.truncate(stocks_to_destroy);
|
||
|
||
for stock_id in &stock_ids_to_destroy {
|
||
// Lösche zuerst den Lagerbestand
|
||
conn.prepare("delete_inventory_personal", QUERY_DELETE_INVENTORY_PERSONAL)?;
|
||
conn.execute("delete_inventory_personal", &[stock_id])?;
|
||
|
||
// Lösche dann das Stock selbst
|
||
conn.prepare("delete_stock_personal", QUERY_DELETE_STOCK_PERSONAL)?;
|
||
conn.execute("delete_stock_personal", &[stock_id])?;
|
||
|
||
destroyed_stocks += 1;
|
||
}
|
||
}
|
||
|
||
// 6. Sicherstelle, dass Lagerbestand <= Lageranzahl für alle verbleibenden Stocks
|
||
let remaining_stock_rows = conn.execute("get_user_stocks", &[¶ms.user_id, &stock_type_id])?;
|
||
|
||
for row in remaining_stock_rows {
|
||
let stock_id: Option<i32> = row
|
||
.get("stock_id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let stock_id = match stock_id {
|
||
Some(id) => id,
|
||
None => continue,
|
||
};
|
||
|
||
let stock_capacity: i32 = row
|
||
.get("stock_capacity")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(0);
|
||
|
||
let total_inventory_quantity: i32 = row
|
||
.get("inventory_quantity")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(0);
|
||
|
||
// Wenn Gesamt-Lagerbestand > Lageranzahl, reduziere proportional
|
||
if total_inventory_quantity > stock_capacity && stock_capacity > 0 {
|
||
// Hole alle Inventar-Einträge für diesen Stock
|
||
conn.prepare("get_stock_inventory_personal", QUERY_GET_STOCK_INVENTORY_PERSONAL)?;
|
||
let inventory_items = conn.execute("get_stock_inventory_personal", &[&stock_id])?;
|
||
|
||
// Reduziere proportional, sodass Gesamtmenge = stock_capacity
|
||
let reduction_factor = stock_capacity as f64 / total_inventory_quantity as f64;
|
||
|
||
for item_row in inventory_items {
|
||
let inventory_id: Option<i32> = item_row
|
||
.get("id")
|
||
.and_then(|v| v.parse::<i32>().ok());
|
||
|
||
let inventory_id = match inventory_id {
|
||
Some(id) => id,
|
||
None => continue,
|
||
};
|
||
|
||
let item_quantity: i32 = item_row
|
||
.get("quantity")
|
||
.and_then(|v| v.parse::<i32>().ok())
|
||
.unwrap_or(0);
|
||
|
||
let new_quantity = (item_quantity as f64 * reduction_factor).round() as i32;
|
||
|
||
conn.prepare("cap_inventory_personal", QUERY_CAP_INVENTORY_PERSONAL)?;
|
||
conn.execute("cap_inventory_personal", &[&new_quantity, &inventory_id])?;
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(StorageDamageInfo {
|
||
inventory_damage_percent,
|
||
storage_destruction_percent,
|
||
affected_stocks,
|
||
destroyed_stocks,
|
||
})
|
||
}
|
||
}
|
||
|
||
impl Worker for EventsWorker {
|
||
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>| {
|
||
Self::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();
|
||
}
|
||
}
|
||
|
||
// Hilfsfunktion zum Prüfen, ob ein Intervall abgelaufen ist
|
||
fn should_run_interval(last: Option<Instant>, now: Instant, interval: Duration) -> bool {
|
||
match last {
|
||
None => true,
|
||
Some(last_time) => now.saturating_duration_since(last_time) >= interval,
|
||
}
|
||
}
|
||
|