- 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.
2047 lines
78 KiB
Rust
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, ¬ification_json.to_string())?;
|
|
|
|
// Sende Benachrichtigung über WebSocket
|
|
let mut notification = json!({
|
|
"event": "random_event",
|
|
"event_id": event.id,
|
|
"event_type": "personal",
|
|
"user_id": user_id,
|
|
"title": event.title,
|
|
"description": event.description,
|
|
"effects": effect_results
|
|
});
|
|
|
|
if let Some(cid) = top_character_id {
|
|
notification["character_id"] = json!(cid);
|
|
}
|
|
|
|
broker.publish(notification.to_string());
|
|
eprintln!(
|
|
"[EventsWorker] Persönliches Ereignis '{}' für Spieler {} verarbeitet",
|
|
event.id, user_id
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn trigger_sudden_infant_death(
|
|
pool: &ConnectionPool,
|
|
broker: &MessageBroker,
|
|
event: &RandomEvent,
|
|
rng: &mut impl Rng,
|
|
) -> Result<(), DbError> {
|
|
let mut conn = pool
|
|
.get()
|
|
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
|
|
|
// Finde ein zufälliges Kind unter 2 Jahren
|
|
// Maximalalter: 730 Tage (2 Jahre) - festgelegt in der WHERE-Klausel unten
|
|
conn.prepare("get_random_infant", QUERY_GET_RANDOM_INFANT)?;
|
|
let rows = conn.execute("get_random_infant", &[])?;
|
|
|
|
let character_id: Option<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, ¬ification_json.to_string())?;
|
|
|
|
// Sende Benachrichtigung über WebSocket
|
|
let notification = json!({
|
|
"event": "random_event",
|
|
"event_id": event.id,
|
|
"event_type": "personal",
|
|
"user_id": user_id,
|
|
"character_id": character_id,
|
|
"title": event.title,
|
|
"description": event.description,
|
|
"effects": effect_results
|
|
});
|
|
|
|
broker.publish(notification.to_string());
|
|
eprintln!(
|
|
"[EventsWorker] Plötzlicher Kindstod für Charakter {} (Spieler {}) verarbeitet",
|
|
character_id, user_id
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn trigger_regional_event(
|
|
pool: &ConnectionPool,
|
|
broker: &MessageBroker,
|
|
event: &RandomEvent,
|
|
rng: &mut impl Rng,
|
|
) -> Result<(), DbError> {
|
|
let mut conn = pool
|
|
.get()
|
|
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
|
|
|
// Hole eine zufällige Stadt-Region
|
|
conn.prepare("get_random_city", QUERY_GET_RANDOM_CITY)?;
|
|
let rows = conn.execute("get_random_city", &[])?;
|
|
|
|
let region_id: Option<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", &[®ion_id])?;
|
|
|
|
// Sende Benachrichtigung an jeden betroffenen User einzeln
|
|
let mut notified_users = 0;
|
|
for row in user_rows {
|
|
let user_id: Option<i32> = row
|
|
.get("user_id")
|
|
.and_then(|v| v.parse::<i32>().ok());
|
|
|
|
if let Some(uid) = user_id {
|
|
// Schreibe Benachrichtigung in die Datenbank mit Event-Details
|
|
let notification_json = serde_json::json!({
|
|
"tr": format!("random_event.{}", event.id),
|
|
"event_id": event.id,
|
|
"event_type": "regional",
|
|
"region_id": region_id,
|
|
"effects": effect_results
|
|
});
|
|
if let Err(err) = Self::notify_user(pool, broker, uid, ¬ification_json.to_string()) {
|
|
eprintln!("[EventsWorker] Fehler beim Schreiben der Benachrichtigung für User {}: {}", uid, err);
|
|
}
|
|
|
|
// Sende Benachrichtigung über WebSocket
|
|
let notification = json!({
|
|
"event": "random_event",
|
|
"event_id": event.id,
|
|
"event_type": "regional",
|
|
"region_id": region_id,
|
|
"user_id": uid,
|
|
"title": event.title,
|
|
"description": event.description,
|
|
"effects": effect_results
|
|
});
|
|
|
|
broker.publish(notification.to_string());
|
|
notified_users += 1;
|
|
}
|
|
}
|
|
|
|
eprintln!(
|
|
"[EventsWorker] Regionales Ereignis '{}' für Region {} verarbeitet, {} User benachrichtigt",
|
|
event.id, region_id, notified_users
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Hilfsfunktionen zum Anwenden von Effekten
|
|
|
|
fn apply_money_change(
|
|
conn: &mut DbConnection,
|
|
user_id: i32,
|
|
percent_change: f64,
|
|
) -> Result<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", &[®ion_id])?;
|
|
|
|
if stock_rows.is_empty() {
|
|
eprintln!(
|
|
"[EventsWorker] Keine Stocks in Region {} gefunden",
|
|
region_id
|
|
);
|
|
return Ok((0, 0.0));
|
|
}
|
|
|
|
// Berechne die prozentuale Änderung
|
|
let percent_change = rng.gen_range(min_percent..=max_percent);
|
|
|
|
// Reduziere die Kapazität aller Stocks
|
|
conn.prepare("update_stock_capacity_regional", QUERY_UPDATE_STOCK_CAPACITY_REGIONAL)?;
|
|
|
|
let mut affected_stocks = 0;
|
|
for row in stock_rows {
|
|
let stock_id: Option<i32> = row
|
|
.get("stock_id")
|
|
.and_then(|v| v.parse::<i32>().ok());
|
|
|
|
let stock_id = match stock_id {
|
|
Some(id) => id,
|
|
None => continue,
|
|
};
|
|
|
|
let current_capacity: i32 = row
|
|
.get("current_capacity")
|
|
.and_then(|v| v.parse::<i32>().ok())
|
|
.unwrap_or(0);
|
|
|
|
if current_capacity > 0 {
|
|
conn.execute("update_stock_capacity_regional", &[&percent_change, &stock_id])?;
|
|
affected_stocks += 1;
|
|
}
|
|
}
|
|
|
|
eprintln!(
|
|
"[EventsWorker] Regionale Lagerkapazitätsänderung für Region {}: {:.2}% bei {} Stocks",
|
|
region_id, percent_change, affected_stocks
|
|
);
|
|
|
|
Ok((affected_stocks, percent_change))
|
|
}
|
|
|
|
fn apply_regional_house_quality_change(
|
|
conn: &mut DbConnection,
|
|
region_id: i32,
|
|
min_change: i32,
|
|
max_change: i32,
|
|
rng: &mut impl Rng,
|
|
) -> Result<(i32, i32), DbError> {
|
|
// Hole alle Häuser in der Region
|
|
conn.prepare("get_region_houses", QUERY_GET_REGION_HOUSES)?;
|
|
let house_rows = conn.execute("get_region_houses", &[®ion_id])?;
|
|
|
|
if house_rows.is_empty() {
|
|
eprintln!(
|
|
"[EventsWorker] Keine Häuser in Region {} gefunden",
|
|
region_id
|
|
);
|
|
return Ok((0, 0));
|
|
}
|
|
|
|
// Berechne die Änderung
|
|
let quality_change = rng.gen_range(min_change..=max_change);
|
|
|
|
// Reduziere die Qualität aller Häuser
|
|
conn.prepare("update_house_quality", QUERY_UPDATE_HOUSE_QUALITY)?;
|
|
|
|
let mut affected_houses = 0;
|
|
for row in house_rows {
|
|
let house_id: Option<i32> = row
|
|
.get("house_id")
|
|
.and_then(|v| v.parse::<i32>().ok());
|
|
|
|
let house_id = match house_id {
|
|
Some(id) => id,
|
|
None => continue,
|
|
};
|
|
|
|
conn.execute("update_house_quality", &[&quality_change, &house_id])?;
|
|
affected_houses += 1;
|
|
}
|
|
|
|
eprintln!(
|
|
"[EventsWorker] Regionale Hausqualitätsänderung für Region {}: {} Punkte bei {} Häusern",
|
|
region_id, quality_change, affected_houses
|
|
);
|
|
|
|
Ok((affected_houses, quality_change))
|
|
}
|
|
|
|
fn apply_weather_change(
|
|
conn: &mut DbConnection,
|
|
region_id: i32,
|
|
) -> Result<(), DbError> {
|
|
// Wähle ein zufälliges Wetter
|
|
conn.prepare("change_weather", QUERY_CHANGE_WEATHER)?;
|
|
conn.execute("change_weather", &[®ion_id])?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn apply_production_quality_change(
|
|
_conn: &mut DbConnection,
|
|
region_id: i32,
|
|
change: i32,
|
|
) -> Result<(), DbError> {
|
|
// TODO: Implementierung für temporäre Produktionsqualitätsänderung
|
|
// Dies könnte eine temporäre Modifikation sein, die nach einer bestimmten Zeit abläuft
|
|
eprintln!(
|
|
"[EventsWorker] Produktionsqualitätsänderung für Region {}: {} (noch nicht implementiert)",
|
|
region_id, change
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn apply_price_change(
|
|
_conn: &mut DbConnection,
|
|
region_id: i32,
|
|
percent_change: f64,
|
|
) -> Result<(), DbError> {
|
|
// TODO: Implementierung für temporäre Preisänderung
|
|
// Dies könnte eine temporäre Modifikation der worth_percent Werte sein
|
|
eprintln!(
|
|
"[EventsWorker] Preisänderung für Region {}: {:.2}% (noch nicht implementiert)",
|
|
region_id, percent_change
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn apply_transport_speed_change(
|
|
_conn: &mut DbConnection,
|
|
region_id: i32,
|
|
percent_change: f64,
|
|
) -> Result<(), DbError> {
|
|
// TODO: Implementierung für temporäre Transportgeschwindigkeitsänderung
|
|
eprintln!(
|
|
"[EventsWorker] Transportgeschwindigkeitsänderung für Region {}: {:.2}% (noch nicht implementiert)",
|
|
region_id, percent_change
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn apply_character_health_change(
|
|
conn: &mut DbConnection,
|
|
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", &[®ion_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", &[®ion_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", &[¶ms.stock_type_label])?;
|
|
|
|
let stock_type_id: Option<i32> = stock_type_rows
|
|
.first()
|
|
.and_then(|r| r.get("id"))
|
|
.and_then(|v| v.parse::<i32>().ok());
|
|
|
|
let stock_type_id = match stock_type_id {
|
|
Some(id) => id,
|
|
None => {
|
|
eprintln!(
|
|
"[EventsWorker] Stock-Typ '{}' nicht gefunden",
|
|
params.stock_type_label
|
|
);
|
|
return Err(DbError::new(format!(
|
|
"Stock-Typ '{}' nicht gefunden",
|
|
params.stock_type_label
|
|
)));
|
|
}
|
|
};
|
|
|
|
// 2. Hole alle Stocks dieses Typs in der Region mit ihren Branches
|
|
conn.prepare("get_region_stocks", QUERY_GET_REGION_STOCKS)?;
|
|
let stock_rows = conn.execute("get_region_stocks", &[¶ms.region_id, &stock_type_id])?;
|
|
|
|
if stock_rows.is_empty() {
|
|
eprintln!(
|
|
"[EventsWorker] Keine Stocks vom Typ '{}' in Region {} gefunden",
|
|
params.stock_type_label, params.region_id
|
|
);
|
|
return Ok(StorageDamageInfo {
|
|
inventory_damage_percent: 0.0,
|
|
storage_destruction_percent: 0.0,
|
|
affected_stocks: 0,
|
|
destroyed_stocks: 0,
|
|
});
|
|
}
|
|
|
|
// 3. Berechne Schäden
|
|
let inventory_damage_percent =
|
|
rng.gen_range(params.inventory_damage_min_percent..=params.inventory_damage_max_percent);
|
|
let storage_destruction_percent =
|
|
rng.gen_range(params.storage_destruction_min_percent..=params.storage_destruction_max_percent);
|
|
|
|
let total_stocks = stock_rows.len();
|
|
let stocks_to_destroy = ((total_stocks as f64 * storage_destruction_percent / 100.0)
|
|
.round() as usize)
|
|
.min(total_stocks);
|
|
|
|
// 4. Reduziere Lagerbestand in allen betroffenen Stocks
|
|
// Hole alle Inventar-Einträge für diese Stocks
|
|
conn.prepare("get_inventory_items", QUERY_GET_INVENTORY_ITEMS)?;
|
|
let inventory_rows = conn.execute("get_inventory_items", &[¶ms.region_id, &stock_type_id])?;
|
|
|
|
let mut affected_stocks = 0;
|
|
let mut processed_stocks = std::collections::HashSet::new();
|
|
|
|
for row in &inventory_rows {
|
|
let inventory_id: Option<i32> = row
|
|
.get("inventory_id")
|
|
.and_then(|v| v.parse::<i32>().ok());
|
|
|
|
let inventory_id = match inventory_id {
|
|
Some(id) => id,
|
|
None => continue,
|
|
};
|
|
|
|
let stock_id: Option<i32> = row
|
|
.get("stock_id")
|
|
.and_then(|v| v.parse::<i32>().ok());
|
|
|
|
let stock_id = match stock_id {
|
|
Some(id) => id,
|
|
None => continue,
|
|
};
|
|
|
|
let inventory_quantity: i32 = row
|
|
.get("inventory_quantity")
|
|
.and_then(|v| v.parse::<i32>().ok())
|
|
.unwrap_or(0);
|
|
|
|
if inventory_quantity > 0 {
|
|
let damage = (inventory_quantity as f64 * inventory_damage_percent / 100.0).round() as i32;
|
|
let new_quantity = (inventory_quantity - damage).max(0);
|
|
|
|
// Reduziere Lagerbestand pro Inventar-Eintrag
|
|
conn.prepare("reduce_inventory", QUERY_REDUCE_INVENTORY)?;
|
|
conn.execute("reduce_inventory", &[&new_quantity, &inventory_id])?;
|
|
|
|
if !processed_stocks.contains(&stock_id) {
|
|
processed_stocks.insert(stock_id);
|
|
affected_stocks += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. Zerstöre zufällig ausgewählte Stocks
|
|
let mut destroyed_stocks = 0;
|
|
if stocks_to_destroy > 0 {
|
|
// Wähle zufällige Stocks zum Zerstören
|
|
let mut stock_ids_to_destroy: Vec<i32> = stock_rows
|
|
.iter()
|
|
.filter_map(|row| row.get("stock_id").and_then(|v| v.parse::<i32>().ok()))
|
|
.collect();
|
|
|
|
// Mische die Liste zufällig
|
|
stock_ids_to_destroy.shuffle(rng);
|
|
stock_ids_to_destroy.truncate(stocks_to_destroy);
|
|
|
|
for stock_id in &stock_ids_to_destroy {
|
|
// Lösche zuerst den Lagerbestand
|
|
conn.prepare("delete_inventory", QUERY_DELETE_INVENTORY)?;
|
|
conn.execute("delete_inventory", &[stock_id])?;
|
|
|
|
// Lösche dann das Stock selbst
|
|
conn.prepare("delete_stock", QUERY_DELETE_STOCK)?;
|
|
conn.execute("delete_stock", &[stock_id])?;
|
|
|
|
destroyed_stocks += 1;
|
|
}
|
|
}
|
|
|
|
// 6. Sicherstelle, dass Lagerbestand <= Lageranzahl für alle verbleibenden Stocks
|
|
// Hole alle verbleibenden Stocks mit ihrem Lagerbestand
|
|
let remaining_stock_rows = conn.execute("get_region_stocks", &[¶ms.region_id, &stock_type_id])?;
|
|
|
|
for row in remaining_stock_rows {
|
|
let stock_id: Option<i32> = row
|
|
.get("stock_id")
|
|
.and_then(|v| v.parse::<i32>().ok());
|
|
|
|
let stock_id = match stock_id {
|
|
Some(id) => id,
|
|
None => continue,
|
|
};
|
|
|
|
let stock_capacity: i32 = row
|
|
.get("stock_capacity")
|
|
.and_then(|v| v.parse::<i32>().ok())
|
|
.unwrap_or(0);
|
|
|
|
let total_inventory_quantity: i32 = row
|
|
.get("inventory_quantity")
|
|
.and_then(|v| v.parse::<i32>().ok())
|
|
.unwrap_or(0);
|
|
|
|
// Wenn Gesamt-Lagerbestand > Lageranzahl, reduziere proportional
|
|
if total_inventory_quantity > stock_capacity && stock_capacity > 0 {
|
|
// Hole alle Inventar-Einträge für diesen Stock
|
|
conn.prepare("get_stock_inventory", QUERY_GET_STOCK_INVENTORY)?;
|
|
let inventory_items = conn.execute("get_stock_inventory", &[&stock_id])?;
|
|
|
|
// Reduziere proportional, sodass Gesamtmenge = stock_capacity
|
|
let reduction_factor = stock_capacity as f64 / total_inventory_quantity as f64;
|
|
|
|
for item_row in inventory_items {
|
|
let inventory_id: Option<i32> = item_row
|
|
.get("id")
|
|
.and_then(|v| v.parse::<i32>().ok());
|
|
|
|
let inventory_id = match inventory_id {
|
|
Some(id) => id,
|
|
None => continue,
|
|
};
|
|
|
|
let item_quantity: i32 = item_row
|
|
.get("quantity")
|
|
.and_then(|v| v.parse::<i32>().ok())
|
|
.unwrap_or(0);
|
|
|
|
let new_quantity = (item_quantity as f64 * reduction_factor).round() as i32;
|
|
|
|
conn.prepare("cap_inventory", QUERY_CAP_INVENTORY)?;
|
|
conn.execute("cap_inventory", &[&new_quantity, &inventory_id])?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(StorageDamageInfo {
|
|
inventory_damage_percent,
|
|
storage_destruction_percent,
|
|
affected_stocks,
|
|
destroyed_stocks,
|
|
})
|
|
}
|
|
|
|
fn apply_personal_storage_damage(
|
|
conn: &mut DbConnection,
|
|
params: PersonalStorageDamageParams,
|
|
rng: &mut impl Rng,
|
|
) -> Result<StorageDamageInfo, DbError> {
|
|
// 1. Finde Stock-Typ-ID basierend auf Label
|
|
conn.prepare("get_stock_type_id_personal", QUERY_GET_STOCK_TYPE_ID)?;
|
|
let stock_type_rows = conn.execute("get_stock_type_id_personal", &[¶ms.stock_type_label])?;
|
|
|
|
let stock_type_id: Option<i32> = stock_type_rows
|
|
.first()
|
|
.and_then(|r| r.get("id"))
|
|
.and_then(|v| v.parse::<i32>().ok());
|
|
|
|
let stock_type_id = match stock_type_id {
|
|
Some(id) => id,
|
|
None => {
|
|
eprintln!(
|
|
"[EventsWorker] Stock-Typ '{}' nicht gefunden",
|
|
params.stock_type_label
|
|
);
|
|
return Err(DbError::new(format!(
|
|
"Stock-Typ '{}' nicht gefunden",
|
|
params.stock_type_label
|
|
)));
|
|
}
|
|
};
|
|
|
|
// 2. Hole alle Stocks dieses Typs für alle Branches des Spielers
|
|
conn.prepare("get_user_stocks", QUERY_GET_USER_STOCKS)?;
|
|
let stock_rows = conn.execute("get_user_stocks", &[¶ms.user_id, &stock_type_id])?;
|
|
|
|
if stock_rows.is_empty() {
|
|
eprintln!(
|
|
"[EventsWorker] Keine Stocks vom Typ '{}' für Spieler {} gefunden",
|
|
params.stock_type_label, params.user_id
|
|
);
|
|
return Ok(StorageDamageInfo {
|
|
inventory_damage_percent: 0.0,
|
|
storage_destruction_percent: 0.0,
|
|
affected_stocks: 0,
|
|
destroyed_stocks: 0,
|
|
});
|
|
}
|
|
|
|
// 3. Berechne Schäden
|
|
let inventory_damage_percent =
|
|
rng.gen_range(params.inventory_damage_min_percent..=params.inventory_damage_max_percent);
|
|
let storage_destruction_percent =
|
|
rng.gen_range(params.storage_destruction_min_percent..=params.storage_destruction_max_percent);
|
|
|
|
let total_stocks = stock_rows.len();
|
|
let stocks_to_destroy = ((total_stocks as f64 * storage_destruction_percent / 100.0)
|
|
.round() as usize)
|
|
.min(total_stocks);
|
|
|
|
// 4. Reduziere Lagerbestand in allen betroffenen Stocks
|
|
// Hole alle Inventar-Einträge für diese Stocks
|
|
conn.prepare("get_user_inventory_items", QUERY_GET_USER_INVENTORY_ITEMS)?;
|
|
let inventory_rows = conn.execute("get_user_inventory_items", &[¶ms.user_id, &stock_type_id])?;
|
|
|
|
let mut affected_stocks = 0;
|
|
let mut processed_stocks = std::collections::HashSet::new();
|
|
|
|
for row in &inventory_rows {
|
|
let inventory_id: Option<i32> = row
|
|
.get("inventory_id")
|
|
.and_then(|v| v.parse::<i32>().ok());
|
|
|
|
let inventory_id = match inventory_id {
|
|
Some(id) => id,
|
|
None => continue,
|
|
};
|
|
|
|
let stock_id: Option<i32> = row
|
|
.get("stock_id")
|
|
.and_then(|v| v.parse::<i32>().ok());
|
|
|
|
let stock_id = match stock_id {
|
|
Some(id) => id,
|
|
None => continue,
|
|
};
|
|
|
|
let inventory_quantity: i32 = row
|
|
.get("inventory_quantity")
|
|
.and_then(|v| v.parse::<i32>().ok())
|
|
.unwrap_or(0);
|
|
|
|
if inventory_quantity > 0 {
|
|
let damage = (inventory_quantity as f64 * inventory_damage_percent / 100.0).round() as i32;
|
|
let new_quantity = (inventory_quantity - damage).max(0);
|
|
|
|
// Reduziere Lagerbestand pro Inventar-Eintrag
|
|
conn.prepare("reduce_inventory_personal", QUERY_REDUCE_INVENTORY_PERSONAL)?;
|
|
conn.execute("reduce_inventory_personal", &[&new_quantity, &inventory_id])?;
|
|
|
|
if !processed_stocks.contains(&stock_id) {
|
|
processed_stocks.insert(stock_id);
|
|
affected_stocks += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. Zerstöre zufällig ausgewählte Stocks (nur für field und wood)
|
|
let mut destroyed_stocks = 0;
|
|
if stocks_to_destroy > 0 && (params.stock_type_label == "field" || params.stock_type_label == "wood") {
|
|
// Wähle zufällige Stocks zum Zerstören
|
|
let mut stock_ids_to_destroy: Vec<i32> = stock_rows
|
|
.iter()
|
|
.filter_map(|row| row.get("stock_id").and_then(|v| v.parse::<i32>().ok()))
|
|
.collect();
|
|
|
|
// Mische die Liste zufällig
|
|
stock_ids_to_destroy.shuffle(rng);
|
|
stock_ids_to_destroy.truncate(stocks_to_destroy);
|
|
|
|
for stock_id in &stock_ids_to_destroy {
|
|
// Lösche zuerst den Lagerbestand
|
|
conn.prepare("delete_inventory_personal", QUERY_DELETE_INVENTORY_PERSONAL)?;
|
|
conn.execute("delete_inventory_personal", &[stock_id])?;
|
|
|
|
// Lösche dann das Stock selbst
|
|
conn.prepare("delete_stock_personal", QUERY_DELETE_STOCK_PERSONAL)?;
|
|
conn.execute("delete_stock_personal", &[stock_id])?;
|
|
|
|
destroyed_stocks += 1;
|
|
}
|
|
}
|
|
|
|
// 6. Sicherstelle, dass Lagerbestand <= Lageranzahl für alle verbleibenden Stocks
|
|
let remaining_stock_rows = conn.execute("get_user_stocks", &[¶ms.user_id, &stock_type_id])?;
|
|
|
|
for row in remaining_stock_rows {
|
|
let stock_id: Option<i32> = row
|
|
.get("stock_id")
|
|
.and_then(|v| v.parse::<i32>().ok());
|
|
|
|
let stock_id = match stock_id {
|
|
Some(id) => id,
|
|
None => continue,
|
|
};
|
|
|
|
let stock_capacity: i32 = row
|
|
.get("stock_capacity")
|
|
.and_then(|v| v.parse::<i32>().ok())
|
|
.unwrap_or(0);
|
|
|
|
let total_inventory_quantity: i32 = row
|
|
.get("inventory_quantity")
|
|
.and_then(|v| v.parse::<i32>().ok())
|
|
.unwrap_or(0);
|
|
|
|
// Wenn Gesamt-Lagerbestand > Lageranzahl, reduziere proportional
|
|
if total_inventory_quantity > stock_capacity && stock_capacity > 0 {
|
|
// Hole alle Inventar-Einträge für diesen Stock
|
|
conn.prepare("get_stock_inventory_personal", QUERY_GET_STOCK_INVENTORY_PERSONAL)?;
|
|
let inventory_items = conn.execute("get_stock_inventory_personal", &[&stock_id])?;
|
|
|
|
// Reduziere proportional, sodass Gesamtmenge = stock_capacity
|
|
let reduction_factor = stock_capacity as f64 / total_inventory_quantity as f64;
|
|
|
|
for item_row in inventory_items {
|
|
let inventory_id: Option<i32> = item_row
|
|
.get("id")
|
|
.and_then(|v| v.parse::<i32>().ok());
|
|
|
|
let inventory_id = match inventory_id {
|
|
Some(id) => id,
|
|
None => continue,
|
|
};
|
|
|
|
let item_quantity: i32 = item_row
|
|
.get("quantity")
|
|
.and_then(|v| v.parse::<i32>().ok())
|
|
.unwrap_or(0);
|
|
|
|
let new_quantity = (item_quantity as f64 * reduction_factor).round() as i32;
|
|
|
|
conn.prepare("cap_inventory_personal", QUERY_CAP_INVENTORY_PERSONAL)?;
|
|
conn.execute("cap_inventory_personal", &[&new_quantity, &inventory_id])?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(StorageDamageInfo {
|
|
inventory_damage_percent,
|
|
storage_destruction_percent,
|
|
affected_stocks,
|
|
destroyed_stocks,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Worker for EventsWorker {
|
|
fn start_worker_thread(&mut self) {
|
|
let pool = self.base.pool.clone();
|
|
let broker = self.base.broker.clone();
|
|
|
|
self.base
|
|
.start_worker_with_loop(move |state: Arc<WorkerState>| {
|
|
Self::run_loop(pool.clone(), broker.clone(), state);
|
|
});
|
|
}
|
|
|
|
fn stop_worker_thread(&mut self) {
|
|
self.base.stop_worker();
|
|
}
|
|
|
|
fn enable_watchdog(&mut self) {
|
|
self.base.start_watchdog();
|
|
}
|
|
}
|
|
|
|
// Hilfsfunktion zum Prüfen, ob ein Intervall abgelaufen ist
|
|
fn should_run_interval(last: Option<Instant>, now: Instant, interval: Duration) -> bool {
|
|
match last {
|
|
None => true,
|
|
Some(last_time) => now.saturating_duration_since(last_time) >= interval,
|
|
}
|
|
}
|
|
|