diff --git a/migrations/010_falukant_marriage_started_at.sql b/migrations/010_falukant_marriage_started_at.sql new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/migrations/010_falukant_marriage_started_at.sql @@ -0,0 +1 @@ + diff --git a/migrations/011_falukant_character_planned_pregnancy.sql b/migrations/011_falukant_character_planned_pregnancy.sql new file mode 100644 index 0000000..f109585 --- /dev/null +++ b/migrations/011_falukant_character_planned_pregnancy.sql @@ -0,0 +1,16 @@ +-- Geplante Schwangerschaft auf dem Charakter (Weg A; vgl. backend/sql/add_character_pregnancy.sql). +-- Kann bereits existieren — nur ergänzen, was fehlt. + +ALTER TABLE falukant_data.character + ADD COLUMN IF NOT EXISTS pregnancy_due_at timestamptz NULL, + ADD COLUMN IF NOT EXISTS pregnancy_father_character_id integer NULL + REFERENCES falukant_data.character (id) ON DELETE SET NULL; + +COMMENT ON COLUMN falukant_data.character.pregnancy_due_at IS + 'Erwarteter Geburtstermin (Admin/Spiel); Daemon liefert Geburt wenn fällig.'; +COMMENT ON COLUMN falukant_data.character.pregnancy_father_character_id IS + 'Vater-Charakter für geplante Geburt; NULL = Daemon überspringt bis Policy geklärt.'; + +CREATE INDEX IF NOT EXISTS idx_character_pregnancy_due + ON falukant_data.character (pregnancy_due_at) + WHERE pregnancy_due_at IS NOT NULL; diff --git a/src/worker/sql.rs b/src/worker/sql.rs index 6a56c53..b14eea6 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -1675,6 +1675,7 @@ pub const QUERY_AUTOBATISM: &str = r#" // Migration: `008_falukant_marriage_pregnancy_due.sql`. /// Fällige Geburten (Ehe): `marriage_pregnancy_due_at <= NOW()`. +/// B2: keine Ehe-Geburt, solange die Mutter eine **geplante** Schwangerschaft (`character.pregnancy_due_at`) hat. pub const QUERY_GET_MARRIAGE_BIRTH_DELIVERIES: &str = r#" SELECT r.id AS relationship_id, @@ -1699,7 +1700,8 @@ pub const QUERY_GET_MARRIAGE_BIRTH_DELIVERIES: &str = r#" WHERE r.marriage_pregnancy_due_at IS NOT NULL AND r.marriage_pregnancy_due_at <= NOW() AND ((c1.gender = 'male' AND c2.gender = 'female') - OR (c1.gender = 'female' AND c2.gender = 'male')); + OR (c1.gender = 'female' AND c2.gender = 'male')) + AND c_female.pregnancy_due_at IS NULL; "#; pub const QUERY_CLEAR_MARRIAGE_PREGNANCY_DUE: &str = r#" @@ -1748,6 +1750,7 @@ pub const QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE: &str = r#" WHERE r.marriage_pregnancy_due_at IS NULL AND ((c1.gender = 'male' AND c2.gender = 'female') OR (c1.gender = 'female' AND c2.gender = 'male')) + AND c_female.pregnancy_due_at IS NULL ), mother_age AS ( SELECT @@ -1858,8 +1861,9 @@ pub const QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES: &str = r#" ) 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 (c1.gender = 'male' AND c2.gender = 'female') - OR (c1.gender = 'female' AND c2.gender = 'male') + WHERE ((c1.gender = 'male' AND c2.gender = 'female') + OR (c1.gender = 'female' AND c2.gender = 'male')) + AND c_female.pregnancy_due_at IS NULL ), mother_age AS ( SELECT @@ -1934,6 +1938,83 @@ pub const QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES: &str = r#" AND random() < prob_year; "#; +/// Spalten `pregnancy_due_at` / `pregnancy_father_character_id` auf `character` (Migration `011_…`). +pub const QUERY_CHARACTER_PLANNED_PREGNANCY_COLUMNS_READY: &str = r#" + SELECT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'falukant_data' + AND table_name = 'character' + AND column_name = 'pregnancy_due_at' + ) AS ready; +"#; + +/// Fällige **geplante** Geburten (Weg A): Mutter trägt Termin + Vater; kein Ehe-`relationship`-Bezug. +/// Vater NULL oder = Mutter: keine Zeile (Daemon überspringt; optional im Log). +pub const QUERY_GET_PLANNED_CHARACTER_BIRTH_DELIVERIES: &str = r#" + SELECT + c_m.id AS mother_cid, + c_f.id AS father_cid, + CASE WHEN c_f.gender = 'male' THEN c_f.title_of_nobility ELSE c_m.title_of_nobility END AS title_of_nobility, + CASE WHEN c_f.gender = 'male' THEN c_f.last_name ELSE c_m.last_name END AS last_name, + CASE WHEN c_f.gender = 'male' THEN c_f.region_id ELSE c_m.region_id END AS region_id, + fu_f.id AS father_uid, + fu_m.id AS mother_uid, + CASE + WHEN EXISTS ( + SELECT 1 + FROM falukant_data.relationship r + JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id AND rt.tr = 'lover' + WHERE (r.character1_id = c_f.id AND r.character2_id = c_m.id) + OR (r.character1_id = c_m.id AND r.character2_id = c_f.id) + ) THEN 'lover' + ELSE 'marriage' + END AS birth_context + FROM falukant_data.character c_m + JOIN falukant_data.character c_f ON c_f.id = c_m.pregnancy_father_character_id + LEFT JOIN falukant_data.falukant_user fu_m ON fu_m.id = c_m.user_id + LEFT JOIN falukant_data.falukant_user fu_f ON fu_f.id = c_f.user_id + WHERE c_m.pregnancy_due_at IS NOT NULL + AND c_m.pregnancy_due_at <= NOW() + AND c_m.pregnancy_father_character_id IS NOT NULL + AND c_m.pregnancy_father_character_id <> c_m.id + AND c_m.health > 0; +"#; + +pub const QUERY_CLEAR_CHARACTER_PREGNANCY_AFTER_BIRTH: &str = r#" + UPDATE falukant_data.character + SET pregnancy_due_at = NULL, + pregnancy_father_character_id = NULL, + updated_at = NOW() + WHERE id = $1::int; +"#; + +/// Wie Node-Admin: `birth_context` / `legitimacy` / `public_known` je nach Liebschaft vs. Ehe. +pub const QUERY_INSERT_CHILD_RELATION_PLANNED_BIRTH: &str = r#" + INSERT INTO falukant_data.child_relation ( + father_character_id, + mother_character_id, + child_character_id, + name_set, + legitimacy, + birth_context, + public_known, + created_at, + updated_at + ) + VALUES ( + $1::int, + $2::int, + $3::int, + FALSE, + CASE WHEN $4::text = 'lover' THEN 'hidden_bastard'::varchar ELSE 'legitimate'::varchar END, + CASE WHEN $4::text = 'lover' THEN 'lover'::varchar ELSE 'marriage'::varchar END, + CASE WHEN $4::text = 'lover' THEN FALSE ELSE TRUE END, + NOW(), + NOW() + ); +"#; + pub const QUERY_INSERT_CHILD: &str = r#" INSERT INTO falukant_data.character ( user_id, diff --git a/src/worker/user_character.rs b/src/worker/user_character.rs index 69490eb..b1056a4 100644 --- a/src/worker/user_character.rs +++ b/src/worker/user_character.rs @@ -38,8 +38,12 @@ use crate::worker::sql::{ QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES, QUERY_GET_MARRIAGE_BIRTH_DELIVERIES, QUERY_MARRIAGE_PREGNANCY_COLUMN_READY, QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE, + QUERY_CHARACTER_PLANNED_PREGNANCY_COLUMNS_READY, + QUERY_CLEAR_CHARACTER_PREGNANCY_AFTER_BIRTH, + QUERY_GET_PLANNED_CHARACTER_BIRTH_DELIVERIES, QUERY_INSERT_CHILD, QUERY_INSERT_CHILD_RELATION, + QUERY_INSERT_CHILD_RELATION_PLANNED_BIRTH, QUERY_INSERT_NOTIFICATION, QUERY_DELETE_DIRECTOR, QUERY_DELETE_RELATIONSHIP, @@ -69,6 +73,8 @@ pub struct UserCharacterWorker { last_marriage_fertility_date: Option, /// `None` = noch nicht geprüft, ob Migration 008 (`marriage_pregnancy_due_at`) existiert. marriage_pregnancy_column_ready: Option, + /// Migration `011` — `character.pregnancy_due_at` / geplante Geburt (Weg A). + character_planned_pregnancy_columns_ready: Option, last_mood_run: Option, last_death_check_run: Option, } @@ -89,6 +95,7 @@ impl UserCharacterWorker { last_pregnancy_run: None, last_marriage_fertility_date: None, marriage_pregnancy_column_ready: None, + character_planned_pregnancy_columns_ready: None, last_mood_run: None, last_death_check_run: None, } @@ -550,7 +557,7 @@ impl UserCharacterWorker { // Schwangerschafts-Logik (portiert aus processPregnancies) fn process_pregnancies(&mut self, hourly: bool, daily_fertility: bool) -> Result<(), DbError> { - self.ensure_marriage_pregnancy_schema()?; + self.ensure_falukant_pregnancy_schema()?; let mut conn = self .base @@ -560,13 +567,26 @@ impl UserCharacterWorker { conn.prepare("insert_child", QUERY_INSERT_CHILD)?; conn.prepare("insert_child_relation", QUERY_INSERT_CHILD_RELATION)?; + conn.prepare("insert_child_rel_planned", QUERY_INSERT_CHILD_RELATION_PLANNED_BIRTH)?; + conn.prepare("clear_char_preg", QUERY_CLEAR_CHARACTER_PREGNANCY_AFTER_BIRTH)?; let use_gestation = self.marriage_pregnancy_column_ready.unwrap_or(false); + let planned_pregnancy_ready = self.character_planned_pregnancy_columns_ready.unwrap_or(false); if hourly { conn.prepare("autobatism", QUERY_AUTOBATISM)?; conn.execute("autobatism", &[])?; + if planned_pregnancy_ready { + conn.prepare("get_planned_births", QUERY_GET_PLANNED_CHARACTER_BIRTH_DELIVERIES)?; + let planned_rows = conn.execute("get_planned_births", &[])?; + for row in planned_rows { + if let Err(e) = self.process_single_planned_character_birth(&mut conn, &row) { + eprintln!("[UserCharacterWorker] geplante Geburt: {e}"); + } + } + } + if use_gestation { conn.prepare("get_deliveries", QUERY_GET_MARRIAGE_BIRTH_DELIVERIES)?; conn.prepare("clear_preg", QUERY_CLEAR_MARRIAGE_PREGNANCY_DUE)?; @@ -597,8 +617,10 @@ impl UserCharacterWorker { Ok(()) } - fn ensure_marriage_pregnancy_schema(&mut self) -> Result<(), DbError> { - if self.marriage_pregnancy_column_ready.is_some() { + fn ensure_falukant_pregnancy_schema(&mut self) -> Result<(), DbError> { + if self.marriage_pregnancy_column_ready.is_some() + && self.character_planned_pregnancy_columns_ready.is_some() + { return Ok(()); } let mut conn = self @@ -606,15 +628,89 @@ impl UserCharacterWorker { .pool .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; - conn.prepare("mp_ready", QUERY_MARRIAGE_PREGNANCY_COLUMN_READY)?; - let ready_rows = conn.execute("mp_ready", &[])?; - self.marriage_pregnancy_column_ready = Some( - ready_rows - .first() - .and_then(|r| r.get("ready")) - .map(|v| v == "true" || v == "t") - .unwrap_or(false), - ); + + if self.marriage_pregnancy_column_ready.is_none() { + conn.prepare("mp_ready", QUERY_MARRIAGE_PREGNANCY_COLUMN_READY)?; + let ready_rows = conn.execute("mp_ready", &[])?; + self.marriage_pregnancy_column_ready = Some( + ready_rows + .first() + .and_then(|r| r.get("ready")) + .map(|v| v == "true" || v == "t") + .unwrap_or(false), + ); + } + + if self.character_planned_pregnancy_columns_ready.is_none() { + conn.prepare("cpp_ready", QUERY_CHARACTER_PLANNED_PREGNANCY_COLUMNS_READY)?; + let ready_rows = conn.execute("cpp_ready", &[])?; + self.character_planned_pregnancy_columns_ready = Some( + ready_rows + .first() + .and_then(|r| r.get("ready")) + .map(|v| v == "true" || v == "t") + .unwrap_or(false), + ); + } + + Ok(()) + } + + /// Weg A: `character.pregnancy_due_at` / `pregnancy_father_character_id` (Admin/Spiel). + fn process_single_planned_character_birth( + &mut self, + conn: &mut crate::db::DbConnection, + row: &crate::db::Row, + ) -> Result<(), DbError> { + let mother_cid = parse_i32(row, "mother_cid", -1); + let father_cid = parse_i32(row, "father_cid", -1); + if mother_cid < 0 || father_cid < 0 { + return Ok(()); + } + + let title_of_nobility = parse_i32(row, "title_of_nobility", 0); + let last_name = parse_i32(row, "last_name", 0); + let region_id = parse_i32(row, "region_id", 0); + + let father_uid = parse_opt_i32(row, "father_uid"); + let mother_uid = parse_opt_i32(row, "mother_uid"); + + let birth_context = row + .get("birth_context") + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .unwrap_or("marriage"); + + let gender = if self.dist.sample(&mut self.rng) < 0.5 { + "male" + } else { + "female" + }; + + let inserted = + conn.execute("insert_child", &[®ion_id, &gender, &last_name, &title_of_nobility])?; + let child_cid = inserted + .first() + .and_then(|r| r.get("child_cid")) + .and_then(|v| v.parse::().ok()) + .unwrap_or(-1); + if child_cid < 0 { + return Ok(()); + } + + conn.execute( + "insert_child_rel_planned", + &[&father_cid, &mother_cid, &child_cid, &birth_context], + )?; + conn.execute("clear_char_preg", &[&mother_cid])?; + + if let Some(f_uid) = father_uid { + self.send_children_update_and_status(f_uid); + } + if let Some(m_uid) = mother_uid { + self.send_children_update_and_status(m_uid); + } + Ok(()) }