5 Commits

Author SHA1 Message Date
Torsten Schulz (local)
8f8fb8418a Ändere Dateiberechtigungen auf ausführbar für mehrere Dateien
All checks were successful
Deploy yourpart (blue-green) / deploy (push) Successful in 2m45s
2026-07-16 09:16:49 +02:00
Torsten Schulz (local)
106c3950eb death fix
All checks were successful
Deploy yourpart (blue-green) / deploy (push) Successful in 1m51s
2026-06-22 16:17:29 +02:00
Torsten Schulz (local)
6ed88f7ad7 multiple fixes
Some checks failed
Deploy yourpart (blue-green) / deploy (push) Has been cancelled
2026-06-22 16:17:20 +02:00
Torsten Schulz (local)
41c0e5a6f2 code fixes
All checks were successful
Deploy yourpart (blue-green) / deploy (push) Successful in 1m52s
2026-06-06 13:26:45 +02:00
Torsten Schulz (local)
35377e3b59 Wahlproblem gefixt
Some checks failed
Deploy yourpart (blue-green) / deploy (push) Failing after 1m22s
2026-06-06 12:35:27 +02:00
82 changed files with 136 additions and 241 deletions

0
.gitea/workflows/deploy-daemon.yml Normal file → Executable file
View File

0
.gitignore vendored Normal file → Executable file
View File

0
.vscode/tasks.json vendored Normal file → Executable file
View File

0
AGENTS.md Normal file → Executable file
View File

2
Cargo.lock generated Normal file → Executable file
View File

@@ -11,7 +11,7 @@ dependencies = [
"futures-util",
"libsystemd",
"postgres",
"rand 0.8.5",
"rand 0.9.2",
"rustls-pemfile",
"serde",
"serde_json",

0
Cargo.toml Normal file → Executable file
View File

0
YpDaemon/Cargo.toml Normal file → Executable file
View File

0
YpDaemon/src/worker/base.rs Normal file → Executable file
View File

0
YpDaemon/src/worker/character_creation.rs Normal file → Executable file
View File

0
YpDaemon/src/worker/director.rs Normal file → Executable file
View File

0
YpDaemon/src/worker/events.rs Normal file → Executable file
View File

0
YpDaemon/src/worker/politics.rs Normal file → Executable file
View File

0
YpDaemon/src/worker/sql.rs Normal file → Executable file
View File

0
YpDaemon/src/worker/user_character.rs Normal file → Executable file
View File

0
build_output.txt Normal file → Executable file
View File

8
docs/DEBUG_DEATH_SUCCESSION_QUERIES.sql Normal file → Executable file
View File

@@ -153,8 +153,8 @@ WITH removed AS (
),
archived_removed AS (
INSERT INTO falukant_log.political_office_history
(character_id, office_type_id, region_id, start_date, end_date)
SELECT character_id, office_type_id, region_id, created_at, NOW()
(character_id, office_type_id, region_id, start_date, end_date, created_at, updated_at)
SELECT character_id, office_type_id, region_id, created_at, NOW(), NOW(), NOW()
FROM removed
),
affected AS (
@@ -183,8 +183,8 @@ to_delete AS (
),
archived_trim AS (
INSERT INTO falukant_log.political_office_history
(character_id, office_type_id, region_id, start_date, end_date)
SELECT character_id, office_type_id, region_id, created_at, NOW()
(character_id, office_type_id, region_id, start_date, end_date, created_at, updated_at)
SELECT character_id, office_type_id, region_id, created_at, NOW(), NOW(), NOW()
FROM to_delete
)
DELETE FROM falukant_data.political_office

0
docs/FALUKANT_CHURCH_DAEMON.md Normal file → Executable file
View File

0
docs/FALUKANT_DAEMON_AENDERUNGSNOTIZ_ZEITMASSSTAB.md Normal file → Executable file
View File

0
docs/FALUKANT_DAEMON_HANDOFF.md Normal file → Executable file
View File

0
docs/FALUKANT_DEATH_SUCCESSION_SMOKE_TEST.md Normal file → Executable file
View File

0
docs/FALUKANT_DEBTORS_DAEMON.md Normal file → Executable file
View File

0
docs/FALUKANT_DIRECTOR_AUTO_ADJUST_INCOME.md Normal file → Executable file
View File

0
docs/FALUKANT_DIRECTOR_PRODUCTION_COST.md Normal file → Executable file
View File

0
docs/FALUKANT_MARRIAGE_HOUSEPEACE_DAEMON_HANDOFF.md Normal file → Executable file
View File

0
docs/FALUKANT_PRODUCTION_CERTIFICATE.md Normal file → Executable file
View File

0
docs/FALUKANT_SERVANTS_DAEMON.md Normal file → Executable file
View File

0
docs/FALUKANT_TRANSPORT_RAID_DAEMON.md Normal file → Executable file
View File

0
docs/FALUKANT_UI_WEBSOCKET.md Normal file → Executable file
View File

0
docs/FALUKANT_UNDERGROUND_INVESTIGATE_AFFAIR.md Normal file → Executable file
View File

0
docs/FALUKANT_WORKER_SCHEDULE_SOCKET.md Normal file → Executable file
View File

0
docs/LOVER_STAND_EFFECTS.md Normal file → Executable file
View File

0
migrations/001_falukant_family_lovers.sql Normal file → Executable file
View File

View File

0
migrations/003_falukant_family_marriage_buffs.sql Normal file → Executable file
View File

0
migrations/004_falukant_servants_daemon.sql Normal file → Executable file
View File

0
migrations/005_falukant_marriage_housepeace.sql Normal file → Executable file
View File

0
migrations/006_falukant_lover_installments.sql Normal file → Executable file
View File

0
migrations/007_falukant_character_church_career.sql Normal file → Executable file
View File

0
migrations/008_falukant_marriage_pregnancy_due.sql Normal file → Executable file
View File

0
migrations/008_falukant_transport_raid.sql Normal file → Executable file
View File

0
migrations/009_falukant_region_display_name.sql Normal file → Executable file
View File

0
migrations/010_falukant_marriage_started_at.sql Normal file → Executable file
View File

View File

0
migrations/012_falukant_political_benefits_daemon.sql Normal file → Executable file
View File

0
migrations/013_falukant_political_daily_salary.sql Normal file → Executable file
View File

View File

View File

View File

@@ -8,13 +8,20 @@ CREATE TABLE IF NOT EXISTS falukant_log.political_office_history (
region_id INTEGER,
start_date TIMESTAMPTZ NOT NULL,
end_date TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Tabelle kann bereits vom Backend existieren (ohne region_id o. Ä.): CREATE TABLE IF NOT EXISTS ergänzt keine Spalten.
ALTER TABLE falukant_log.political_office_history
ADD COLUMN IF NOT EXISTS region_id INTEGER;
ALTER TABLE falukant_log.political_office_history
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
ALTER TABLE falukant_log.political_office_history
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
CREATE INDEX IF NOT EXISTS idx_pol_office_hist_character
ON falukant_log.political_office_history (character_id);

0
migrations/017_falukant_money_precision_14_2.sql Normal file → Executable file
View File

0
migrations/018_worker_schedule_socket_right.sql Normal file → Executable file
View File

0
migrations/README.md Normal file → Executable file
View File

0
renovate.json Normal file → Executable file
View File

0
src/config.rs Normal file → Executable file
View File

0
src/db/connection.rs Normal file → Executable file
View File

0
src/db/mod.rs Normal file → Executable file
View File

0
src/main.rs Normal file → Executable file
View File

0
src/message_broker.rs Normal file → Executable file
View File

0
src/websocket_server.rs Normal file → Executable file
View File

0
src/worker/base.rs Normal file → Executable file
View File

21
src/worker/character_creation.rs Normal file → Executable file
View File

@@ -1,8 +1,7 @@
use crate::db::{ConnectionPool, DbError, Rows};
use crate::message_broker::MessageBroker;
use rand::distributions::{Distribution, Uniform};
use rand::rngs::StdRng;
use rand::{thread_rng, Rng, SeedableRng};
use rand::{Rng, SeedableRng};
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
@@ -29,7 +28,6 @@ use crate::worker::sql::{
pub struct CharacterCreationWorker {
pub(crate) base: BaseWorker,
rng: StdRng,
dist: Uniform<i32>,
first_name_cache: HashMap<String, HashSet<i32>>,
last_name_cache: HashSet<i32>,
death_check_running: Arc<AtomicBool>,
@@ -46,8 +44,8 @@ impl CharacterCreationWorker {
/// Interner Konstruktor, der optional den NPC-Todes-Monitor startet.
fn new_internal(pool: ConnectionPool, broker: MessageBroker, start_death_thread: bool) -> Self {
let base = BaseWorker::new("CharacterCreationWorker", pool.clone(), broker.clone());
let rng = StdRng::from_entropy();
let dist = Uniform::from(2..=3);
let mut base_rng = rand::rng();
let rng = StdRng::from_rng(&mut base_rng);
let death_check_running = Arc::new(AtomicBool::new(start_death_thread));
let death_thread = if start_death_thread {
@@ -80,7 +78,6 @@ impl CharacterCreationWorker {
Self {
base,
rng,
dist,
first_name_cache: HashMap::new(),
last_name_cache: HashSet::new(),
death_check_running,
@@ -139,7 +136,7 @@ impl CharacterCreationWorker {
for &nobility in &nobility_stands {
for &gender in &genders {
let num_chars = self.rng.sample(self.dist);
let num_chars = self.rng.random_range(2..=3);
for _ in 0..num_chars {
self.create_character(region_id, gender, nobility);
}
@@ -248,8 +245,8 @@ impl CharacterCreationWorker {
if set.is_empty() {
return -1;
}
let mut rng = thread_rng();
let idx = rng.gen_range(0..set.len());
let mut rng = rand::rng();
let idx = rng.random_range(0..set.len());
*set.iter().nth(idx).unwrap_or(&-1)
}
@@ -390,9 +387,8 @@ impl CharacterCreationWorker {
let death_probability =
base_probability + increase_per_year * (age.saturating_sub(60) as f64);
let mut rng = thread_rng();
let dist = Uniform::from(0.0..1.0);
let roll: f64 = dist.sample(&mut rng);
let mut rng = rand::rng();
let roll: f64 = rng.random_range(0.0..1.0);
roll < death_probability
}
@@ -537,4 +533,3 @@ impl CharacterCreationWorker {
}
}

0
src/worker/death_log.rs Normal file → Executable file
View File

4
src/worker/director.rs Normal file → Executable file
View File

@@ -193,7 +193,7 @@ impl DirectorWorker {
.filter_map(Self::map_row_to_resignation_candidate)
.collect();
let mut rng = rand::thread_rng();
let mut rng = rand::rng();
for candidate in candidates {
let sat = candidate.satisfaction.clamp(0, 100) as f64;
let resignation_probability = 1.0 - (sat / 100.0);
@@ -214,7 +214,7 @@ impl DirectorWorker {
);
let _ = conn.execute("insert_notification", &[&candidate.employer_user_id, &payload]);
}
let roll: f64 = rng.gen_range(0.0..1.0);
let roll: f64 = rng.random_range(0.0..1.0);
if roll >= resignation_probability {
continue;
}

42
src/worker/events.rs Normal file → Executable file
View File

@@ -1,7 +1,7 @@
use crate::db::{ConnectionPool, DbConnection, DbError};
use crate::message_broker::MessageBroker;
use rand::Rng;
use rand::seq::SliceRandom;
use rand::prelude::SliceRandom;
use serde_json::json;
use std::sync::atomic::Ordering;
use std::sync::Arc;
@@ -447,7 +447,7 @@ impl EventsWorker {
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
let mut last_event_check = None;
let mut rng = rand::thread_rng();
let mut rng = rand::rng();
let events = Self::initialize_events();
loop {
@@ -502,7 +502,7 @@ impl EventsWorker {
// Prüfe jedes mögliche Ereignis
for event in events {
// Zufällige Prüfung basierend auf Wahrscheinlichkeit
let roll = rng.gen_range(0.0..=1.0);
let roll = rng.random_range(0.0..=1.0);
let effective_prob = event.probability_per_minute * Self::EVENT_RATE_SCALE;
if roll < effective_prob {
eprintln!(
@@ -560,7 +560,7 @@ impl EventsWorker {
// Wende Effekte an
let mut effect_results = Vec::new();
for effect in &event.effects {
let effect_roll = rng.gen_range(0.0..=1.0);
let effect_roll = rng.random_range(0.0..=1.0);
match effect {
EventEffect::MoneyChange {
probability,
@@ -750,7 +750,7 @@ impl EventsWorker {
// Wende Effekte an (in diesem Fall nur CharacterDeath)
let mut effect_results = Vec::new();
for effect in &event.effects {
let effect_roll = rng.gen_range(0.0..=1.0);
let effect_roll = rng.random_range(0.0..=1.0);
match effect {
EventEffect::CharacterDeath { probability } => {
if effect_roll < *probability
@@ -835,7 +835,7 @@ impl EventsWorker {
let mut price_change_note: Option<serde_json::Value> = None;
let mut production_quality_note: Option<serde_json::Value> = None;
for effect in &event.effects {
let effect_roll = rng.gen_range(0.0..=1.0);
let effect_roll = rng.random_range(0.0..=1.0);
match effect {
EventEffect::WeatherChange { probability } => {
if effect_roll < *probability {
@@ -851,7 +851,7 @@ impl EventsWorker {
max_change,
} => {
if effect_roll < *probability {
let change = rng.gen_range(*min_change..=*max_change);
let change = rng.random_range(*min_change..=*max_change);
Self::apply_production_quality_change(&mut conn, region_id, change)?;
effect_results.push(json!({
"type": "production_quality_change",
@@ -879,7 +879,7 @@ impl EventsWorker {
max_percent,
} => {
if effect_roll < *probability {
let percent_change = rng.gen_range(*min_percent..=*max_percent);
let percent_change = rng.random_range(*min_percent..=*max_percent);
Self::apply_price_change(&mut conn, region_id, percent_change)?;
effect_results.push(json!({
"type": "price_change",
@@ -907,7 +907,7 @@ impl EventsWorker {
max_percent,
} => {
if effect_roll < *probability {
let percent_change = rng.gen_range(*min_percent..=*max_percent);
let percent_change = rng.random_range(*min_percent..=*max_percent);
Self::apply_transport_speed_change(&mut conn, region_id, percent_change)?;
effect_results.push(json!({
"type": "transport_speed_change",
@@ -1218,7 +1218,7 @@ impl EventsWorker {
}
// Berechne die prozentuale Änderung
let percent_change = rng.gen_range(min_percent..=max_percent);
let percent_change = rng.random_range(min_percent..=max_percent);
// Reduziere die Kapazität aller Stocks
conn.prepare("update_stock_capacity_regional", QUERY_UPDATE_STOCK_CAPACITY_REGIONAL)?;
@@ -1283,7 +1283,7 @@ impl EventsWorker {
}
// Berechne die Änderung
let quality_change = rng.gen_range(min_change..=max_change);
let quality_change = rng.random_range(min_change..=max_change);
// Reduziere die Qualität aller Häuser
conn.prepare("update_house_quality", QUERY_UPDATE_HOUSE_QUALITY)?;
@@ -1393,7 +1393,7 @@ impl EventsWorker {
// Spezialfall: Unerwarteter Geldsegen -> absoluter Zufallsbetrag 1..500
if event.id == "windfall" {
let absolute_change: f64 = rng.gen_range(1.0..=500.0);
let absolute_change: f64 = rng.random_range(1.0..=500.0);
let percent_change = if current_money > 0.0 {
(absolute_change / current_money) * 100.0
} else {
@@ -1430,7 +1430,7 @@ impl EventsWorker {
return Ok(None);
}
let loss: f64 = rng.gen_range(1.0..=max_loss);
let loss: f64 = rng.random_range(1.0..=max_loss);
let percent_change = -(loss / current_money) * 100.0;
let absolute_change = -loss;
@@ -1456,7 +1456,7 @@ impl EventsWorker {
}
// Standardfall: prozentuale Änderung, optional mit Mindestbetrag für positive Änderungen
let percent_change = rng.gen_range(min_percent..=max_percent);
let percent_change = rng.random_range(min_percent..=max_percent);
let computed_change = current_money * (percent_change / 100.0);
let effective_change = match min_absolute_positive {
Some(min_abs) if percent_change > 0.0 && computed_change < *min_abs => *min_abs,
@@ -1491,7 +1491,7 @@ impl EventsWorker {
return Ok(None);
}
let percent_change = rng.gen_range(min_percent..=max_percent);
let percent_change = rng.random_range(min_percent..=max_percent);
Self::apply_storage_capacity_change(conn, user_id, percent_change)?;
Ok(Some(json!({
@@ -1744,7 +1744,7 @@ impl EventsWorker {
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(100);
let health_change = rng.gen_range(min_change..=max_change);
let health_change = rng.random_range(min_change..=max_change);
// Durch Unfälle/Krankheiten soll Gesundheit von Spieler-Charakteren nicht auf 0 fallen
let new_health = (current_health + health_change).clamp(1, 100);
@@ -1816,7 +1816,7 @@ impl EventsWorker {
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(100);
let health_change = rng.gen_range(min_change..=max_change);
let health_change = rng.random_range(min_change..=max_change);
let user_id: Option<i32> = row
.get("user_id")
.and_then(|v| v.parse::<i32>().ok());
@@ -2235,9 +2235,9 @@ impl EventsWorker {
// 3. Berechne Schäden
let inventory_damage_percent =
rng.gen_range(params.inventory_damage_min_percent..=params.inventory_damage_max_percent);
rng.random_range(params.inventory_damage_min_percent..=params.inventory_damage_max_percent);
let storage_destruction_percent =
rng.gen_range(params.storage_destruction_min_percent..=params.storage_destruction_max_percent);
rng.random_range(params.storage_destruction_min_percent..=params.storage_destruction_max_percent);
let total_stocks = stock_rows.len();
let stocks_to_destroy_requested = ((total_stocks as f64 * storage_destruction_percent / 100.0)
@@ -2455,9 +2455,9 @@ impl EventsWorker {
// 3. Berechne Schäden
let inventory_damage_percent =
rng.gen_range(params.inventory_damage_min_percent..=params.inventory_damage_max_percent);
rng.random_range(params.inventory_damage_min_percent..=params.inventory_damage_max_percent);
let storage_destruction_percent =
rng.gen_range(params.storage_destruction_min_percent..=params.storage_destruction_max_percent);
rng.random_range(params.storage_destruction_min_percent..=params.storage_destruction_max_percent);
let total_stocks = stock_rows.len();
let stocks_to_destroy_requested = ((total_stocks as f64 * storage_destruction_percent / 100.0)

0
src/worker/falukant_certificate.rs Normal file → Executable file
View File

0
src/worker/falukant_debtors.rs Normal file → Executable file
View File

56
src/worker/falukant_family.rs Normal file → Executable file
View File

@@ -12,9 +12,8 @@ use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant};
use rand::distributions::{Distribution, Uniform};
use rand::rngs::StdRng;
use rand::SeedableRng;
use rand::{Rng, SeedableRng};
use super::base::{BaseWorker, Worker, WorkerState};
use super::sql::{
@@ -46,7 +45,6 @@ const GAME_MONTH_SLICE_INTERVAL: Duration = Duration::from_secs(2 * 3600);
pub struct FalukantFamilyWorker {
base: BaseWorker,
rng: StdRng,
dist: Uniform<f64>,
last_daily: Option<Instant>,
last_monthly: Option<Instant>,
/// Gemeinsamer Tick für Liebschafts-Raten + Dienerschaft (Monatstick ≈ 2 h).
@@ -63,8 +61,10 @@ impl FalukantFamilyWorker {
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
Self {
base: BaseWorker::new("FalukantFamilyWorker", pool, broker),
rng: StdRng::from_entropy(),
dist: Uniform::from(0.0..1.0),
rng: {
let mut base_rng = rand::rng();
StdRng::from_rng(&mut base_rng)
},
last_daily: None,
last_monthly: None,
last_game_month_slice: None,
@@ -266,6 +266,9 @@ impl FalukantFamilyWorker {
});
let mut v_net = 0i32;
if l.min_age_years <= 12 {
v_net += 3;
}
if l.maintenance_level < 35 {
v_net += 1;
}
@@ -289,6 +292,9 @@ impl FalukantFamilyWorker {
let new_vis = clamp_i32(l.visibility + v_net, 0, 100);
let mut d_net = 0i32;
if l.min_age_years <= 12 {
d_net -= 3;
}
if l.maintenance_level >= 70 {
d_net += 1;
}
@@ -361,7 +367,7 @@ impl FalukantFamilyWorker {
}
}
}
if breakup_risk_pct > 0.0 && self.dist.sample(&mut self.rng) * 100.0 < breakup_risk_pct {
if breakup_risk_pct > 0.0 && self.rng.random_range(0.0..1.0) * 100.0 < breakup_risk_pct {
conn.execute("deactivate_lover", &[&l.rel_id])?;
ended_rel_ids.insert(l.rel_id);
push_user_id(&mut breakup_socket_users, l.user1_id);
@@ -452,8 +458,8 @@ impl FalukantFamilyWorker {
{
delta -= 1;
}
if touching.iter().any(|l| l.min_age_years <= 15) {
delta -= 1;
if touching.iter().any(|l| l.min_age_years <= 12) {
delta -= 4;
}
// Standsabhängige Deltas aus der Liebschaften-Verarbeitung anwenden
if let Some(extra_delta) = spouse_satisfaction_delta.get(&m.id) {
@@ -499,8 +505,8 @@ impl FalukantFamilyWorker {
if (g1 as i32 - g2 as i32).abs() >= 2 {
pst -= 1;
}
if touching.iter().any(|l| l.min_age_years <= 15) {
pst -= 2;
if touching.iter().any(|l| l.min_age_years <= 12) {
pst -= 4;
}
let ho_avg = (m.household_order_1 + m.household_order_2) / 2;
if ho_avg < 35 {
@@ -676,7 +682,7 @@ impl FalukantFamilyWorker {
p -= 2.0;
}
p = p.clamp(0.0, 25.0);
if self.dist.sample(&mut self.rng) * 100.0 < p {
if self.rng.random_range(0.0..1.0) * 100.0 < p {
self.base.broker.publish(format!(
r#"{{"event":"falukant_family_scandal_hint","relationship_id":{}}}"#,
l.rel_id
@@ -709,7 +715,7 @@ impl FalukantFamilyWorker {
}
let mut rng_malus_cids: Vec<i32> = Vec::new();
for cid in unique_cids.iter().copied() {
let r = self.dist.sample(&mut self.rng) * 100.0;
let r = self.rng.random_range(0.0..1.0) * 100.0;
let malus = if r < 1.0 {
-5.0
} else if r < 3.0 {
@@ -1307,12 +1313,10 @@ fn rank_rep_modifier(g: u8, scandal: bool, order_ok: bool) -> f64 {
/// Spec 5a: Altersmalus (jüngeres Alter der Beteiligten), zusätzlich zur Basis-Reputation.
fn age_reputation_delta(min_age_years: i32) -> f64 {
if min_age_years <= 13 {
-1.5
} else if min_age_years <= 15 {
-0.8
} else if min_age_years <= 17 {
-0.3
if min_age_years <= 10 {
-6.0
} else if min_age_years <= 12 {
-3.0
} else {
0.0
}
@@ -1320,8 +1324,10 @@ fn age_reputation_delta(min_age_years: i32) -> f64 {
/// Spec 5a: minAge <= 15 und hohe Sichtbarkeit.
fn visibility_young_penalty(min_age_years: i32, visibility: i32) -> f64 {
if min_age_years <= 15 && visibility >= 50 {
-0.5
if min_age_years <= 10 && visibility >= 20 {
-4.0
} else if min_age_years <= 12 && visibility >= 25 {
-2.0
} else {
0.0
}
@@ -1414,12 +1420,10 @@ fn breakup_risk_percent(l: &LoverData) -> f64 {
/// Spec Skandalrisiko: stufenweise Zusatz (exklusiv).
fn scandal_age_extra_pct(min_age_years: i32) -> f64 {
if min_age_years <= 13 {
6.0
} else if min_age_years <= 15 {
3.0
} else if min_age_years <= 17 {
1.0
if min_age_years <= 10 {
12.0
} else if min_age_years <= 12 {
8.0
} else {
0.0
}

0
src/worker/falukant_servants.rs Normal file → Executable file
View File

14
src/worker/falukant_transport_raid.rs Normal file → Executable file
View File

@@ -177,19 +177,19 @@ fn try_resolve_one_raid(
return Ok(());
}
let mut rng = rand::thread_rng();
let mut rng = rand::rng();
// Begegnung (Bande + Sichtbarkeit großer Ladung)
let visibility_penalty = ((transport_size as f64 / vehicle_cap as f64).min(2.0) - 1.0) * 0.04;
let encounter_p = (0.08_f64 + band_size as f64 * 0.012 + visibility_penalty).clamp(0.05, 0.45);
if !rng.gen_bool(encounter_p) {
if !rng.random_bool(encounter_p) {
return Ok(());
}
// Kampf
let raid_power = band_size + rng.gen_range(0..=band_size);
let raid_power = band_size + rng.random_range(0..=band_size);
let gmax = guard_count.max(1);
let guard_power = guard_count + rng.gen_range(0..=gmax);
let guard_power = guard_count + rng.random_range(0..=gmax);
let outcome = if raid_power > (guard_power as f64 * 1.25) as i32 {
"major_success"
@@ -218,15 +218,15 @@ fn try_resolve_one_raid(
// Beuteanteil (nie Totalverlust)
let base_loot = match outcome {
"major_success" => rng.gen_range(0.35..0.60),
_ => rng.gen_range(0.15..0.45),
"major_success" => rng.random_range(0.35..0.60),
_ => rng.random_range(0.15..0.45),
};
let guard_mul = (1.0_f64 - 0.05_f64 * (guard_count.min(12) as f64)).max(0.35);
let loot_share = (base_loot * guard_mul).clamp(0.05, 0.55);
let mut stolen = ((transport_size as f64) * loot_share).floor() as i32;
stolen = stolen.clamp(1, transport_size - 1);
let carry_loss = rng.gen_range(0.65..0.90);
let carry_loss = rng.random_range(0.65..0.90);
let mut to_store = (stolen as f64 * carry_loss).floor() as i32;
to_store = to_store.max(0).min(stolen);

0
src/worker/house.rs Normal file → Executable file
View File

0
src/worker/mod.rs Normal file → Executable file
View File

0
src/worker/notify.rs Normal file → Executable file
View File

0
src/worker/political_benefits.rs Normal file → Executable file
View File

0
src/worker/politics.rs Normal file → Executable file
View File

0
src/worker/produce.rs Normal file → Executable file
View File

0
src/worker/simple.rs Normal file → Executable file
View File

157
src/worker/sql.rs Normal file → Executable file
View File

@@ -1000,7 +1000,16 @@ SELECT child_character_id FROM falukant_data.child_relation WHERE father_charact
"#;
pub const QUERY_SET_CHARACTER_USER: &str = r#"
UPDATE falukant_data.character SET user_id = $1, updated_at = NOW() WHERE id = $2;
WITH cleared AS (
UPDATE falukant_data.character
SET user_id = NULL,
updated_at = NOW()
WHERE user_id = $1
)
UPDATE falukant_data.character
SET user_id = $1,
updated_at = NOW()
WHERE id = $2;
"#;
pub const QUERY_GET_CURRENT_MONEY: &str = r#"
@@ -1134,8 +1143,8 @@ pub const QUERY_SELECT_NEEDED_ELECTIONS: &str = r#"
WHERE (po.created_at + (pot.term_length * INTERVAL '1 day'))::date = td.election_date
),
archived_expiring AS (
INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date)
SELECT character_id, office_type_id, region_id, start_date, end_date
INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date, created_at, updated_at)
SELECT character_id, office_type_id, region_id, start_date, end_date, NOW(), NOW()
FROM offices_ending_today
),
expired_today AS (
@@ -1265,8 +1274,8 @@ pub const QUERY_PROCESS_EXPIRED_AND_SCHEDULE: &str = r#"
WHERE (po.created_at + (pot.term_length * INTERVAL '1 day')) <= NOW()
),
archived_expired AS (
INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date)
SELECT character_id, office_type_id, region_id, start_date, term_end
INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date, created_at, updated_at)
SELECT character_id, office_type_id, region_id, start_date, term_end, NOW(), NOW()
FROM doomed
),
expired_offices AS (
@@ -1653,8 +1662,8 @@ pub const QUERY_TRIM_EXCESS_OFFICES_GLOBAL: &str = r#"
WHERE rn > seats_total
),
archived_trim AS (
INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date)
SELECT character_id, office_type_id, region_id, created_at, NOW()
INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date, created_at, updated_at)
SELECT character_id, office_type_id, region_id, created_at, NOW(), NOW(), NOW()
FROM to_delete
)
DELETE FROM falukant_data.political_office
@@ -2161,13 +2170,7 @@ pub const QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE: &str = r#"
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,
(
SELECT COUNT(*)::int
FROM falukant_data.child_relation cr
WHERE cr.father_character_id = (CASE WHEN c1.gender = 'male' THEN c1.id ELSE c2.id END)
AND cr.mother_character_id = (CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END)
) AS shared_children_count
(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'
@@ -2194,58 +2197,13 @@ pub const QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE: &str = r#"
p.father_uid,
p.mother_uid,
p.mother_age_days,
p.shared_children_count,
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_raw
0.72::float AS prob_year
FROM paired p
),
mother_age_final AS (
SELECT
ma.*,
CASE
WHEN ma.shared_children_count = 0
AND ma.mother_age_days >= 6570
AND ma.mother_age_days < 16000
THEN GREATEST(ma.prob_year_raw, 1.0)
ELSE ma.prob_year_raw
END AS prob_year
FROM mother_age ma
),
conceivable AS (
SELECT ma.relationship_id
FROM mother_age_final ma
WHERE ma.mother_age_days >= 4380
AND ma.mother_age_days < 18993
FROM mother_age ma
WHERE ma.mother_age_days >= 5110
AND random() < ma.prob_year
)
UPDATE falukant_data.relationship r
@@ -2275,13 +2233,7 @@ pub const QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES: &str = r#"
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,
(
SELECT COUNT(*)::int
FROM falukant_data.child_relation cr
WHERE cr.father_character_id = (CASE WHEN c1.gender = 'male' THEN c1.id ELSE c2.id END)
AND cr.mother_character_id = (CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END)
) AS shared_children_count
(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'
@@ -2312,52 +2264,8 @@ pub const QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES: &str = r#"
p.father_uid,
p.mother_uid,
p.mother_age_days,
p.shared_children_count,
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_raw
0.72::float AS prob_year
FROM paired p
),
mother_age_final AS (
SELECT
ma.*,
CASE
WHEN ma.shared_children_count = 0
AND ma.mother_age_days >= 6570
AND ma.mother_age_days < 16000
THEN GREATEST(ma.prob_year_raw, 1.0)
ELSE ma.prob_year_raw
END AS prob_year
FROM mother_age ma
)
SELECT
father_cid,
@@ -2369,9 +2277,8 @@ pub const QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES: &str = r#"
mother_uid,
mother_age_days,
prob_year * 100.0 AS prob_pct
FROM mother_age_final
WHERE mother_age_days >= 4380
AND mother_age_days < 18993
FROM mother_age
WHERE mother_age_days >= 5110
AND random() < prob_year;
"#;
@@ -2532,8 +2439,8 @@ pub const QUERY_DELETE_POLITICAL_OFFICE: &str = r#"
RETURNING character_id, office_type_id, region_id, created_at
),
archived_removed AS (
INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date)
SELECT character_id, office_type_id, region_id, created_at, NOW()
INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date, created_at, updated_at)
SELECT character_id, office_type_id, region_id, created_at, NOW(), NOW(), NOW()
FROM removed
),
affected AS (
@@ -2575,8 +2482,8 @@ pub const QUERY_DELETE_POLITICAL_OFFICE: &str = r#"
WHERE rn > seats_total
),
archived_trim AS (
INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date)
SELECT character_id, office_type_id, region_id, created_at, NOW()
INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date, created_at, updated_at)
SELECT character_id, office_type_id, region_id, created_at, NOW(), NOW(), NOW()
FROM to_delete
)
DELETE FROM falukant_data.political_office
@@ -3927,8 +3834,7 @@ pub const QUERY_GET_LOVER_PREGNANCY_CANDIDATES: &str = r#"
WHERE cr.mother_character_id = c_female.id
AND cr.created_at::date = CURRENT_DATE
)
AND (CURRENT_DATE - c_female.birthdate::date) >= 4380
AND (CURRENT_DATE - c_female.birthdate::date) < 18993
AND (CURRENT_DATE - c_female.birthdate::date) >= 5110
AND random() * 100.0 < (
LEAST(12.0, GREATEST(0.0,
CASE rs.lover_role
@@ -3939,11 +3845,6 @@ pub const QUERY_GET_LOVER_PREGNANCY_CANDIDATES: &str = r#"
END
+ CASE WHEN rs.affection >= 75 THEN 2.0 ELSE 0.0 END
+ CASE WHEN rs.visibility >= 70 AND rs.affection < 50 THEN -2.0 ELSE 0.0 END
+ CASE
WHEN (CURRENT_DATE - c_female.birthdate::date) > 14600
THEN -3.0
ELSE 0.0
END
))
);
"#;

10
src/worker/stockage_manager.rs Normal file → Executable file
View File

@@ -1,8 +1,6 @@
use crate::db::{ConnectionPool, DbError};
use crate::message_broker::MessageBroker;
use rand::distributions::{Distribution, Uniform};
use rand::rngs::StdRng;
use rand::SeedableRng;
use rand::Rng;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant};
@@ -64,8 +62,7 @@ impl StockageManager {
}
fn add_local_stocks(pool: &ConnectionPool, broker: &MessageBroker) -> Result<(), DbError> {
let mut rng = StdRng::from_entropy();
let dist = Uniform::from(0.0..1.0);
let mut rng = rand::rng();
let town_ids = Self::get_town_ids(pool)?;
eprintln!("[StockageManager] Prüfe {} Städte auf neue Lager", town_ids.len());
@@ -76,7 +73,7 @@ impl StockageManager {
// Das bedeutet: 2/217 ≈ 0.92% pro Stadt pro Durchlauf
// Pro Tag (1440 Durchläufe): ~73% Chance pro Stadt
// Erhöht von 2/2161 (0.0926%) auf 2/217 (0.92%) = 10x höher
let roll: f64 = dist.sample(&mut rng) * 216.0_f64;
let roll: f64 = rng.random_range(0.0..1.0) * 216.0_f64;
let chance = roll.round();
if chance <= 1.0 {
eprintln!("[StockageManager] Erstelle Lager für Stadt {}", town_id);
@@ -185,4 +182,3 @@ impl Worker for StockageManager {
}
}

0
src/worker/transport.rs Normal file → Executable file
View File

29
src/worker/underground.rs Normal file → Executable file
View File

@@ -1,7 +1,6 @@
use crate::db::{ConnectionPool, DbError, Row, Rows};
use crate::message_broker::MessageBroker;
use rand::distributions::{Distribution, Uniform};
use rand::seq::SliceRandom;
use rand::prelude::{IndexedRandom, SliceRandom};
use rand::Rng;
use serde_json::json;
use serde_json::Value as Json;
@@ -388,9 +387,8 @@ impl UndergroundWorker {
}
let current = parse_i32(&rows[0], "health", 0);
let mut rng = rand::thread_rng();
let dist = Uniform::from(0..=current.max(0));
let new_health = dist.sample(&mut rng);
let mut rng = rand::rng();
let new_health = rng.random_range(0..=current.max(0));
conn.execute("ug_update_char_health", &[&victim_id, &new_health])?;
@@ -705,7 +703,7 @@ impl UndergroundWorker {
}));
}
let mut rng = rand::thread_rng();
let mut rng = rand::rng();
let mut to_remove = random_ll(1, cap);
rows.shuffle(&mut rng);
@@ -867,7 +865,7 @@ impl UndergroundWorker {
let cap = max(1_i64, total / 2);
let mut to_remove = random_ll(1, cap);
let mut rng = rand::thread_rng();
let mut rng = rand::rng();
rows.shuffle(&mut rng);
let mut affected = Vec::new();
@@ -1107,7 +1105,7 @@ impl UndergroundWorker {
}
let pick = *best_indices
.as_slice()
.choose(&mut rand::thread_rng())
.choose(&mut rand::rng())
.unwrap_or(&scored[0].1);
let target = &lover_rows[pick];
@@ -1542,18 +1540,18 @@ fn parse_opt_i32(row: &Row, key: &str) -> Option<i32> {
// Hilfsfunktionen für Zufall und Parsing
fn random_int(lo: i32, hi: i32) -> i32 {
let mut rng = rand::thread_rng();
rng.gen_range(lo..=hi)
let mut rng = rand::rng();
rng.random_range(lo..=hi)
}
fn random_ll(lo: i64, hi: i64) -> i64 {
let mut rng = rand::thread_rng();
rng.gen_range(lo..=hi)
let mut rng = rand::rng();
rng.random_range(lo..=hi)
}
fn random_indices(n: usize, k: usize) -> Vec<usize> {
let mut idx: Vec<usize> = (0..n).collect();
let mut rng = rand::thread_rng();
let mut rng = rand::rng();
idx.shuffle(&mut rng);
if k < idx.len() {
idx.truncate(k);
@@ -1562,8 +1560,8 @@ fn random_indices(n: usize, k: usize) -> Vec<usize> {
}
fn random_double(lo: f64, hi: f64) -> f64 {
let mut rng = rand::thread_rng();
rng.gen_range(lo..hi)
let mut rng = rand::rng();
rng.random_range(lo..hi)
}
fn parse_i32(row: &Row, key: &str, default: i32) -> i32 {
@@ -1620,4 +1618,3 @@ fn change_falukant_user_money(
Ok(())
}

25
src/worker/user_character.rs Normal file → Executable file
View File

@@ -1,9 +1,8 @@
use crate::db::{ConnectionPool, DbError, Rows};
use crate::message_broker::MessageBroker;
use chrono::{Local, NaiveDate};
use rand::distributions::{Distribution, Uniform};
use rand::rngs::StdRng;
use rand::SeedableRng;
use rand::{Rng, SeedableRng};
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant};
@@ -68,7 +67,6 @@ struct Character {
pub struct UserCharacterWorker {
base: BaseWorker,
rng: StdRng,
dist: Uniform<f64>,
last_hourly_run: Option<Instant>,
last_pregnancy_run: Option<Instant>,
/// Letzter Kalendertag, an dem Ehe-Konzeption (oder Legacy-Instant) gelaufen ist — höchstens 1×/Tag.
@@ -86,13 +84,12 @@ pub struct UserCharacterWorker {
impl UserCharacterWorker {
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
let base = BaseWorker::new("UserCharacterWorker", pool, broker);
let rng = StdRng::from_entropy();
let dist = Uniform::from(0.0..1.0);
let mut base_rng = rand::rng();
let rng = StdRng::from_rng(&mut base_rng);
Self {
base,
rng,
dist,
last_hourly_run: None,
last_pregnancy_run: None,
last_marriage_fertility_date: None,
@@ -192,7 +189,7 @@ impl UserCharacterWorker {
Ok(())
}
/// Ehe: Geburten/stale stündlich; Konzeption höchstens einmal pro Kalendertag (ein Jahreswurf, `prob_year`).
/// Ehe/geplante Geburten zeitnah; Konzeption höchstens einmal pro Kalendertag (ein Jahreswurf, `prob_year`).
/// Konzeption setzt `marriage_pregnancy_due_at` (+18 Stunden); Geburt über `QUERY_GET_MARRIAGE_BIRTH_DELIVERIES`.
/// Kein separates „Hochzeitsnacht“-Event — erste Konzeption am nächsten Tageslauf nach der Hochzeit.
fn maybe_run_hourly_pregnancies(&mut self) {
@@ -201,7 +198,7 @@ impl UserCharacterWorker {
let run_hourly = match self.last_pregnancy_run {
None => true,
Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(3600),
Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(60),
};
let run_daily_fertility = self.last_marriage_fertility_date != Some(today);
@@ -301,15 +298,14 @@ impl UserCharacterWorker {
if age >= 45 {
let probability = (0.1 + (age - 45) as f64 * 0.02).min(1.0);
if self.dist.sample(&mut self.rng) < probability {
let damage_dist = Uniform::from(1..=10);
return -damage_dist.sample(&mut self.rng);
if self.rng.random_range(0.0..1.0) < probability {
return -self.rng.random_range(1..=10);
}
return 0;
}
let probability = (age - 30) as f64 / 30.0;
if self.dist.sample(&mut self.rng) < probability {
if self.rng.random_range(0.0..1.0) < probability {
-1
} else {
0
@@ -686,7 +682,7 @@ impl UserCharacterWorker {
.filter(|s| !s.is_empty())
.unwrap_or("marriage");
let gender = if self.dist.sample(&mut self.rng) < 0.5 {
let gender = if self.rng.random_range(0.0..1.0) < 0.5 {
"male"
} else {
"female"
@@ -741,7 +737,7 @@ impl UserCharacterWorker {
return Ok(());
}
let gender = if self.dist.sample(&mut self.rng) < 0.5 {
let gender = if self.rng.random_range(0.0..1.0) < 0.5 {
"male"
} else {
"female"
@@ -1263,4 +1259,3 @@ impl Worker for UserCharacterWorker {
}
}

0
src/worker/value_recalculation.rs Normal file → Executable file
View File

0
src/worker/weather.rs Normal file → Executable file
View File