Files
yourpart-daemon/src/worker/events.rs

2385 lines
91 KiB
Rust
Raw Blame History

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