Files
yourpart-daemon/src/worker/events.rs
Torsten Schulz (local) 10bc1e5a52 Refactor SQL queries into a dedicated module
- Moved SQL queries from multiple worker files into `src/worker/sql.rs` for better organization and maintainability.
- Updated references in `stockage_manager.rs`, `transport.rs`, `underground.rs`, `user_character.rs`, and `value_recalculation.rs` to use the new centralized SQL queries.
- Improved code readability by replacing `.get(0)` with `.first()` for better clarity when retrieving the first row from query results.
- Cleaned up unnecessary comments and consolidated related SQL queries.
2025-12-13 11:57:28 +01:00

2047 lines
78 KiB
Rust

use crate::db::{ConnectionPool, DbConnection, DbError};
use crate::message_broker::MessageBroker;
use rand::Rng;
use rand::seq::SliceRandom;
use serde_json::json;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant};
use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::sql::{
QUERY_GET_RANDOM_USER,
QUERY_GET_RANDOM_INFANT,
QUERY_GET_RANDOM_CITY,
QUERY_GET_AFFECTED_USERS,
QUERY_INSERT_NOTIFICATION,
QUERY_GET_MONEY,
QUERY_UPDATE_MONEY,
QUERY_GET_REGION_STOCKS,
QUERY_GET_USER_STOCKS,
QUERY_UPDATE_STOCK_CAPACITY,
QUERY_UPDATE_STOCK_CAPACITY_REGIONAL,
QUERY_GET_REGION_HOUSES,
QUERY_UPDATE_HOUSE_QUALITY,
QUERY_CHANGE_WEATHER,
QUERY_GET_RANDOM_CHARACTER,
QUERY_UPDATE_HEALTH,
QUERY_GET_REGION_CHARACTERS,
QUERY_GET_INVENTORY_ITEMS,
QUERY_REDUCE_INVENTORY,
QUERY_DELETE_INVENTORY,
QUERY_DELETE_STOCK,
QUERY_GET_STOCK_INVENTORY,
QUERY_CAP_INVENTORY,
QUERY_GET_USER_INVENTORY_ITEMS,
QUERY_GET_STOCK_TYPE_ID,
QUERY_REDUCE_INVENTORY_PERSONAL,
QUERY_DELETE_INVENTORY_PERSONAL,
QUERY_DELETE_STOCK_PERSONAL,
QUERY_GET_STOCK_INVENTORY_PERSONAL,
QUERY_CAP_INVENTORY_PERSONAL,
QUERY_DELETE_DIRECTOR,
QUERY_DELETE_RELATIONSHIP,
QUERY_GET_USER_ID,
QUERY_DELETE_CHILD_RELATION,
QUERY_DELETE_CHARACTER,
QUERY_GET_HEIR,
QUERY_SET_CHARACTER_USER,
QUERY_GET_CURRENT_MONEY,
QUERY_GET_HOUSE_VALUE,
QUERY_GET_SETTLEMENT_VALUE,
QUERY_GET_INVENTORY_VALUE,
QUERY_GET_CREDIT_DEBT,
QUERY_COUNT_CHILDREN,
};
/// Typisierung von Ereignissen
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EventType {
/// Persönliches Ereignis für einen einzelnen Spieler
Personal,
/// Regionales Ereignis, das eine ganze Region betrifft
Regional,
}
/// Mögliche Effekte, die ein Ereignis haben kann
#[derive(Debug, Clone)]
pub enum EventEffect {
/// Änderung des Geldes eines Spielers (in Prozent)
MoneyChange { probability: f64, min_percent: f64, max_percent: f64 },
/// Änderung der Produktionsqualität in einer Region (in Prozentpunkten)
ProductionQualityChange { probability: f64, min_change: i32, max_change: i32 },
/// Änderung der Verkaufspreise in einer Region (in Prozent)
PriceChange { probability: f64, min_percent: f64, max_percent: f64 },
/// Änderung der Wetterbedingungen in einer Region
WeatherChange { probability: f64 },
/// Änderung der Lagerkapazität eines Spielers (in Prozent)
StorageCapacityChange { probability: f64, min_percent: f64, max_percent: f64 },
/// Änderung der Transportgeschwindigkeit (in Prozent)
TransportSpeedChange { probability: f64, min_percent: f64, max_percent: f64 },
/// Änderung der Gesundheit eines Charakters (in Punkten, kann negativ sein)
CharacterHealthChange { probability: f64, min_change: i32, max_change: i32 },
/// Ein Charakter stirbt (kann persönlich oder regional sein)
CharacterDeath { probability: f64 },
/// Beschädigung von Lagern und Lagerbestand in einer Region
StorageDamage {
probability: f64,
/// Stock-Typ Label (z.B. "field" oder "wood")
stock_type_label: String,
/// Prozent des Lagerbestands, der betroffen ist (min-max)
inventory_damage_min_percent: f64,
inventory_damage_max_percent: f64,
/// Prozent der Lager, die zerstört werden (min-max)
storage_destruction_min_percent: f64,
storage_destruction_max_percent: f64,
},
/// Änderung der Hausqualität (in Punkten, kann negativ sein)
HouseQualityChange { probability: f64, min_change: i32, max_change: i32 },
}
/// Definition eines zufälligen Ereignisses
#[derive(Debug, Clone)]
pub struct RandomEvent {
/// Eindeutiger Identifikations-String für das Ereignis
pub id: String,
/// Wahrscheinlichkeit pro Minute, dass dieses Ereignis auftritt (0.0 - 1.0)
pub probability_per_minute: f64,
/// Typ des Ereignisses (persönlich oder regional)
pub event_type: EventType,
/// Liste der möglichen Effekte dieses Ereignisses
pub effects: Vec<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.01, // 1% pro Minute
event_type: EventType::Personal,
title: "Unerwarteter Geldsegen".to_string(),
description: "Du findest eine vergessene Geldbörse auf der Straße.".to_string(),
effects: vec![
EventEffect::MoneyChange {
probability: 1.0,
min_percent: 5.0,
max_percent: 15.0,
},
],
},
RandomEvent {
id: "theft".to_string(),
probability_per_minute: 0.008, // 0.8% pro Minute
event_type: EventType::Personal,
title: "Diebstahl".to_string(),
description: "Ein Dieb hat einen Teil deines Geldes gestohlen.".to_string(),
effects: vec![
EventEffect::MoneyChange {
probability: 1.0,
min_percent: -10.0,
max_percent: -5.0,
},
],
},
RandomEvent {
id: "regional_storm".to_string(),
probability_per_minute: 0.005, // 0.5% pro Minute
event_type: EventType::Regional,
title: "Sturm in der Region".to_string(),
description: "Ein schwerer Sturm hat die Region getroffen.".to_string(),
effects: vec![
EventEffect::WeatherChange {
probability: 1.0,
},
EventEffect::ProductionQualityChange {
probability: 0.8,
min_change: -10,
max_change: -5,
},
EventEffect::TransportSpeedChange {
probability: 0.6,
min_percent: -20.0,
max_percent: -10.0,
},
EventEffect::StorageDamage {
probability: 1.0,
stock_type_label: "field".to_string(),
inventory_damage_min_percent: 5.0,
inventory_damage_max_percent: 75.0,
storage_destruction_min_percent: 0.0,
storage_destruction_max_percent: 50.0,
},
EventEffect::StorageDamage {
probability: 1.0,
stock_type_label: "wood".to_string(),
inventory_damage_min_percent: 0.0,
inventory_damage_max_percent: 25.0,
storage_destruction_min_percent: 0.0,
storage_destruction_max_percent: 10.0,
},
// Verbleibende Lager können durch den Sturm beschädigt werden und Kapazität verlieren
EventEffect::StorageCapacityChange {
probability: 0.7, // 70% Chance, dass verbleibende Lager beschädigt werden
min_percent: -10.0,
max_percent: -3.0,
},
],
},
RandomEvent {
id: "regional_festival".to_string(),
probability_per_minute: 0.003, // 0.3% pro Minute
event_type: EventType::Regional,
title: "Regionales Fest".to_string(),
description: "Ein großes Fest findet in der Region statt.".to_string(),
effects: vec![
EventEffect::PriceChange {
probability: 0.9,
min_percent: 5.0,
max_percent: 15.0,
},
EventEffect::ProductionQualityChange {
probability: 0.5,
min_change: 2,
max_change: 5,
},
],
},
RandomEvent {
id: "warehouse_fire".to_string(),
probability_per_minute: 0.002, // 0.2% pro Minute
event_type: EventType::Personal,
title: "Lagerbrand".to_string(),
description: "Ein Feuer hat Teile deines Lagers beschädigt.".to_string(),
effects: vec![
// Feldlager: Lagerbestand kann zerstört werden, Lager können zerstört werden
EventEffect::StorageDamage {
probability: 1.0,
stock_type_label: "field".to_string(),
inventory_damage_min_percent: 0.0,
inventory_damage_max_percent: 100.0,
storage_destruction_min_percent: 0.0,
storage_destruction_max_percent: 50.0,
},
// Holzlager: Lagerbestand kann zerstört werden, Lager können zerstört werden
EventEffect::StorageDamage {
probability: 1.0,
stock_type_label: "wood".to_string(),
inventory_damage_min_percent: 0.0,
inventory_damage_max_percent: 100.0,
storage_destruction_min_percent: 0.0,
storage_destruction_max_percent: 50.0,
},
// Verbleibende Lager können durch das Feuer beschädigt werden und Kapazität verlieren
EventEffect::StorageCapacityChange {
probability: 0.8, // 80% Chance, dass verbleibende Lager beschädigt werden
min_percent: -15.0,
max_percent: -5.0,
},
],
},
RandomEvent {
id: "character_illness".to_string(),
probability_per_minute: 0.01, // 1% pro Minute
event_type: EventType::Personal,
title: "Krankheit".to_string(),
description: "Ein Charakter ist erkrankt und hat an Gesundheit verloren.".to_string(),
effects: vec![
EventEffect::CharacterHealthChange {
probability: 1.0,
min_change: -20,
max_change: -5,
},
],
},
RandomEvent {
id: "character_recovery".to_string(),
probability_per_minute: 0.008, // 0.8% pro Minute
event_type: EventType::Personal,
title: "Genesung".to_string(),
description: "Ein Charakter hat sich von einer Krankheit erholt.".to_string(),
effects: vec![
EventEffect::CharacterHealthChange {
probability: 1.0,
min_change: 5,
max_change: 15,
},
],
},
RandomEvent {
id: "character_accident".to_string(),
probability_per_minute: 0.003, // 0.3% pro Minute
event_type: EventType::Personal,
title: "Unfall".to_string(),
description: "Ein schwerer Unfall hat einen Charakter schwer verletzt.".to_string(),
effects: vec![
EventEffect::CharacterHealthChange {
probability: 1.0,
min_change: -50,
max_change: -30,
},
EventEffect::CharacterDeath {
probability: 0.2, // 20% Chance auf Tod
},
],
},
RandomEvent {
id: "regional_epidemic".to_string(),
probability_per_minute: 0.002, // 0.2% pro Minute
event_type: EventType::Regional,
title: "Epidemie".to_string(),
description: "Eine Seuche hat die Region erfasst.".to_string(),
effects: vec![
EventEffect::CharacterHealthChange {
probability: 0.8, // 80% der Charaktere betroffen
min_change: -15,
max_change: -5,
},
EventEffect::CharacterDeath {
probability: 0.1, // 10% Chance auf Tod pro Charakter
},
],
},
RandomEvent {
id: "sudden_infant_death".to_string(),
// Wahrscheinlichkeit pro Minute: 0.0005 (0.05%)
// Im Mittelalter starben etwa 30-40% der Kinder vor dem 2. Geburtstag
// Diese Wahrscheinlichkeit führt bei regelmäßiger Prüfung zu einer
// realistischen mittelalterlichen Säuglingssterblichkeit
probability_per_minute: 0.0005,
event_type: EventType::Personal,
title: "Plötzlicher Kindstod".to_string(),
description: "Ein Kleinkind ist plötzlich verstorben.".to_string(),
effects: vec![
EventEffect::CharacterDeath {
probability: 1.0, // Wenn das Ereignis auftritt, stirbt das Kind
},
],
},
RandomEvent {
id: "earthquake".to_string(),
probability_per_minute: 0.001, // 0.1% pro Minute (sehr selten)
event_type: EventType::Regional,
title: "Erdbeben".to_string(),
description: "Ein Erdbeben hat die Region erschüttert.".to_string(),
effects: vec![
EventEffect::CharacterHealthChange {
probability: 0.3, // 30% Chance auf Gesundheitsschäden (geringe Wahrscheinlichkeit)
min_change: -20,
max_change: -5,
},
EventEffect::CharacterDeath {
probability: 0.05, // 5% Chance auf Tod (sehr geringe Wahrscheinlichkeit)
},
EventEffect::StorageCapacityChange {
probability: 1.0, // Alle Lager werden beschädigt
min_percent: -20.0,
max_percent: -5.0,
},
EventEffect::HouseQualityChange {
probability: 1.0, // Alle Häuser werden beschädigt
min_change: -15,
max_change: -5,
},
],
},
]
}
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
let mut last_event_check = None;
let mut rng = rand::thread_rng();
let events = Self::initialize_events();
loop {
if !state.running_worker.load(Ordering::Relaxed) {
break;
}
let now = Instant::now();
// Ereignisse einmal pro Minute prüfen
if should_run_interval(last_event_check, now, Duration::from_secs(60)) {
if let Err(err) = Self::check_and_trigger_events_inner(
&pool,
&broker,
&state,
&mut rng,
&events,
) {
eprintln!("[EventsWorker] Fehler beim Prüfen von Ereignissen: {err}");
}
last_event_check = Some(now);
}
// 10-Sekunden-Wartezeit in kurze Scheiben aufteilen, damit ein Shutdown
// (running_worker = false) schnell greift.
const SLICE_MS: u64 = 500;
let total_ms = 10_000;
let mut slept = 0;
while slept < total_ms {
if !state.running_worker.load(Ordering::Relaxed) {
break;
}
let remaining = total_ms - slept;
let slice = SLICE_MS.min(remaining);
std::thread::sleep(Duration::from_millis(slice));
slept += slice;
}
}
}
// Globaler Skalierungsfaktor für Ereignisfrequenz (1.0 = unverändert).
// Setze auf 0.05, um Ereignisse auf 1/20 der ursprünglichen Häufigkeit zu reduzieren.
const EVENT_RATE_SCALE: f64 = 0.05;
fn check_and_trigger_events_inner(
pool: &ConnectionPool,
broker: &MessageBroker,
_state: &Arc<WorkerState>,
rng: &mut impl Rng,
events: &[RandomEvent],
) -> Result<(), DbError> {
// Prüfe jedes mögliche Ereignis
for event in events {
// Zufällige Prüfung basierend auf Wahrscheinlichkeit
let roll = rng.gen_range(0.0..=1.0);
let effective_prob = event.probability_per_minute * Self::EVENT_RATE_SCALE;
if roll < effective_prob {
eprintln!(
"[EventsWorker] Ereignis '{}' wurde ausgelöst (Wahrscheinlichkeit: {:.4}% -> skaliert {:.4}%)",
event.id,
event.probability_per_minute * 100.0,
effective_prob * 100.0
);
match event.event_type {
EventType::Personal => {
Self::trigger_personal_event(pool, broker, event, rng)?;
}
EventType::Regional => {
Self::trigger_regional_event(pool, broker, event, rng)?;
}
}
}
}
Ok(())
}
fn trigger_personal_event(
pool: &ConnectionPool,
broker: &MessageBroker,
event: &RandomEvent,
rng: &mut impl Rng,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
// Spezielle Behandlung für plötzlichen Kindstod: Finde ein zufälliges Kind unter 2 Jahren
if event.id == "sudden_infant_death" {
return Self::trigger_sudden_infant_death(pool, broker, event, rng);
}
conn.prepare("get_random_user", QUERY_GET_RANDOM_USER)?;
let rows = conn.execute("get_random_user", &[])?;
let user_id: Option<i32> = rows
.first()
.and_then(|r| r.get("id"))
.and_then(|v| v.parse::<i32>().ok());
let user_id = match user_id {
Some(id) => id,
None => {
eprintln!("[EventsWorker] Kein Spieler gefunden für persönliches Ereignis");
return Ok(());
}
};
// Wende Effekte an
let mut effect_results = Vec::new();
for effect in &event.effects {
let effect_roll = rng.gen_range(0.0..=1.0);
match effect {
EventEffect::MoneyChange {
probability,
min_percent,
max_percent,
} => {
if effect_roll < *probability {
let percent_change = rng.gen_range(*min_percent..=*max_percent);
if let Ok(absolute_change) = Self::apply_money_change(&mut conn, user_id, percent_change) {
effect_results.push(json!({
"type": "money_change",
"percent": percent_change,
"absolute": absolute_change
}));
}
}
}
EventEffect::StorageCapacityChange {
probability,
min_percent,
max_percent,
} => {
if effect_roll < *probability {
let percent_change = rng.gen_range(*min_percent..=*max_percent);
Self::apply_storage_capacity_change(&mut conn, user_id, percent_change)?;
effect_results.push(json!({
"type": "storage_capacity_change",
"percent": percent_change
}));
}
}
EventEffect::CharacterHealthChange {
probability,
min_change,
max_change,
} => {
if effect_roll < *probability
&& let Ok((character_id, health_change)) = Self::apply_character_health_change(
&mut conn,
user_id,
*min_change,
*max_change,
rng,
)
{
effect_results.push(json!({
"type": "character_health_change",
"character_id": character_id,
"change": health_change
}));
}
}
EventEffect::CharacterDeath { probability } => {
if effect_roll < *probability
&& let Ok(character_id) = Self::apply_character_death(&mut conn, user_id, pool, broker)
{
effect_results.push(json!({
"type": "character_death",
"character_id": character_id
}));
}
}
EventEffect::StorageDamage {
probability,
stock_type_label,
inventory_damage_min_percent,
inventory_damage_max_percent,
storage_destruction_min_percent,
storage_destruction_max_percent,
} => {
if effect_roll < *probability
&& let Ok(damage_info) = Self::apply_personal_storage_damage(
&mut conn,
PersonalStorageDamageParams {
user_id,
stock_type_label,
inventory_damage_min_percent: *inventory_damage_min_percent,
inventory_damage_max_percent: *inventory_damage_max_percent,
storage_destruction_min_percent: *storage_destruction_min_percent,
storage_destruction_max_percent: *storage_destruction_max_percent,
},
rng,
)
{
effect_results.push(json!({
"type": "storage_damage",
"stock_type": stock_type_label,
"inventory_damage_percent": damage_info.inventory_damage_percent,
"storage_destruction_percent": damage_info.storage_destruction_percent,
"affected_stocks": damage_info.affected_stocks,
"destroyed_stocks": damage_info.destroyed_stocks,
}));
}
}
_ => {
eprintln!(
"[EventsWorker] Effekt {:?} wird für persönliche Ereignisse noch nicht unterstützt",
effect
);
}
}
}
// Schreibe Benachrichtigung in die Datenbank mit Event-Details
// If any effect contains a character_id, include it at top-level for the notification
let top_character_id = effect_results.iter().find_map(|eff| {
eff.get("character_id").and_then(|v| v.as_i64()).map(|n| n as i32)
});
let mut notification_json = serde_json::json!({
"tr": format!("random_event.{}", event.id),
"event_id": event.id,
"event_type": "personal",
"effects": effect_results
});
if let Some(cid) = top_character_id {
notification_json["character_id"] = serde_json::json!(cid);
}
Self::notify_user(pool, broker, user_id, &notification_json.to_string())?;
// Sende Benachrichtigung über WebSocket
let mut notification = json!({
"event": "random_event",
"event_id": event.id,
"event_type": "personal",
"user_id": user_id,
"title": event.title,
"description": event.description,
"effects": effect_results
});
if let Some(cid) = top_character_id {
notification["character_id"] = json!(cid);
}
broker.publish(notification.to_string());
eprintln!(
"[EventsWorker] Persönliches Ereignis '{}' für Spieler {} verarbeitet",
event.id, user_id
);
Ok(())
}
fn trigger_sudden_infant_death(
pool: &ConnectionPool,
broker: &MessageBroker,
event: &RandomEvent,
rng: &mut impl Rng,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
// Finde ein zufälliges Kind unter 2 Jahren
// Maximalalter: 730 Tage (2 Jahre) - festgelegt in der WHERE-Klausel unten
conn.prepare("get_random_infant", QUERY_GET_RANDOM_INFANT)?;
let rows = conn.execute("get_random_infant", &[])?;
let character_id: Option<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(());
}
};
// Wende Effekte an (in diesem Fall nur CharacterDeath)
let mut effect_results = Vec::new();
for effect in &event.effects {
let effect_roll = rng.gen_range(0.0..=1.0);
match effect {
EventEffect::CharacterDeath { probability } => {
if effect_roll < *probability
&& Self::handle_character_death(pool, broker, character_id).is_ok()
{
effect_results.push(json!({
"type": "character_death",
"character_id": character_id
}));
}
}
_ => {
eprintln!(
"[EventsWorker] Effekt {:?} wird für plötzlichen Kindstod nicht unterstützt",
effect
);
}
}
}
// Schreibe Benachrichtigung in die Datenbank mit Event-Details
let notification_json = serde_json::json!({
"tr": format!("random_event.{}", event.id),
"event_id": event.id,
"event_type": "personal",
"character_id": character_id,
"effects": effect_results
});
Self::notify_user(pool, broker, user_id, &notification_json.to_string())?;
// Sende Benachrichtigung über WebSocket
let notification = json!({
"event": "random_event",
"event_id": event.id,
"event_type": "personal",
"user_id": user_id,
"character_id": character_id,
"title": event.title,
"description": event.description,
"effects": effect_results
});
broker.publish(notification.to_string());
eprintln!(
"[EventsWorker] Plötzlicher Kindstod für Charakter {} (Spieler {}) verarbeitet",
character_id, user_id
);
Ok(())
}
fn trigger_regional_event(
pool: &ConnectionPool,
broker: &MessageBroker,
event: &RandomEvent,
rng: &mut impl Rng,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
// Hole eine zufällige Stadt-Region
conn.prepare("get_random_city", QUERY_GET_RANDOM_CITY)?;
let rows = conn.execute("get_random_city", &[])?;
let region_id: Option<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,
} => {
if effect_roll < *probability {
// Für regionale Ereignisse: Betrifft alle Charaktere in der Region
if let Ok(affected_characters) = Self::apply_regional_character_health_change(
&mut conn,
region_id,
*min_change,
*max_change,
rng,
) {
effect_results.push(json!({
"type": "character_health_change",
"affected_count": affected_characters.len(),
"characters": affected_characters
}));
}
}
}
EventEffect::CharacterDeath { probability } => {
if effect_roll < *probability {
// Für regionale Ereignisse: Betrifft alle Charaktere in der Region
if let Ok(dead_characters) = Self::apply_regional_character_death(
&mut conn,
region_id,
pool,
broker,
) {
effect_results.push(json!({
"type": "character_death",
"dead_count": dead_characters.len(),
"characters": dead_characters
}));
}
}
}
EventEffect::StorageDamage {
probability,
stock_type_label,
inventory_damage_min_percent,
inventory_damage_max_percent,
storage_destruction_min_percent,
storage_destruction_max_percent,
} => {
if effect_roll < *probability
&& let Ok(damage_info) = Self::apply_storage_damage(
&mut conn,
StorageDamageParams {
region_id,
stock_type_label,
inventory_damage_min_percent: *inventory_damage_min_percent,
inventory_damage_max_percent: *inventory_damage_max_percent,
storage_destruction_min_percent: *storage_destruction_min_percent,
storage_destruction_max_percent: *storage_destruction_max_percent,
},
rng,
)
{
effect_results.push(json!({
"type": "storage_damage",
"stock_type": stock_type_label,
"inventory_damage_percent": damage_info.inventory_damage_percent,
"storage_destruction_percent": damage_info.storage_destruction_percent,
"affected_stocks": damage_info.affected_stocks,
"destroyed_stocks": damage_info.destroyed_stocks,
}));
}
}
EventEffect::StorageCapacityChange {
probability,
min_percent,
max_percent,
} => {
if effect_roll < *probability
&& let Ok((affected_stocks, percent_change)) = Self::apply_regional_storage_capacity_change(
&mut conn,
region_id,
*min_percent,
*max_percent,
rng,
)
{
effect_results.push(json!({
"type": "storage_capacity_change",
"percent": percent_change,
"affected_stocks": affected_stocks,
}));
}
}
EventEffect::HouseQualityChange {
probability,
min_change,
max_change,
} => {
if effect_roll < *probability
&& let Ok((affected_houses, quality_change)) = Self::apply_regional_house_quality_change(
&mut conn,
region_id,
*min_change,
*max_change,
rng,
)
{
effect_results.push(json!({
"type": "house_quality_change",
"change": quality_change,
"affected_houses": affected_houses,
}));
}
}
_ => {
eprintln!(
"[EventsWorker] Effekt {:?} wird für regionale Ereignisse noch nicht unterstützt",
effect
);
}
}
}
// Finde alle betroffenen User in dieser Region (User mit Branches)
conn.prepare("get_affected_users", QUERY_GET_AFFECTED_USERS)?;
let user_rows = conn.execute("get_affected_users", &[&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),
"event_id": event.id,
"event_type": "regional",
"region_id": region_id,
"effects": effect_results
});
if let Err(err) = Self::notify_user(pool, broker, uid, &notification_json.to_string()) {
eprintln!("[EventsWorker] Fehler beim Schreiben der Benachrichtigung für User {}: {}", uid, err);
}
// Sende Benachrichtigung über WebSocket
let notification = json!({
"event": "random_event",
"event_id": event.id,
"event_type": "regional",
"region_id": region_id,
"user_id": uid,
"title": event.title,
"description": event.description,
"effects": effect_results
});
broker.publish(notification.to_string());
notified_users += 1;
}
}
eprintln!(
"[EventsWorker] Regionales Ereignis '{}' für Region {} verarbeitet, {} User benachrichtigt",
event.id, region_id, notified_users
);
Ok(())
}
// Hilfsfunktionen zum Anwenden von Effekten
fn apply_money_change(
conn: &mut DbConnection,
user_id: i32,
percent_change: f64,
) -> Result<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);
let action = format!("Zufallsereignis: Geldänderung {:.2}%", percent_change);
// Verwende parametrisierte Queries für Sicherheit gegen SQL-Injection
conn.prepare("update_money_event", QUERY_UPDATE_MONEY)?;
let _ = conn.execute("update_money_event", &[&user_id, &change, &action])?;
// Best-effort money_history insert for UI/history visibility.
let money_str = format!("{:.2}", change);
fn escape_sql_literal(s: &str) -> String { s.replace('\'', "''") }
let escaped_action = escape_sql_literal(&action);
let create_sql = r#"
CREATE TABLE IF NOT EXISTS falukant_log.money_history (
id BIGSERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
change NUMERIC(10,2) NOT NULL,
action TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
"#;
let _ = conn.query(create_sql);
let history_sql = format!(
"INSERT INTO falukant_log.money_history (user_id, change, action, created_at) VALUES ({uid}, {money}::numeric, '{act}', NOW());",
uid = user_id,
money = money_str,
act = escaped_action
);
if let Err(err) = conn.query(&history_sql) {
eprintln!(
"[EventsWorker] Warning: inserting money_history failed for user {}: {}",
user_id, err
);
}
Ok(change)
}
fn apply_storage_capacity_change(
conn: &mut DbConnection,
user_id: i32,
percent_change: f64,
) -> Result<(), DbError> {
// Hole alle Stocks des Spielers
conn.prepare("get_user_stocks_capacity", QUERY_GET_USER_STOCKS)?;
let stock_rows = conn.execute("get_user_stocks_capacity", &[&user_id])?;
if stock_rows.is_empty() {
eprintln!(
"[EventsWorker] Keine Stocks für Spieler {} gefunden",
user_id
);
return Ok(());
}
// Reduziere die Kapazität aller Stocks
conn.prepare("update_stock_capacity", QUERY_UPDATE_STOCK_CAPACITY)?;
let mut affected_stocks = 0;
for row in stock_rows {
let stock_id: Option<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,
user_id: i32,
min_change: i32,
max_change: i32,
rng: &mut impl Rng,
) -> Result<(i32, i32), DbError> {
// Hole einen zufälligen Charakter des Spielers
conn.prepare("get_random_character", QUERY_GET_RANDOM_CHARACTER)?;
let rows = conn.execute("get_random_character", &[&user_id])?;
let character_id: Option<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);
let health_change = rng.gen_range(min_change..=max_change);
let new_health = (current_health + health_change).clamp(0, 100);
// Update Gesundheit
conn.prepare("update_health", QUERY_UPDATE_HEALTH)?;
conn.execute("update_health", &[&new_health, &character_id])?;
Ok((character_id, health_change))
}
fn apply_character_death(
conn: &mut DbConnection,
user_id: i32,
pool: &ConnectionPool,
broker: &MessageBroker,
) -> Result<i32, 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()));
}
};
// 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<Vec<(i32, 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();
for row in rows {
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));
}
Ok(affected_characters)
}
fn apply_regional_character_death(
conn: &mut DbConnection,
region_id: i32,
pool: &ConnectionPool,
broker: &MessageBroker,
) -> 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 {
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")?;
}
}
// 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")?;
}
}
// 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")?;
}
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")?;
}
}
// 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());
let heir_id = match heir_id {
Some(id) if id > 0 => id,
_ => {
// Kein Erbe gefunden - Vermögen geht verloren
eprintln!(
"[EventsWorker] Kein Erbe für Charakter {} gefunden, Vermögen geht verloren",
deceased_character_id
);
return Ok(());
}
};
// 2) Setze den Erben als neuen Spieler-Charakter
conn.prepare("set_character_user", QUERY_SET_CHARACTER_USER)?;
conn.execute("set_character_user", &[&falukant_user_id, &heir_id])?;
// 3) Berechne das neue Vermögen basierend auf dem gesamten Vermögen
// Hole alle Vermögenswerte (analog zu UserCharacterWorker::calculate_new_money)
conn.prepare("get_current_money", QUERY_GET_CURRENT_MONEY)?;
conn.prepare("get_house_value", QUERY_GET_HOUSE_VALUE)?;
conn.prepare("get_settlement_value", QUERY_GET_SETTLEMENT_VALUE)?;
conn.prepare("get_inventory_value", QUERY_GET_INVENTORY_VALUE)?;
conn.prepare("get_credit_debt", QUERY_GET_CREDIT_DEBT)?;
conn.prepare("count_children", QUERY_COUNT_CHILDREN)?;
let cash: f64 = conn
.execute("get_current_money", &[&falukant_user_id])?
.first()
.and_then(|r| r.get("money"))
.and_then(|v| v.parse::<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,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("insert_notification", QUERY_INSERT_NOTIFICATION)?;
conn.execute("insert_notification", &[&user_id, &event_type])?;
// falukantUpdateStatus
let update_message =
format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
broker.publish(update_message);
// ursprüngliche Benachrichtigung
let message =
format!(r#"{{"event":"{event_type}","user_id":{}}}"#, user_id);
broker.publish(message);
Ok(())
}
fn apply_storage_damage(
conn: &mut DbConnection,
params: StorageDamageParams,
rng: &mut impl Rng,
) -> Result<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,
}
}