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::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_INSERT_NOTIFICATION, 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_SET_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, }; /// 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 }, } /// 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, /// 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 { vec![ RandomEvent { id: "windfall".to_string(), probability_per_minute: 0.01, // 1% pro Minute 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.008, // 0.8% pro Minute 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.005, // 0.5% pro Minute 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.003, // 0.3% pro Minute 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.002, // 0.2% pro Minute 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.01, // 1% pro Minute 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, min_change: -20, max_change: -5, }, ], }, RandomEvent { id: "character_recovery".to_string(), probability_per_minute: 0.008, // 0.8% pro Minute 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_accident".to_string(), probability_per_minute: 0.003, // 0.3% pro Minute 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.002, // 0.2% pro Minute 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.0005 (0.05%) // 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.0005, 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.001, // 0.1% pro Minute (sehr selten) 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) { let mut last_event_check = None; let mut rng = rand::thread_rng(); let events = Self::initialize_events(); loop { if !state.running_worker.load(Ordering::Relaxed) { break; } let now = 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); } // 10-Sekunden-Wartezeit in kurze Scheiben aufteilen, damit ein Shutdown // (running_worker = false) schnell greift. 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; } } } // Globaler Skalierungsfaktor für Ereignisfrequenz (1.0 = unverändert). // Setze auf 0.05, um Ereignisse auf 1/20 der ursprünglichen Häufigkeit zu reduzieren. const EVENT_RATE_SCALE: f64 = 0.05; fn check_and_trigger_events_inner( pool: &ConnectionPool, broker: &MessageBroker, _state: &Arc, rng: &mut impl Rng, events: &[RandomEvent], ) -> Result<(), DbError> { // 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 * Self::EVENT_RATE_SCALE; if roll < effective_prob { 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)?; } } } } Ok(()) } fn trigger_personal_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}")))?; // Spezielle Behandlung für plötzlichen Kindstod: Finde ein zufälliges Kind unter 2 Jahren if event.id == "sudden_infant_death" { return Self::trigger_sudden_infant_death(pool, broker, event, rng); } conn.prepare("get_random_user", QUERY_GET_RANDOM_USER)?; let rows = conn.execute("get_random_user", &[])?; let user_id: Option = rows .first() .and_then(|r| r.get("id")) .and_then(|v| v.parse::().ok()); let user_id = match user_id { Some(id) => id, None => { eprintln!("[EventsWorker] Kein Spieler gefunden für persönliches 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::MoneyChange { probability, min_percent, max_percent, } => { if effect_roll < *probability { let percent_change = rng.gen_range(*min_percent..=*max_percent); if let Ok(absolute_change) = Self::apply_money_change(&mut conn, user_id, percent_change) { effect_results.push(json!({ "type": "money_change", "percent": percent_change, "absolute": absolute_change })); } } } EventEffect::StorageCapacityChange { probability, min_percent, max_percent, } => { if effect_roll < *probability { let percent_change = rng.gen_range(*min_percent..=*max_percent); Self::apply_storage_capacity_change(&mut conn, user_id, percent_change)?; effect_results.push(json!({ "type": "storage_capacity_change", "percent": percent_change })); } } EventEffect::CharacterHealthChange { probability, min_change, max_change, } => { if effect_roll < *probability && let Ok((character_id, health_change)) = Self::apply_character_health_change( &mut conn, user_id, *min_change, *max_change, rng, ) { effect_results.push(json!({ "type": "character_health_change", "character_id": character_id, "change": health_change })); } } EventEffect::CharacterDeath { probability } => { if effect_roll < *probability && let Ok(character_id) = Self::apply_character_death(&mut conn, user_id, pool, broker) { effect_results.push(json!({ "type": "character_death", "character_id": character_id })); } } 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_personal_storage_damage( &mut conn, PersonalStorageDamageParams { user_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, })); } } _ => { eprintln!( "[EventsWorker] Effekt {:?} wird für persönliche Ereignisse noch nicht unterstützt", effect ); } } } // Schreibe Benachrichtigung in die Datenbank mit Event-Details // If any effect contains a character_id, include it at top-level for the notification let top_character_id = effect_results.iter().find_map(|eff| { eff.get("character_id").and_then(|v| v.as_i64()).map(|n| n as i32) }); 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) = top_character_id { notification_json["character_id"] = serde_json::json!(cid); } Self::notify_user(pool, broker, user_id, ¬ification_json.to_string())?; // Sende Benachrichtigung über WebSocket let mut 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) = top_character_id { notification["character_id"] = json!(cid); } broker.publish(notification.to_string()); eprintln!( "[EventsWorker] Persönliches Ereignis '{}' für Spieler {} verarbeitet", event.id, user_id ); 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 = rows .first() .and_then(|r| r.get("character_id")) .and_then(|v| v.parse::().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 = rows .first() .and_then(|r| r.get("user_id")) .and_then(|v| v.parse::().ok()); let user_id = match user_id { Some(id) => id, None => { eprintln!("[EventsWorker] Kein user_id für Kind {} gefunden", character_id); return Ok(()); } }; // 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 })); } } _ => { 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), "event_id": event.id, "event_type": "personal", "character_id": character_id, "effects": effect_results }); Self::notify_user(pool, broker, user_id, ¬ification_json.to_string())?; // 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 = rows .first() .and_then(|r| r.get("region_id")) .and_then(|v| v.parse::().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, } => { if effect_roll < *probability { // Für regionale Ereignisse: Betrifft alle Charaktere in der Region if let Ok(affected_characters) = Self::apply_regional_character_health_change( &mut conn, region_id, *min_change, *max_change, rng, ) { effect_results.push(json!({ "type": "character_health_change", "affected_count": affected_characters.len(), "characters": affected_characters })); } } } EventEffect::CharacterDeath { probability } => { if effect_roll < *probability { // Für regionale Ereignisse: Betrifft alle Charaktere in der Region if let Ok(dead_characters) = Self::apply_regional_character_death( &mut conn, region_id, pool, broker, ) { 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 = row .get("user_id") .and_then(|v| v.parse::().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), "event_id": event.id, "event_type": "regional", "region_id": region_id, "effects": effect_results }); if let Err(err) = Self::notify_user(pool, broker, uid, ¬ification_json.to_string()) { 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 { // Hole aktuelles Geld conn.prepare("get_money", QUERY_GET_MONEY)?; let rows = conn.execute("get_money", &[&user_id])?; let current_money: Option = rows .first() .and_then(|r| r.get("money")) .and_then(|v| v.parse::().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); let action = format!("Zufallsereignis: Geldänderung {:.2}%", percent_change); // 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 = row .get("stock_id") .and_then(|v| v.parse::().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::().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 = row .get("stock_id") .and_then(|v| v.parse::().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::().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 = row .get("house_id") .and_then(|v| v.parse::().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, user_id: i32, min_change: i32, max_change: i32, rng: &mut impl Rng, ) -> Result<(i32, i32), 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 = rows .first() .and_then(|r| r.get("id")) .and_then(|v| v.parse::().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::().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", QUERY_UPDATE_HEALTH)?; conn.execute("update_health", &[&new_health, &character_id])?; Ok((character_id, health_change)) } fn apply_character_death( conn: &mut DbConnection, user_id: i32, pool: &ConnectionPool, broker: &MessageBroker, ) -> Result { // 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 = rows .first() .and_then(|r| r.get("id")) .and_then(|v| v.parse::().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())); } }; // Verwende die existierende Logik zum Löschen von Charakteren // (ähnlich wie in CharacterCreationWorker) Self::handle_character_death(pool, broker, character_id)?; Ok(character_id) } fn apply_regional_character_health_change( conn: &mut DbConnection, region_id: i32, min_change: i32, max_change: i32, rng: &mut impl Rng, ) -> Result, 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(); for row in rows { let character_id: Option = row .get("id") .and_then(|v| v.parse::().ok()); let character_id = match character_id { Some(id) => id, None => continue, }; let current_health: i32 = row .get("health") .and_then(|v| v.parse::().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)); } Ok(affected_characters) } fn apply_regional_character_death( conn: &mut DbConnection, region_id: i32, pool: &ConnectionPool, broker: &MessageBroker, ) -> Result, 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 { let character_id: Option = row .get("id") .and_then(|v| v.parse::().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::().ok()) { Self::notify_user(pool, broker, user_id, "director_death")?; } } // 2) Relationships löschen und betroffene User benachrichtigen conn.prepare("delete_relationship", QUERY_DELETE_RELATIONSHIP)?; let rel_result = conn.execute("delete_relationship", &[&character_id])?; for row in rel_result { if let Some(related_user_id) = row .get("related_user_id") .and_then(|v| v.parse::().ok()) { Self::notify_user(pool, broker, related_user_id, "relationship_death")?; } } // 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 = user_rows .first() .and_then(|r| r.get("user_id")) .and_then(|v| v.parse::().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::().ok()) { Self::notify_user(pool, broker, father_user_id, "child_death")?; } if let Some(mother_user_id) = row .get("mother_user_id") .and_then(|v| v.parse::().ok()) { Self::notify_user(pool, broker, mother_user_id, "child_death")?; } } // 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 = heir_rows .first() .and_then(|r| r.get("child_character_id")) .and_then(|v| v.parse::().ok()); let heir_id = match heir_id { Some(id) if id > 0 => id, _ => { // Kein Erbe gefunden - Vermögen geht verloren eprintln!( "[EventsWorker] Kein Erbe für Charakter {} gefunden, Vermögen geht verloren", deceased_character_id ); return Ok(()); } }; // 2) Setze den Erben als neuen Spieler-Charakter conn.prepare("set_character_user", QUERY_SET_CHARACTER_USER)?; conn.execute("set_character_user", &[&falukant_user_id, &heir_id])?; // 3) 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::().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::().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::().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::().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::().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::().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, ) -> Result<(), DbError> { let mut conn = pool .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; conn.prepare("insert_notification", QUERY_INSERT_NOTIFICATION)?; conn.execute("insert_notification", &[&user_id, &event_type])?; // falukantUpdateStatus let update_message = format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id); broker.publish(update_message); // 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 { // 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 = stock_type_rows .first() .and_then(|r| r.get("id")) .and_then(|v| v.parse::().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 = row .get("inventory_id") .and_then(|v| v.parse::().ok()); let inventory_id = match inventory_id { Some(id) => id, None => continue, }; let stock_id: Option = row .get("stock_id") .and_then(|v| v.parse::().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::().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 = stock_rows .iter() .filter_map(|row| row.get("stock_id").and_then(|v| v.parse::().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 = row .get("stock_id") .and_then(|v| v.parse::().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::().ok()) .unwrap_or(0); let total_inventory_quantity: i32 = row .get("inventory_quantity") .and_then(|v| v.parse::().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 = item_row .get("id") .and_then(|v| v.parse::().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::().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 { // 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 = stock_type_rows .first() .and_then(|r| r.get("id")) .and_then(|v| v.parse::().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 = row .get("inventory_id") .and_then(|v| v.parse::().ok()); let inventory_id = match inventory_id { Some(id) => id, None => continue, }; let stock_id: Option = row .get("stock_id") .and_then(|v| v.parse::().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::().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 = stock_rows .iter() .filter_map(|row| row.get("stock_id").and_then(|v| v.parse::().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 = row .get("stock_id") .and_then(|v| v.parse::().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::().ok()) .unwrap_or(0); let total_inventory_quantity: i32 = row .get("inventory_quantity") .and_then(|v| v.parse::().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 = item_row .get("id") .and_then(|v| v.parse::().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::().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| { 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, now: Instant, interval: Duration) -> bool { match last { None => true, Some(last_time) => now.saturating_duration_since(last_time) >= interval, } }