diff --git a/src/main.rs b/src/main.rs index 853dce5..90b2708 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,9 +13,9 @@ use libsystemd::daemon::{self, NotifyState}; use message_broker::MessageBroker; use websocket_server::WebSocketServer; use worker::{ - CharacterCreationWorker, ConnectionPool, DirectorWorker, HouseWorker, PoliticsWorker, - ProduceWorker, StockageManager, TransportWorker, UndergroundWorker, UserCharacterWorker, - ValueRecalculationWorker, WeatherWorker, Worker, + CharacterCreationWorker, ConnectionPool, DirectorWorker, EventsWorker, HouseWorker, + PoliticsWorker, ProduceWorker, StockageManager, TransportWorker, UndergroundWorker, + UserCharacterWorker, ValueRecalculationWorker, WeatherWorker, Worker, }; static KEEP_RUNNING: AtomicBool = AtomicBool::new(true); @@ -141,7 +141,8 @@ fn create_workers(pool: ConnectionPool, broker: MessageBroker) -> 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, +} + +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, + }, + ], + }, + 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, + }, + // Für alle anderen Lagerarten: nur Lagerbestand kann zerstört werden + // (storage_destruction_max_percent = 0.0 bedeutet keine Lager-Zerstörung) + // Hinweis: Weitere Stock-Typen können hier hinzugefügt werden + ], + }, + 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 + }, + ], + }, + ] + } + + 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; + } + } + } + + 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); + if roll < event.probability_per_minute { + eprintln!( + "[EventsWorker] Ereignis '{}' wurde ausgelöst (Wahrscheinlichkeit: {:.2}%)", + event.id, + event.probability_per_minute * 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); + } + + // Hole einen zufälligen aktiven Spieler + const QUERY_GET_RANDOM_USER: &str = r#" + SELECT id + FROM falukant_data.falukant_user + ORDER BY RANDOM() + LIMIT 1; + "#; + + conn.prepare("get_random_user", QUERY_GET_RANDOM_USER)?; + let rows = conn.execute("get_random_user", &[])?; + + let user_id: Option = rows + .get(0) + .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); + Self::apply_money_change(&mut conn, user_id, percent_change)?; + effect_results.push(json!({ + "type": "money_change", + "percent": percent_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 { + if 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 { + if 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 { + if let Ok(damage_info) = Self::apply_personal_storage_damage( + &mut conn, + user_id, + stock_type_label, + *inventory_damage_min_percent, + *inventory_damage_max_percent, + *storage_destruction_min_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 + ); + } + } + } + + // Sende Benachrichtigung + let 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 + }); + + 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 + const QUERY_GET_RANDOM_INFANT: &str = r#" + SELECT + c.id AS character_id, + c.user_id, + CURRENT_DATE - c.birthdate::date AS age_days + FROM falukant_data."character" c + WHERE c.user_id IS NOT NULL + AND c.health > 0 + AND CURRENT_DATE - c.birthdate::date <= 730 -- Maximalalter: 2 Jahre (730 Tage) + ORDER BY RANDOM() + LIMIT 1; + "#; + + conn.prepare("get_random_infant", QUERY_GET_RANDOM_INFANT)?; + let rows = conn.execute("get_random_infant", &[])?; + + let character_id: Option = rows + .get(0) + .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 + .get(0) + .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 { + if 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 + ); + } + } + } + + // Sende Benachrichtigung + 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 + const QUERY_GET_RANDOM_CITY: &str = r#" + SELECT r.id AS region_id + FROM falukant_data.region r + JOIN falukant_type.region tr ON r.region_type_id = tr.id + WHERE tr.label_tr = 'city' + ORDER BY RANDOM() + LIMIT 1; + "#; + + conn.prepare("get_random_city", QUERY_GET_RANDOM_CITY)?; + let rows = conn.execute("get_random_city", &[])?; + + let region_id: Option = rows + .get(0) + .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 { + if let Ok(damage_info) = Self::apply_storage_damage( + &mut conn, + region_id, + stock_type_label, + *inventory_damage_min_percent, + *inventory_damage_max_percent, + *storage_destruction_min_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 regionale Ereignisse noch nicht unterstützt", + effect + ); + } + } + } + + // Sende Benachrichtigung an alle Spieler mit Branches in dieser Region + let notification = json!({ + "event": "random_event", + "event_id": event.id, + "event_type": "regional", + "region_id": region_id, + "title": event.title, + "description": event.description, + "effects": effect_results + }); + + broker.publish(notification.to_string()); + eprintln!( + "[EventsWorker] Regionales Ereignis '{}' für Region {} verarbeitet", + event.id, region_id + ); + + Ok(()) + } + + // Hilfsfunktionen zum Anwenden von Effekten + + fn apply_money_change( + conn: &mut DbConnection, + user_id: i32, + percent_change: f64, + ) -> Result<(), DbError> { + // Hole aktuelles Geld + const QUERY_GET_MONEY: &str = r#" + SELECT money + FROM falukant_data.falukant_user + WHERE id = $1; + "#; + + conn.prepare("get_money", QUERY_GET_MONEY)?; + let rows = conn.execute("get_money", &[&user_id])?; + + let current_money: Option = rows + .get(0) + .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 Ok(()); + } + }; + + let change = current_money * (percent_change / 100.0); + let action = format!("Zufallsereignis: Geldänderung {:.2}%", percent_change); + + // Verwende die existierende update_money Funktion + let escaped_action = action.replace('\'', "''"); + let sql = format!( + "SELECT falukant_data.update_money({},{},'{}');", + user_id, change, escaped_action + ); + let _ = conn.query(&sql)?; + + Ok(()) + } + + fn apply_storage_capacity_change( + _conn: &mut DbConnection, + user_id: i32, + percent_change: f64, + ) -> Result<(), DbError> { + // TODO: Implementierung für Lagerkapazitätsänderung + // Dies könnte eine temporäre Modifikation sein oder eine permanente Änderung + eprintln!( + "[EventsWorker] Lagerkapazitätsänderung für Spieler {}: {:.2}% (noch nicht implementiert)", + user_id, percent_change + ); + Ok(()) + } + + fn apply_weather_change( + conn: &mut DbConnection, + region_id: i32, + ) -> Result<(), DbError> { + // Wähle ein zufälliges Wetter + const QUERY_CHANGE_WEATHER: &str = r#" + UPDATE falukant_data.weather + SET weather_type_id = ( + SELECT id + FROM falukant_type.weather + ORDER BY RANDOM() + LIMIT 1 + ) + WHERE region_id = $1; + "#; + + 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 + const QUERY_GET_RANDOM_CHARACTER: &str = r#" + SELECT id, health + FROM falukant_data."character" + WHERE user_id = $1 + AND health > 0 + ORDER BY RANDOM() + LIMIT 1; + "#; + + conn.prepare("get_random_character", QUERY_GET_RANDOM_CHARACTER)?; + let rows = conn.execute("get_random_character", &[&user_id])?; + + let character_id: Option = rows + .get(0) + .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 + .get(0) + .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 as i32 + health_change).max(0).min(100); + + // Update Gesundheit + const QUERY_UPDATE_HEALTH: &str = r#" + UPDATE falukant_data."character" + SET health = $1 + WHERE id = $2; + "#; + + 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 + const QUERY_GET_RANDOM_CHARACTER: &str = r#" + SELECT id + FROM falukant_data."character" + WHERE user_id = $1 + AND health > 0 + ORDER BY RANDOM() + LIMIT 1; + "#; + + 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 + .get(0) + .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 + const QUERY_GET_REGION_CHARACTERS: &str = r#" + SELECT id, health + FROM falukant_data."character" + WHERE region_id = $1 + AND health > 0; + "#; + + 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 as i32 + health_change).max(0).min(100); + + // Update Gesundheit + const QUERY_UPDATE_HEALTH: &str = r#" + UPDATE falukant_data."character" + SET health = $1 + WHERE id = $2; + "#; + + 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 + const QUERY_GET_REGION_CHARACTERS: &str = r#" + SELECT id + FROM falukant_data."character" + WHERE region_id = $1 + AND health > 0; + "#; + + 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 + const QUERY_DELETE_DIRECTOR: &str = r#" + DELETE FROM falukant_data.director + WHERE director_character_id = $1 + RETURNING employer_user_id; + "#; + + 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 + const QUERY_DELETE_RELATIONSHIP: &str = r#" + WITH deleted AS ( + DELETE FROM falukant_data.relationship + WHERE character1_id = $1 + OR character2_id = $1 + RETURNING + CASE + WHEN character1_id = $1 THEN character2_id + ELSE character1_id + END AS related_character_id, + relationship_type_id + ) + SELECT + c.user_id AS related_user_id + FROM deleted d + JOIN falukant_data.character c + ON c.id = d.related_character_id; + "#; + + 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) Child-Relations löschen und Eltern benachrichtigen + const QUERY_DELETE_CHILD_RELATION: &str = r#" + WITH deleted AS ( + DELETE FROM falukant_data.child_relation + WHERE child_character_id = $1 + RETURNING + father_character_id, + mother_character_id + ) + SELECT + cf.user_id AS father_user_id, + cm.user_id AS mother_user_id + FROM deleted d + JOIN falukant_data.character cf + ON cf.id = d.father_character_id + JOIN falukant_data.character cm + ON cm.id = d.mother_character_id; + "#; + + 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")?; + } + } + + // 4) Charakter löschen + const QUERY_DELETE_CHARACTER: &str = r#" + DELETE FROM falukant_data.character + WHERE id = $1; + "#; + + conn.prepare("delete_character", QUERY_DELETE_CHARACTER)?; + conn.execute("delete_character", &[&character_id])?; + + 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}")))?; + + const QUERY_INSERT_NOTIFICATION: &str = r#" + INSERT INTO falukant_log.notification ( + user_id, + tr, + shown, + created_at, + updated_at + ) VALUES ($1, $2, FALSE, NOW(), NOW()); + "#; + + 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, + region_id: i32, + 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 { + // 1. Finde Stock-Typ-ID basierend auf Label + const QUERY_GET_STOCK_TYPE_ID: &str = r#" + SELECT id + FROM falukant_type.stock + WHERE label_tr = $1 + LIMIT 1; + "#; + + conn.prepare("get_stock_type_id", QUERY_GET_STOCK_TYPE_ID)?; + let stock_type_rows = conn.execute("get_stock_type_id", &[&stock_type_label])?; + + let stock_type_id: Option = stock_type_rows + .get(0) + .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", + stock_type_label + ); + return Err(DbError::new(format!( + "Stock-Typ '{}' nicht gefunden", + stock_type_label + ))); + } + }; + + // 2. Hole alle Stocks dieses Typs in der Region mit ihren Branches + const QUERY_GET_REGION_STOCKS: &str = r#" + SELECT + s.id AS stock_id, + s.quantity AS stock_capacity, + COALESCE(SUM(i.quantity), 0) AS inventory_quantity, + s.branch_id + FROM falukant_data.stock s + JOIN falukant_data.branch b ON s.branch_id = b.id + LEFT JOIN falukant_data.inventory i ON i.stock_id = s.id + WHERE b.region_id = $1 + AND s.stock_type_id = $2 + GROUP BY s.id, s.quantity, s.branch_id; + "#; + + conn.prepare("get_region_stocks", QUERY_GET_REGION_STOCKS)?; + let stock_rows = conn.execute("get_region_stocks", &[®ion_id, &stock_type_id])?; + + if stock_rows.is_empty() { + eprintln!( + "[EventsWorker] Keine Stocks vom Typ '{}' in Region {} gefunden", + stock_type_label, 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(inventory_damage_min_percent..=inventory_damage_max_percent); + let storage_destruction_percent = + rng.gen_range(storage_destruction_min_percent..=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 + const QUERY_GET_INVENTORY_ITEMS: &str = r#" + SELECT + i.id AS inventory_id, + i.quantity AS inventory_quantity, + i.stock_id + FROM falukant_data.inventory i + JOIN falukant_data.stock s ON i.stock_id = s.id + JOIN falukant_data.branch b ON s.branch_id = b.id + WHERE b.region_id = $1 + AND s.stock_type_id = $2; + "#; + + conn.prepare("get_inventory_items", QUERY_GET_INVENTORY_ITEMS)?; + let inventory_rows = conn.execute("get_inventory_items", &[®ion_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 + const QUERY_REDUCE_INVENTORY: &str = r#" + UPDATE falukant_data.inventory + SET quantity = $1 + WHERE id = $2; + "#; + + 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 + const QUERY_DELETE_INVENTORY: &str = r#" + DELETE FROM falukant_data.inventory + WHERE stock_id = $1; + "#; + + conn.prepare("delete_inventory", QUERY_DELETE_INVENTORY)?; + conn.execute("delete_inventory", &[stock_id])?; + + // Lösche dann das Stock selbst + const QUERY_DELETE_STOCK: &str = r#" + DELETE FROM falukant_data.stock + WHERE id = $1; + "#; + + 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", &[®ion_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 + const QUERY_GET_STOCK_INVENTORY: &str = r#" + SELECT id, quantity + FROM falukant_data.inventory + WHERE stock_id = $1; + "#; + + 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; + + const QUERY_CAP_INVENTORY: &str = r#" + UPDATE falukant_data.inventory + SET quantity = $1 + WHERE id = $2; + "#; + + 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, + user_id: i32, + 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 { + // 1. Finde Stock-Typ-ID basierend auf Label + const QUERY_GET_STOCK_TYPE_ID: &str = r#" + SELECT id + FROM falukant_type.stock + WHERE label_tr = $1 + LIMIT 1; + "#; + + conn.prepare("get_stock_type_id_personal", QUERY_GET_STOCK_TYPE_ID)?; + let stock_type_rows = conn.execute("get_stock_type_id_personal", &[&stock_type_label])?; + + let stock_type_id: Option = stock_type_rows + .get(0) + .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", + stock_type_label + ); + return Err(DbError::new(format!( + "Stock-Typ '{}' nicht gefunden", + stock_type_label + ))); + } + }; + + // 2. Hole alle Stocks dieses Typs für alle Branches des Spielers + const QUERY_GET_USER_STOCKS: &str = r#" + SELECT + s.id AS stock_id, + s.quantity AS stock_capacity, + COALESCE(SUM(i.quantity), 0) AS inventory_quantity, + s.branch_id + FROM falukant_data.stock s + JOIN falukant_data.branch b ON s.branch_id = b.id + LEFT JOIN falukant_data.inventory i ON i.stock_id = s.id + WHERE b.falukant_user_id = $1 + AND s.stock_type_id = $2 + GROUP BY s.id, s.quantity, s.branch_id; + "#; + + conn.prepare("get_user_stocks", QUERY_GET_USER_STOCKS)?; + let stock_rows = conn.execute("get_user_stocks", &[&user_id, &stock_type_id])?; + + if stock_rows.is_empty() { + eprintln!( + "[EventsWorker] Keine Stocks vom Typ '{}' für Spieler {} gefunden", + stock_type_label, 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(inventory_damage_min_percent..=inventory_damage_max_percent); + let storage_destruction_percent = + rng.gen_range(storage_destruction_min_percent..=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 + const QUERY_GET_USER_INVENTORY_ITEMS: &str = r#" + SELECT + i.id AS inventory_id, + i.quantity AS inventory_quantity, + i.stock_id + FROM falukant_data.inventory i + JOIN falukant_data.stock s ON i.stock_id = s.id + JOIN falukant_data.branch b ON s.branch_id = b.id + WHERE b.falukant_user_id = $1 + AND s.stock_type_id = $2; + "#; + + conn.prepare("get_user_inventory_items", QUERY_GET_USER_INVENTORY_ITEMS)?; + let inventory_rows = conn.execute("get_user_inventory_items", &[&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 + const QUERY_REDUCE_INVENTORY_PERSONAL: &str = r#" + UPDATE falukant_data.inventory + SET quantity = $1 + WHERE id = $2; + "#; + + 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 && (stock_type_label == "field" || 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 + const QUERY_DELETE_INVENTORY_PERSONAL: &str = r#" + DELETE FROM falukant_data.inventory + WHERE stock_id = $1; + "#; + + conn.prepare("delete_inventory_personal", QUERY_DELETE_INVENTORY_PERSONAL)?; + conn.execute("delete_inventory_personal", &[stock_id])?; + + // Lösche dann das Stock selbst + const QUERY_DELETE_STOCK_PERSONAL: &str = r#" + DELETE FROM falukant_data.stock + WHERE id = $1; + "#; + + 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", &[&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 + const QUERY_GET_STOCK_INVENTORY_PERSONAL: &str = r#" + SELECT id, quantity + FROM falukant_data.inventory + WHERE stock_id = $1; + "#; + + 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; + + const QUERY_CAP_INVENTORY_PERSONAL: &str = r#" + UPDATE falukant_data.inventory + SET quantity = $1 + WHERE id = $2; + "#; + + 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, + } +} + diff --git a/src/worker/mod.rs b/src/worker/mod.rs index 3fb5365..004bb6c 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -10,6 +10,7 @@ mod value_recalculation; mod user_character; mod transport; mod weather; +mod events; pub use base::Worker; pub use crate::db::ConnectionPool; @@ -24,4 +25,5 @@ pub use value_recalculation::ValueRecalculationWorker; pub use user_character::UserCharacterWorker; pub use transport::TransportWorker; pub use weather::WeatherWorker; +pub use events::EventsWorker; diff --git a/src/worker/user_character.rs b/src/worker/user_character.rs index 87aa8b0..d2d9039 100644 --- a/src/worker/user_character.rs +++ b/src/worker/user_character.rs @@ -290,7 +290,7 @@ const QUERY_GET_PREGNANCY_CANDIDATES: &str = r#" + (CURRENT_DATE - c2.birthdate::date)) / 2 ) - 2.638267 )) - ) / 2; + ); "#; const QUERY_INSERT_CHILD: &str = r#"