From f09033a99d9254cd9f21e2d3ead5e390845bf501 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 27 May 2026 17:51:39 +0200 Subject: [PATCH] =?UTF-8?q?F=C3=BCge=20neue=20SQL-Abfragen=20f=C3=BCr=20di?= =?UTF-8?q?e=20Abfrage=20von=20Lagerbest=C3=A4nden=20nach=20Typ=20hinzu:?= =?UTF-8?q?=20Implementiere=20`QUERY=5FGET=5FREGION=5FSTOCKS=5FBY=5FTYPE`?= =?UTF-8?q?=20und=20`QUERY=5FGET=5FUSER=5FSTOCKS=5FBY=5FTYPE`,=20um=20die?= =?UTF-8?q?=20Abfrage=20von=20Lagerbest=C3=A4nden=20zu=20optimieren.=20Akt?= =?UTF-8?q?ualisiere=20die=20Logik=20in=20`events.rs`,=20um=20die=20neuen?= =?UTF-8?q?=20Abfragen=20zu=20verwenden=20und=20erweitere=20die=20Struktur?= =?UTF-8?q?=20`StorageDamageInfo`,=20um=20zus=C3=A4tzliche=20Informationen?= =?UTF-8?q?=20zu=20zerst=C3=B6rten=20Einheiten=20und=20betroffenen=20Regio?= =?UTF-8?q?nen=20zu=20speichern.=20Erg=C3=A4nze=20die=20Dokumentation=20in?= =?UTF-8?q?=20`AGENTS.md`=20mit=20Projektleitlinien=20und=20Entwicklungsbe?= =?UTF-8?q?fehlen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 29 +++++++ src/worker/events.rs | 178 ++++++++++++++++++++++++++++++++++++------- src/worker/sql.rs | 35 ++++++++- 3 files changed, 214 insertions(+), 28 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0ea101c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- **Daemon-Worker Architecture**: A standalone Rust daemon for the Falukant game backend, managing game mechanics through background workers. +- **Workers (`./src/worker/`)**: Modular task handlers (e.g., `PoliticsWorker`, `FalukantFamilyWorker`, `EventsWorker`) that execute periodic game logic ("ticks"). Most workers extend the `BaseWorker` logic. +- **Database Layer**: Uses PostgreSQL for persistence. Core SQL queries are centralized in `./src/worker/sql.rs`, while connection management resides in `./src/db/`. +- **Real-time Communication**: `./src/websocket_server.rs` manages client connections, broadcasting game state updates via `./src/message_broker.rs`. +- **Migrations**: Database schema changes are managed manually via SQL scripts in `./migrations/`. +- **Documentation**: Detailed technical specs and handoff notes are located in `./docs/`. + +## Build, Test, and Development Commands +- `cargo build`: Compile the project. +- `cargo clippy`: Run the linter to ensure code quality. +- `cargo run`: Start the daemon. +- `cargo fmt`: Format the codebase according to Rust standards. +- `cargo check`: Rapidly check for compilation errors. + +## Coding Style & Naming Conventions +- **Rust Standards**: Follows idiomatic Rust conventions (snake_case for variables/functions, PascalCase for types). +- **Linter**: Enforced via `clippy`. Use `cargo clippy` before committing. +- **Database Logic**: Prefer centralizing complex SQL queries in `./src/worker/sql.rs` rather than inlining them in worker logic. + +## Testing Guidelines +- **Manual Verification**: No automated test suite exists (`cargo test` returns no results). Verify changes by running the daemon and inspecting database state or WebSocket output. +- **Smoke Tests**: Follow instructions in `./docs/` for feature-specific manual testing (e.g., `./docs/FALUKANT_DEATH_SUCCESSION_SMOKE_TEST.md`). + +## Commit & Pull Request Guidelines +- **Commit Messages**: Primarily written in German, though English is acceptable. Messages typically start with an imperative verb (e.g., "Füge", "Verbessere", "Behebe", "Refactor", "Enhance"). +- **Context**: Reference specific features or workers in the commit subject to maintain clarity in the history. diff --git a/src/worker/events.rs b/src/worker/events.rs index d09947c..9ac67c4 100644 --- a/src/worker/events.rs +++ b/src/worker/events.rs @@ -20,6 +20,8 @@ use crate::worker::sql::{ QUERY_INSERT_MONEY_HISTORY, QUERY_GET_REGION_STOCKS, QUERY_GET_USER_STOCKS, + QUERY_GET_REGION_STOCKS_BY_TYPE, + QUERY_GET_USER_STOCKS_BY_TYPE, QUERY_UPDATE_STOCK_CAPACITY, QUERY_UPDATE_STOCK_CAPACITY_REGIONAL, QUERY_GET_REGION_HOUSES, @@ -139,6 +141,8 @@ struct StorageDamageInfo { storage_destruction_percent: f64, affected_stocks: i32, destroyed_stocks: i32, + destroyed_inventory_units: i64, + affected_region_ids: Vec, } /// Parameter für regionale Lager-Schäden @@ -224,18 +228,18 @@ impl EventsWorker { EventEffect::StorageDamage { probability: 1.0, stock_type_label: "field".to_string(), - inventory_damage_min_percent: 5.0, - inventory_damage_max_percent: 75.0, + inventory_damage_min_percent: 35.0, + inventory_damage_max_percent: 90.0, storage_destruction_min_percent: 0.0, - storage_destruction_max_percent: 50.0, + storage_destruction_max_percent: 0.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, + inventory_damage_min_percent: 20.0, + inventory_damage_max_percent: 55.0, + storage_destruction_min_percent: 8.0, + storage_destruction_max_percent: 25.0, }, // Verbleibende Lager können durch den Sturm beschädigt werden und Kapazität verlieren EventEffect::StorageCapacityChange { @@ -271,23 +275,32 @@ impl EventsWorker { 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 + // Feldlager: haeufig Inhaltsverlust, aber Lager selbst bleibt erhalten EventEffect::StorageDamage { probability: 1.0, stock_type_label: "field".to_string(), - inventory_damage_min_percent: 0.0, - inventory_damage_max_percent: 100.0, + inventory_damage_min_percent: 45.0, + inventory_damage_max_percent: 95.0, storage_destruction_min_percent: 0.0, - storage_destruction_max_percent: 50.0, + storage_destruction_max_percent: 0.0, }, - // Holzlager: Lagerbestand kann zerstört werden, Lager können zerstört werden + // Holzlager: Brand richtet hohen Schaden an Inhalt und Lager an EventEffect::StorageDamage { probability: 1.0, stock_type_label: "wood".to_string(), - inventory_damage_min_percent: 0.0, - inventory_damage_max_percent: 100.0, + inventory_damage_min_percent: 35.0, + inventory_damage_max_percent: 85.0, + storage_destruction_min_percent: 12.0, + storage_destruction_max_percent: 35.0, + }, + // Steinlager: nur durch Brand betroffen, selten und nur geringer Inhaltsverlust + EventEffect::StorageDamage { + probability: 0.25, + stock_type_label: "stone".to_string(), + inventory_damage_min_percent: 3.0, + inventory_damage_max_percent: 12.0, storage_destruction_min_percent: 0.0, - storage_destruction_max_percent: 50.0, + storage_destruction_max_percent: 0.0, }, // Verbleibende Lager können durch das Feuer beschädigt werden und Kapazität verlieren EventEffect::StorageCapacityChange { @@ -297,6 +310,33 @@ impl EventsWorker { }, ], }, + RandomEvent { + id: "warehouse_raid".to_string(), + probability_per_minute: 0.003, // 0.3% pro Minute + event_type: EventType::Personal, + title: "Lagerraub".to_string(), + description: "Raeuber haben Teile deiner Lager gepluendert.".to_string(), + effects: vec![ + // Holzlager: bei Raub oft stark betroffen, inkl. Lagerschaden + EventEffect::StorageDamage { + probability: 1.0, + stock_type_label: "wood".to_string(), + inventory_damage_min_percent: 25.0, + inventory_damage_max_percent: 70.0, + storage_destruction_min_percent: 6.0, + storage_destruction_max_percent: 20.0, + }, + // Feldlager: haeufig Inhaltsverlust, aber kein Lagerschaden + EventEffect::StorageDamage { + probability: 1.0, + stock_type_label: "field".to_string(), + inventory_damage_min_percent: 30.0, + inventory_damage_max_percent: 80.0, + storage_destruction_min_percent: 0.0, + storage_destruction_max_percent: 0.0, + }, + ], + }, RandomEvent { id: "character_illness".to_string(), probability_per_minute: 0.004, // 0.4% pro Minute (reduziert) @@ -1130,6 +1170,16 @@ impl EventsWorker { .and_then(|v| v.parse::().ok()) .unwrap_or(0); + let stock_type_label = row + .get("stock_type_label") + .map(|v| v.as_str()) + .unwrap_or(""); + + // Eisenlager sind unzerstoerbar (kein struktureller Event-Schaden) + if stock_type_label == "iron" { + continue; + } + if current_capacity > 0 { conn.execute("update_stock_capacity", &[&percent_change, &stock_id])?; affected_stocks += 1; @@ -1185,6 +1235,16 @@ impl EventsWorker { .and_then(|v| v.parse::().ok()) .unwrap_or(0); + let stock_type_label = row + .get("stock_type_label") + .map(|v| v.as_str()) + .unwrap_or(""); + + // Eisenlager sind unzerstoerbar (kein struktureller Event-Schaden) + if stock_type_label == "iron" { + continue; + } + if current_capacity > 0 { conn.execute("update_stock_capacity_regional", &[&percent_change, &stock_id])?; affected_stocks += 1; @@ -1532,6 +1592,8 @@ impl EventsWorker { "storage_destruction_percent": damage_info.storage_destruction_percent, "affected_stocks": damage_info.affected_stocks, "destroyed_stocks": damage_info.destroyed_stocks, + "destroyed_inventory_units": damage_info.destroyed_inventory_units, + "affected_region_ids": damage_info.affected_region_ids, }))); } @@ -1571,10 +1633,13 @@ impl EventsWorker { return Ok(Some(json!({ "type": "storage_damage", "stock_type": stock_type_label, + "region_id": region_id, "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, + "destroyed_inventory_units": damage_info.destroyed_inventory_units, + "affected_region_ids": damage_info.affected_region_ids, }))); } @@ -2138,8 +2203,8 @@ impl EventsWorker { }; // 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])?; + conn.prepare("get_region_stocks_by_type", QUERY_GET_REGION_STOCKS_BY_TYPE)?; + let stock_rows = conn.execute("get_region_stocks_by_type", &[¶ms.region_id, &stock_type_id])?; if stock_rows.is_empty() { eprintln!( @@ -2151,6 +2216,8 @@ impl EventsWorker { storage_destruction_percent: 0.0, affected_stocks: 0, destroyed_stocks: 0, + destroyed_inventory_units: 0, + affected_region_ids: vec![params.region_id], }); } @@ -2161,7 +2228,7 @@ impl EventsWorker { 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) + let stocks_to_destroy_requested = ((total_stocks as f64 * storage_destruction_percent / 100.0) .round() as usize) .min(total_stocks); @@ -2172,6 +2239,7 @@ impl EventsWorker { let mut affected_stocks = 0; let mut processed_stocks = std::collections::HashSet::new(); + let mut destroyed_inventory_units: i64 = 0; for row in &inventory_rows { let inventory_id: Option = row @@ -2200,6 +2268,7 @@ impl EventsWorker { 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); + destroyed_inventory_units += (inventory_quantity - new_quantity).max(0) as i64; // Reduziere Lagerbestand pro Inventar-Eintrag conn.prepare("reduce_inventory", QUERY_REDUCE_INVENTORY)?; @@ -2214,18 +2283,39 @@ impl EventsWorker { // 5. Zerstöre zufällig ausgewählte Stocks let mut destroyed_stocks = 0; - if stocks_to_destroy > 0 { + // Regelwerk: + // - Nur Holzlager koennen als Lager zerstoert werden. + // - Niemals mehr Lager zerstoeren als Lager mit betroffenem Inhalt. + if stocks_to_destroy_requested > 0 && params.stock_type_label == "wood" { // Wähle zufällige Stocks zum Zerstören let mut stock_ids_to_destroy: Vec = stock_rows .iter() .filter_map(|row| row.get("stock_id").and_then(|v| v.parse::().ok())) .collect(); + stock_ids_to_destroy.retain(|sid| processed_stocks.contains(sid)); + let stocks_to_destroy = stocks_to_destroy_requested + .min(stock_ids_to_destroy.len()) + .min(affected_stocks as usize); + // 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 { + // Restbestand im zu zerstoerenden Lager zaehlen (zusaetzlicher Verlust) + conn.prepare("get_stock_inventory", QUERY_GET_STOCK_INVENTORY)?; + let doomed_items = conn.execute("get_stock_inventory", &[stock_id])?; + for item_row in doomed_items { + let qty = item_row + .get("quantity") + .and_then(|v| v.parse::().ok()) + .unwrap_or(0); + if qty > 0 { + destroyed_inventory_units += qty as i64; + } + } + // Lösche zuerst den Lagerbestand conn.prepare("delete_inventory", QUERY_DELETE_INVENTORY)?; conn.execute("delete_inventory", &[stock_id])?; @@ -2240,7 +2330,7 @@ impl EventsWorker { // 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])?; + let remaining_stock_rows = conn.execute("get_region_stocks_by_type", &[¶ms.region_id, &stock_type_id])?; for row in remaining_stock_rows { let stock_id: Option = row @@ -2299,6 +2389,8 @@ impl EventsWorker { storage_destruction_percent, affected_stocks, destroyed_stocks, + destroyed_inventory_units, + affected_region_ids: vec![params.region_id], }) } @@ -2331,8 +2423,8 @@ impl EventsWorker { }; // 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])?; + conn.prepare("get_user_stocks_by_type", QUERY_GET_USER_STOCKS_BY_TYPE)?; + let stock_rows = conn.execute("get_user_stocks_by_type", &[¶ms.user_id, &stock_type_id])?; if stock_rows.is_empty() { eprintln!( @@ -2344,6 +2436,8 @@ impl EventsWorker { storage_destruction_percent: 0.0, affected_stocks: 0, destroyed_stocks: 0, + destroyed_inventory_units: 0, + affected_region_ids: Vec::new(), }); } @@ -2354,7 +2448,7 @@ impl EventsWorker { 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) + let stocks_to_destroy_requested = ((total_stocks as f64 * storage_destruction_percent / 100.0) .round() as usize) .min(total_stocks); @@ -2365,6 +2459,7 @@ impl EventsWorker { let mut affected_stocks = 0; let mut processed_stocks = std::collections::HashSet::new(); + let mut destroyed_inventory_units: i64 = 0; for row in &inventory_rows { let inventory_id: Option = row @@ -2393,6 +2488,7 @@ impl EventsWorker { 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); + destroyed_inventory_units += (inventory_quantity - new_quantity).max(0) as i64; // Reduziere Lagerbestand pro Inventar-Eintrag conn.prepare("reduce_inventory_personal", QUERY_REDUCE_INVENTORY_PERSONAL)?; @@ -2405,20 +2501,38 @@ impl EventsWorker { } } - // 5. Zerstöre zufällig ausgewählte Stocks (nur für field und wood) + // 5. Zerstöre zufällig ausgewählte Stocks (nur Holzlager) let mut destroyed_stocks = 0; - if stocks_to_destroy > 0 && (params.stock_type_label == "field" || params.stock_type_label == "wood") { + if stocks_to_destroy_requested > 0 && params.stock_type_label == "wood" { // Wähle zufällige Stocks zum Zerstören let mut stock_ids_to_destroy: Vec = stock_rows .iter() .filter_map(|row| row.get("stock_id").and_then(|v| v.parse::().ok())) .collect(); + stock_ids_to_destroy.retain(|sid| processed_stocks.contains(sid)); + let stocks_to_destroy = stocks_to_destroy_requested + .min(stock_ids_to_destroy.len()) + .min(affected_stocks as usize); + // 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 { + // Restbestand im zu zerstoerenden Lager zaehlen (zusaetzlicher Verlust) + conn.prepare("get_stock_inventory_personal", QUERY_GET_STOCK_INVENTORY_PERSONAL)?; + let doomed_items = conn.execute("get_stock_inventory_personal", &[stock_id])?; + for item_row in doomed_items { + let qty = item_row + .get("quantity") + .and_then(|v| v.parse::().ok()) + .unwrap_or(0); + if qty > 0 { + destroyed_inventory_units += qty as i64; + } + } + // Lösche zuerst den Lagerbestand conn.prepare("delete_inventory_personal", QUERY_DELETE_INVENTORY_PERSONAL)?; conn.execute("delete_inventory_personal", &[stock_id])?; @@ -2432,7 +2546,7 @@ impl EventsWorker { } // 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])?; + let remaining_stock_rows = conn.execute("get_user_stocks_by_type", &[¶ms.user_id, &stock_type_id])?; for row in remaining_stock_rows { let stock_id: Option = row @@ -2486,11 +2600,23 @@ impl EventsWorker { } } + let mut affected_region_ids = std::collections::BTreeSet::new(); + for row in &stock_rows { + if let Some(rid) = row + .get("region_id") + .and_then(|v| v.parse::().ok()) + { + affected_region_ids.insert(rid); + } + } + Ok(StorageDamageInfo { inventory_damage_percent, storage_destruction_percent, affected_stocks, destroyed_stocks, + destroyed_inventory_units, + affected_region_ids: affected_region_ids.into_iter().collect(), }) } } diff --git a/src/worker/sql.rs b/src/worker/sql.rs index d7b5b08..b616540 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -753,9 +753,12 @@ UPDATE falukant_data.inventory SET quantity = $1 WHERE id = $2; "#; pub const QUERY_GET_USER_STOCKS: &str = r#" -SELECT s.id AS stock_id, s.quantity AS current_capacity +SELECT s.id AS stock_id, + s.quantity AS current_capacity, + st.label_tr AS stock_type_label FROM falukant_data.stock s JOIN falukant_data.branch b ON s.branch_id = b.id +LEFT JOIN falukant_type.stock st ON st.id = s.stock_type_id WHERE b.falukant_user_id = $1; "#; @@ -766,9 +769,12 @@ UPDATE falukant_data.stock "#; pub const QUERY_GET_REGION_STOCKS: &str = r#" -SELECT s.id AS stock_id, s.quantity AS current_capacity +SELECT s.id AS stock_id, + s.quantity AS current_capacity, + st.label_tr AS stock_type_label FROM falukant_data.stock s JOIN falukant_data.branch b ON s.branch_id = b.id +LEFT JOIN falukant_type.stock st ON st.id = s.stock_type_id WHERE b.region_id = $1; "#; @@ -2699,6 +2705,18 @@ pub const QUERY_GET_INVENTORY_ITEMS: &str = r#" SELECT i.id AS inventory_id, i.quantity AS inventory_quantity, i.stock_id FROM falukant_data.inventory i JOIN falukant_data.stock s ON i.stock_id = s.id JOIN falukant_data.branch b ON s.branch_id = b.id WHERE b.region_id = $1 AND s.stock_type_id = $2; "#; +pub const QUERY_GET_REGION_STOCKS_BY_TYPE: &str = r#" +SELECT s.id AS stock_id, + s.quantity AS stock_capacity, + COALESCE(SUM(i.quantity), 0)::int AS inventory_quantity +FROM falukant_data.stock s +JOIN falukant_data.branch b ON s.branch_id = b.id +LEFT JOIN falukant_data.inventory i ON i.stock_id = s.id +WHERE b.region_id = $1 + AND s.stock_type_id = $2 +GROUP BY s.id, s.quantity; +"#; + pub const QUERY_REDUCE_INVENTORY: &str = r#" UPDATE falukant_data.inventory SET quantity = $1 WHERE id = $2; "#; @@ -2726,6 +2744,19 @@ JOIN falukant_data.stock s ON i.stock_id = s.id JOIN falukant_data.branch b ON s.branch_id = b.id WHERE b.falukant_user_id = $1 AND s.stock_type_id = $2; "#; + +pub const QUERY_GET_USER_STOCKS_BY_TYPE: &str = r#" +SELECT s.id AS stock_id, + b.region_id AS region_id, + s.quantity AS stock_capacity, + COALESCE(SUM(i.quantity), 0)::int AS inventory_quantity +FROM falukant_data.stock s +JOIN falukant_data.branch b ON s.branch_id = b.id +LEFT JOIN falukant_data.inventory i ON i.stock_id = s.id +WHERE b.falukant_user_id = $1 + AND s.stock_type_id = $2 +GROUP BY s.id, b.region_id, s.quantity; +"#; // Produce worker queries pub const QUERY_GET_FINISHED_PRODUCTIONS: &str = r#" SELECT DISTINCT ON (p.id)