diff --git a/migrations/008_falukant_marriage_pregnancy_due.sql b/migrations/008_falukant_marriage_pregnancy_due.sql new file mode 100644 index 0000000..1c617a1 --- /dev/null +++ b/migrations/008_falukant_marriage_pregnancy_due.sql @@ -0,0 +1,8 @@ +-- Ehe-Schwangerschaft: Konzeption setzt Fälligkeit; Geburt erst nach Ablauf (Daemon: 5 Tage Wartezeit). +-- Verhindert „sofortige“ Geburt beim stündlichen Wurf und entspricht der Erwartung, dass nach wenigen Tagen etwas passiert. + +ALTER TABLE falukant_data.relationship + ADD COLUMN IF NOT EXISTS marriage_pregnancy_due_at timestamptz NULL; + +COMMENT ON COLUMN falukant_data.relationship.marriage_pregnancy_due_at IS + 'Ehe: Nach erfolgreicher Konzeption (stündlicher Wurf) Zeitpunkt der Geburt; NULL = nicht schwanger'; diff --git a/src/worker/falukant_family.rs b/src/worker/falukant_family.rs index 2cd18e7..a118ff1 100644 --- a/src/worker/falukant_family.rs +++ b/src/worker/falukant_family.rs @@ -664,6 +664,12 @@ impl FalukantFamilyWorker { notify.extend(tension_socket_users); self.publish_falukant_update_family_batch(¬ify, "daily"); + drop(conn); + // Liebschafts-Geburt: früher nur alle ~30 Tage in process_monthly — zu selten für kurze Testphasen. + if let Err(e) = self.process_lover_births() { + eprintln!("[FalukantFamilyWorker] process_lover_births: {e}"); + } + Ok(()) } diff --git a/src/worker/sql.rs b/src/worker/sql.rs index 0cab899..9903ab6 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -1591,7 +1591,141 @@ pub const QUERY_AUTOBATISM: &str = r#" // dieselbe kumulative Jahresaufteilung wie früher 1×/Tag mit P_tag = 1 - (1 - P_jahr)^(1/365). // Grenzen in Tagen: 1 Jahr ≈ 365 Tage (mother_age_days). // WICHTIG: Vater/Mutter und Alter immer über gender ableiten — nicht character1/2 fest als Mutter! -pub const QUERY_GET_PREGNANCY_CANDIDATES: &str = r#" +// +// Ablauf Ehe: (1) Konzeption setzt `marriage_pregnancy_due_at` (+5 Tage), (2) Geburt wenn fällig. +// Migration: `008_falukant_marriage_pregnancy_due.sql`. + +/// Fällige Geburten (Ehe): `marriage_pregnancy_due_at <= NOW()`. +pub const QUERY_GET_MARRIAGE_BIRTH_DELIVERIES: &str = r#" + SELECT + r.id AS relationship_id, + CASE WHEN c1.gender = 'male' THEN c1.id ELSE c2.id END AS father_cid, + CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END AS mother_cid, + CASE WHEN c1.gender = 'male' THEN c1.title_of_nobility ELSE c2.title_of_nobility END AS title_of_nobility, + CASE WHEN c1.gender = 'male' THEN c1.last_name ELSE c2.last_name END AS last_name, + CASE WHEN c1.gender = 'male' THEN c1.region_id ELSE c2.region_id END AS region_id, + CASE WHEN c1.gender = 'male' THEN fu1.id ELSE fu2.id END AS father_uid, + CASE WHEN c1.gender = 'female' THEN fu1.id ELSE fu2.id END AS mother_uid, + (CURRENT_DATE - c_female.birthdate::date)::int AS mother_age_days + 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 + JOIN falukant_data.character c_female ON c_female.id = ( + CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END + ) + 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 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')); +"#; + +pub const QUERY_CLEAR_MARRIAGE_PREGNANCY_DUE: &str = r#" + UPDATE falukant_data.relationship + SET marriage_pregnancy_due_at = NULL + WHERE id = $1::int; +"#; + +/// Stündlicher Konzeptionswurf (Ehe): bei Treffer wird `marriage_pregnancy_due_at` auf +5 Tage gesetzt. +pub const QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE: &str = r#" + WITH paired AS ( + SELECT + r.id AS relationship_id, + CASE WHEN c1.gender = 'male' THEN c1.id ELSE c2.id END AS father_cid, + CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END AS mother_cid, + CASE WHEN c1.gender = 'male' THEN c1.title_of_nobility ELSE c2.title_of_nobility END AS title_of_nobility, + CASE WHEN c1.gender = 'male' THEN c1.last_name ELSE c2.last_name END AS last_name, + CASE WHEN c1.gender = 'male' THEN c1.region_id ELSE c2.region_id END AS region_id, + CASE WHEN c1.gender = 'male' THEN fu1.id ELSE fu2.id END AS father_uid, + CASE WHEN c1.gender = 'female' THEN fu1.id ELSE fu2.id END AS mother_uid, + (CURRENT_DATE - c_female.birthdate::date)::int AS mother_age_days + 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 + JOIN falukant_data.character c_female ON c_female.id = ( + CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END + ) + 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 r.marriage_pregnancy_due_at IS NULL + AND ((c1.gender = 'male' AND c2.gender = 'female') + OR (c1.gender = 'female' AND c2.gender = 'male')) + ), + mother_age AS ( + SELECT + p.relationship_id, + p.father_cid, + p.mother_cid, + p.title_of_nobility, + p.last_name, + p.region_id, + p.father_uid, + p.mother_uid, + p.mother_age_days, + CASE + WHEN p.mother_age_days < 4380 THEN 0.005 + WHEN p.mother_age_days < 4745 THEN 0.30 + WHEN p.mother_age_days < 5110 THEN 0.45 + WHEN p.mother_age_days < 5475 THEN 0.55 + WHEN p.mother_age_days < 5840 THEN 0.60 + WHEN p.mother_age_days < 6205 THEN 0.725 + WHEN p.mother_age_days < 6570 THEN 0.80 + WHEN p.mother_age_days < 7305 THEN 0.855 + WHEN p.mother_age_days < 9125 THEN 0.875 + WHEN p.mother_age_days < 10950 THEN 0.84 + WHEN p.mother_age_days < 11315 THEN 0.785 + WHEN p.mother_age_days < 11680 THEN 0.765 + WHEN p.mother_age_days < 12045 THEN 0.74 + WHEN p.mother_age_days < 12410 THEN 0.72 + WHEN p.mother_age_days < 12775 THEN 0.695 + WHEN p.mother_age_days < 13140 THEN 0.65 + WHEN p.mother_age_days < 13505 THEN 0.63 + WHEN p.mother_age_days < 13870 THEN 0.60 + WHEN p.mother_age_days < 14235 THEN 0.55 + WHEN p.mother_age_days < 14600 THEN 0.50 + WHEN p.mother_age_days < 14965 THEN 0.45 + WHEN p.mother_age_days < 15330 THEN 0.35 + WHEN p.mother_age_days < 15695 THEN 0.25 + WHEN p.mother_age_days < 16060 THEN 0.15 + WHEN p.mother_age_days < 16425 THEN 0.075 + WHEN p.mother_age_days < 16790 THEN 0.03 + WHEN p.mother_age_days < 17155 THEN 0.02 + WHEN p.mother_age_days < 17520 THEN 0.015 + WHEN p.mother_age_days < 18250 THEN 0.005 + ELSE 0.001 + END AS prob_year + FROM paired p + ), + conceivable AS ( + SELECT ma.relationship_id + FROM mother_age ma + WHERE ma.mother_age_days >= 4380 + AND ma.mother_age_days < 18993 + AND random() < (1 - POWER(1 - ma.prob_year, 1.0/8760.0)) + ) + UPDATE falukant_data.relationship r + SET marriage_pregnancy_due_at = NOW() + INTERVAL '5 days' + FROM conceivable c + WHERE r.id = c.relationship_id; +"#; + +pub const QUERY_MARRIAGE_PREGNANCY_COLUMN_READY: &str = r#" + SELECT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'falukant_data' + AND table_name = 'relationship' + AND column_name = 'marriage_pregnancy_due_at' + ) AS ready; +"#; + +/// Ehe: ohne Migration 008 — stündlicher Wurf legt sofort ein Kind an (wie früher). +pub const QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES: &str = r#" WITH paired AS ( SELECT CASE WHEN c1.gender = 'male' THEN c1.id ELSE c2.id END AS father_cid, @@ -3059,7 +3193,7 @@ pub const QUERY_GET_LOVER_PREGNANCY_CANDIDATES: &str = r#" AND rs.affection >= 45 AND rs.maintenance_level >= 30 AND rs.last_monthly_processed_at IS NOT NULL - AND date_trunc('month', rs.last_monthly_processed_at) = date_trunc('month', CURRENT_TIMESTAMP) + AND rs.last_monthly_processed_at >= NOW() - INTERVAL '50 days' AND NOT EXISTS ( SELECT 1 FROM falukant_data.child_relation cr diff --git a/src/worker/user_character.rs b/src/worker/user_character.rs index 2e1fae4..8d3f760 100644 --- a/src/worker/user_character.rs +++ b/src/worker/user_character.rs @@ -32,7 +32,9 @@ use crate::worker::sql::{ QUERY_UPDATE_USER_MONEY, QUERY_GET_FALUKANT_USER_ID, QUERY_AUTOBATISM, - QUERY_GET_PREGNANCY_CANDIDATES, + QUERY_CLEAR_MARRIAGE_PREGNANCY_DUE, QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES, + QUERY_GET_MARRIAGE_BIRTH_DELIVERIES, QUERY_MARRIAGE_PREGNANCY_COLUMN_READY, + QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE, QUERY_INSERT_CHILD, QUERY_INSERT_CHILD_RELATION, QUERY_INSERT_NOTIFICATION, @@ -60,6 +62,8 @@ pub struct UserCharacterWorker { dist: Uniform, last_hourly_run: Option, last_pregnancy_run: Option, + /// `None` = noch nicht geprüft, ob Migration 008 (`marriage_pregnancy_due_at`) existiert. + marriage_pregnancy_column_ready: Option, last_mood_run: Option, last_death_check_run: Option, } @@ -78,6 +82,7 @@ impl UserCharacterWorker { dist, last_hourly_run: None, last_pregnancy_run: None, + marriage_pregnancy_column_ready: None, last_mood_run: None, last_death_check_run: None, } @@ -173,7 +178,8 @@ impl UserCharacterWorker { } /// Ehe-Schwangerschaft: höchstens einmal pro Stunde (Daemon-Schleife ~1 s). - /// SQL nutzt stündliche Zerlegung der Jahres-Wahrscheinlichkeit (`1/8760`), siehe `QUERY_GET_PREGNANCY_CANDIDATES`. + /// Konzeption setzt `marriage_pregnancy_due_at` (+5 Tage); Geburt über `QUERY_GET_MARRIAGE_BIRTH_DELIVERIES`. + /// Stündliche Zerlegung der Jahres-Wahrscheinlichkeit (`1/8760`), siehe `QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE`. fn maybe_run_hourly_pregnancies(&mut self) { let now = Instant::now(); let should_run = match self.last_pregnancy_run { @@ -537,27 +543,53 @@ impl UserCharacterWorker { conn.prepare("autobatism", QUERY_AUTOBATISM)?; conn.execute("autobatism", &[])?; - conn.prepare("get_pregnancy_candidates", QUERY_GET_PREGNANCY_CANDIDATES)?; - let rows = conn.execute("get_pregnancy_candidates", &[])?; + 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), + ); + } + let use_gestation = self.marriage_pregnancy_column_ready.unwrap_or(false); conn.prepare("insert_child", QUERY_INSERT_CHILD)?; conn.prepare("insert_child_relation", QUERY_INSERT_CHILD_RELATION)?; - for row in rows { - self.process_single_pregnancy_candidate(&mut conn, &row)?; + if use_gestation { + conn.prepare("get_deliveries", QUERY_GET_MARRIAGE_BIRTH_DELIVERIES)?; + conn.prepare("clear_preg", QUERY_CLEAR_MARRIAGE_PREGNANCY_DUE)?; + + let delivery_rows = conn.execute("get_deliveries", &[])?; + for row in delivery_rows { + self.process_single_marriage_delivery(&mut conn, &row)?; + } + + conn.prepare("try_conception", QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE)?; + conn.execute("try_conception", &[])?; + } else { + conn.prepare("legacy_preg", QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES)?; + let legacy_rows = conn.execute("legacy_preg", &[])?; + for row in legacy_rows { + self.process_single_marriage_delivery(&mut conn, &row)?; + } } Ok(()) } - fn process_single_pregnancy_candidate( + fn process_single_marriage_delivery( &mut self, conn: &mut crate::db::DbConnection, row: &crate::db::Row, ) -> Result<(), DbError> { + let relationship_id = parse_i32(row, "relationship_id", -1); let father_cid = parse_i32(row, "father_cid", -1); let mother_cid = parse_i32(row, "mother_cid", -1); - if father_cid < 0 || mother_cid < 0 { + if relationship_id < 0 || father_cid < 0 || mother_cid < 0 { return Ok(()); } @@ -590,6 +622,10 @@ impl UserCharacterWorker { &[&father_cid, &mother_cid, &child_cid], )?; + if relationship_id >= 0 { + conn.execute("clear_preg", &[&relationship_id])?; + } + if let Some(f_uid) = father_uid { self.send_children_update_and_status(f_uid); }