diff --git a/src/websocket_server.rs b/src/websocket_server.rs index 537de42..6435ee8 100644 --- a/src/websocket_server.rs +++ b/src/websocket_server.rs @@ -86,15 +86,7 @@ async fn append_ws_log( .map(|s| s.to_string()); let target_user = json .get("user_id") - .and_then(|v| { - if let Some(s) = v.as_str() { - Some(s.to_string()) - } else if let Some(n) = v.as_i64() { - Some(n.to_string()) - } else { - None - } - }); + .and_then(|v| v.as_str().map(|s| s.to_string()).or_else(|| v.as_i64().map(|n| n.to_string()))); (event, target_user) } else { (None, None) @@ -510,10 +502,8 @@ async fn handle_connection( .and_then(|v| { if let Some(s) = v.as_str() { s.parse::().ok() - } else if let Some(n) = v.as_i64() { - Some(n) } else { - None + v.as_i64() } }) .map(|v| v == numeric_uid) diff --git a/src/worker/base.rs b/src/worker/base.rs index 2e5145d..72bf738 100644 --- a/src/worker/base.rs +++ b/src/worker/base.rs @@ -166,7 +166,7 @@ impl BaseWorker { let rows = conn.execute("get_money_for_clamp", &[&falukant_user_id])?; let current_money: f64 = rows - .get(0) + .first() .and_then(|r| r.get("money")) .and_then(|v| v.parse::().ok()) .unwrap_or(0.0); diff --git a/src/worker/character_creation.rs b/src/worker/character_creation.rs index 5f42a78..704338a 100644 --- a/src/worker/character_creation.rs +++ b/src/worker/character_creation.rs @@ -10,6 +10,19 @@ use std::thread; use std::time::Duration; use super::base::{BaseWorker, Worker, WorkerState}; +use crate::worker::sql::{ + QUERY_IS_PREVIOUS_DAY_CHARACTER_CREATED, + QUERY_GET_TOWN_REGION_IDS, + QUERY_LOAD_FIRST_NAMES, + QUERY_LOAD_LAST_NAMES, + QUERY_INSERT_CHARACTER, + QUERY_GET_ELIGIBLE_NPC_FOR_DEATH, + QUERY_DELETE_DIRECTOR, + QUERY_DELETE_RELATIONSHIP, + QUERY_DELETE_CHILD_RELATION, + QUERY_INSERT_NOTIFICATION, + QUERY_MARK_CHARACTER_DECEASED, +}; pub struct CharacterCreationWorker { pub(crate) base: BaseWorker, @@ -21,145 +34,7 @@ pub struct CharacterCreationWorker { death_thread: Option>, } -// SQL-Queries analog zur C++-Implementierung -const QUERY_IS_PREVIOUS_DAY_CHARACTER_CREATED: &str = r#" - SELECT created_at - FROM falukant_data."character" - WHERE user_id IS NULL - AND created_at::date = CURRENT_DATE - ORDER BY created_at DESC - LIMIT 1; -"#; -const QUERY_GET_TOWN_REGION_IDS: &str = r#" - SELECT fdr.id - FROM falukant_data.region fdr - JOIN falukant_type.region ftr ON fdr.region_type_id = ftr.id - WHERE ftr.label_tr = 'city'; -"#; - -const QUERY_LOAD_FIRST_NAMES: &str = r#" - SELECT id, gender - FROM falukant_predefine.firstname; -"#; - -const QUERY_LOAD_LAST_NAMES: &str = r#" - SELECT id - FROM falukant_predefine.lastname; -"#; - -const QUERY_INSERT_CHARACTER: &str = r#" - INSERT INTO falukant_data.character( - user_id, - region_id, - first_name, - last_name, - birthdate, - gender, - created_at, - updated_at, - title_of_nobility - ) VALUES ( - NULL, - $1, - $2, - $3, - NOW(), - $4, - NOW(), - NOW(), - $5 - ); -"#; - -const QUERY_GET_ELIGIBLE_NPC_FOR_DEATH: &str = r#" - WITH aged AS ( - SELECT - c.id, - (current_date - c.birthdate::date) AS age, - c.user_id - FROM - falukant_data.character c - WHERE - c.user_id IS NULL - AND (current_date - c.birthdate::date) > 60 - ), - always_sel AS ( - SELECT * - FROM aged - WHERE age > 85 - ), - random_sel AS ( - SELECT * - FROM aged - WHERE age <= 85 - ORDER BY random() - LIMIT 10 - ) - SELECT * - FROM always_sel - UNION ALL - SELECT * - FROM random_sel; -"#; - -const QUERY_DELETE_DIRECTOR: &str = r#" - DELETE FROM falukant_data.director - WHERE director_character_id = $1 - RETURNING employer_user_id; -"#; - -const QUERY_DELETE_RELATIONSHIP: &str = r#" - WITH deleted AS ( - DELETE FROM falukant_data.relationship - WHERE character1_id = $1 - OR character2_id = $1 - RETURNING - CASE - WHEN character1_id = $1 THEN character2_id - ELSE character1_id - END AS related_character_id, - relationship_type_id - ) - SELECT - c.user_id AS related_user_id - FROM deleted d - JOIN falukant_data.character c - ON c.id = d.related_character_id; -"#; - -const QUERY_DELETE_CHILD_RELATION: &str = r#" - WITH deleted AS ( - DELETE FROM falukant_data.child_relation - WHERE child_character_id = $1 - RETURNING - father_character_id, - mother_character_id - ) - SELECT - cf.user_id AS father_user_id, - cm.user_id AS mother_user_id - FROM deleted d - JOIN falukant_data.character cf - ON cf.id = d.father_character_id - JOIN falukant_data.character cm - ON cm.id = d.mother_character_id; -"#; - -const QUERY_INSERT_NOTIFICATION: &str = r#" - INSERT INTO falukant_log.notification ( - user_id, - tr, - shown, - created_at, - updated_at - ) VALUES ($1, 'director_death', FALSE, NOW(), NOW()); -"#; - -const QUERY_MARK_CHARACTER_DECEASED: &str = r#" - DELETE FROM falukant_data.character - WHERE id = $1; -"#; impl CharacterCreationWorker { pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self { @@ -329,10 +204,10 @@ impl CharacterCreationWorker { } fn load_names(&mut self) { - if self.first_name_cache.is_empty() || self.last_name_cache.is_empty() { - if let Err(err) = self.load_first_and_last_names() { - eprintln!("[CharacterCreationWorker] Fehler in loadNames: {err}"); - } + if (self.first_name_cache.is_empty() || self.last_name_cache.is_empty()) + && let Err(err) = self.load_first_and_last_names() + { + eprintln!("[CharacterCreationWorker] Fehler in loadNames: {err}"); } } @@ -491,12 +366,12 @@ impl CharacterCreationWorker { .and_then(|v| v.parse::().ok()) .unwrap_or(0); - if character_id > 0 && Self::calculate_death_probability(age) { - if let Err(err) = Self::handle_character_death(pool, broker, character_id) { - eprintln!( - "[CharacterCreationWorker] Fehler beim Bearbeiten des NPC-Todes (id={character_id}): {err}" - ); - } + if character_id > 0 && Self::calculate_death_probability(age) + && let Err(err) = Self::handle_character_death(pool, broker, character_id) + { + eprintln!( + "[CharacterCreationWorker] Fehler beim Bearbeiten des NPC-Todes (id={character_id}): {err}" + ); } } @@ -531,13 +406,12 @@ impl CharacterCreationWorker { // 1) Director löschen und User benachrichtigen conn.prepare("delete_director", QUERY_DELETE_DIRECTOR)?; let dir_result = conn.execute("delete_director", &[&character_id])?; - if let Some(row) = dir_result.get(0) { - if let Some(user_id) = row + if let Some(row) = dir_result.first() + && let Some(user_id) = row .get("employer_user_id") .and_then(|v| v.parse::().ok()) - { - Self::notify_user(pool, broker, user_id, "director_death")?; - } + { + Self::notify_user(pool, broker, user_id, "director_death")?; } // 2) Relationships löschen und betroffene User benachrichtigen diff --git a/src/worker/director.rs b/src/worker/director.rs index d3fd284..0431e3e 100644 --- a/src/worker/director.rs +++ b/src/worker/director.rs @@ -31,6 +31,10 @@ use crate::worker::sql::{ QUERY_GET_BRANCH_REGION, QUERY_GET_AVERAGE_WORTH, QUERY_UPDATE_INVENTORY_QTY, + QUERY_GET_PRODUCT_COST, + QUERY_GET_USER_OFFICES, + QUERY_CUMULATIVE_TAX_NO_EXEMPT, + QUERY_CUMULATIVE_TAX_WITH_EXEMPT, }; #[derive(Debug, Clone)] @@ -103,7 +107,7 @@ impl DirectorWorker { } } - fn run_iteration(&mut self, state: &WorkerState) { + fn run_iteration(&mut self, _state: &WorkerState) { self.base.set_current_step("DirectorWorker iteration"); let now = Instant::now(); @@ -119,11 +123,7 @@ impl DirectorWorker { self.last_run = Some(now); } - std::thread::sleep(Duration::from_secs(1)); - - if !state.running_worker.load(Ordering::Relaxed) { - return; - } + std::thread::sleep(Duration::from_secs(1)); } fn perform_all_tasks(&mut self) -> Result<(), DbError> { @@ -222,7 +222,7 @@ impl DirectorWorker { return Ok(()); } - let mut base_plan = match Self::map_row_to_production_plan(&rows[0]) { + let mut base_plan = match rows.first().and_then(Self::map_row_to_production_plan) { Some(p) => p, None => { eprintln!( @@ -374,7 +374,7 @@ impl DirectorWorker { let one_piece_cost = Self::calc_one_piece_cost(plan); let max_money_production = Self::calc_max_money_production(plan, one_piece_cost); - let to_produce = free_capacity.min(max_money_production).min(100).max(0); + let to_produce = (free_capacity.min(max_money_production)).clamp(0, 100); eprintln!( "[DirectorWorker] Produktionsberechnung: free_capacity={}, one_piece_cost={}, max_money_production={}, to_produce={}, running_productions={}", @@ -718,15 +718,19 @@ impl DirectorWorker { // Helper: get one_piece_cost from DB row fallback logic fn resolve_one_piece_cost(conn: &mut DbConnection, product_id: i32, fallback: f64) -> Result { - conn.prepare("get_product_cost", "SELECT original_sell_cost, sell_cost FROM falukant_type.product WHERE id = $1")?; + conn.prepare("get_product_cost", QUERY_GET_PRODUCT_COST)?; let rows = conn.execute("get_product_cost", &[&product_id])?; - if let Some(row) = rows.get(0) { - if let Some(osc) = row.get("original_sell_cost") { - if let Ok(v) = osc.parse::() { return Ok(v); } - } - if let Some(sc) = row.get("sell_cost") { - if let Ok(v) = sc.parse::() { return Ok(v); } - } + if let Some(row) = rows.first() + && let Some(osc) = row.get("original_sell_cost") + && let Ok(v) = osc.parse::() + { + return Ok(v); + } + if let Some(row) = rows.first() + && let Some(sc) = row.get("sell_cost") + && let Ok(v) = sc.parse::() + { + return Ok(v); } Ok(fallback) } @@ -736,15 +740,12 @@ impl DirectorWorker { // Default let mut cumulative_tax_percent = DEFAULT_TAX_PERCENT; - conn.prepare("get_branch_region", "SELECT region_id FROM falukant_data.branch WHERE id = $1;")?; + conn.prepare("get_branch_region", QUERY_GET_BRANCH_REGION)?; let branch_rows = conn.execute("get_branch_region", &[&branch_id])?; - let branch_region_id: Option = branch_rows.get(0).and_then(|r| r.get("region_id")).and_then(|v| v.parse().ok()); + let branch_region_id: Option = branch_rows.first().and_then(|r| r.get("region_id")).and_then(|v| v.parse().ok()); if let Some(region_id) = branch_region_id { - conn.prepare( - "get_user_offices", - "SELECT po.id AS office_id, pot.name AS office_name, po.region_id, rt.label_tr AS region_type FROM falukant_data.political_office po JOIN falukant_type.political_office_type pot ON pot.id = po.office_type_id JOIN falukant_data.region r ON r.id = po.region_id JOIN falukant_type.region_type rt ON rt.id = r.region_type_id WHERE po.holder_id = $1 AND (po.end_date IS NULL OR po.end_date > NOW());", - )?; + conn.prepare("get_user_offices", QUERY_GET_USER_OFFICES)?; let offices = conn.execute("get_user_offices", &[&user_id])?; let mut exempt_types: Vec = Vec::new(); @@ -767,27 +768,19 @@ impl DirectorWorker { } if exempt_types.is_empty() { - conn.prepare( - "cumulative_tax_no_exempt", - "WITH RECURSIVE ancestors AS (SELECT id, parent_id, COALESCE(tax_percent,0.0) AS tax_percent FROM falukant_data.region WHERE id = $1 UNION ALL SELECT r.id, r.parent_id, COALESCE(r.tax_percent,0.0) FROM falukant_data.region r JOIN ancestors a ON r.id = a.parent_id) SELECT COALESCE(SUM(tax_percent),0.0) AS total_percent FROM ancestors;", - )?; + conn.prepare("cumulative_tax_no_exempt", QUERY_CUMULATIVE_TAX_NO_EXEMPT)?; let res = conn.execute("cumulative_tax_no_exempt", &[®ion_id])?; - if let Some(row) = res.get(0) { - if let Some(tp) = row.get("total_percent") { - cumulative_tax_percent = tp.parse::().unwrap_or(DEFAULT_TAX_PERCENT); - } + if let Some(row) = res.first() + && let Some(tp) = row.get("total_percent") + { + cumulative_tax_percent = tp.parse::().unwrap_or(DEFAULT_TAX_PERCENT); } } else { - conn.prepare( - "cumulative_tax_with_exempt", - "WITH RECURSIVE ancestors AS (SELECT r.id, r.parent_id, CASE WHEN rt.label_tr = ANY($2::text[]) THEN 0.0 ELSE COALESCE(r.tax_percent,0.0) END AS tax_percent FROM falukant_data.region r JOIN falukant_type.region_type rt ON rt.id = r.region_type_id WHERE r.id = $1 UNION ALL SELECT r.id, r.parent_id, CASE WHEN rt.label_tr = ANY($2::text[]) THEN 0.0 ELSE COALESCE(r.tax_percent,0.0) END FROM falukant_data.region r JOIN falukant_type.region_type rt ON rt.id = r.region_type_id JOIN ancestors a ON r.id = a.parent_id) SELECT COALESCE(SUM(tax_percent),0.0) AS total_percent FROM ancestors;", - )?; + conn.prepare("cumulative_tax_with_exempt", QUERY_CUMULATIVE_TAX_WITH_EXEMPT)?; let exempt_array: Vec<&str> = exempt_types.iter().map(|s| s.as_str()).collect(); let res = conn.execute("cumulative_tax_with_exempt", &[®ion_id, &exempt_array])?; - if let Some(row) = res.get(0) { - if let Some(tp) = row.get("total_percent") { - cumulative_tax_percent = tp.parse::().unwrap_or(DEFAULT_TAX_PERCENT); - } + if let Some(row) = res.first() && let Some(tp) = row.get("total_percent") { + cumulative_tax_percent = tp.parse::().unwrap_or(DEFAULT_TAX_PERCENT); } } } @@ -828,10 +821,8 @@ impl DirectorWorker { } let payout_amount = (payout_cents as f64) / 100.0; - if payout_cents != 0 { - if let Err(err) = self.base.change_falukant_user_money(item.user_id, payout_amount, "sell products") { - eprintln!("[DirectorWorker] Fehler bei change_falukant_user_money (sell products): {err}"); - } + if payout_cents != 0 && let Err(err) = self.base.change_falukant_user_money(item.user_id, payout_amount, "sell products") { + eprintln!("[DirectorWorker] Fehler bei change_falukant_user_money (sell products): {err}"); } // Debug: Log vor dem DB-Aufruf diff --git a/src/worker/events.rs b/src/worker/events.rs index f886d82..b86f9b3 100644 --- a/src/worker/events.rs +++ b/src/worker/events.rs @@ -16,6 +16,42 @@ use crate::worker::sql::{ 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 @@ -91,6 +127,26 @@ struct StorageDamageInfo { 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 { @@ -433,7 +489,7 @@ impl EventsWorker { let rows = conn.execute("get_random_user", &[])?; let user_id: Option = rows - .get(0) + .first() .and_then(|r| r.get("id")) .and_then(|v| v.parse::().ok()); @@ -485,30 +541,30 @@ impl EventsWorker { min_change, max_change, } => { - if effect_roll < *probability { - if let Ok((character_id, health_change)) = Self::apply_character_health_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 - })); - } + ) + { + effect_results.push(json!({ + "type": "character_health_change", + "character_id": character_id, + "change": health_change + })); } } EventEffect::CharacterDeath { probability } => { - if effect_roll < *probability { - if let Ok(character_id) = Self::apply_character_death(&mut conn, user_id, pool, broker) { - effect_results.push(json!({ - "type": "character_death", - "character_id": character_id - })); - } + 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 { @@ -519,26 +575,28 @@ impl EventsWorker { storage_destruction_min_percent, storage_destruction_max_percent, } => { - if effect_roll < *probability { - if let Ok(damage_info) = Self::apply_personal_storage_damage( + if effect_roll < *probability + && let Ok(damage_info) = Self::apply_personal_storage_damage( &mut conn, - user_id, - stock_type_label, - *inventory_damage_min_percent, - *inventory_damage_max_percent, - *storage_destruction_min_percent, - *storage_destruction_max_percent, + 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, - })); - } + ) + { + 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, + })); } } _ => { @@ -609,7 +667,7 @@ impl EventsWorker { let rows = conn.execute("get_random_infant", &[])?; let character_id: Option = rows - .get(0) + .first() .and_then(|r| r.get("character_id")) .and_then(|v| v.parse::().ok()); @@ -622,7 +680,7 @@ impl EventsWorker { }; let user_id: Option = rows - .get(0) + .first() .and_then(|r| r.get("user_id")) .and_then(|v| v.parse::().ok()); @@ -640,13 +698,13 @@ impl EventsWorker { let effect_roll = rng.gen_range(0.0..=1.0); match effect { EventEffect::CharacterDeath { probability } => { - if effect_roll < *probability { - if Self::handle_character_death(pool, broker, character_id).is_ok() { - effect_results.push(json!({ - "type": "character_death", - "character_id": character_id - })); - } + 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 + })); } } _ => { @@ -704,7 +762,7 @@ impl EventsWorker { let rows = conn.execute("get_random_city", &[])?; let region_id: Option = rows - .get(0) + .first() .and_then(|r| r.get("region_id")) .and_then(|v| v.parse::().ok()); @@ -818,26 +876,28 @@ impl EventsWorker { storage_destruction_min_percent, storage_destruction_max_percent, } => { - if effect_roll < *probability { - if let Ok(damage_info) = Self::apply_storage_damage( + if effect_roll < *probability + && let Ok(damage_info) = Self::apply_storage_damage( &mut conn, - region_id, - stock_type_label, - *inventory_damage_min_percent, - *inventory_damage_max_percent, - *storage_destruction_min_percent, - *storage_destruction_max_percent, + 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, - })); - } + ) + { + 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 { @@ -845,20 +905,20 @@ impl EventsWorker { min_percent, max_percent, } => { - if effect_roll < *probability { - if let Ok((affected_stocks, percent_change)) = Self::apply_regional_storage_capacity_change( + 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, - })); - } + ) + { + effect_results.push(json!({ + "type": "storage_capacity_change", + "percent": percent_change, + "affected_stocks": affected_stocks, + })); } } EventEffect::HouseQualityChange { @@ -866,20 +926,20 @@ impl EventsWorker { min_change, max_change, } => { - if effect_roll < *probability { - if let Ok((affected_houses, quality_change)) = Self::apply_regional_house_quality_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, - })); - } + ) + { + effect_results.push(json!({ + "type": "house_quality_change", + "change": quality_change, + "affected_houses": affected_houses, + })); } } _ => { @@ -952,7 +1012,7 @@ impl EventsWorker { let rows = conn.execute("get_money", &[&user_id])?; let current_money: Option = rows - .get(0) + .first() .and_then(|r| r.get("money")) .and_then(|v| v.parse::().ok()); @@ -1008,14 +1068,7 @@ impl EventsWorker { percent_change: f64, ) -> Result<(), DbError> { // Hole alle Stocks des Spielers - const QUERY_GET_USER_STOCKS: &str = r#" - SELECT s.id AS stock_id, s.quantity AS current_capacity - FROM falukant_data.stock s - JOIN falukant_data.branch b ON s.branch_id = b.id - WHERE b.falukant_user_id = $1; - "#; - - conn.prepare("get_user_stocks_capacity", QUERY_GET_USER_STOCKS)?; + 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() { @@ -1027,13 +1080,7 @@ impl EventsWorker { } // Reduziere die Kapazität aller Stocks - const QUERY_UPDATE_STOCK_CAPACITY: &str = r#" - UPDATE falukant_data.stock - SET quantity = GREATEST(1, ROUND(quantity * (1 + $1 / 100.0))) - WHERE id = $2; - "#; - - conn.prepare("update_stock_capacity", QUERY_UPDATE_STOCK_CAPACITY)?; + conn.prepare("update_stock_capacity", QUERY_UPDATE_STOCK_CAPACITY)?; let mut affected_stocks = 0; for row in stock_rows { @@ -1073,14 +1120,7 @@ impl EventsWorker { rng: &mut impl Rng, ) -> Result<(i32, f64), DbError> { // Hole alle Stocks in der Region - const QUERY_GET_REGION_STOCKS: &str = r#" - SELECT s.id AS stock_id, s.quantity AS current_capacity - FROM falukant_data.stock s - JOIN falukant_data.branch b ON s.branch_id = b.id - WHERE b.region_id = $1; - "#; - - conn.prepare("get_region_stocks_capacity", QUERY_GET_REGION_STOCKS)?; + 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() { @@ -1095,13 +1135,7 @@ impl EventsWorker { let percent_change = rng.gen_range(min_percent..=max_percent); // Reduziere die Kapazität aller Stocks - const QUERY_UPDATE_STOCK_CAPACITY_REGIONAL: &str = r#" - UPDATE falukant_data.stock - SET quantity = GREATEST(1, ROUND(quantity * (1 + $1 / 100.0))) - WHERE id = $2; - "#; - - conn.prepare("update_stock_capacity_regional", QUERY_UPDATE_STOCK_CAPACITY_REGIONAL)?; + conn.prepare("update_stock_capacity_regional", QUERY_UPDATE_STOCK_CAPACITY_REGIONAL)?; let mut affected_stocks = 0; for row in stock_rows { @@ -1141,23 +1175,7 @@ impl EventsWorker { rng: &mut impl Rng, ) -> Result<(i32, i32), DbError> { // Hole alle Häuser in der Region - const QUERY_GET_REGION_HOUSES: &str = r#" - SELECT uh.id AS house_id, - uh.roof_condition, - uh.floor_condition, - uh.wall_condition, - uh.window_condition - FROM falukant_data.user_house uh - JOIN falukant_data.character c ON c.user_id = uh.user_id - WHERE c.region_id = $1 - AND uh.house_type_id NOT IN ( - SELECT id - FROM falukant_type.house h - WHERE h.label_tr = 'under_bridge' - ); - "#; - - conn.prepare("get_region_houses", QUERY_GET_REGION_HOUSES)?; + conn.prepare("get_region_houses", QUERY_GET_REGION_HOUSES)?; let house_rows = conn.execute("get_region_houses", &[®ion_id])?; if house_rows.is_empty() { @@ -1172,16 +1190,7 @@ impl EventsWorker { let quality_change = rng.gen_range(min_change..=max_change); // Reduziere die Qualität aller Häuser - const QUERY_UPDATE_HOUSE_QUALITY: &str = r#" - UPDATE falukant_data.user_house - SET roof_condition = GREATEST(0, LEAST(100, roof_condition + $1)), - floor_condition = GREATEST(0, LEAST(100, floor_condition + $1)), - wall_condition = GREATEST(0, LEAST(100, wall_condition + $1)), - window_condition = GREATEST(0, LEAST(100, window_condition + $1)) - WHERE id = $2; - "#; - - conn.prepare("update_house_quality", QUERY_UPDATE_HOUSE_QUALITY)?; + conn.prepare("update_house_quality", QUERY_UPDATE_HOUSE_QUALITY)?; let mut affected_houses = 0; for row in house_rows { @@ -1211,18 +1220,7 @@ impl EventsWorker { region_id: i32, ) -> Result<(), DbError> { // Wähle ein zufälliges Wetter - const QUERY_CHANGE_WEATHER: &str = r#" - UPDATE falukant_data.weather - SET weather_type_id = ( - SELECT id - FROM falukant_type.weather - ORDER BY RANDOM() - LIMIT 1 - ) - WHERE region_id = $1; - "#; - - conn.prepare("change_weather", QUERY_CHANGE_WEATHER)?; + conn.prepare("change_weather", QUERY_CHANGE_WEATHER)?; conn.execute("change_weather", &[®ion_id])?; Ok(()) @@ -1277,20 +1275,11 @@ impl EventsWorker { rng: &mut impl Rng, ) -> Result<(i32, i32), DbError> { // Hole einen zufälligen Charakter des Spielers - const QUERY_GET_RANDOM_CHARACTER: &str = r#" - SELECT id, health - FROM falukant_data."character" - WHERE user_id = $1 - AND health > 0 - ORDER BY RANDOM() - LIMIT 1; - "#; - - conn.prepare("get_random_character", QUERY_GET_RANDOM_CHARACTER)?; + conn.prepare("get_random_character", QUERY_GET_RANDOM_CHARACTER)?; let rows = conn.execute("get_random_character", &[&user_id])?; let character_id: Option = rows - .get(0) + .first() .and_then(|r| r.get("id")) .and_then(|v| v.parse::().ok()); @@ -1303,22 +1292,16 @@ impl EventsWorker { }; let current_health: i32 = rows - .get(0) + .first() .and_then(|r| r.get("health")) .and_then(|v| v.parse::().ok()) .unwrap_or(100); let health_change = rng.gen_range(min_change..=max_change); - let new_health = (current_health as i32 + health_change).max(0).min(100); + let new_health = (current_health + health_change).clamp(0, 100); // Update Gesundheit - const QUERY_UPDATE_HEALTH: &str = r#" - UPDATE falukant_data."character" - SET health = $1 - WHERE id = $2; - "#; - - conn.prepare("update_health", QUERY_UPDATE_HEALTH)?; + conn.prepare("update_health", QUERY_UPDATE_HEALTH)?; conn.execute("update_health", &[&new_health, &character_id])?; Ok((character_id, health_change)) @@ -1331,20 +1314,11 @@ impl EventsWorker { broker: &MessageBroker, ) -> Result { // Hole einen zufälligen Charakter des Spielers - const QUERY_GET_RANDOM_CHARACTER: &str = r#" - SELECT id - FROM falukant_data."character" - WHERE user_id = $1 - AND health > 0 - ORDER BY RANDOM() - LIMIT 1; - "#; - - conn.prepare("get_random_character_death", QUERY_GET_RANDOM_CHARACTER)?; + conn.prepare("get_random_character_death", QUERY_GET_RANDOM_CHARACTER)?; let rows = conn.execute("get_random_character_death", &[&user_id])?; let character_id: Option = rows - .get(0) + .first() .and_then(|r| r.get("id")) .and_then(|v| v.parse::().ok()); @@ -1371,14 +1345,7 @@ impl EventsWorker { rng: &mut impl Rng, ) -> Result, DbError> { // Hole alle lebenden Charaktere in der Region - const QUERY_GET_REGION_CHARACTERS: &str = r#" - SELECT id, health - FROM falukant_data."character" - WHERE region_id = $1 - AND health > 0; - "#; - - conn.prepare("get_region_characters", QUERY_GET_REGION_CHARACTERS)?; + conn.prepare("get_region_characters", QUERY_GET_REGION_CHARACTERS)?; let rows = conn.execute("get_region_characters", &[®ion_id])?; let mut affected_characters = Vec::new(); @@ -1399,16 +1366,10 @@ impl EventsWorker { .unwrap_or(100); let health_change = rng.gen_range(min_change..=max_change); - let new_health = (current_health as i32 + health_change).max(0).min(100); + let new_health = (current_health + health_change).clamp(0, 100); // Update Gesundheit - const QUERY_UPDATE_HEALTH: &str = r#" - UPDATE falukant_data."character" - SET health = $1 - WHERE id = $2; - "#; - - conn.prepare("update_health_regional", QUERY_UPDATE_HEALTH)?; + conn.prepare("update_health_regional", QUERY_UPDATE_HEALTH)?; conn.execute("update_health_regional", &[&new_health, &character_id])?; affected_characters.push((character_id, health_change)); @@ -1424,14 +1385,7 @@ impl EventsWorker { broker: &MessageBroker, ) -> Result, DbError> { // Hole alle lebenden Charaktere in der Region - const QUERY_GET_REGION_CHARACTERS: &str = r#" - SELECT id - FROM falukant_data."character" - WHERE region_id = $1 - AND health > 0; - "#; - - conn.prepare("get_region_characters_death", QUERY_GET_REGION_CHARACTERS)?; + 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(); @@ -1467,13 +1421,7 @@ impl EventsWorker { .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; // 1) Director löschen und User benachrichtigen - const QUERY_DELETE_DIRECTOR: &str = r#" - DELETE FROM falukant_data.director - WHERE director_character_id = $1 - RETURNING employer_user_id; - "#; - - conn.prepare("delete_director", QUERY_DELETE_DIRECTOR)?; + 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 @@ -1485,26 +1433,7 @@ impl EventsWorker { } // 2) Relationships löschen und betroffene User benachrichtigen - const QUERY_DELETE_RELATIONSHIP: &str = r#" - WITH deleted AS ( - DELETE FROM falukant_data.relationship - WHERE character1_id = $1 - OR character2_id = $1 - RETURNING - CASE - WHEN character1_id = $1 THEN character2_id - ELSE character1_id - END AS related_character_id, - relationship_type_id - ) - SELECT - c.user_id AS related_user_id - FROM deleted d - JOIN falukant_data.character c - ON c.id = d.related_character_id; - "#; - - conn.prepare("delete_relationship", QUERY_DELETE_RELATIONSHIP)?; + 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 @@ -1517,17 +1446,11 @@ impl EventsWorker { // 3) Erben-Logik für Spieler-Charaktere (VOR dem Löschen der Child-Relations!) // Prüfe, ob der Charakter ein Spieler-Charakter ist - const QUERY_GET_USER_ID: &str = r#" - SELECT user_id - FROM falukant_data.character - WHERE id = $1; - "#; - - conn.prepare("get_user_id", QUERY_GET_USER_ID)?; + conn.prepare("get_user_id", QUERY_GET_USER_ID)?; let user_rows = conn.execute("get_user_id", &[&character_id])?; let user_id: Option = user_rows - .get(0) + .first() .and_then(|r| r.get("user_id")) .and_then(|v| v.parse::().ok()); @@ -1538,25 +1461,7 @@ impl EventsWorker { } // 4) Child-Relations löschen und Eltern benachrichtigen - const QUERY_DELETE_CHILD_RELATION: &str = r#" - WITH deleted AS ( - DELETE FROM falukant_data.child_relation - WHERE child_character_id = $1 - RETURNING - father_character_id, - mother_character_id - ) - SELECT - cf.user_id AS father_user_id, - cm.user_id AS mother_user_id - FROM deleted d - JOIN falukant_data.character cf - ON cf.id = d.father_character_id - JOIN falukant_data.character cm - ON cm.id = d.mother_character_id; - "#; - - conn.prepare("delete_child_relation", QUERY_DELETE_CHILD_RELATION)?; + 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 @@ -1574,12 +1479,7 @@ impl EventsWorker { } // 5) Charakter löschen - const QUERY_DELETE_CHARACTER: &str = r#" - DELETE FROM falukant_data.character - WHERE id = $1; - "#; - - conn.prepare("delete_character", QUERY_DELETE_CHARACTER)?; + conn.prepare("delete_character", QUERY_DELETE_CHARACTER)?; conn.execute("delete_character", &[&character_id])?; Ok(()) @@ -1593,21 +1493,11 @@ impl EventsWorker { falukant_user_id: i32, ) -> Result<(), DbError> { // 1) Finde den Erben (bevorzugt is_heir = TRUE) - const QUERY_GET_HEIR: &str = r#" - SELECT child_character_id - FROM falukant_data.child_relation - WHERE father_character_id = $1 - OR mother_character_id = $1 - ORDER BY (is_heir IS TRUE) DESC, - updated_at DESC - LIMIT 1; - "#; - - conn.prepare("get_heir", QUERY_GET_HEIR)?; + conn.prepare("get_heir", QUERY_GET_HEIR)?; let heir_rows = conn.execute("get_heir", &[&deceased_character_id])?; let heir_id: Option = heir_rows - .get(0) + .first() .and_then(|r| r.get("child_character_id")) .and_then(|v| v.parse::().ok()); @@ -1624,105 +1514,56 @@ impl EventsWorker { }; // 2) Setze den Erben als neuen Spieler-Charakter - const QUERY_SET_CHARACTER_USER: &str = r#" - UPDATE falukant_data.character - SET user_id = $1, - updated_at = NOW() - WHERE id = $2; - "#; - - conn.prepare("set_character_user", QUERY_SET_CHARACTER_USER)?; + 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) - const QUERY_GET_CURRENT_MONEY: &str = r#" - SELECT money - FROM falukant_data.falukant_user - WHERE id = $1; - "#; - - const QUERY_GET_HOUSE_VALUE: &str = r#" - SELECT COALESCE(SUM(h.cost), 0) AS sum - FROM falukant_data.user_house AS uh - JOIN falukant_type.house AS h ON uh.house_type_id = h.id - WHERE uh.user_id = $1; - "#; - - const QUERY_GET_SETTLEMENT_VALUE: &str = r#" - SELECT COALESCE(SUM(b.base_cost), 0) AS sum - FROM falukant_data.branch AS br - JOIN falukant_type.branch AS b ON br.branch_type_id = b.id - WHERE br.falukant_user_id = $1; - "#; - - const QUERY_GET_INVENTORY_VALUE: &str = r#" - SELECT COALESCE(SUM(i.quantity * p.sell_cost), 0) AS sum - FROM falukant_data.inventory AS i - JOIN falukant_type.product AS p ON i.product_id = p.id - JOIN falukant_data.stock AS s ON i.stock_id = s.id - JOIN falukant_data.branch AS br ON s.branch_id = br.id - WHERE br.falukant_user_id = $1; - "#; - - const QUERY_GET_CREDIT_DEBT: &str = r#" - SELECT COALESCE(SUM(remaining_amount), 0) AS sum - FROM falukant_data.credit - WHERE falukant_user_id = $1; - "#; - - const QUERY_COUNT_CHILDREN: &str = r#" - SELECT COUNT(*) AS cnt - FROM falukant_data.child_relation - WHERE (father_character_id = $1 OR mother_character_id = $1) - AND child_character_id != $2; - "#; - - 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)?; + 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])? - .get(0) + .first() .and_then(|r| r.get("money")) .and_then(|v| v.parse::().ok()) .unwrap_or(0.0); let houses: f64 = conn .execute("get_house_value", &[&falukant_user_id])? - .get(0) + .first() .and_then(|r| r.get("sum")) .and_then(|v| v.parse::().ok()) .unwrap_or(0.0); let settlements: f64 = conn .execute("get_settlement_value", &[&falukant_user_id])? - .get(0) + .first() .and_then(|r| r.get("sum")) .and_then(|v| v.parse::().ok()) .unwrap_or(0.0); let inventory: f64 = conn .execute("get_inventory_value", &[&falukant_user_id])? - .get(0) + .first() .and_then(|r| r.get("sum")) .and_then(|v| v.parse::().ok()) .unwrap_or(0.0); let debt: f64 = conn .execute("get_credit_debt", &[&falukant_user_id])? - .get(0) + .first() .and_then(|r| r.get("sum")) .and_then(|v| v.parse::().ok()) .unwrap_or(0.0); let child_count: i32 = conn .execute("count_children", &[&deceased_character_id, &heir_id])? - .get(0) + .first() .and_then(|r| r.get("cnt")) .and_then(|v| v.parse::().ok()) .unwrap_or(0); @@ -1791,27 +1632,15 @@ impl EventsWorker { fn apply_storage_damage( conn: &mut DbConnection, - region_id: i32, - stock_type_label: &str, - inventory_damage_min_percent: f64, - inventory_damage_max_percent: f64, - storage_destruction_min_percent: f64, - storage_destruction_max_percent: f64, + params: StorageDamageParams, rng: &mut impl Rng, ) -> Result { - // 1. Finde Stock-Typ-ID basierend auf Label - const QUERY_GET_STOCK_TYPE_ID: &str = r#" - SELECT id - FROM falukant_type.stock - WHERE label_tr = $1 - LIMIT 1; - "#; - - conn.prepare("get_stock_type_id", QUERY_GET_STOCK_TYPE_ID)?; - let stock_type_rows = conn.execute("get_stock_type_id", &[&stock_type_label])?; + // 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 = stock_type_rows - .get(0) + .first() .and_then(|r| r.get("id")) .and_then(|v| v.parse::().ok()); @@ -1820,37 +1649,23 @@ impl EventsWorker { None => { eprintln!( "[EventsWorker] Stock-Typ '{}' nicht gefunden", - stock_type_label + params.stock_type_label ); return Err(DbError::new(format!( - "Stock-Typ '{}' nicht gefunden", - stock_type_label + "Stock-Typ '{}' nicht gefunden", + params.stock_type_label ))); } }; - // 2. Hole alle Stocks dieses Typs in der Region mit ihren Branches - const QUERY_GET_REGION_STOCKS: &str = r#" - SELECT - s.id AS stock_id, - s.quantity AS stock_capacity, - COALESCE(SUM(i.quantity), 0) AS inventory_quantity, - s.branch_id - FROM falukant_data.stock s - JOIN falukant_data.branch b ON s.branch_id = b.id - LEFT JOIN falukant_data.inventory i ON i.stock_id = s.id - WHERE b.region_id = $1 - AND s.stock_type_id = $2 - GROUP BY s.id, s.quantity, s.branch_id; - "#; - - conn.prepare("get_region_stocks", QUERY_GET_REGION_STOCKS)?; - let stock_rows = conn.execute("get_region_stocks", &[®ion_id, &stock_type_id])?; + // 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", - stock_type_label, region_id + params.stock_type_label, params.region_id ); return Ok(StorageDamageInfo { inventory_damage_percent: 0.0, @@ -1862,31 +1677,19 @@ impl EventsWorker { // 3. Berechne Schäden let inventory_damage_percent = - rng.gen_range(inventory_damage_min_percent..=inventory_damage_max_percent); + rng.gen_range(params.inventory_damage_min_percent..=params.inventory_damage_max_percent); let storage_destruction_percent = - rng.gen_range(storage_destruction_min_percent..=storage_destruction_max_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 - const QUERY_GET_INVENTORY_ITEMS: &str = r#" - SELECT - i.id AS inventory_id, - i.quantity AS inventory_quantity, - i.stock_id - FROM falukant_data.inventory i - JOIN falukant_data.stock s ON i.stock_id = s.id - JOIN falukant_data.branch b ON s.branch_id = b.id - WHERE b.region_id = $1 - AND s.stock_type_id = $2; - "#; - - conn.prepare("get_inventory_items", QUERY_GET_INVENTORY_ITEMS)?; - let inventory_rows = conn.execute("get_inventory_items", &[®ion_id, &stock_type_id])?; + // 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(); @@ -1920,12 +1723,6 @@ impl EventsWorker { let new_quantity = (inventory_quantity - damage).max(0); // Reduziere Lagerbestand pro Inventar-Eintrag - const QUERY_REDUCE_INVENTORY: &str = r#" - UPDATE falukant_data.inventory - SET quantity = $1 - WHERE id = $2; - "#; - conn.prepare("reduce_inventory", QUERY_REDUCE_INVENTORY)?; conn.execute("reduce_inventory", &[&new_quantity, &inventory_id])?; @@ -1951,20 +1748,10 @@ impl EventsWorker { for stock_id in &stock_ids_to_destroy { // Lösche zuerst den Lagerbestand - const QUERY_DELETE_INVENTORY: &str = r#" - DELETE FROM falukant_data.inventory - WHERE stock_id = $1; - "#; - conn.prepare("delete_inventory", QUERY_DELETE_INVENTORY)?; conn.execute("delete_inventory", &[stock_id])?; // Lösche dann das Stock selbst - const QUERY_DELETE_STOCK: &str = r#" - DELETE FROM falukant_data.stock - WHERE id = $1; - "#; - conn.prepare("delete_stock", QUERY_DELETE_STOCK)?; conn.execute("delete_stock", &[stock_id])?; @@ -1974,7 +1761,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", &[®ion_id, &stock_type_id])?; + 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 = row @@ -1999,12 +1786,6 @@ impl EventsWorker { // Wenn Gesamt-Lagerbestand > Lageranzahl, reduziere proportional if total_inventory_quantity > stock_capacity && stock_capacity > 0 { // Hole alle Inventar-Einträge für diesen Stock - const QUERY_GET_STOCK_INVENTORY: &str = r#" - SELECT id, quantity - FROM falukant_data.inventory - WHERE stock_id = $1; - "#; - conn.prepare("get_stock_inventory", QUERY_GET_STOCK_INVENTORY)?; let inventory_items = conn.execute("get_stock_inventory", &[&stock_id])?; @@ -2028,12 +1809,6 @@ impl EventsWorker { let new_quantity = (item_quantity as f64 * reduction_factor).round() as i32; - const QUERY_CAP_INVENTORY: &str = r#" - UPDATE falukant_data.inventory - SET quantity = $1 - WHERE id = $2; - "#; - conn.prepare("cap_inventory", QUERY_CAP_INVENTORY)?; conn.execute("cap_inventory", &[&new_quantity, &inventory_id])?; } @@ -2050,27 +1825,15 @@ impl EventsWorker { fn apply_personal_storage_damage( conn: &mut DbConnection, - user_id: i32, - stock_type_label: &str, - inventory_damage_min_percent: f64, - inventory_damage_max_percent: f64, - storage_destruction_min_percent: f64, - storage_destruction_max_percent: f64, + params: PersonalStorageDamageParams, rng: &mut impl Rng, ) -> Result { - // 1. Finde Stock-Typ-ID basierend auf Label - const QUERY_GET_STOCK_TYPE_ID: &str = r#" - SELECT id - FROM falukant_type.stock - WHERE label_tr = $1 - LIMIT 1; - "#; - - conn.prepare("get_stock_type_id_personal", QUERY_GET_STOCK_TYPE_ID)?; - let stock_type_rows = conn.execute("get_stock_type_id_personal", &[&stock_type_label])?; + // 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 = stock_type_rows - .get(0) + .first() .and_then(|r| r.get("id")) .and_then(|v| v.parse::().ok()); @@ -2079,37 +1842,23 @@ impl EventsWorker { None => { eprintln!( "[EventsWorker] Stock-Typ '{}' nicht gefunden", - stock_type_label + params.stock_type_label ); return Err(DbError::new(format!( "Stock-Typ '{}' nicht gefunden", - stock_type_label + params.stock_type_label ))); } }; - // 2. Hole alle Stocks dieses Typs für alle Branches des Spielers - const QUERY_GET_USER_STOCKS: &str = r#" - SELECT - s.id AS stock_id, - s.quantity AS stock_capacity, - COALESCE(SUM(i.quantity), 0) AS inventory_quantity, - s.branch_id - FROM falukant_data.stock s - JOIN falukant_data.branch b ON s.branch_id = b.id - LEFT JOIN falukant_data.inventory i ON i.stock_id = s.id - WHERE b.falukant_user_id = $1 - AND s.stock_type_id = $2 - GROUP BY s.id, s.quantity, s.branch_id; - "#; - - conn.prepare("get_user_stocks", QUERY_GET_USER_STOCKS)?; - let stock_rows = conn.execute("get_user_stocks", &[&user_id, &stock_type_id])?; + // 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", - stock_type_label, user_id + params.stock_type_label, params.user_id ); return Ok(StorageDamageInfo { inventory_damage_percent: 0.0, @@ -2121,31 +1870,19 @@ impl EventsWorker { // 3. Berechne Schäden let inventory_damage_percent = - rng.gen_range(inventory_damage_min_percent..=inventory_damage_max_percent); + rng.gen_range(params.inventory_damage_min_percent..=params.inventory_damage_max_percent); let storage_destruction_percent = - rng.gen_range(storage_destruction_min_percent..=storage_destruction_max_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 - const QUERY_GET_USER_INVENTORY_ITEMS: &str = r#" - SELECT - i.id AS inventory_id, - i.quantity AS inventory_quantity, - i.stock_id - FROM falukant_data.inventory i - JOIN falukant_data.stock s ON i.stock_id = s.id - JOIN falukant_data.branch b ON s.branch_id = b.id - WHERE b.falukant_user_id = $1 - AND s.stock_type_id = $2; - "#; - - conn.prepare("get_user_inventory_items", QUERY_GET_USER_INVENTORY_ITEMS)?; - let inventory_rows = conn.execute("get_user_inventory_items", &[&user_id, &stock_type_id])?; + // 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(); @@ -2179,12 +1916,6 @@ impl EventsWorker { let new_quantity = (inventory_quantity - damage).max(0); // Reduziere Lagerbestand pro Inventar-Eintrag - const QUERY_REDUCE_INVENTORY_PERSONAL: &str = r#" - UPDATE falukant_data.inventory - SET quantity = $1 - WHERE id = $2; - "#; - conn.prepare("reduce_inventory_personal", QUERY_REDUCE_INVENTORY_PERSONAL)?; conn.execute("reduce_inventory_personal", &[&new_quantity, &inventory_id])?; @@ -2197,7 +1928,7 @@ impl EventsWorker { // 5. Zerstöre zufällig ausgewählte Stocks (nur für field und wood) let mut destroyed_stocks = 0; - if stocks_to_destroy > 0 && (stock_type_label == "field" || stock_type_label == "wood") { + 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 = stock_rows .iter() @@ -2210,20 +1941,10 @@ impl EventsWorker { for stock_id in &stock_ids_to_destroy { // Lösche zuerst den Lagerbestand - const QUERY_DELETE_INVENTORY_PERSONAL: &str = r#" - DELETE FROM falukant_data.inventory - WHERE stock_id = $1; - "#; - conn.prepare("delete_inventory_personal", QUERY_DELETE_INVENTORY_PERSONAL)?; conn.execute("delete_inventory_personal", &[stock_id])?; // Lösche dann das Stock selbst - const QUERY_DELETE_STOCK_PERSONAL: &str = r#" - DELETE FROM falukant_data.stock - WHERE id = $1; - "#; - conn.prepare("delete_stock_personal", QUERY_DELETE_STOCK_PERSONAL)?; conn.execute("delete_stock_personal", &[stock_id])?; @@ -2232,7 +1953,7 @@ impl EventsWorker { } // 6. Sicherstelle, dass Lagerbestand <= Lageranzahl für alle verbleibenden Stocks - let remaining_stock_rows = conn.execute("get_user_stocks", &[&user_id, &stock_type_id])?; + 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 = row @@ -2257,12 +1978,6 @@ impl EventsWorker { // Wenn Gesamt-Lagerbestand > Lageranzahl, reduziere proportional if total_inventory_quantity > stock_capacity && stock_capacity > 0 { // Hole alle Inventar-Einträge für diesen Stock - const QUERY_GET_STOCK_INVENTORY_PERSONAL: &str = r#" - SELECT id, quantity - FROM falukant_data.inventory - WHERE stock_id = $1; - "#; - conn.prepare("get_stock_inventory_personal", QUERY_GET_STOCK_INVENTORY_PERSONAL)?; let inventory_items = conn.execute("get_stock_inventory_personal", &[&stock_id])?; @@ -2286,12 +2001,6 @@ impl EventsWorker { let new_quantity = (item_quantity as f64 * reduction_factor).round() as i32; - const QUERY_CAP_INVENTORY_PERSONAL: &str = r#" - UPDATE falukant_data.inventory - SET quantity = $1 - WHERE id = $2; - "#; - conn.prepare("cap_inventory_personal", QUERY_CAP_INVENTORY_PERSONAL)?; conn.execute("cap_inventory_personal", &[&new_quantity, &inventory_id])?; } diff --git a/src/worker/house.rs b/src/worker/house.rs index 9e33bd8..28ec5ab 100644 --- a/src/worker/house.rs +++ b/src/worker/house.rs @@ -5,48 +5,17 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use super::base::{BaseWorker, Worker, WorkerState}; +use crate::worker::sql::{ + QUERY_GET_NEW_HOUSE_DATA, + QUERY_ADD_NEW_BUYABLE_HOUSE, + QUERY_UPDATE_BUYABLE_HOUSE_STATE, + QUERY_UPDATE_USER_HOUSE_STATE, +}; pub struct HouseWorker { base: BaseWorker, } -// SQL-Queries analog zu `houseworker.h` -const QUERY_GET_NEW_HOUSE_DATA: &str = r#" - SELECT - h.id AS house_id - FROM - falukant_type.house AS h - WHERE - random() < 0.0001 - AND label_tr <> 'under_bridge'; -"#; - -const QUERY_ADD_NEW_BUYABLE_HOUSE: &str = r#" - INSERT INTO falukant_data.buyable_house (house_type_id) - VALUES ($1); -"#; - -const QUERY_UPDATE_BUYABLE_HOUSE_STATE: &str = r#" - UPDATE falukant_data.buyable_house - SET roof_condition = ROUND(roof_condition - random() * (3 + 0 * id)), - floor_condition = ROUND(floor_condition - random() * (3 + 0 * id)), - wall_condition = ROUND(wall_condition - random() * (3 + 0 * id)), - window_condition = ROUND(window_condition - random() * (3 + 0 * id)); -"#; - -const QUERY_UPDATE_USER_HOUSE_STATE: &str = r#" - UPDATE falukant_data.user_house - SET roof_condition = ROUND(roof_condition - random() * (3 + 0 * id)), - floor_condition = ROUND(floor_condition - random() * (3 + 0 * id)), - wall_condition = ROUND(wall_condition - random() * (3 + 0 * id)), - window_condition = ROUND(window_condition - random() * (3 + 0 * id)) - WHERE house_type_id NOT IN ( - SELECT id - FROM falukant_type.house h - WHERE h.label_tr = 'under_bridge' - ); -"#; - impl HouseWorker { pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self { Self { diff --git a/src/worker/politics.rs b/src/worker/politics.rs index 21bdc56..7ae9f37 100644 --- a/src/worker/politics.rs +++ b/src/worker/politics.rs @@ -6,6 +6,23 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use super::base::{BaseWorker, Worker, WorkerState}; +use crate::worker::sql::{ + QUERY_COUNT_OFFICES_PER_REGION, + QUERY_FIND_OFFICE_GAPS, + QUERY_SELECT_NEEDED_ELECTIONS, + QUERY_INSERT_CANDIDATES, + QUERY_SELECT_ELECTIONS_NEEDING_CANDIDATES, + QUERY_PROCESS_EXPIRED_AND_FILL, + QUERY_USERS_IN_CITIES_OF_REGIONS, + QUERY_NOTIFY_OFFICE_EXPIRATION, + QUERY_NOTIFY_ELECTION_CREATED, + QUERY_NOTIFY_OFFICE_FILLED, + QUERY_GET_USERS_WITH_EXPIRING_OFFICES, + QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS, + QUERY_GET_USERS_WITH_FILLED_OFFICES, + QUERY_PROCESS_ELECTIONS, + QUERY_TRIM_EXCESS_OFFICES_GLOBAL, +}; pub struct PoliticsWorker { base: BaseWorker, @@ -42,466 +59,6 @@ struct Office { // --- SQL-Konstanten (1:1 aus politics_worker.h übernommen) ------------------ -const QUERY_COUNT_OFFICES_PER_REGION: &str = r#" - WITH - seats_per_region AS ( - SELECT - pot.id AS office_type_id, - rt.id AS region_id, - pot.seats_per_region AS seats_total - FROM falukant_type.political_office_type AS pot - JOIN falukant_type.region AS rt - ON pot.region_type = rt.label_tr - ), - occupied AS ( - SELECT - po.office_type_id, - po.region_id, - COUNT(*) AS occupied_count - FROM falukant_data.political_office AS po - GROUP BY po.office_type_id, po.region_id - ), - combined AS ( - SELECT - spr.region_id, - spr.seats_total AS required_count, - COALESCE(o.occupied_count, 0) AS occupied_count - FROM seats_per_region AS spr - LEFT JOIN occupied AS o - ON spr.office_type_id = o.office_type_id - AND spr.region_id = o.region_id - ) - SELECT - region_id, - SUM(required_count) AS required_count, - SUM(occupied_count) AS occupied_count - FROM combined - GROUP BY region_id; -"#; - -/// Findet alle Kombinationen aus Amtstyp und Region, für die laut -/// `seats_per_region` mehr Sitze existieren sollten, als aktuell in -/// `falukant_data.political_office` belegt sind. Es werden ausschließlich -/// positive Differenzen (Gaps) zurückgegeben – wenn `occupied > required` -/// (z. B. nach Reduktion der Sitzzahl), wird **nichts** gelöscht und die -/// Kombination erscheint hier nicht. -const QUERY_FIND_OFFICE_GAPS: &str = r#" - WITH - seats AS ( - SELECT - pot.id AS office_type_id, - rt.id AS region_id, - pot.seats_per_region AS seats_total - FROM falukant_type.political_office_type AS pot - JOIN falukant_type.region AS rt - ON pot.region_type = rt.label_tr - ), - occupied AS ( - SELECT - po.office_type_id, - po.region_id, - COUNT(*) AS occupied_count - FROM falukant_data.political_office AS po - GROUP BY po.office_type_id, po.region_id - ) - SELECT - s.office_type_id, - s.region_id, - (s.seats_total - COALESCE(o.occupied_count, 0)) AS gaps - FROM seats AS s - LEFT JOIN occupied AS o - ON s.office_type_id = o.office_type_id - AND s.region_id = o.region_id - WHERE (s.seats_total - COALESCE(o.occupied_count, 0)) > 0; -"#; - -const QUERY_SELECT_NEEDED_ELECTIONS: &str = r#" - WITH - target_date AS ( - SELECT NOW()::date AS election_date - ), - expired_today AS ( - DELETE FROM falukant_data.political_office AS po - USING falukant_type.political_office_type AS pot - WHERE po.office_type_id = pot.id - AND (po.created_at + (pot.term_length * INTERVAL '1 day'))::date - = (SELECT election_date FROM target_date) - RETURNING - pot.id AS office_type_id, - po.region_id AS region_id - ), - gaps_per_region AS ( - SELECT - office_type_id, - region_id, - COUNT(*) AS gaps - FROM expired_today - GROUP BY office_type_id, region_id - ), - to_schedule AS ( - SELECT - g.office_type_id, - g.region_id, - g.gaps, - td.election_date - FROM gaps_per_region AS g - CROSS JOIN target_date AS td - WHERE NOT EXISTS ( - SELECT 1 - FROM falukant_data.election AS e - WHERE e.office_type_id = g.office_type_id - AND e.region_id = g.region_id - AND e.date::date = td.election_date - ) - ), - new_elections AS ( - INSERT INTO falukant_data.election - (office_type_id, date, posts_to_fill, created_at, updated_at, region_id) - SELECT - ts.office_type_id, - ts.election_date, - ts.gaps, - NOW(), - NOW(), - ts.region_id - FROM to_schedule AS ts - RETURNING - id AS election_id, - region_id, - posts_to_fill - ) - SELECT - ne.election_id, - ne.region_id, - ne.posts_to_fill - FROM new_elections AS ne - ORDER BY ne.region_id, ne.election_id; -"#; - -const QUERY_INSERT_CANDIDATES: &str = r#" - INSERT INTO falukant_data.candidate - (election_id, character_id, created_at, updated_at) - SELECT - $1 AS election_id, - sub.id AS character_id, - NOW() AS created_at, - NOW() AS updated_at - FROM ( - WITH RECURSIVE region_tree AS ( - SELECT r.id - FROM falukant_data.region AS r - WHERE r.id = $2 - UNION ALL - SELECT r2.id - FROM falukant_data.region AS r2 - JOIN region_tree AS rt - ON r2.parent_id = rt.id - ) - SELECT ch.id - FROM falukant_data.character AS ch - JOIN region_tree AS rt2 - ON ch.region_id = rt2.id - WHERE ch.user_id IS NULL - AND ch.birthdate <= NOW() - INTERVAL '21 days' - AND ch.title_of_nobility IN ( - SELECT id - FROM falukant_type.title - WHERE label_tr != 'noncivil' - ) - ORDER BY RANDOM() - LIMIT ($3 * 2) - ) AS sub(id); -"#; - -/// Wahlen finden, für die noch keine Kandidaten existieren. -/// Das umfasst sowohl frisch eingetragene Wahlen als auch manuell -/// angelegte Wahlen, solange: -/// - das Wahl-Datum heute oder in der Zukunft liegt und -/// - noch kein Eintrag in `falukant_data.candidate` existiert. -const QUERY_SELECT_ELECTIONS_NEEDING_CANDIDATES: &str = r#" - SELECT - e.id AS election_id, - e.region_id AS region_id, - e.posts_to_fill - FROM falukant_data.election AS e - WHERE e.region_id IS NOT NULL - AND e.posts_to_fill > 0 - AND e.date::date >= CURRENT_DATE - AND NOT EXISTS ( - SELECT 1 - FROM falukant_data.candidate AS c - WHERE c.election_id = e.id - ); -"#; - -const QUERY_PROCESS_EXPIRED_AND_FILL: &str = r#" - WITH - expired_offices AS ( - DELETE FROM falukant_data.political_office AS po - USING falukant_type.political_office_type AS pot - WHERE po.office_type_id = pot.id - AND (po.created_at + (pot.term_length * INTERVAL '1 day')) <= NOW() - RETURNING - pot.id AS office_type_id, - po.region_id AS region_id - ), - distinct_types AS ( - SELECT DISTINCT office_type_id, region_id FROM expired_offices - ), - votes_per_candidate AS ( - SELECT - dt.office_type_id, - dt.region_id, - c.character_id, - COUNT(v.id) AS vote_count - FROM distinct_types AS dt - JOIN falukant_data.election AS e - ON e.office_type_id = dt.office_type_id - JOIN falukant_data.vote AS v - ON v.election_id = e.id - JOIN falukant_data.candidate AS c - ON c.election_id = e.id - AND c.id = v.candidate_id - WHERE e.date >= (NOW() - INTERVAL '30 days') - GROUP BY dt.office_type_id, dt.region_id, c.character_id - ), - ranked_winners AS ( - SELECT - vpc.office_type_id, - vpc.region_id, - vpc.character_id, - ROW_NUMBER() OVER ( - PARTITION BY vpc.office_type_id, vpc.region_id - ORDER BY vpc.vote_count DESC - ) AS rn - FROM votes_per_candidate AS vpc - ), - selected_winners AS ( - SELECT - rw.office_type_id, - rw.region_id, - rw.character_id - FROM ranked_winners AS rw - JOIN falukant_type.political_office_type AS pot - ON pot.id = rw.office_type_id - WHERE rw.rn <= pot.seats_per_region - ), - insert_winners AS ( - INSERT INTO falukant_data.political_office - (office_type_id, character_id, created_at, updated_at, region_id) - SELECT - sw.office_type_id, - sw.character_id, - NOW(), - NOW(), - sw.region_id - FROM selected_winners AS sw - RETURNING id AS new_office_id, office_type_id, character_id, region_id - ), - count_inserted AS ( - SELECT - office_type_id, - region_id, - COUNT(*) AS inserted_count - FROM insert_winners - GROUP BY office_type_id, region_id - ), - needed_to_fill AS ( - SELECT - dt.office_type_id, - dt.region_id, - (pot.seats_per_region - COALESCE(ci.inserted_count, 0)) AS gaps - FROM distinct_types AS dt - JOIN falukant_type.political_office_type AS pot - ON pot.id = dt.office_type_id - LEFT JOIN count_inserted AS ci - ON ci.office_type_id = dt.office_type_id - AND ci.region_id = dt.region_id - WHERE (pot.seats_per_region - COALESCE(ci.inserted_count, 0)) > 0 - ), - random_candidates AS ( - SELECT - rtf.office_type_id, - rtf.region_id, - ch.id AS character_id, - ROW_NUMBER() OVER ( - PARTITION BY rtf.office_type_id, rtf.region_id - ORDER BY RANDOM() - ) AS rn - FROM needed_to_fill AS rtf - JOIN falukant_data.character AS ch - ON ch.region_id = rtf.region_id - AND ch.user_id IS NULL - AND ch.birthdate <= NOW() - INTERVAL '21 days' - AND ch.title_of_nobility IN ( - SELECT id FROM falukant_type.title WHERE label_tr != 'noncivil' - ) - AND NOT EXISTS ( - SELECT 1 - FROM falukant_data.political_office AS po2 - JOIN falukant_type.political_office_type AS pot2 - ON pot2.id = po2.office_type_id - WHERE po2.character_id = ch.id - AND (po2.created_at + (pot2.term_length * INTERVAL '1 day')) > - NOW() + INTERVAL '2 days' - ) - ), - insert_random AS ( - INSERT INTO falukant_data.political_office - (office_type_id, character_id, created_at, updated_at, region_id) - SELECT - rc.office_type_id, - rc.character_id, - NOW(), - NOW(), - rc.region_id - FROM random_candidates AS rc - JOIN needed_to_fill AS rtf - ON rtf.office_type_id = rc.office_type_id - AND rtf.region_id = rc.region_id - WHERE rc.rn <= rtf.gaps - RETURNING id AS new_office_id, office_type_id, character_id, region_id - ) - SELECT - new_office_id AS office_id, - office_type_id, - character_id, - region_id - FROM insert_winners - UNION ALL - SELECT - new_office_id AS office_id, - office_type_id, - character_id, - region_id - FROM insert_random; -"#; - -const QUERY_USERS_IN_CITIES_OF_REGIONS: &str = r#" - WITH RECURSIVE region_tree AS ( - SELECT id - FROM falukant_data.region - WHERE id = $1 - UNION ALL - SELECT r2.id - FROM falukant_data.region AS r2 - JOIN region_tree AS rt - ON r2.parent_id = rt.id - ) - SELECT DISTINCT ch.user_id - FROM falukant_data.character AS ch - JOIN region_tree AS rt2 - ON ch.region_id = rt2.id - WHERE ch.user_id IS NOT NULL; -"#; - -const QUERY_NOTIFY_OFFICE_EXPIRATION: &str = r#" - INSERT INTO falukant_log.notification - (user_id, tr, created_at, updated_at) - SELECT - po.character_id, - 'notify_office_expiring', - NOW(), - NOW() - FROM falukant_data.political_office AS po - JOIN falukant_type.political_office_type AS pot - ON po.office_type_id = pot.id - WHERE (po.created_at + (pot.term_length * INTERVAL '1 day')) - BETWEEN (NOW() + INTERVAL '2 days') - AND (NOW() + INTERVAL '2 days' + INTERVAL '1 second'); -"#; - -const QUERY_NOTIFY_ELECTION_CREATED: &str = r#" - INSERT INTO falukant_log.notification - (user_id, tr, created_at, updated_at) - VALUES - ($1, 'notify_election_created', NOW(), NOW()); -"#; - -const QUERY_NOTIFY_OFFICE_FILLED: &str = r#" - INSERT INTO falukant_log.notification - (user_id, tr, created_at, updated_at) - VALUES - ($1, 'notify_office_filled', NOW(), NOW()); -"#; - -const QUERY_GET_USERS_WITH_EXPIRING_OFFICES: &str = r#" - SELECT DISTINCT ch.user_id - FROM falukant_data.political_office AS po - JOIN falukant_type.political_office_type AS pot - ON po.office_type_id = pot.id - JOIN falukant_data.character AS ch - ON po.character_id = ch.id - WHERE ch.user_id IS NOT NULL - AND (po.created_at + (pot.term_length * INTERVAL '1 day')) - BETWEEN (NOW() + INTERVAL '2 days') - AND (NOW() + INTERVAL '2 days' + INTERVAL '1 second'); -"#; - -const QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS: &str = r#" - SELECT DISTINCT ch.user_id - FROM falukant_data.election AS e - JOIN falukant_data.character AS ch - ON ch.region_id = e.region_id - WHERE ch.user_id IS NOT NULL - AND e.date >= NOW() - INTERVAL '1 day'; -"#; - -const QUERY_GET_USERS_WITH_FILLED_OFFICES: &str = r#" - SELECT DISTINCT ch.user_id - FROM falukant_data.political_office AS po - JOIN falukant_data.character AS ch - ON po.character_id = ch.id - WHERE ch.user_id IS NOT NULL - AND po.created_at >= NOW() - INTERVAL '1 minute'; -"#; - -const QUERY_PROCESS_ELECTIONS: &str = r#" - SELECT office_id, office_type_id, character_id, region_id - FROM falukant_data.process_elections(); -"#; - -/// Schneidet für alle Amtstyp/Region-Kombinationen überzählige Einträge in -/// `falukant_data.political_office` ab, so dass höchstens -/// `seats_per_region` Ämter pro Kombination übrig bleiben. -/// -/// Die Auswahl, welche Ämter entfernt werden, erfolgt deterministisch über -/// `created_at DESC`: die **neuesten** Ämter bleiben bevorzugt im Amt, -/// ältere Einträge werden zuerst entfernt. Damit lässt sich das Verhalten -/// später leicht anpassen (z. B. nach bestimmten Prioritäten). -const QUERY_TRIM_EXCESS_OFFICES_GLOBAL: &str = r#" - WITH seats AS ( - SELECT - pot.id AS office_type_id, - rt.id AS region_id, - pot.seats_per_region AS seats_total - FROM falukant_type.political_office_type AS pot - JOIN falukant_type.region AS rt - ON pot.region_type = rt.label_tr - ), - ranked AS ( - SELECT - po.id, - po.office_type_id, - po.region_id, - s.seats_total, - ROW_NUMBER() OVER ( - PARTITION BY po.office_type_id, po.region_id - ORDER BY po.created_at DESC - ) AS rn - FROM falukant_data.political_office AS po - JOIN seats AS s - ON s.office_type_id = po.office_type_id - AND s.region_id = po.region_id - ), - to_delete AS ( - SELECT id - FROM ranked - WHERE rn > seats_total - ) - DELETE FROM falukant_data.political_office - WHERE id IN (SELECT id FROM to_delete); -"#; impl PoliticsWorker { pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self { @@ -542,11 +99,11 @@ impl PoliticsWorker { broker: &MessageBroker, ) -> Result<(), DbError> { // 1) Optional: Positionen evaluieren (aktuell nur Logging/Struktur) - let _ = Self::evaluate_political_positions(pool)?; + Self::evaluate_political_positions(pool)?; // 2) Schema-Änderungen abgleichen: neue / zusätzliche Ämter anlegen, // ohne bestehende Amtsinhaber bei Reduktion zu entfernen. - let _ = Self::sync_offices_with_types(pool)?; + Self::sync_offices_with_types(pool)?; // 3) Ämter, die bald auslaufen, benachrichtigen Self::notify_office_expirations(pool, broker)?; diff --git a/src/worker/produce.rs b/src/worker/produce.rs index 09eb236..ddd5db2 100644 --- a/src/worker/produce.rs +++ b/src/worker/produce.rs @@ -7,6 +7,14 @@ use std::time::{Duration, Instant}; use crate::db::ConnectionPool; use super::base::{BaseWorker, Worker, WorkerState}; +use crate::worker::sql::{ + QUERY_GET_FINISHED_PRODUCTIONS, + QUERY_GET_AVAILABLE_STOCKS, + QUERY_DELETE_PRODUCTION, + QUERY_INSERT_INVENTORY, + QUERY_INSERT_UPDATE_PRODUCTION_LOG, + QUERY_ADD_OVERPRODUCTION_NOTIFICATION, +}; /// Abbildet eine abgeschlossene Produktion aus der Datenbank. #[derive(Debug, Clone)] @@ -28,128 +36,6 @@ struct StockInfo { filled: i32, } -// SQL-Queries analog zur C++-Implementierung -// Wichtig: Pro `production.id` darf hier **genau eine Zeile** zurückkommen. -// Durch die Joins auf Director/Knowledge/Wetter kann es sonst zu Mehrfachzeilen mit -// unterschiedlicher berechneter Qualität kommen. Deshalb wird die Qualität -// über MAX() aggregiert und nach `production_id` gruppiert. -const QUERY_GET_FINISHED_PRODUCTIONS: &str = r#" - SELECT - p.id AS production_id, - p.branch_id, - p.product_id, - p.quantity, - p.start_timestamp, - pr.production_time, - -- Aggregierte Qualitätsbewertung pro Produktion inkl. Wettereinfluss - MAX( - GREATEST( - 0, - LEAST( - 100, - ROUND( - ( - CASE - WHEN k2.id IS NOT NULL - THEN (k.knowledge * 2 + k2.knowledge) / 3 - ELSE k.knowledge - END - )::numeric - + COALESCE(pwe.quality_effect, 0) * 2.5 - ) - ) - )::int - ) AS quality, - br.region_id, - br.falukant_user_id AS user_id - FROM falukant_data.production p - JOIN falukant_type.product pr - ON p.product_id = pr.id - JOIN falukant_data.branch br - ON p.branch_id = br.id - JOIN falukant_data.character c - ON c.user_id = br.falukant_user_id - JOIN falukant_data.knowledge k - ON p.product_id = k.product_id - AND k.character_id = c.id - JOIN falukant_data.stock s - ON s.branch_id = br.id - -- Optionaler Wettereinfluss: pro (Produkt, Wetter) genau ein Datensatz - LEFT JOIN falukant_type.product_weather_effect pwe - ON pwe.product_id = p.product_id - AND pwe.weather_type_id = p.weather_type_id - LEFT JOIN falukant_data.director d - ON d.employer_user_id = c.user_id - LEFT JOIN falukant_data.knowledge k2 - ON k2.character_id = d.director_character_id - AND k2.product_id = p.product_id - WHERE p.start_timestamp + INTERVAL '1 minute' * pr.production_time <= NOW() - GROUP BY - p.id, - p.branch_id, - p.product_id, - p.quantity, - p.start_timestamp, - pr.production_time, - br.region_id, - br.falukant_user_id - ORDER BY p.start_timestamp; -"#; - -const QUERY_GET_AVAILABLE_STOCKS: &str = r#" - SELECT - stock.id, - stock.quantity AS total_capacity, - ( - SELECT COALESCE(SUM(inventory.quantity), 0) - FROM falukant_data.inventory - WHERE inventory.stock_id = stock.id - ) AS filled, - stock.branch_id - FROM falukant_data.stock stock - JOIN falukant_data.branch branch - ON stock.branch_id = branch.id - WHERE branch.id = $1 - ORDER BY total_capacity DESC; -"#; - -const QUERY_DELETE_PRODUCTION: &str = r#" - DELETE FROM falukant_data.production - WHERE id = $1; -"#; - -const QUERY_INSERT_INVENTORY: &str = r#" - INSERT INTO falukant_data.inventory ( - stock_id, - product_id, - quantity, - quality, - produced_at - ) VALUES ($1, $2, $3, $4, NOW()); -"#; - -const QUERY_INSERT_UPDATE_PRODUCTION_LOG: &str = r#" - INSERT INTO falukant_log.production ( - region_id, - product_id, - quantity, - producer_id, - production_date - ) VALUES ($1, $2, $3, $4, CURRENT_DATE) - ON CONFLICT (producer_id, product_id, region_id, production_date) - DO UPDATE - SET quantity = falukant_log.production.quantity + EXCLUDED.quantity; -"#; - -const QUERY_ADD_OVERPRODUCTION_NOTIFICATION: &str = r#" - INSERT INTO falukant_log.notification ( - user_id, - tr, - shown, - created_at, - updated_at - ) VALUES ($1, $2, FALSE, NOW(), NOW()); -"#; pub struct ProduceWorker { base: BaseWorker, diff --git a/src/worker/sql.rs b/src/worker/sql.rs index 9a3a3c4..9a5c827 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -40,6 +40,11 @@ pub const QUERY_INSERT_NOTIFICATION: &str = r#" INSERT INTO falukant_log.notification (user_id, tr, shown, created_at, updated_at) VALUES ($1, $2, FALSE, NOW(), NOW()); "#; +// Product pricing +pub const QUERY_GET_PRODUCT_COST: &str = r#" +SELECT original_sell_cost, sell_cost FROM falukant_type.product WHERE id = $1; +"#; + pub const QUERY_GET_DIRECTORS: &str = r#" SELECT d.may_produce, d.may_sell, d.may_start_transport, b.id AS branch_id, fu.id AS falukantUserId, d.id FROM falukant_data.director d @@ -72,6 +77,93 @@ pub const QUERY_INSERT_PRODUCTION: &str = r#" INSERT INTO falukant_data.production (branch_id, product_id, quantity, weather_type_id) VALUES ($1, $2, $3, (SELECT weather_type_id FROM falukant_data.weather WHERE region_id = $4)); "#; +// Character creation related queries (missing from earlier extraction) +pub const QUERY_IS_PREVIOUS_DAY_CHARACTER_CREATED: &str = r#" + SELECT created_at + FROM falukant_data."character" + WHERE user_id IS NULL + AND created_at::date = CURRENT_DATE + ORDER BY created_at DESC + LIMIT 1; +"#; + +pub const QUERY_GET_TOWN_REGION_IDS: &str = r#" + SELECT fdr.id + FROM falukant_data.region fdr + JOIN falukant_type.region ftr ON fdr.region_type_id = ftr.id + WHERE ftr.label_tr = 'city'; +"#; + +pub const QUERY_LOAD_FIRST_NAMES: &str = r#" + SELECT id, gender + FROM falukant_predefine.firstname; +"#; + +pub const QUERY_LOAD_LAST_NAMES: &str = r#" + SELECT id + FROM falukant_predefine.lastname; +"#; + +pub const QUERY_INSERT_CHARACTER: &str = r#" + INSERT INTO falukant_data.character( + user_id, + region_id, + first_name, + last_name, + birthdate, + gender, + created_at, + updated_at, + title_of_nobility + ) VALUES ( + NULL, + $1, + $2, + $3, + NOW(), + $4, + NOW(), + NOW(), + $5 + ); +"#; + +pub const QUERY_GET_ELIGIBLE_NPC_FOR_DEATH: &str = r#" + WITH aged AS ( + SELECT + c.id, + (current_date - c.birthdate::date) AS age, + c.user_id + FROM + falukant_data.character c + WHERE + c.user_id IS NULL + AND (current_date - c.birthdate::date) > 60 + ), + always_sel AS ( + SELECT * + FROM aged + WHERE age > 85 + ), + random_sel AS ( + SELECT * + FROM aged + WHERE age <= 85 + ORDER BY random() + LIMIT 10 + ) + SELECT * + FROM always_sel + UNION ALL + SELECT * + FROM random_sel; +"#; + +pub const QUERY_MARK_CHARACTER_DECEASED: &str = r#" + DELETE FROM falukant_data.character + WHERE id = $1; +"#; + pub const QUERY_GET_BRANCH_CAPACITY: &str = r#" SELECT (SELECT SUM(quantity) FROM falukant_data.stock fds WHERE fds.branch_id = $1) AS stock_size, COALESCE((SELECT SUM(COALESCE(fdi.quantity, 0)) FROM falukant_data.stock fds JOIN falukant_data.inventory fdi ON fdi.stock_id = fds.id WHERE fds.branch_id = $1), 0) AS used_in_stock, @@ -99,11 +191,100 @@ pub const QUERY_ADD_SELL_LOG: &str = r#" INSERT INTO falukant_log.sell (region_id, product_id, quantity, seller_id) VALUES ($1, $2, $3, $4) ON CONFLICT (region_id, product_id, seller_id) DO UPDATE SET quantity = falukant_log.sell.quantity + EXCLUDED.quantity; "#; +pub const QUERY_GET_ARRIVED_TRANSPORTS: &str = r#" +SELECT + t.id, + t.product_id, + t.size, + t.vehicle_id, + t.source_region_id, + t.target_region_id, + b_target.id AS target_branch_id, + b_source.id AS source_branch_id, + rd.distance AS distance, + v.falukant_user_id AS user_id +FROM falukant_data.transport AS t +JOIN falukant_data.vehicle AS v ON v.id = t.vehicle_id +JOIN falukant_type.vehicle AS vt ON vt.id = v.vehicle_type_id +JOIN falukant_data.region_distance AS rd ON ((rd.source_region_id = t.source_region_id AND rd.target_region_id = t.target_region_id) OR (rd.source_region_id = t.target_region_id AND rd.target_region_id = t.source_region_id)) AND (rd.transport_mode = vt.transport_mode OR rd.transport_mode IS NULL) +LEFT JOIN falukant_data.branch AS b_target ON b_target.region_id = t.target_region_id AND b_target.falukant_user_id = v.falukant_user_id +LEFT JOIN falukant_data.branch AS b_source ON b_source.region_id = t.source_region_id AND b_source.falukant_user_id = v.falukant_user_id +WHERE vt.speed > 0 AND t.created_at + (rd.distance / vt.speed::double precision) * INTERVAL '1 minute' <= NOW(); +"#; + +pub const QUERY_GET_AVAILABLE_STOCKS: &str = r#" +SELECT + stock.id, + stock.quantity AS total_capacity, + ( + SELECT COALESCE(SUM(inventory.quantity), 0) + FROM falukant_data.inventory + WHERE inventory.stock_id = stock.id + ) AS filled +FROM falukant_data.stock stock +JOIN falukant_data.branch branch + ON stock.branch_id = branch.id +WHERE branch.id = $1 +ORDER BY total_capacity DESC; +"#; + +pub const QUERY_INSERT_INVENTORY: &str = r#" +INSERT INTO falukant_data.inventory (stock_id, product_id, quantity, quality, produced_at) VALUES ($1, $2, $3, $4, NOW()); +"#; + +pub const QUERY_UPDATE_VEHICLE_AFTER_TRANSPORT: &str = r#" +UPDATE falukant_data.vehicle SET region_id = $2, condition = GREATEST(0, condition - $3::int), available_from = NOW(), updated_at = NOW() WHERE id = $1; +"#; + +pub const QUERY_DELETE_TRANSPORT: &str = r#" +DELETE FROM falukant_data.transport WHERE id = $1; +"#; + +pub const QUERY_ADD_TRANSPORT_WAITING_NOTIFICATION: &str = r#" +INSERT INTO falukant_log.notification (user_id, tr, shown, created_at, updated_at) VALUES ($1, $2, FALSE, NOW(), NOW()); +"#; + +pub const QUERY_UPDATE_TRANSPORT_SIZE: &str = r#" +UPDATE falukant_data.transport + SET size = $2, + updated_at = NOW() + WHERE id = $1; +"#; pub const QUERY_GET_REGION_WORTH_FOR_PRODUCT: &str = r#" SELECT tpw.region_id, tpw.product_id, tpw.worth_percent FROM falukant_data.town_product_worth tpw JOIN falukant_data.branch b ON b.region_id = tpw.region_id WHERE b.falukant_user_id = $1 AND tpw.product_id = $2; "#; +// Political offices and cumulative tax +pub const QUERY_GET_USER_OFFICES: &str = r#" +SELECT po.id AS office_id, pot.name AS office_name, po.region_id, rt.label_tr AS region_type +FROM falukant_data.political_office po +JOIN falukant_type.political_office_type pot ON pot.id = po.office_type_id +JOIN falukant_data.region r ON r.id = po.region_id +JOIN falukant_type.region_type rt ON rt.id = r.region_type_id +WHERE po.holder_id = $1 AND (po.end_date IS NULL OR po.end_date > NOW()); +"#; + +pub const QUERY_CUMULATIVE_TAX_NO_EXEMPT: &str = r#" +WITH RECURSIVE ancestors AS ( + SELECT id, parent_id, COALESCE(tax_percent,0.0) AS tax_percent FROM falukant_data.region WHERE id = $1 + UNION ALL + SELECT r.id, r.parent_id, COALESCE(r.tax_percent,0.0) FROM falukant_data.region r JOIN ancestors a ON r.id = a.parent_id +) +SELECT COALESCE(SUM(tax_percent),0.0) AS total_percent FROM ancestors; +"#; + +pub const QUERY_CUMULATIVE_TAX_WITH_EXEMPT: &str = r#" +WITH RECURSIVE ancestors AS ( + SELECT r.id, r.parent_id, CASE WHEN rt.label_tr = ANY($2::text[]) THEN 0.0 ELSE COALESCE(r.tax_percent,0.0) END AS tax_percent + FROM falukant_data.region r JOIN falukant_type.region_type rt ON rt.id = r.region_type_id WHERE r.id = $1 + UNION ALL + SELECT r.id, r.parent_id, CASE WHEN rt.label_tr = ANY($2::text[]) THEN 0.0 ELSE COALESCE(r.tax_percent,0.0) END + FROM falukant_data.region r JOIN falukant_type.region_type rt ON rt.id = r.region_type_id JOIN ancestors a ON r.id = a.parent_id +) +SELECT COALESCE(SUM(tax_percent),0.0) AS total_percent FROM ancestors; +"#; + pub const QUERY_GET_TRANSPORT_VEHICLES_FOR_ROUTE: &str = r#" SELECT v.id AS vehicle_id, vt.capacity AS capacity FROM falukant_data.vehicle v @@ -175,5 +356,1360 @@ pub const QUERY_UPDATE_INVENTORY_QTY: &str = r#" 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 +FROM falukant_data.stock s +JOIN falukant_data.branch b ON s.branch_id = b.id +WHERE b.falukant_user_id = $1; +"#; + +pub const QUERY_UPDATE_STOCK_CAPACITY: &str = r#" +UPDATE falukant_data.stock + SET quantity = GREATEST(1, ROUND(quantity * (1 + $1 / 100.0))) + WHERE id = $2; +"#; + +pub const QUERY_GET_REGION_STOCKS: &str = r#" +SELECT s.id AS stock_id, s.quantity AS current_capacity +FROM falukant_data.stock s +JOIN falukant_data.branch b ON s.branch_id = b.id +WHERE b.region_id = $1; +"#; + +pub const QUERY_UPDATE_STOCK_CAPACITY_REGIONAL: &str = r#" +UPDATE falukant_data.stock + SET quantity = GREATEST(1, ROUND(quantity * (1 + $1 / 100.0))) + WHERE id = $2; +"#; + +// Stockage manager specific queries +pub const QUERY_GET_TOWNS: &str = r#" + SELECT fdr.id + FROM falukant_data.region fdr + JOIN falukant_type.region ftr + ON ftr.id = fdr.region_type_id + WHERE ftr.label_tr = 'city'; +"#; + +pub const QUERY_INSERT_STOCK: &str = r#" + INSERT INTO falukant_data.buyable_stock (region_id, stock_type_id, quantity) + SELECT + $1 AS region_id, + s.id AS stock_type_id, + GREATEST(1, ROUND(RANDOM() * 5 * COUNT(br.id))) AS quantity + FROM falukant_data.branch AS br + CROSS JOIN falukant_type.stock AS s + WHERE br.region_id = $1 + GROUP BY s.id + ORDER BY RANDOM() + LIMIT GREATEST( + ROUND(RANDOM() * (SELECT COUNT(id) FROM falukant_type.stock)), + 1 + ); +"#; + +pub const QUERY_CLEANUP_STOCK: &str = r#" + DELETE FROM falukant_data.buyable_stock + WHERE quantity <= 0; +"#; + +pub const QUERY_GET_REGION_USERS: &str = r#" + SELECT c.user_id + FROM falukant_data.character c + WHERE c.region_id = $1 + AND c.user_id IS NOT NULL; +"#; + +pub const QUERY_GET_REGION_HOUSES: &str = r#" +SELECT uh.id AS house_id, uh.roof_condition, uh.floor_condition, uh.wall_condition, uh.window_condition +FROM falukant_data.user_house uh +JOIN falukant_data.character c ON c.user_id = uh.user_id +WHERE c.region_id = $1 + AND uh.house_type_id NOT IN ( + SELECT id FROM falukant_type.house h WHERE h.label_tr = 'under_bridge' + ); +"#; + +// House worker queries +pub const QUERY_GET_NEW_HOUSE_DATA: &str = r#" + SELECT + h.id AS house_id + FROM + falukant_type.house AS h + WHERE + random() < 0.0001 + AND label_tr <> 'under_bridge'; +"#; + +pub const QUERY_ADD_NEW_BUYABLE_HOUSE: &str = r#" + INSERT INTO falukant_data.buyable_house (house_type_id) + VALUES ($1); +"#; + +pub const QUERY_UPDATE_BUYABLE_HOUSE_STATE: &str = r#" + UPDATE falukant_data.buyable_house + SET roof_condition = ROUND(roof_condition - random() * (3 + 0 * id)), + floor_condition = ROUND(floor_condition - random() * (3 + 0 * id)), + wall_condition = ROUND(wall_condition - random() * (3 + 0 * id)), + window_condition = ROUND(window_condition - random() * (3 + 0 * id)); +"#; + +pub const QUERY_UPDATE_USER_HOUSE_STATE: &str = r#" + UPDATE falukant_data.user_house + SET roof_condition = ROUND(roof_condition - random() * (3 + 0 * id)), + floor_condition = ROUND(floor_condition - random() * (3 + 0 * id)), + wall_condition = ROUND(wall_condition - random() * (3 + 0 * id)), + window_condition = ROUND(window_condition - random() * (3 + 0 * id)) + WHERE house_type_id NOT IN ( + SELECT id + FROM falukant_type.house h + WHERE h.label_tr = 'under_bridge' + ); +"#; + +pub const QUERY_UPDATE_HOUSE_QUALITY: &str = r#" +UPDATE falukant_data.user_house + SET roof_condition = GREATEST(0, LEAST(100, roof_condition + $1)), + floor_condition = GREATEST(0, LEAST(100, floor_condition + $1)), + wall_condition = GREATEST(0, LEAST(100, wall_condition + $1)), + window_condition = GREATEST(0, LEAST(100, window_condition + $1)) + WHERE id = $2; +"#; + +pub const QUERY_CHANGE_WEATHER: &str = r#" +UPDATE falukant_data.weather +SET weather_type_id = ( + SELECT id FROM falukant_type.weather ORDER BY RANDOM() LIMIT 1 +) +WHERE region_id = $1; +"#; + +pub const QUERY_GET_RANDOM_CHARACTER: &str = r#" +SELECT id, health +FROM falukant_data."character" +WHERE user_id = $1 AND health > 0 +ORDER BY RANDOM() LIMIT 1; +"#; + +pub const QUERY_UPDATE_HEALTH: &str = r#" +UPDATE falukant_data."character" SET health = $1 WHERE id = $2; +"#; + +pub const QUERY_GET_REGION_CHARACTERS: &str = r#" +SELECT id, health FROM falukant_data."character" WHERE region_id = $1 AND health > 0; +"#; + +pub const QUERY_DELETE_DIRECTOR: &str = r#" +DELETE FROM falukant_data.director WHERE director_character_id = $1 RETURNING employer_user_id; +"#; + +pub const QUERY_DELETE_RELATIONSHIP: &str = r#" +WITH deleted AS ( + DELETE FROM falukant_data.relationship + WHERE character1_id = $1 OR character2_id = $1 + RETURNING CASE WHEN character1_id = $1 THEN character2_id ELSE character1_id END AS related_character_id, relationship_type_id +) +SELECT c.user_id AS related_user_id FROM deleted d JOIN falukant_data.character c ON c.id = d.related_character_id; +"#; + +pub const QUERY_GET_USER_ID: &str = r#" +SELECT user_id FROM falukant_data.character WHERE id = $1; +"#; + +pub const QUERY_DELETE_CHILD_RELATION: &str = r#" +WITH deleted AS ( + DELETE FROM falukant_data.child_relation WHERE child_character_id = $1 RETURNING father_character_id, mother_character_id +) +SELECT cf.user_id AS father_user_id, cm.user_id AS mother_user_id FROM deleted d JOIN falukant_data.character cf ON cf.id = d.father_character_id JOIN falukant_data.character cm ON cm.id = d.mother_character_id; +"#; + +pub const QUERY_DELETE_CHARACTER: &str = r#" +DELETE FROM falukant_data.character WHERE id = $1; +"#; + +pub const QUERY_GET_HEIR: &str = r#" +SELECT child_character_id FROM falukant_data.child_relation WHERE father_character_id = $1 OR mother_character_id = $1 ORDER BY (is_heir IS TRUE) DESC, updated_at DESC LIMIT 1; +"#; + +pub const QUERY_SET_CHARACTER_USER: &str = r#" +UPDATE falukant_data.character SET user_id = $1, updated_at = NOW() WHERE id = $2; +"#; + +pub const QUERY_GET_CURRENT_MONEY: &str = r#" +SELECT money FROM falukant_data.falukant_user WHERE id = $1; +"#; + +pub const QUERY_GET_HOUSE_VALUE: &str = r#" +SELECT COALESCE(SUM(h.cost), 0) AS sum FROM falukant_data.user_house AS uh JOIN falukant_type.house AS h ON uh.house_type_id = h.id WHERE uh.user_id = $1; +"#; + +pub const QUERY_GET_SETTLEMENT_VALUE: &str = r#" +SELECT COALESCE(SUM(b.base_cost), 0) AS sum FROM falukant_data.branch AS br JOIN falukant_type.branch AS b ON br.branch_type_id = b.id WHERE br.falukant_user_id = $1; +"#; + +pub const QUERY_GET_INVENTORY_VALUE: &str = r#" +SELECT COALESCE(SUM(i.quantity * p.sell_cost), 0) AS sum FROM falukant_data.inventory AS i JOIN falukant_type.product AS p ON i.product_id = p.id JOIN falukant_data.stock AS s ON i.stock_id = s.id JOIN falukant_data.branch AS br ON s.branch_id = br.id WHERE br.falukant_user_id = $1; +"#; + +pub const QUERY_GET_CREDIT_DEBT: &str = r#" +SELECT COALESCE(SUM(remaining_amount), 0) AS sum FROM falukant_data.credit WHERE falukant_user_id = $1; +"#; + +pub const QUERY_COUNT_CHILDREN: &str = r#" +SELECT COUNT(*) AS cnt FROM falukant_data.child_relation WHERE (father_character_id = $1 OR mother_character_id = $1) AND child_character_id != $2; +"#; + +// user_character worker queries +pub const QUERY_GET_USERS_TO_UPDATE: &str = r#" + SELECT id, CURRENT_DATE - birthdate::date AS age, health + FROM falukant_data."character" + WHERE user_id IS NOT NULL; +"#; + +// politics worker queries +pub const QUERY_COUNT_OFFICES_PER_REGION: &str = r#" + WITH + seats_per_region AS ( + SELECT + pot.id AS office_type_id, + rt.id AS region_id, + pot.seats_per_region AS seats_total + FROM falukant_type.political_office_type AS pot + JOIN falukant_type.region AS rt + ON pot.region_type = rt.label_tr + ), + occupied AS ( + SELECT + po.office_type_id, + po.region_id, + COUNT(*) AS occupied_count + FROM falukant_data.political_office AS po + GROUP BY po.office_type_id, po.region_id + ), + combined AS ( + SELECT + spr.region_id, + spr.seats_total AS required_count, + COALESCE(o.occupied_count, 0) AS occupied_count + FROM seats_per_region AS spr + LEFT JOIN occupied AS o + ON spr.office_type_id = o.office_type_id + AND spr.region_id = o.region_id + ) + SELECT + region_id, + SUM(required_count) AS required_count, + SUM(occupied_count) AS occupied_count + FROM combined + GROUP BY region_id; +"#; + +pub const QUERY_FIND_OFFICE_GAPS: &str = r#" + WITH + seats AS ( + SELECT + pot.id AS office_type_id, + rt.id AS region_id, + pot.seats_per_region AS seats_total + FROM falukant_type.political_office_type AS pot + JOIN falukant_type.region AS rt + ON pot.region_type = rt.label_tr + ), + occupied AS ( + SELECT + po.office_type_id, + po.region_id, + COUNT(*) AS occupied_count + FROM falukant_data.political_office AS po + GROUP BY po.office_type_id, po.region_id + ) + SELECT + s.office_type_id, + s.region_id, + (s.seats_total - COALESCE(o.occupied_count, 0)) AS gaps + FROM seats AS s + LEFT JOIN occupied AS o + ON s.office_type_id = o.office_type_id + AND s.region_id = o.region_id + WHERE (s.seats_total - COALESCE(o.occupied_count, 0)) > 0; +"#; + +pub const QUERY_SELECT_NEEDED_ELECTIONS: &str = r#" + WITH + target_date AS ( + SELECT NOW()::date AS election_date + ), + expired_today AS ( + DELETE FROM falukant_data.political_office AS po + USING falukant_type.political_office_type AS pot + WHERE po.office_type_id = pot.id + AND (po.created_at + (pot.term_length * INTERVAL '1 day'))::date + = (SELECT election_date FROM target_date) + RETURNING + pot.id AS office_type_id, + po.region_id AS region_id + ), + gaps_per_region AS ( + SELECT + office_type_id, + region_id, + COUNT(*) AS gaps + FROM expired_today + GROUP BY office_type_id, region_id + ), + to_schedule AS ( + SELECT + g.office_type_id, + g.region_id, + g.gaps, + td.election_date + FROM gaps_per_region AS g + CROSS JOIN target_date AS td + WHERE NOT EXISTS ( + SELECT 1 + FROM falukant_data.election AS e + WHERE e.office_type_id = g.office_type_id + AND e.region_id = g.region_id + AND e.date::date = td.election_date + ) + ), + new_elections AS ( + INSERT INTO falukant_data.election + (office_type_id, date, posts_to_fill, created_at, updated_at, region_id) + SELECT + ts.office_type_id, + ts.election_date, + ts.gaps, + NOW(), + NOW(), + ts.region_id + FROM to_schedule AS ts + RETURNING + id AS election_id, + region_id, + posts_to_fill + ) + SELECT + ne.election_id, + ne.region_id, + ne.posts_to_fill + FROM new_elections AS ne + ORDER BY ne.region_id, ne.election_id; +"#; + +pub const QUERY_INSERT_CANDIDATES: &str = r#" + INSERT INTO falukant_data.candidate + (election_id, character_id, created_at, updated_at) + SELECT + $1 AS election_id, + sub.id AS character_id, + NOW() AS created_at, + NOW() AS updated_at + FROM ( + WITH RECURSIVE region_tree AS ( + SELECT r.id + FROM falukant_data.region AS r + WHERE r.id = $2 + UNION ALL + SELECT r2.id + FROM falukant_data.region AS r2 + JOIN region_tree AS rt + ON r2.parent_id = rt.id + ) + SELECT ch.id + FROM falukant_data.character AS ch + JOIN region_tree AS rt2 + ON ch.region_id = rt2.id + WHERE ch.user_id IS NULL + AND ch.birthdate <= NOW() - INTERVAL '21 days' + AND ch.title_of_nobility IN ( + SELECT id + FROM falukant_type.title + WHERE label_tr != 'noncivil' + ) + ORDER BY RANDOM() + LIMIT ($3 * 2) + ) AS sub(id); +"#; + +pub const QUERY_SELECT_ELECTIONS_NEEDING_CANDIDATES: &str = r#" + SELECT + e.id AS election_id, + e.region_id AS region_id, + e.posts_to_fill + FROM falukant_data.election AS e + WHERE e.region_id IS NOT NULL + AND e.posts_to_fill > 0 + AND e.date::date >= CURRENT_DATE + AND NOT EXISTS ( + SELECT 1 + FROM falukant_data.candidate AS c + WHERE c.election_id = e.id + ); +"#; + +pub const QUERY_PROCESS_EXPIRED_AND_FILL: &str = r#" + WITH + expired_offices AS ( + DELETE FROM falukant_data.political_office AS po + USING falukant_type.political_office_type AS pot + WHERE po.office_type_id = pot.id + AND (po.created_at + (pot.term_length * INTERVAL '1 day')) <= NOW() + RETURNING + pot.id AS office_type_id, + po.region_id AS region_id + ), + distinct_types AS ( + SELECT DISTINCT office_type_id, region_id FROM expired_offices + ), + votes_per_candidate AS ( + SELECT + dt.office_type_id, + dt.region_id, + c.character_id, + COUNT(v.id) AS vote_count + FROM distinct_types AS dt + JOIN falukant_data.election AS e + ON e.office_type_id = dt.office_type_id + JOIN falukant_data.vote AS v + ON v.election_id = e.id + JOIN falukant_data.candidate AS c + ON c.election_id = e.id + AND c.id = v.candidate_id + WHERE e.date >= (NOW() - INTERVAL '30 days') + GROUP BY dt.office_type_id, dt.region_id, c.character_id + ), + ranked_winners AS ( + SELECT + vpc.office_type_id, + vpc.region_id, + vpc.character_id, + ROW_NUMBER() OVER ( + PARTITION BY vpc.office_type_id, vpc.region_id + ORDER BY vpc.vote_count DESC + ) AS rn + FROM votes_per_candidate AS vpc + ), + selected_winners AS ( + SELECT + rw.office_type_id, + rw.region_id, + rw.character_id + FROM ranked_winners AS rw + JOIN falukant_type.political_office_type AS pot + ON pot.id = rw.office_type_id + WHERE rw.rn <= pot.seats_per_region + ), + insert_winners AS ( + INSERT INTO falukant_data.political_office + (office_type_id, character_id, created_at, updated_at, region_id) + SELECT + sw.office_type_id, + sw.character_id, + NOW(), + NOW(), + sw.region_id + FROM selected_winners AS sw + RETURNING id AS new_office_id, office_type_id, character_id, region_id + ), + count_inserted AS ( + SELECT + office_type_id, + region_id, + COUNT(*) AS inserted_count + FROM insert_winners + GROUP BY office_type_id, region_id + ), + needed_to_fill AS ( + SELECT + dt.office_type_id, + dt.region_id, + (pot.seats_per_region - COALESCE(ci.inserted_count, 0)) AS gaps + FROM distinct_types AS dt + JOIN falukant_type.political_office_type AS pot + ON pot.id = dt.office_type_id + LEFT JOIN count_inserted AS ci + ON ci.office_type_id = dt.office_type_id + AND ci.region_id = dt.region_id + WHERE (pot.seats_per_region - COALESCE(ci.inserted_count, 0)) > 0 + ), + random_candidates AS ( + SELECT + rtf.office_type_id, + rtf.region_id, + ch.id AS character_id, + ROW_NUMBER() OVER ( + PARTITION BY rtf.office_type_id, rtf.region_id + ORDER BY RANDOM() + ) AS rn + FROM needed_to_fill AS rtf + JOIN falukant_data.character AS ch + ON ch.region_id = rtf.region_id + AND ch.user_id IS NULL + AND ch.birthdate <= NOW() - INTERVAL '21 days' + AND ch.title_of_nobility IN ( + SELECT id FROM falukant_type.title WHERE label_tr != 'noncivil' + ) + AND NOT EXISTS ( + SELECT 1 + FROM falukant_data.political_office AS po2 + JOIN falukant_type.political_office_type AS pot2 + ON pot2.id = po2.office_type_id + WHERE po2.character_id = ch.id + AND (po2.created_at + (pot2.term_length * INTERVAL '1 day')) > + NOW() + INTERVAL '2 days' + ) + ), + insert_random AS ( + INSERT INTO falukant_data.political_office + (office_type_id, character_id, created_at, updated_at, region_id) + SELECT + rc.office_type_id, + rc.character_id, + NOW(), + NOW(), + rc.region_id + FROM random_candidates AS rc + JOIN needed_to_fill AS rtf + ON rtf.office_type_id = rc.office_type_id + AND rtf.region_id = rc.region_id + WHERE rc.rn <= rtf.gaps + RETURNING id AS new_office_id, office_type_id, character_id, region_id + ) + SELECT + new_office_id AS office_id, + office_type_id, + character_id, + region_id + FROM insert_winners + UNION ALL + SELECT + new_office_id AS office_id, + office_type_id, + character_id, + region_id + FROM insert_random; +"#; + +pub const QUERY_USERS_IN_CITIES_OF_REGIONS: &str = r#" + WITH RECURSIVE region_tree AS ( + SELECT id + FROM falukant_data.region + WHERE id = $1 + UNION ALL + SELECT r2.id + FROM falukant_data.region AS r2 + JOIN region_tree AS rt + ON r2.parent_id = rt.id + ) + SELECT DISTINCT ch.user_id + FROM falukant_data.character AS ch + JOIN region_tree AS rt2 + ON ch.region_id = rt2.id + WHERE ch.user_id IS NOT NULL; +"#; + +pub const QUERY_NOTIFY_OFFICE_EXPIRATION: &str = r#" + INSERT INTO falukant_log.notification + (user_id, tr, created_at, updated_at) + SELECT + po.character_id, + 'notify_office_expiring', + NOW(), + NOW() + FROM falukant_data.political_office AS po + JOIN falukant_type.political_office_type AS pot + ON po.office_type_id = pot.id + WHERE (po.created_at + (pot.term_length * INTERVAL '1 day')) + BETWEEN (NOW() + INTERVAL '2 days') + AND (NOW() + INTERVAL '2 days' + INTERVAL '1 second'); +"#; + +pub const QUERY_NOTIFY_ELECTION_CREATED: &str = r#" + INSERT INTO falukant_log.notification + (user_id, tr, created_at, updated_at) + VALUES + ($1, 'notify_election_created', NOW(), NOW()); +"#; + +pub const QUERY_NOTIFY_OFFICE_FILLED: &str = r#" + INSERT INTO falukant_log.notification + (user_id, tr, created_at, updated_at) + VALUES + ($1, 'notify_office_filled', NOW(), NOW()); +"#; + +pub const QUERY_GET_USERS_WITH_EXPIRING_OFFICES: &str = r#" + SELECT DISTINCT ch.user_id + FROM falukant_data.political_office AS po + JOIN falukant_type.political_office_type AS pot + ON po.office_type_id = pot.id + JOIN falukant_data.character AS ch + ON po.character_id = ch.id + WHERE ch.user_id IS NOT NULL + AND (po.created_at + (pot.term_length * INTERVAL '1 day')) + BETWEEN (NOW() + INTERVAL '2 days') + AND (NOW() + INTERVAL '2 days' + INTERVAL '1 second'); +"#; + +pub const QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS: &str = r#" + SELECT DISTINCT ch.user_id + FROM falukant_data.election AS e + JOIN falukant_data.character AS ch + ON ch.region_id = e.region_id + WHERE ch.user_id IS NOT NULL + AND e.date >= NOW() - INTERVAL '1 day'; +"#; + +pub const QUERY_GET_USERS_WITH_FILLED_OFFICES: &str = r#" + SELECT DISTINCT ch.user_id + FROM falukant_data.political_office AS po + JOIN falukant_data.character AS ch + ON po.character_id = ch.id + WHERE ch.user_id IS NOT NULL + AND po.created_at >= NOW() - INTERVAL '1 minute'; +"#; + +pub const QUERY_PROCESS_ELECTIONS: &str = r#" + SELECT office_id, office_type_id, character_id, region_id + FROM falukant_data.process_elections(); +"#; + +pub const QUERY_TRIM_EXCESS_OFFICES_GLOBAL: &str = r#" + WITH seats AS ( + SELECT + pot.id AS office_type_id, + rt.id AS region_id, + pot.seats_per_region AS seats_total + FROM falukant_type.political_office_type AS pot + JOIN falukant_type.region AS rt + ON pot.region_type = rt.label_tr + ), + ranked AS ( + SELECT + po.id, + po.office_type_id, + po.region_id, + s.seats_total, + ROW_NUMBER() OVER ( + PARTITION BY po.office_type_id, po.region_id + ORDER BY po.created_at DESC + ) AS rn + FROM falukant_data.political_office AS po + JOIN seats AS s + ON s.office_type_id = po.office_type_id + AND s.region_id = po.region_id + ), + to_delete AS ( + SELECT id + FROM ranked + WHERE rn > seats_total + ) + DELETE FROM falukant_data.political_office + WHERE id IN (SELECT id FROM to_delete); +"#; + +pub const QUERY_UPDATE_CHARACTERS_HEALTH: &str = r#" + UPDATE falukant_data."character" + SET health = $1 + WHERE id = $2; +"#; + +pub const QUERY_UPDATE_MOOD: &str = r#" + UPDATE falukant_data."character" AS c + SET mood_id = falukant_data.get_random_mood_id() + WHERE c.health > 0 + AND random() < (1.0 / 50.0); +"#; + +pub const QUERY_UPDATE_GET_ITEMS_TO_UPDATE: &str = r#" + SELECT id, product_id, producer_id, quantity + FROM falukant_log.production p + WHERE p.production_timestamp::date < current_date; +"#; + +pub const QUERY_UPDATE_GET_CHARACTER_IDS: &str = r#" + SELECT fu.id AS user_id, + c.id AS character_id, + c2.id AS director_id + FROM falukant_data.falukant_user fu + JOIN falukant_data.character c + ON c.user_id = fu.id + LEFT JOIN falukant_data.director d + ON d.employer_user_id = fu.id + LEFT JOIN falukant_data.character c2 + ON c2.id = d.director_character_id + WHERE fu.id = $1; +"#; + +pub const QUERY_UPDATE_KNOWLEDGE: &str = r#" + UPDATE falukant_data.knowledge + SET knowledge = LEAST(knowledge + $3, 100) + WHERE character_id = $1 + AND product_id = $2; +"#; + +pub const QUERY_DELETE_LOG_ENTRY: &str = r#" + DELETE FROM falukant_log.production + WHERE id = $1; +"#; + +pub const QUERY_GET_OPEN_CREDITS: &str = r#" + SELECT + c.id AS credit_id, + c.amount, + c.remaining_amount, + c.interest_rate, + fu.id AS user_id, + fu.money, + c2.id AS character_id, + dp.created_at AS debitor_prism_start, + dp.created_at::date < current_date AS prism_started_previously + FROM falukant_data.credit c + JOIN falukant_data.falukant_user fu + ON fu.id = c.falukant_user_id + JOIN falukant_data.character c2 + ON c2.user_id = c.falukant_user_id + LEFT JOIN falukant_data.debtors_prism dp + ON dp.character_id = c2.id + WHERE c.remaining_amount > 0 + AND c.updated_at::date < current_date; +"#; + +pub const QUERY_UPDATE_CREDIT: &str = r#" + UPDATE falukant_data.credit c + SET remaining_amount = $1 + WHERE falukant_user_id = $2; +"#; + +pub const QUERY_CLEANUP_CREDITS: &str = r#" + DELETE FROM falukant_data.credit + WHERE remaining_amount <= 0.01; +"#; + +pub const QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM: &str = r#" + INSERT INTO falukant_data.debtors_prism (character_id) + VALUES ($1); +"#; + +pub const QUERY_RANDOM_HEIR: &str = r#" + WITH chosen AS ( + SELECT + cr.id AS relation_id, + cr.child_character_id + FROM + falukant_data.child_relation AS cr + JOIN + falukant_data.character AS ch + ON ch.id = cr.child_character_id + WHERE + (cr.father_character_id = $1 OR cr.mother_character_id = $1) + AND ch.region_id = ( + SELECT region_id + FROM falukant_data.character + WHERE id = $1 + ) + AND ch.birthdate >= NOW() - INTERVAL '10 days' + AND ch.title_of_nobility = ( + SELECT id + FROM falukant_type.title + WHERE label_tr = 'noncivil' + ) + ORDER BY RANDOM() + LIMIT 1 + ) + UPDATE + falukant_data.child_relation AS cr2 + SET + is_heir = TRUE, + updated_at = NOW() + FROM + chosen + WHERE + cr2.id = chosen.relation_id + RETURNING + chosen.child_character_id; +"#; + +pub const QUERY_UPDATE_USER_MONEY: &str = r#" + UPDATE falukant_data.falukant_user + SET money = $1, + updated_at = NOW() + WHERE id = $2; +"#; + +pub const QUERY_GET_FALUKANT_USER_ID: &str = r#" + SELECT user_id + FROM falukant_data.character + WHERE id = $1 + LIMIT 1; +"#; + +pub const QUERY_AUTOBATISM: &str = r#" + UPDATE falukant_data.child_relation + SET name_set = TRUE + WHERE id IN ( + SELECT cr.id + FROM falukant_data.child_relation cr + JOIN falukant_data.character c + ON c.id = cr.child_character_id + WHERE cr.name_set = FALSE + AND c.birthdate < current_date - INTERVAL '5 days' + ); +"#; + +pub const QUERY_GET_PREGNANCY_CANDIDATES: &str = r#" + SELECT + r.character1_id AS father_cid, + r.character2_id AS mother_cid, + c1.title_of_nobility, + c1.last_name, + c1.region_id, + fu1.id AS father_uid, + fu2.id AS mother_uid, + ((CURRENT_DATE - c1.birthdate::date) + + (CURRENT_DATE - c2.birthdate::date)) / 2 AS avg_age_days, + 100.0 / + (1 + EXP( + 0.0647 * ( + ((CURRENT_DATE - c1.birthdate::date) + + (CURRENT_DATE - c2.birthdate::date)) / 2 + ) - 0.0591 + )) AS prob_pct + FROM falukant_data.relationship r + JOIN falukant_type.relationship r2 + ON r2.id = r.relationship_type_id + AND r2.tr = 'married' + JOIN falukant_data.character c1 + ON c1.id = r.character1_id + JOIN falukant_data.character c2 + ON c2.id = r.character2_id + LEFT JOIN falukant_data.falukant_user fu1 + ON fu1.id = c1.user_id + LEFT JOIN falukant_data.falukant_user fu2 + ON fu2.id = c2.user_id + WHERE random() * 100 < ( + 100.0 / + (1 + EXP( + 0.11166347 * ( + ((CURRENT_DATE - c1.birthdate::date) + + (CURRENT_DATE - c2.birthdate::date)) / 2 + ) - 2.638267 + )) + ) / 2; +"#; + +pub const QUERY_INSERT_CHILD: &str = r#" + INSERT INTO falukant_data.character ( + user_id, + region_id, + first_name, + last_name, + birthdate, + gender, + title_of_nobility, + mood_id, + created_at, + updated_at + ) VALUES ( + NULL, + $1::int, + ( + SELECT id + FROM falukant_predefine.firstname + WHERE gender = $2 + ORDER BY RANDOM() + LIMIT 1 + ), + $3::int, + NOW(), + $2::varchar, + $4::int, + ( + SELECT id + FROM falukant_type.mood + ORDER BY RANDOM() + LIMIT 1 + ), + NOW(), + NOW() + ) + RETURNING id AS child_cid; +"#; + +pub const QUERY_INSERT_CHILD_RELATION: &str = r#" + INSERT INTO falukant_data.child_relation ( + father_character_id, + mother_character_id, + child_character_id, + name_set, + created_at, + updated_at + ) + VALUES ( + $1::int, + $2::int, + $3::int, + FALSE, + NOW(), NOW() + ); +"#; + +pub const QUERY_DELETE_KNOWLEDGE: &str = r#" + DELETE FROM falukant_data.knowledge + WHERE character_id = $1; +"#; + +pub const QUERY_DELETE_DEBTORS_PRISM: &str = r#" + DELETE FROM falukant_data.debtors_prism + WHERE character_id = $1; +"#; + +pub const QUERY_DELETE_POLITICAL_OFFICE: &str = r#" + WITH removed AS ( + DELETE FROM falukant_data.political_office + WHERE character_id = $1 + RETURNING office_type_id, region_id + ), + affected AS ( + SELECT DISTINCT office_type_id, region_id + FROM removed + ), + seats AS ( + SELECT + pot.id AS office_type_id, + rt.id AS region_id, + pot.seats_per_region AS seats_total + FROM falukant_type.political_office_type AS pot + JOIN falukant_type.region AS rt + ON pot.region_type = rt.label_tr + JOIN affected AS a + ON a.office_type_id = pot.id + AND a.region_id = rt.id + ), + ranked AS ( + SELECT + po.id, + po.office_type_id, + po.region_id, + s.seats_total, + ROW_NUMBER() OVER ( + PARTITION BY po.office_type_id, po.region_id + ORDER BY po.created_at DESC + ) AS rn + FROM falukant_data.political_office AS po + JOIN seats AS s + ON s.office_type_id = po.office_type_id + AND s.region_id = po.region_id + ), + to_delete AS ( + SELECT id + FROM ranked + WHERE rn > seats_total + ) + DELETE FROM falukant_data.political_office + WHERE id IN (SELECT id FROM to_delete); +"#; + +pub const QUERY_DELETE_ELECTION_CANDIDATE: &str = r#" + DELETE FROM falukant_data.election_candidate + WHERE character_id = $1; +"#; + +pub const QUERY_GET_STOCK_TYPE_ID: &str = r#" +SELECT id FROM falukant_type.stock WHERE label_tr = $1 LIMIT 1; +"#; + +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_REDUCE_INVENTORY: &str = r#" +UPDATE falukant_data.inventory SET quantity = $1 WHERE id = $2; +"#; + +pub const QUERY_DELETE_INVENTORY: &str = r#" +DELETE FROM falukant_data.inventory WHERE stock_id = $1; +"#; + +pub const QUERY_DELETE_STOCK: &str = r#" +DELETE FROM falukant_data.stock WHERE id = $1; +"#; + +pub const QUERY_GET_STOCK_INVENTORY: &str = r#" +SELECT id, quantity FROM falukant_data.inventory WHERE stock_id = $1; +"#; + +pub const QUERY_CAP_INVENTORY: &str = r#" +UPDATE falukant_data.inventory SET quantity = $1 WHERE id = $2; +"#; + +pub const QUERY_GET_USER_INVENTORY_ITEMS: &str = r#" +SELECT i.id AS inventory_id, i.quantity AS inventory_quantity, i.stock_id +FROM falukant_data.inventory i +JOIN falukant_data.stock s ON i.stock_id = s.id +JOIN falukant_data.branch b ON s.branch_id = b.id +WHERE b.falukant_user_id = $1 AND s.stock_type_id = $2; +"#; +// Produce worker queries +pub const QUERY_GET_FINISHED_PRODUCTIONS: &str = r#" + SELECT + p.id AS production_id, + p.branch_id, + p.product_id, + p.quantity, + p.start_timestamp, + pr.production_time, + -- Aggregierte Qualitätsbewertung pro Produktion inkl. Wettereinfluss + MAX( + GREATEST( + 0, + LEAST( + 100, + MAX( + GREATEST( + 0, + LEAST( + 100, + ROUND( + ( + COALESCE(k.knowledge, 0) + COALESCE(k2.knowledge, 0) + )::numeric + + COALESCE(pwe.quality_effect, 0) * 2.5 + ) + ) + )::int + ) AS quality, + JOIN falukant_type.product pr + ON p.product_id = pr.id + JOIN falukant_data.branch br + ON p.branch_id = br.id + JOIN falukant_data.character c + ON c.user_id = br.falukant_user_id + JOIN falukant_data.knowledge k + ON p.product_id = k.product_id + AND k.character_id = c.id + JOIN falukant_data.stock s + ON s.branch_id = br.id + -- Optionaler Wettereinfluss: pro (Produkt, Wetter) genau ein Datensatz + LEFT JOIN falukant_type.product_weather_effect pwe + ON pwe.product_id = p.product_id + AND pwe.weather_type_id = p.weather_type_id + LEFT JOIN falukant_data.director d + ON d.employer_user_id = c.user_id + LEFT JOIN falukant_data.knowledge k2 + ON k2.character_id = d.director_character_id + AND k2.product_id = p.product_id + WHERE p.start_timestamp + INTERVAL '1 minute' * pr.production_time <= NOW() + GROUP BY + p.id, + p.branch_id, + p.product_id, + p.quantity, + p.start_timestamp, + pr.production_time, + br.region_id, + br.falukant_user_id + ORDER BY p.start_timestamp; +"#; + +pub const QUERY_DELETE_PRODUCTION: &str = r#" + DELETE FROM falukant_data.production + WHERE id = $1; +"#; + +pub const QUERY_INSERT_UPDATE_PRODUCTION_LOG: &str = r#" + INSERT INTO falukant_log.production ( + region_id, + product_id, + quantity, + producer_id, + production_date + ) VALUES ($1, $2, $3, $4, CURRENT_DATE) + ON CONFLICT (producer_id, product_id, region_id, production_date) + DO UPDATE + SET quantity = falukant_log.production.quantity + EXCLUDED.quantity; +"#; + +pub const QUERY_ADD_OVERPRODUCTION_NOTIFICATION: &str = r#" + INSERT INTO falukant_log.notification ( + user_id, + tr, + shown, + created_at, + updated_at + ) VALUES ($1, $2, FALSE, NOW(), NOW()); +"#; + +// Aliases for personal variants (keeps original prepared statement names used in events.worker) +pub const QUERY_REDUCE_INVENTORY_PERSONAL: &str = QUERY_REDUCE_INVENTORY; +pub const QUERY_DELETE_INVENTORY_PERSONAL: &str = QUERY_DELETE_INVENTORY; +pub const QUERY_DELETE_STOCK_PERSONAL: &str = QUERY_DELETE_STOCK; +pub const QUERY_GET_STOCK_INVENTORY_PERSONAL: &str = QUERY_GET_STOCK_INVENTORY; +pub const QUERY_CAP_INVENTORY_PERSONAL: &str = QUERY_CAP_INVENTORY; + +// value_recalculation worker queries +pub const QUERY_UPDATE_PRODUCT_KNOWLEDGE_USER: &str = r#" + UPDATE falukant_data.knowledge k + SET knowledge = LEAST(100, k.knowledge + 1) + FROM falukant_data.character c + JOIN falukant_log.production p + ON DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day' + WHERE c.id = k.character_id + AND c.user_id = 18 + AND k.product_id = 10; +"#; + +pub const QUERY_DELETE_OLD_PRODUCTIONS: &str = r#" + DELETE FROM falukant_log.production flp + WHERE DATE(flp.production_timestamp) < CURRENT_DATE; +"#; + +pub const QUERY_GET_PRODUCERS_LAST_DAY: &str = r#" + SELECT p.producer_id + FROM falukant_log.production p + WHERE DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day' + GROUP BY producer_id; +"#; + +pub const QUERY_UPDATE_REGION_SELL_PRICE: &str = r#" + UPDATE falukant_data.town_product_worth tpw + SET worth_percent = + GREATEST( + 0, + LEAST( + CASE + WHEN s.quantity > avg_sells THEN tpw.worth_percent - 1 + WHEN s.quantity < avg_sells THEN tpw.worth_percent + 1 + ELSE tpw.worth_percent + END, + 100 + ) + ) + FROM ( + SELECT region_id, + product_id, + quantity, + (SELECT AVG(quantity) + FROM falukant_log.sell avs + WHERE avs.product_id = s.product_id) AS avg_sells + FROM falukant_log.sell s + WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day' + ) s + WHERE tpw.region_id = s.region_id + AND tpw.product_id = s.product_id; +"#; + +pub const QUERY_DELETE_REGION_SELL_PRICE: &str = r#" + DELETE FROM falukant_log.sell s + WHERE DATE(s.sell_timestamp) < CURRENT_DATE; +"#; + +pub const QUERY_GET_SELL_REGIONS: &str = r#" + SELECT s.region_id + FROM falukant_log.sell s + WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day' + GROUP BY region_id; +"#; + +pub const QUERY_HOURLY_PRICE_RECALCULATION: &str = r#" + WITH city_sales AS ( + SELECT + s.region_id, + s.product_id, + SUM(s.quantity) AS total_sold + FROM falukant_log.sell s + WHERE s.sell_timestamp >= NOW() - INTERVAL '1 hour' + GROUP BY s.region_id, s.product_id + ), + world_avg_sales AS ( + SELECT + product_id, + AVG(total_sold) AS avg_sold + FROM city_sales + GROUP BY product_id + ), + parent_region_sales AS ( + SELECT + r.parent_region_id, + cs.product_id, + AVG(cs.total_sold) AS avg_sold + FROM city_sales cs + JOIN falukant_data.region r ON r.id = cs.region_id + WHERE r.parent_region_id IS NOT NULL + GROUP BY r.parent_region_id, cs.product_id + ), + price_updates_world AS ( + SELECT + cs.region_id, + cs.product_id, + cs.total_sold, + COALESCE(wa.avg_sold, 0) AS world_avg, + tpw.worth_percent AS current_price, + CASE + WHEN cs.total_sold > COALESCE(wa.avg_sold, 0) * 1.05 + THEN tpw.worth_percent * 1.1 + WHEN cs.total_sold < COALESCE(wa.avg_sold, 0) * 0.95 + THEN tpw.worth_percent * 0.9 + ELSE tpw.worth_percent + END AS price_after_world + FROM city_sales cs + JOIN world_avg_sales wa ON wa.product_id = cs.product_id + JOIN falukant_data.town_product_worth tpw + ON tpw.region_id = cs.region_id + AND tpw.product_id = cs.product_id + WHERE cs.total_sold > COALESCE(wa.avg_sold, 0) * 1.05 + OR cs.total_sold < COALESCE(wa.avg_sold, 0) * 0.95 + ), + all_cities_with_prices AS ( + SELECT + cs.region_id, + cs.product_id, + cs.total_sold, + r.parent_region_id, + tpw.worth_percent AS original_price, + COALESCE(puw.price_after_world, tpw.worth_percent) AS price_after_world + FROM city_sales cs + JOIN falukant_data.region r ON r.id = cs.region_id + JOIN falukant_data.town_product_worth tpw + ON tpw.region_id = cs.region_id + AND tpw.product_id = cs.product_id + LEFT JOIN price_updates_world puw + ON puw.region_id = cs.region_id + AND puw.product_id = cs.product_id + ), + price_updates_parent AS ( + SELECT + acwp.region_id, + acwp.product_id, + acwp.total_sold, + acwp.parent_region_id, + COALESCE(prs.avg_sold, 0) AS parent_avg, + acwp.price_after_world AS current_price, + CASE + WHEN acwp.total_sold > COALESCE(prs.avg_sold, 0) * 1.05 + THEN acwp.price_after_world * 1.05 + WHEN acwp.total_sold < COALESCE(prs.avg_sold, 0) * 0.95 + THEN acwp.price_after_world * 0.95 + ELSE acwp.price_after_world + END AS new_price + FROM all_cities_with_prices acwp + LEFT JOIN parent_region_sales prs + ON prs.parent_region_id = acwp.parent_region_id + AND prs.product_id = acwp.product_id + WHERE acwp.parent_region_id IS NOT NULL + AND ( + acwp.total_sold > COALESCE(prs.avg_sold, 0) * 1.05 + OR acwp.total_sold < COALESCE(prs.avg_sold, 0) * 0.95 + ) + ), + final_price_updates AS ( + SELECT + COALESCE(pup.region_id, puw.region_id) AS region_id, + COALESCE(pup.product_id, puw.product_id) AS product_id, + COALESCE(pup.new_price, puw.price_after_world, acwp.original_price) AS final_price + FROM all_cities_with_prices acwp + LEFT JOIN price_updates_world puw + ON puw.region_id = acwp.region_id + AND puw.product_id = acwp.product_id + LEFT JOIN price_updates_parent pup + ON pup.region_id = acwp.region_id + AND pup.product_id = acwp.product_id + WHERE puw.region_id IS NOT NULL + OR pup.region_id IS NOT NULL + ) + UPDATE falukant_data.town_product_worth tpw + SET worth_percent = GREATEST( + 0, + LEAST( + 100, + fpu.final_price + ) + ) + FROM final_price_updates fpu + WHERE tpw.region_id = fpu.region_id + AND tpw.product_id = fpu.product_id; +"#; + +pub const QUERY_SET_MARRIAGES_BY_PARTY: &str = r#" + WITH updated_relations AS ( + UPDATE falukant_data.relationship AS rel + SET relationship_type_id = ( + SELECT id + FROM falukant_type.relationship AS rt + WHERE rt.tr = 'married' + ) + WHERE rel.id IN ( + SELECT rel2.id + FROM falukant_data.party AS p + JOIN falukant_type.party AS pt + ON pt.id = p.party_type_id + AND pt.tr = 'wedding' + JOIN falukant_data.falukant_user AS fu + ON fu.id = p.falukant_user_id + JOIN falukant_data.character AS c + ON c.user_id = fu.id + JOIN falukant_data.relationship AS rel2 + ON rel2.character1_id = c.id + OR rel2.character2_id = c.id + JOIN falukant_type.relationship AS rt2 + ON rt2.id = rel2.relationship_type_id + AND rt2.tr = 'engaged' + WHERE p.created_at <= NOW() - INTERVAL '1 day' + ) + RETURNING character1_id, character2_id + ) + SELECT + c1.user_id AS character1_user, + c2.user_id AS character2_user + FROM updated_relations AS ur + JOIN falukant_data.character AS c1 + ON c1.id = ur.character1_id + JOIN falukant_data.character AS c2 + ON c2.id = ur.character2_id; +"#; + +pub const QUERY_GET_STUDYINGS_TO_EXECUTE: &str = r#" + SELECT + l.id, + l.associated_falukant_user_id, + l.associated_learning_character_id, + l.learn_all_products, + l.learning_recipient_id, + l.product_id, + lr.tr + FROM falukant_data.learning l + JOIN falukant_type.learn_recipient lr + ON lr.id = l.learning_recipient_id + WHERE l.learning_is_executed = FALSE + AND l.created_at + INTERVAL '1 day' < NOW(); +"#; + +pub const QUERY_GET_OWN_CHARACTER_ID: &str = r#" + SELECT id + FROM falukant_data.character c + WHERE c.user_id = $1; +"#; + +pub const QUERY_INCREASE_ONE_PRODUCT_KNOWLEDGE: &str = r#" + UPDATE falukant_data.knowledge k + SET knowledge = LEAST(100, k.knowledge + $1) + WHERE k.character_id = $2 + AND k.product_id = $3; +"#; + +pub const QUERY_INCREASE_ALL_PRODUCTS_KNOWLEDGE: &str = r#" + UPDATE falukant_data.knowledge k + SET knowledge = LEAST(100, k.knowledge + $1) + WHERE k.character_id = $2; +"#; + +pub const QUERY_SET_LEARNING_DONE: &str = r#" + UPDATE falukant_data.learning + SET learning_is_executed = TRUE, + updated_at = NOW() + WHERE id = $1; +"#; + diff --git a/src/worker/stockage_manager.rs b/src/worker/stockage_manager.rs index 7dc646e..882bd49 100644 --- a/src/worker/stockage_manager.rs +++ b/src/worker/stockage_manager.rs @@ -8,49 +8,17 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use super::base::{BaseWorker, Worker, WorkerState}; +use crate::worker::sql::{ + QUERY_GET_TOWNS, + QUERY_INSERT_STOCK, + QUERY_CLEANUP_STOCK, + QUERY_GET_REGION_USERS, +}; pub struct StockageManager { base: BaseWorker, } -// SQL-Queries analog zu `stockagemanager.h` -const QUERY_GET_TOWNS: &str = r#" - SELECT fdr.id - FROM falukant_data.region fdr - JOIN falukant_type.region ftr - ON ftr.id = fdr.region_type_id - WHERE ftr.label_tr = 'city'; -"#; - -const QUERY_INSERT_STOCK: &str = r#" - INSERT INTO falukant_data.buyable_stock (region_id, stock_type_id, quantity) - SELECT - $1 AS region_id, - s.id AS stock_type_id, - GREATEST(1, ROUND(RANDOM() * 5 * COUNT(br.id))) AS quantity - FROM falukant_data.branch AS br - CROSS JOIN falukant_type.stock AS s - WHERE br.region_id = $1 - GROUP BY s.id - ORDER BY RANDOM() - LIMIT GREATEST( - ROUND(RANDOM() * (SELECT COUNT(id) FROM falukant_type.stock)), - 1 - ); -"#; - -const QUERY_CLEANUP_STOCK: &str = r#" - DELETE FROM falukant_data.buyable_stock - WHERE quantity <= 0; -"#; - -const QUERY_GET_REGION_USERS: &str = r#" - SELECT c.user_id - FROM falukant_data.character c - WHERE c.region_id = $1 - AND c.user_id IS NOT NULL; -"#; - impl StockageManager { pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self { Self { diff --git a/src/worker/transport.rs b/src/worker/transport.rs index b787525..e5c0603 100644 --- a/src/worker/transport.rs +++ b/src/worker/transport.rs @@ -6,7 +6,16 @@ use std::sync::Arc; use std::time::Duration; use super::base::{BaseWorker, Worker, WorkerState}; - +use crate::worker::sql::{ + QUERY_GET_ARRIVED_TRANSPORTS, + QUERY_GET_AVAILABLE_STOCKS, + QUERY_INSERT_INVENTORY, + QUERY_UPDATE_VEHICLE_AFTER_TRANSPORT, + QUERY_DELETE_TRANSPORT, + QUERY_ADD_TRANSPORT_WAITING_NOTIFICATION, + QUERY_GET_BRANCH_REGION, + QUERY_UPDATE_TRANSPORT_SIZE, +}; #[derive(Debug, Clone)] struct ArrivedTransport { id: i32, @@ -26,98 +35,6 @@ struct StockInfo { filled: i32, } -// Ermittelt alle Transporte, die gemäß Distanz und Fahrzeuggeschwindigkeit bereits -// angekommen sein sollten. Die Reisezeit wird hier vereinfacht als -// travel_minutes = distance / speed -// interpretiert, d.h. `speed` gibt die Einheiten "Distanz pro Minute" an. -const QUERY_GET_ARRIVED_TRANSPORTS: &str = r#" - SELECT - t.id, - t.product_id, - t.size, - t.vehicle_id, - t.source_region_id, - t.target_region_id, - b_target.id AS target_branch_id, - b_source.id AS source_branch_id, - rd.distance AS distance, - v.falukant_user_id AS user_id - FROM falukant_data.transport AS t - JOIN falukant_data.vehicle AS v - ON v.id = t.vehicle_id - JOIN falukant_type.vehicle AS vt - ON vt.id = v.vehicle_type_id - JOIN falukant_data.region_distance AS rd - ON ((rd.source_region_id = t.source_region_id - AND rd.target_region_id = t.target_region_id) - OR (rd.source_region_id = t.target_region_id - AND rd.target_region_id = t.source_region_id)) - AND (rd.transport_mode = vt.transport_mode OR rd.transport_mode IS NULL) - LEFT JOIN falukant_data.branch AS b_target - ON b_target.region_id = t.target_region_id - AND b_target.falukant_user_id = v.falukant_user_id - LEFT JOIN falukant_data.branch AS b_source - ON b_source.region_id = t.source_region_id - AND b_source.falukant_user_id = v.falukant_user_id - WHERE vt.speed > 0 - AND t.created_at - + (rd.distance / vt.speed::double precision) * INTERVAL '1 minute' - <= NOW(); -"#; - -// Verfügbare Lagerplätze in einem Branch, analog zur Logik im ProduceWorker. -const QUERY_GET_AVAILABLE_STOCKS: &str = r#" - SELECT - stock.id, - stock.quantity AS total_capacity, - ( - SELECT COALESCE(SUM(inventory.quantity), 0) - FROM falukant_data.inventory - WHERE inventory.stock_id = stock.id - ) AS filled - FROM falukant_data.stock stock - JOIN falukant_data.branch branch - ON stock.branch_id = branch.id - WHERE branch.id = $1 - ORDER BY total_capacity DESC; -"#; - -const QUERY_INSERT_INVENTORY: &str = r#" - INSERT INTO falukant_data.inventory ( - stock_id, - product_id, - quantity, - quality, - produced_at - ) VALUES ($1, $2, $3, $4, NOW()); -"#; - -const QUERY_UPDATE_VEHICLE_AFTER_TRANSPORT: &str = r#" - UPDATE falukant_data.vehicle - SET region_id = $2, - condition = GREATEST(0, condition - $3::int), - available_from = NOW(), - updated_at = NOW() - WHERE id = $1; -"#; - -const QUERY_DELETE_TRANSPORT: &str = r#" - DELETE FROM falukant_data.transport - WHERE id = $1; -"#; - -/// Notification-Eintrag, analog zur Overproduction-Notification im ProduceWorker. -/// `tr` wird hier als JSON-String mit Übersetzungs-Key und Werten gespeichert. -const QUERY_ADD_TRANSPORT_WAITING_NOTIFICATION: &str = r#" - INSERT INTO falukant_log.notification ( - user_id, - tr, - shown, - created_at, - updated_at - ) VALUES ($1, $2, FALSE, NOW(), NOW()); -"#; - pub struct TransportWorker { base: BaseWorker, } @@ -313,17 +230,16 @@ impl TransportWorker { } // Nutzer informieren, dass Ware noch im Transportmittel liegt. - if t.user_id > 0 { - if let Err(err) = Self::insert_transport_waiting_notification( - pool, - t.user_id, - product_id, - remaining_quantity, - ) { - eprintln!( - "[TransportWorker] Fehler beim Schreiben der Transport-Waiting-Notification: {err}" - ); - } + if t.user_id > 0 && let Err(err) = Self::insert_transport_waiting_notification( + pool, + t.user_id, + product_id, + remaining_quantity, + ) + { + eprintln!( + "[TransportWorker] Fehler beim Schreiben der Transport-Waiting-Notification: {err}" + ); } } @@ -420,19 +336,12 @@ impl TransportWorker { .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; - // Region des Branches abrufen - const QUERY_GET_BRANCH_REGION: &str = r#" - SELECT region_id - FROM falukant_data.branch - WHERE id = $1 - LIMIT 1; - "#; - - conn.prepare("get_branch_region", QUERY_GET_BRANCH_REGION)?; + // Region des Branches abrufen + conn.prepare("get_branch_region", QUERY_GET_BRANCH_REGION)?; let rows = conn.execute("get_branch_region", &[&target_branch_id])?; let region_id = rows - .get(0) + .first() .and_then(|r| r.get("region_id")) .and_then(|v| v.parse::().ok()) .unwrap_or(-1); @@ -479,14 +388,7 @@ impl TransportWorker { .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; - const QUERY_UPDATE_TRANSPORT_SIZE: &str = r#" - UPDATE falukant_data.transport - SET size = $2, - updated_at = NOW() - WHERE id = $1; - "#; - - conn.prepare("update_transport_size", QUERY_UPDATE_TRANSPORT_SIZE)?; + conn.prepare("update_transport_size", QUERY_UPDATE_TRANSPORT_SIZE)?; conn.execute("update_transport_size", &[&transport_id, &new_size])?; Ok(()) diff --git a/src/worker/underground.rs b/src/worker/underground.rs index dda8e04..869fd3d 100644 --- a/src/worker/underground.rs +++ b/src/worker/underground.rs @@ -190,7 +190,7 @@ impl UndergroundWorker { let task_type = r.get("underground_type").cloned().unwrap_or_default(); let params = r.get("parameters").cloned().unwrap_or_else(|| "{}".into()); - Ok(Self::handle_task(pool, &task_type, performer_id, victim_id, ¶ms)?) + Self::handle_task(pool, &task_type, performer_id, victim_id, ¶ms) } fn handle_task( @@ -348,7 +348,7 @@ impl UndergroundWorker { let rows = conn.execute("ug_select_char_user", &[&character_id])?; Ok(rows - .get(0) + .first() .and_then(|r| r.get("user_id")) .and_then(|v| v.parse::().ok()) .unwrap_or(-1)) @@ -513,10 +513,10 @@ impl UndergroundWorker { let mut out = Vec::new(); for r in rows { - if let Some(t) = r.get("stock_type_id").and_then(|v| v.parse::().ok()) { - if allowed.contains(&t) { - out.push(r.clone()); - } + if let Some(t) = r.get("stock_type_id").and_then(|v| v.parse::().ok()) + && allowed.contains(&t) + { + out.push(r.clone()); } } out diff --git a/src/worker/user_character.rs b/src/worker/user_character.rs index 7f82cf6..c5c76f4 100644 --- a/src/worker/user_character.rs +++ b/src/worker/user_character.rs @@ -8,6 +8,41 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use super::base::{BaseWorker, Worker, WorkerState}; +use crate::worker::sql::{ + QUERY_GET_USERS_TO_UPDATE, + QUERY_UPDATE_CHARACTERS_HEALTH, + QUERY_UPDATE_MOOD, + QUERY_UPDATE_GET_ITEMS_TO_UPDATE, + QUERY_UPDATE_GET_CHARACTER_IDS, + QUERY_UPDATE_KNOWLEDGE, + QUERY_DELETE_LOG_ENTRY, + QUERY_GET_OPEN_CREDITS, + QUERY_UPDATE_CREDIT, + QUERY_CLEANUP_CREDITS, + QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM, + QUERY_GET_CURRENT_MONEY, + QUERY_GET_HOUSE_VALUE, + QUERY_GET_SETTLEMENT_VALUE, + QUERY_GET_INVENTORY_VALUE, + QUERY_GET_CREDIT_DEBT, + QUERY_COUNT_CHILDREN, + QUERY_GET_HEIR, + QUERY_RANDOM_HEIR, + QUERY_SET_CHARACTER_USER, + QUERY_UPDATE_USER_MONEY, + QUERY_GET_FALUKANT_USER_ID, + QUERY_AUTOBATISM, + QUERY_GET_PREGNANCY_CANDIDATES, + QUERY_INSERT_CHILD, + QUERY_INSERT_CHILD_RELATION, + QUERY_DELETE_DIRECTOR, + QUERY_DELETE_RELATIONSHIP, + QUERY_DELETE_CHILD_RELATION, + QUERY_DELETE_KNOWLEDGE, + QUERY_DELETE_DEBTORS_PRISM, + QUERY_DELETE_POLITICAL_OFFICE, + QUERY_DELETE_ELECTION_CANDIDATE, +}; /// Vereinfachtes Abbild eines Characters aus `QUERY_GET_USERS_TO_UPDATE`. #[derive(Debug, Clone)] @@ -26,418 +61,7 @@ pub struct UserCharacterWorker { last_mood_run: Option, } -// SQL-Queries (1:1 aus der C++-Implementierung übernommen, gruppiert nach Themen) -const QUERY_GET_USERS_TO_UPDATE: &str = r#" - SELECT id, CURRENT_DATE - birthdate::date AS age, health - FROM falukant_data."character" - WHERE user_id IS NOT NULL; -"#; - -const QUERY_UPDATE_CHARACTERS_HEALTH: &str = r#" - UPDATE falukant_data."character" - SET health = $1 - WHERE id = $2; -"#; - -// Mood-Update mit zufälliger Auswahl pro Charakter: -// Jeder lebende Charakter hat pro Aufruf (ca. 1x pro Minute) -// eine kleine Chance auf Mood-Wechsel. Die Bedingung `random() < 1.0 / 50.0` -// ergibt im Erwartungswert ca. alle 50 Minuten einen Wechsel, verteilt -// individuell und zufällig. -const QUERY_UPDATE_MOOD: &str = r#" - UPDATE falukant_data."character" AS c - SET mood_id = falukant_data.get_random_mood_id() - WHERE c.health > 0 - AND random() < (1.0 / 50.0); -"#; - -const QUERY_UPDATE_GET_ITEMS_TO_UPDATE: &str = r#" - SELECT id, product_id, producer_id, quantity - FROM falukant_log.production p - WHERE p.production_timestamp::date < current_date; -"#; - -const QUERY_UPDATE_GET_CHARACTER_IDS: &str = r#" - SELECT fu.id AS user_id, - c.id AS character_id, - c2.id AS director_id - FROM falukant_data.falukant_user fu - JOIN falukant_data.character c - ON c.user_id = fu.id - LEFT JOIN falukant_data.director d - ON d.employer_user_id = fu.id - LEFT JOIN falukant_data.character c2 - ON c2.id = d.director_character_id - WHERE fu.id = $1; -"#; - -const QUERY_UPDATE_KNOWLEDGE: &str = r#" - UPDATE falukant_data.knowledge - SET knowledge = LEAST(knowledge + $3, 100) - WHERE character_id = $1 - AND product_id = $2; -"#; - -const QUERY_DELETE_LOG_ENTRY: &str = r#" - DELETE FROM falukant_log.production - WHERE id = $1; -"#; - -// Kredit- und Vermögens-Queries -const QUERY_GET_OPEN_CREDITS: &str = r#" - SELECT - c.id AS credit_id, - c.amount, - c.remaining_amount, - c.interest_rate, - fu.id AS user_id, - fu.money, - c2.id AS character_id, - dp.created_at AS debitor_prism_start, - dp.created_at::date < current_date AS prism_started_previously - FROM falukant_data.credit c - JOIN falukant_data.falukant_user fu - ON fu.id = c.id - JOIN falukant_data.character c2 - ON c2.user_id = c.falukant_user_id - LEFT JOIN falukant_data.debtors_prism dp - ON dp.character_id = c2.id - WHERE c.remaining_amount > 0 - AND c.updated_at::date < current_date; -"#; - -const QUERY_UPDATE_CREDIT: &str = r#" - UPDATE falukant_data.credit c - SET remaining_amount = $1 - WHERE falukant_user_id = $2; -"#; - -const QUERY_CLEANUP_CREDITS: &str = r#" - DELETE FROM falukant_data.credit - WHERE remaining_amount >= 0.01; -"#; - -const QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM: &str = r#" - INSERT INTO falukant_data.debtors_prism (character_id) - VALUES ($1); -"#; - -const QUERY_GET_CURRENT_MONEY: &str = r#" - SELECT COALESCE(money, 0) AS sum - FROM falukant_data.falukant_user - WHERE user_id = $1; -"#; - -const QUERY_HOUSE_VALUE: &str = r#" - SELECT COALESCE(SUM(h.cost), 0) AS sum - FROM falukant_data.user_house AS uh - JOIN falukant_type.house AS h - ON uh.house_type_id = h.id - WHERE uh.user_id = $1; -"#; - -const QUERY_SETTLEMENT_VALUE: &str = r#" - SELECT COALESCE(SUM(b.base_cost), 0) AS sum - FROM falukant_data.branch AS br - JOIN falukant_type.branch AS b - ON br.branch_type_id = b.id - WHERE br.falukant_user_id = $1; -"#; - -const QUERY_INVENTORY_VALUE: &str = r#" - SELECT COALESCE(SUM(i.quantity * p.sell_cost), 0) AS sum - FROM falukant_data.inventory AS i - JOIN falukant_type.product AS p - ON i.product_id = p.id - JOIN falukant_data.branch AS br - ON i.stock_id = br.id - WHERE br.falukant_user_id = $1; -"#; - -const QUERY_CREDIT_DEBT: &str = r#" - SELECT COALESCE(SUM(remaining_amount), 0) AS sum - FROM falukant_data.credit - WHERE falukant_user_id = $1; -"#; - -const QUERY_COUNT_CHILDREN: &str = r#" - SELECT COUNT(*) AS cnt - FROM falukant_data.child_relation - WHERE father_character_id = $1 - OR mother_character_id = $1; -"#; - -// Vererbungs-Queries -const QUERY_GET_HEIR: &str = r#" - SELECT child_character_id - FROM falukant_data.child_relation - WHERE father_character_id = $1 - OR mother_character_id = $1 - ORDER BY (is_heir IS TRUE) DESC, - updated_at DESC - LIMIT 1; -"#; - -const QUERY_RANDOM_HEIR: &str = r#" - WITH chosen AS ( - SELECT - cr.id AS relation_id, - cr.child_character_id - FROM - falukant_data.child_relation AS cr - JOIN - falukant_data.character AS ch - ON ch.id = cr.child_character_id - WHERE - (cr.father_character_id = $1 OR cr.mother_character_id = $1) - AND ch.region_id = ( - SELECT region_id - FROM falukant_data.character - WHERE id = $1 - ) - AND ch.birthdate >= NOW() - INTERVAL '10 days' - AND ch.title_of_nobility = ( - SELECT id - FROM falukant_type.title - WHERE label_tr = 'noncivil' - ) - ORDER BY RANDOM() - LIMIT 1 - ) - UPDATE - falukant_data.child_relation AS cr2 - SET - is_heir = TRUE, - updated_at = NOW() - FROM - chosen - WHERE - cr2.id = chosen.relation_id - RETURNING - chosen.child_character_id; -"#; - -const QUERY_SET_CHARACTER_USER: &str = r#" - UPDATE falukant_data.character - SET user_id = $1, - updated_at = NOW() - WHERE id = $2; -"#; - -const QUERY_UPDATE_USER_MONEY: &str = r#" - UPDATE falukant_data.falukant_user - SET money = $1, - updated_at = NOW() - WHERE user_id = $2; -"#; - -const QUERY_GET_FALUKANT_USER_ID: &str = r#" - SELECT user_id - FROM falukant_data.character - WHERE id = $1 - LIMIT 1; -"#; - -// Schwangerschafts-Queries -const QUERY_AUTOBATISM: &str = r#" - UPDATE falukant_data.child_relation - SET name_set = TRUE - WHERE id IN ( - SELECT cr.id - FROM falukant_data.child_relation cr - JOIN falukant_data.character c - ON c.id = cr.child_character_id - WHERE cr.name_set = FALSE - AND c.birthdate < current_date - INTERVAL '5 days' - ); -"#; - -const QUERY_GET_PREGNANCY_CANDIDATES: &str = r#" - SELECT - r.character1_id AS father_cid, - r.character2_id AS mother_cid, - c1.title_of_nobility, - c1.last_name, - c1.region_id, - fu1.id AS father_uid, - fu2.id AS mother_uid, - ((CURRENT_DATE - c1.birthdate::date) - + (CURRENT_DATE - c2.birthdate::date)) / 2 AS avg_age_days, - 100.0 / - (1 + EXP( - 0.0647 * ( - ((CURRENT_DATE - c1.birthdate::date) - + (CURRENT_DATE - c2.birthdate::date)) / 2 - ) - 0.0591 - )) AS prob_pct - FROM falukant_data.relationship r - JOIN falukant_type.relationship r2 - ON r2.id = r.relationship_type_id - AND r2.tr = 'married' - JOIN falukant_data.character c1 - ON c1.id = r.character1_id - JOIN falukant_data.character c2 - ON c2.id = r.character2_id - LEFT JOIN falukant_data.falukant_user fu1 - ON fu1.id = c1.user_id - LEFT JOIN falukant_data.falukant_user fu2 - ON fu2.id = c2.user_id - WHERE random() * 100 < ( - 100.0 / - (1 + EXP( - 0.11166347 * ( - ((CURRENT_DATE - c1.birthdate::date) - + (CURRENT_DATE - c2.birthdate::date)) / 2 - ) - 2.638267 - )) - ) / 2; - -- Hinweis: Der Divisor `/ 2` halbiert die Wahrscheinlichkeit und ist Teil der - -- ursprünglichen Formel. Wurde vorübergehend entfernt, um die Geburtenrate zu erhöhen, - -- wurde aber wiederhergestellt, um die mathematische Korrektheit der Formel zu gewährleisten. - -- Um die Geburtenrate anzupassen, sollte stattdessen die Formel selbst angepasst werden. -"#; - -const QUERY_INSERT_CHILD: &str = r#" - INSERT INTO falukant_data.character ( - user_id, - region_id, - first_name, - last_name, - birthdate, - gender, - title_of_nobility, - mood_id, - created_at, - updated_at - ) VALUES ( - NULL, - $1::int, - ( - SELECT id - FROM falukant_predefine.firstname - WHERE gender = $2 - ORDER BY RANDOM() - LIMIT 1 - ), - $3::int, - NOW(), - $2::varchar, - $4::int, - ( - SELECT id - FROM falukant_type.mood - ORDER BY RANDOM() - LIMIT 1 - ), - NOW(), - NOW() - ) - RETURNING id AS child_cid; -"#; - -const QUERY_INSERT_CHILD_RELATION: &str = r#" - INSERT INTO falukant_data.child_relation ( - father_character_id, - mother_character_id, - child_character_id, - name_set, - created_at, - updated_at - ) - VALUES ( - $1::int, - $2::int, - $3::int, - FALSE, - NOW(), NOW() - ); -"#; - -// Aufräum-Queries beim Tod eines Characters -const QUERY_DELETE_DIRECTOR: &str = r#" - DELETE FROM falukant_data.director - WHERE director_character_id = $1; -"#; - -const QUERY_DELETE_RELATIONSHIP: &str = r#" - DELETE FROM falukant_data.relationship - WHERE character1_id = $1 - OR character2_id = $1; -"#; - -const QUERY_DELETE_CHILD_RELATION: &str = r#" - DELETE FROM falukant_data.child_relation - WHERE child_character_id = $1 - OR father_character_id = $1 - OR mother_character_id = $1; -"#; - -const QUERY_DELETE_KNOWLEDGE: &str = r#" - DELETE FROM falukant_data.knowledge - WHERE character_id = $1; -"#; - -const QUERY_DELETE_DEBTORS_PRISM: &str = r#" - DELETE FROM falukant_data.debtors_prism - WHERE character_id = $1; -"#; - -/// Löscht alle Ämter eines Charakters und stellt anschließend sicher, dass -/// für die betroffenen Amtstyp/Region-Kombinationen nicht mehr Ämter -/// besetzt sind als durch `seats_per_region` vorgesehen. -/// -/// Die überzähligen Ämter werden deterministisch nach `created_at DESC` -/// gekappt, d. h. neuere Amtsinhaber bleiben bevorzugt im Amt. -const QUERY_DELETE_POLITICAL_OFFICE: &str = r#" - WITH removed AS ( - DELETE FROM falukant_data.political_office - WHERE character_id = $1 - RETURNING office_type_id, region_id - ), - affected AS ( - SELECT DISTINCT office_type_id, region_id - FROM removed - ), - seats AS ( - SELECT - pot.id AS office_type_id, - rt.id AS region_id, - pot.seats_per_region AS seats_total - FROM falukant_type.political_office_type AS pot - JOIN falukant_type.region AS rt - ON pot.region_type = rt.label_tr - JOIN affected AS a - ON a.office_type_id = pot.id - AND a.region_id = rt.id - ), - ranked AS ( - SELECT - po.id, - po.office_type_id, - po.region_id, - s.seats_total, - ROW_NUMBER() OVER ( - PARTITION BY po.office_type_id, po.region_id - ORDER BY po.created_at DESC - ) AS rn - FROM falukant_data.political_office AS po - JOIN seats AS s - ON s.office_type_id = po.office_type_id - AND s.region_id = po.region_id - ), - to_delete AS ( - SELECT id - FROM ranked - WHERE rn > seats_total - ) - DELETE FROM falukant_data.political_office - WHERE id IN (SELECT id FROM to_delete); -"#; - -const QUERY_DELETE_ELECTION_CANDIDATE: &str = r#" - DELETE FROM falukant_data.election_candidate - WHERE character_id = $1; -"#; +// SQL moved to `src/worker/sql.rs` impl UserCharacterWorker { pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self { @@ -470,7 +94,7 @@ impl UserCharacterWorker { } if !state.running_worker.load(Ordering::Relaxed) { - return; + // worker stopping } } @@ -893,8 +517,8 @@ impl UserCharacterWorker { let inserted = conn.execute("insert_child", &[®ion_id, &gender, &last_name, &title_of_nobility])?; - let child_cid = inserted - .get(0) + let child_cid = inserted + .first() .and_then(|r| r.get("child_cid")) .and_then(|v| v.parse::().ok()) .unwrap_or(-1); @@ -1002,7 +626,7 @@ impl UserCharacterWorker { let rows = conn.execute("get_falukant_user_id", &[&character_id])?; Ok(rows - .get(0) + .first() .and_then(|r| r.get("user_id")) .and_then(|v| v.parse::().ok()) .unwrap_or(-1)) @@ -1019,7 +643,7 @@ impl UserCharacterWorker { let rows = conn.execute("get_heir", &[&deceased_character_id])?; Ok(rows - .get(0) + .first() .and_then(|r| r.get("child_character_id")) .and_then(|v| v.parse::().ok()) .unwrap_or(-1)) @@ -1036,7 +660,7 @@ impl UserCharacterWorker { let rows = conn.execute("random_heir", &[&deceased_character_id])?; Ok(rows - .get(0) + .first() .and_then(|r| r.get("child_character_id")) .and_then(|v| v.parse::().ok()) .unwrap_or(-1)) @@ -1084,7 +708,7 @@ impl UserCharacterWorker { let rows = conn.execute("get_current_money", &[&falukant_user_id])?; Ok(rows - .get(0) + .first() .and_then(|r| r.get("sum")) .and_then(|v| v.parse::().ok()) .unwrap_or(0.0)) @@ -1097,11 +721,11 @@ impl UserCharacterWorker { .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; - conn.prepare("house_value", QUERY_HOUSE_VALUE)?; + conn.prepare("house_value", QUERY_GET_HOUSE_VALUE)?; let rows = conn.execute("house_value", &[&falukant_user_id])?; Ok(rows - .get(0) + .first() .and_then(|r| r.get("sum")) .and_then(|v| v.parse::().ok()) .unwrap_or(0.0)) @@ -1114,11 +738,11 @@ impl UserCharacterWorker { .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; - conn.prepare("settlement_value", QUERY_SETTLEMENT_VALUE)?; + conn.prepare("settlement_value", QUERY_GET_SETTLEMENT_VALUE)?; let rows = conn.execute("settlement_value", &[&falukant_user_id])?; Ok(rows - .get(0) + .first() .and_then(|r| r.get("sum")) .and_then(|v| v.parse::().ok()) .unwrap_or(0.0)) @@ -1131,11 +755,11 @@ impl UserCharacterWorker { .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; - conn.prepare("inventory_value", QUERY_INVENTORY_VALUE)?; + conn.prepare("inventory_value", QUERY_GET_INVENTORY_VALUE)?; let rows = conn.execute("inventory_value", &[&falukant_user_id])?; Ok(rows - .get(0) + .first() .and_then(|r| r.get("sum")) .and_then(|v| v.parse::().ok()) .unwrap_or(0.0)) @@ -1148,11 +772,11 @@ impl UserCharacterWorker { .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; - conn.prepare("credit_debt", QUERY_CREDIT_DEBT)?; + conn.prepare("credit_debt", QUERY_GET_CREDIT_DEBT)?; let rows = conn.execute("credit_debt", &[&falukant_user_id])?; Ok(rows - .get(0) + .first() .and_then(|r| r.get("sum")) .and_then(|v| v.parse::().ok()) .unwrap_or(0.0)) @@ -1169,7 +793,7 @@ impl UserCharacterWorker { let rows = conn.execute("count_children", &[&deceased_user_id])?; Ok(rows - .get(0) + .first() .and_then(|r| r.get("cnt")) .and_then(|v| v.parse::().ok()) .unwrap_or(0)) diff --git a/src/worker/value_recalculation.rs b/src/worker/value_recalculation.rs index 76fdfea..3af3990 100644 --- a/src/worker/value_recalculation.rs +++ b/src/worker/value_recalculation.rs @@ -6,291 +6,27 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use super::base::{BaseWorker, Worker, WorkerState}; +use crate::worker::sql::{ + QUERY_UPDATE_PRODUCT_KNOWLEDGE_USER, + QUERY_DELETE_OLD_PRODUCTIONS, + QUERY_GET_PRODUCERS_LAST_DAY, + QUERY_UPDATE_REGION_SELL_PRICE, + QUERY_DELETE_REGION_SELL_PRICE, + QUERY_GET_SELL_REGIONS, + QUERY_HOURLY_PRICE_RECALCULATION, + QUERY_SET_MARRIAGES_BY_PARTY, + QUERY_GET_STUDYINGS_TO_EXECUTE, + QUERY_GET_OWN_CHARACTER_ID, + QUERY_INCREASE_ONE_PRODUCT_KNOWLEDGE, + QUERY_INCREASE_ALL_PRODUCTS_KNOWLEDGE, + QUERY_SET_LEARNING_DONE, +}; pub struct ValueRecalculationWorker { base: BaseWorker, } -// Produktwissen / Produktions-Logs -const QUERY_UPDATE_PRODUCT_KNOWLEDGE_USER: &str = r#" - UPDATE falukant_data.knowledge k - SET knowledge = LEAST(100, k.knowledge + 1) - FROM falukant_data.character c - JOIN falukant_log.production p - ON DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day' - WHERE c.id = k.character_id - AND c.user_id = 18 - AND k.product_id = 10; -"#; -const QUERY_DELETE_OLD_PRODUCTIONS: &str = r#" - DELETE FROM falukant_log.production flp - WHERE DATE(flp.production_timestamp) < CURRENT_DATE; -"#; - -const QUERY_GET_PRODUCERS_LAST_DAY: &str = r#" - SELECT p.producer_id - FROM falukant_log.production p - WHERE DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day' - GROUP BY producer_id; -"#; - -// Regionale Verkaufspreise -const QUERY_UPDATE_REGION_SELL_PRICE: &str = r#" - UPDATE falukant_data.town_product_worth tpw - SET worth_percent = - GREATEST( - 0, - LEAST( - CASE - WHEN s.quantity > avg_sells THEN tpw.worth_percent - 1 - WHEN s.quantity < avg_sells THEN tpw.worth_percent + 1 - ELSE tpw.worth_percent - END, - 100 - ) - ) - FROM ( - SELECT region_id, - product_id, - quantity, - (SELECT AVG(quantity) - FROM falukant_log.sell avs - WHERE avs.product_id = s.product_id) AS avg_sells - FROM falukant_log.sell s - WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day' - ) s - WHERE tpw.region_id = s.region_id - AND tpw.product_id = s.product_id; -"#; - -const QUERY_DELETE_REGION_SELL_PRICE: &str = r#" - DELETE FROM falukant_log.sell s - WHERE DATE(s.sell_timestamp) < CURRENT_DATE; -"#; - -const QUERY_GET_SELL_REGIONS: &str = r#" - SELECT s.region_id - FROM falukant_log.sell s - WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day' - GROUP BY region_id; -"#; - -// Stündliche Preisneuberechnung basierend auf Verkäufen der letzten Stunde -// Zwei Ebenen der Preisberechnung: -// 1. Weltweit: Vergleich Stadt-Verkäufe vs. weltweiter Durchschnitt -// - ±5% Toleranz: Preis bleibt gleich -// - Mehr Verkäufe (>5% über Durchschnitt): Preis +10% -// - Weniger Verkäufe (<5% unter Durchschnitt): Preis -10% -// 2. Parent-Region: Vergleich Stadt-Verkäufe vs. Durchschnitt der parent-region -// - ±5% Toleranz: Preis bleibt gleich -// - Abweichung >±5%: Preis ±5% -const QUERY_HOURLY_PRICE_RECALCULATION: &str = r#" - WITH city_sales AS ( - SELECT - s.region_id, - s.product_id, - SUM(s.quantity) AS total_sold - FROM falukant_log.sell s - WHERE s.sell_timestamp >= NOW() - INTERVAL '1 hour' - GROUP BY s.region_id, s.product_id - ), - world_avg_sales AS ( - SELECT - product_id, - AVG(total_sold) AS avg_sold - FROM city_sales - GROUP BY product_id - ), - parent_region_sales AS ( - SELECT - r.parent_region_id, - cs.product_id, - AVG(cs.total_sold) AS avg_sold - FROM city_sales cs - JOIN falukant_data.region r ON r.id = cs.region_id - WHERE r.parent_region_id IS NOT NULL - GROUP BY r.parent_region_id, cs.product_id - ), - price_updates_world AS ( - SELECT - cs.region_id, - cs.product_id, - cs.total_sold, - COALESCE(wa.avg_sold, 0) AS world_avg, - tpw.worth_percent AS current_price, - CASE - -- Mehr als 5% über dem weltweiten Durchschnitt: 10% teurer - WHEN cs.total_sold > COALESCE(wa.avg_sold, 0) * 1.05 - THEN tpw.worth_percent * 1.1 - -- Weniger als 5% unter dem weltweiten Durchschnitt: 10% billiger - WHEN cs.total_sold < COALESCE(wa.avg_sold, 0) * 0.95 - THEN tpw.worth_percent * 0.9 - -- Innerhalb ±5%: Preis bleibt gleich - ELSE tpw.worth_percent - END AS price_after_world - FROM city_sales cs - JOIN world_avg_sales wa ON wa.product_id = cs.product_id - JOIN falukant_data.town_product_worth tpw - ON tpw.region_id = cs.region_id - AND tpw.product_id = cs.product_id - -- Nur updaten wenn es eine Änderung gibt (außerhalb der ±5% Toleranz) - WHERE cs.total_sold > COALESCE(wa.avg_sold, 0) * 1.05 - OR cs.total_sold < COALESCE(wa.avg_sold, 0) * 0.95 - ), - all_cities_with_prices AS ( - SELECT - cs.region_id, - cs.product_id, - cs.total_sold, - r.parent_region_id, - tpw.worth_percent AS original_price, - COALESCE(puw.price_after_world, tpw.worth_percent) AS price_after_world - FROM city_sales cs - JOIN falukant_data.region r ON r.id = cs.region_id - JOIN falukant_data.town_product_worth tpw - ON tpw.region_id = cs.region_id - AND tpw.product_id = cs.product_id - LEFT JOIN price_updates_world puw - ON puw.region_id = cs.region_id - AND puw.product_id = cs.product_id - ), - price_updates_parent AS ( - SELECT - acwp.region_id, - acwp.product_id, - acwp.total_sold, - acwp.parent_region_id, - COALESCE(prs.avg_sold, 0) AS parent_avg, - acwp.price_after_world AS current_price, - CASE - -- Mehr als 5% über dem parent-region Durchschnitt: 5% teurer - WHEN acwp.total_sold > COALESCE(prs.avg_sold, 0) * 1.05 - THEN acwp.price_after_world * 1.05 - -- Weniger als 5% unter dem parent-region Durchschnitt: 5% billiger - WHEN acwp.total_sold < COALESCE(prs.avg_sold, 0) * 0.95 - THEN acwp.price_after_world * 0.95 - -- Innerhalb ±5%: Preis bleibt gleich (vom world-update) - ELSE acwp.price_after_world - END AS new_price - FROM all_cities_with_prices acwp - LEFT JOIN parent_region_sales prs - ON prs.parent_region_id = acwp.parent_region_id - AND prs.product_id = acwp.product_id - WHERE acwp.parent_region_id IS NOT NULL - AND ( - acwp.total_sold > COALESCE(prs.avg_sold, 0) * 1.05 - OR acwp.total_sold < COALESCE(prs.avg_sold, 0) * 0.95 - ) - ), - final_price_updates AS ( - SELECT - COALESCE(pup.region_id, puw.region_id) AS region_id, - COALESCE(pup.product_id, puw.product_id) AS product_id, - COALESCE(pup.new_price, puw.price_after_world, acwp.original_price) AS final_price - FROM all_cities_with_prices acwp - LEFT JOIN price_updates_world puw - ON puw.region_id = acwp.region_id - AND puw.product_id = acwp.product_id - LEFT JOIN price_updates_parent pup - ON pup.region_id = acwp.region_id - AND pup.product_id = acwp.product_id - WHERE puw.region_id IS NOT NULL - OR pup.region_id IS NOT NULL - ) - UPDATE falukant_data.town_product_worth tpw - SET worth_percent = GREATEST( - 0, - LEAST( - 100, - fpu.final_price - ) - ) - FROM final_price_updates fpu - WHERE tpw.region_id = fpu.region_id - AND tpw.product_id = fpu.product_id; -"#; - -// Ehen / Beziehungen -const QUERY_SET_MARRIAGES_BY_PARTY: &str = r#" - WITH updated_relations AS ( - UPDATE falukant_data.relationship AS rel - SET relationship_type_id = ( - SELECT id - FROM falukant_type.relationship AS rt - WHERE rt.tr = 'married' - ) - WHERE rel.id IN ( - SELECT rel2.id - FROM falukant_data.party AS p - JOIN falukant_type.party AS pt - ON pt.id = p.party_type_id - AND pt.tr = 'wedding' - JOIN falukant_data.falukant_user AS fu - ON fu.id = p.falukant_user_id - JOIN falukant_data.character AS c - ON c.user_id = fu.id - JOIN falukant_data.relationship AS rel2 - ON rel2.character1_id = c.id - OR rel2.character2_id = c.id - JOIN falukant_type.relationship AS rt2 - ON rt2.id = rel2.relationship_type_id - AND rt2.tr = 'engaged' - WHERE p.created_at <= NOW() - INTERVAL '1 day' - ) - RETURNING character1_id, character2_id - ) - SELECT - c1.user_id AS character1_user, - c2.user_id AS character2_user - FROM updated_relations AS ur - JOIN falukant_data.character AS c1 - ON c1.id = ur.character1_id - JOIN falukant_data.character AS c2 - ON c2.id = ur.character2_id; -"#; - -// Lernen / Studium -const QUERY_GET_STUDYINGS_TO_EXECUTE: &str = r#" - SELECT - l.id, - l.associated_falukant_user_id, - l.associated_learning_character_id, - l.learn_all_products, - l.learning_recipient_id, - l.product_id, - lr.tr - FROM falukant_data.learning l - JOIN falukant_type.learn_recipient lr - ON lr.id = l.learning_recipient_id - WHERE l.learning_is_executed = FALSE - AND l.created_at + INTERVAL '1 day' < NOW(); -"#; - -const QUERY_GET_OWN_CHARACTER_ID: &str = r#" - SELECT id - FROM falukant_data.character c - WHERE c.user_id = $1; -"#; - -const QUERY_INCREASE_ONE_PRODUCT_KNOWLEDGE: &str = r#" - UPDATE falukant_data.knowledge k - SET knowledge = LEAST(100, k.knowledge + $1) - WHERE k.character_id = $2 - AND k.product_id = $3; -"#; - -const QUERY_INCREASE_ALL_PRODUCTS_KNOWLEDGE: &str = r#" - UPDATE falukant_data.knowledge k - SET knowledge = LEAST(100, k.knowledge + $1) - WHERE k.character_id = $2; -"#; - -const QUERY_SET_LEARNING_DONE: &str = r#" - UPDATE falukant_data.learning - SET learning_is_executed = TRUE - WHERE id = $1; -"#; impl ValueRecalculationWorker { pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self { @@ -601,7 +337,7 @@ impl ValueRecalculationWorker { let rows = conn.execute("get_own_character_id", &[&falukant_user_id])?; Ok(rows - .get(0) + .first() .and_then(|r| r.get("id")) .and_then(|v| v.parse::().ok())) }