diff --git a/src/worker/director.rs b/src/worker/director.rs index 1e33269..3a6d14b 100644 --- a/src/worker/director.rs +++ b/src/worker/director.rs @@ -208,9 +208,20 @@ impl DirectorWorker { .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + // Initial: Kumulativen Steuerprozentsatz für Netto-Ranking ermitteln. + conn.prepare("get_director_user", QUERY_GET_DIRECTOR_USER)?; + let user_rows = conn.execute("get_director_user", &[&director.id])?; + let falukant_user_id = user_rows + .first() + .and_then(|row| row.get("falukant_user_id")) + .and_then(|v| v.parse::().ok()) + .unwrap_or(DEFAULT_TREASURY_USER_ID); + let tax_percent = Self::get_cumulative_tax_percent(&mut conn, director.branch_id, falukant_user_id) + .unwrap_or(DEFAULT_TAX_PERCENT); + // Initial: Bestes Produkt für diesen Branch ermitteln conn.prepare("get_to_produce_debug", QUERY_GET_BEST_PRODUCTION_DEBUG)?; - let debug_rows = conn.execute("get_to_produce_debug", &[&director.id, &director.branch_id])?; + let debug_rows = conn.execute("get_to_produce_debug", &[&director.id, &director.branch_id, &tax_percent])?; for (idx, row) in debug_rows.iter().enumerate() { let rank = idx + 1; let product_id = row.get("product_id").map(|s| s.as_str()).unwrap_or("?"); @@ -224,9 +235,12 @@ impl DirectorWorker { let effective_percent = row.get("effective_percent").map(|s| s.as_str()).unwrap_or("?"); let one_piece_cost = row.get("one_piece_cost").map(|s| s.as_str()).unwrap_or("?"); let revenue_piece = row.get("revenue_piece").map(|s| s.as_str()).unwrap_or("?"); + let tax_piece = row.get("tax_piece").map(|s| s.as_str()).unwrap_or("?"); + let net_piece = row.get("net_piece").map(|s| s.as_str()).unwrap_or("?"); + let row_tax_percent = row.get("tax_percent").map(|s| s.as_str()).unwrap_or("?"); let worth = row.get("worth").map(|s| s.as_str()).unwrap_or("?"); eprintln!( - "[DirectorWorker][best_production_debug] director_id={} branch_id={} rank={} product_id={} label={} cat={} prod_time={} sell_cost={} market%={} knowledge(char={},dir={}) effective%={} one_piece_cost={} revenue_piece={} worth={}", + "[DirectorWorker][best_production_debug] director_id={} branch_id={} rank={} product_id={} label={} cat={} prod_time={} sell_cost={} market%={} knowledge(char={},dir={}) effective%={} one_piece_cost={} revenue_piece={} tax_piece={} net_piece={} tax%={} worth={}", director.id, director.branch_id, rank, @@ -241,12 +255,15 @@ impl DirectorWorker { effective_percent, one_piece_cost, revenue_piece, + tax_piece, + net_piece, + row_tax_percent, worth ); } conn.prepare("get_to_produce", QUERY_GET_BEST_PRODUCTION)?; - let rows = conn.execute("get_to_produce", &[&director.id, &director.branch_id])?; + let rows = conn.execute("get_to_produce", &[&director.id, &director.branch_id, &tax_percent])?; if rows.is_empty() { return Ok(()); } diff --git a/src/worker/sql.rs b/src/worker/sql.rs index 5841bdd..57c11bc 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -73,12 +73,14 @@ JOIN falukant_data.branch b ON b.region_id = c.region_id AND b.falukant_user_id WHERE current_time BETWEEN '08:00:00' AND '17:00:00'; "#; -/// Bester Produktstart für den Direktor: -/// Wissen geht additiv in den Markt-Prozentsatz `(worth + (2×Char + Dir)/3) / 100`. -/// Für das Ranking werden dieselben Stückkosten wie im DirectorWorker angesetzt: -/// `raw = 6 + category`, mit Headroom-Rabatt über Zertifikat (`min((cert-cat)*0.035, 0.14)`). -/// Skalierung: `/ (300 × production_time)`. -/// Verkauf/Steuer im laufenden Spiel nutzt weiterhin `DirectorWorker::compute_piece_sell_price` — hier nur Ranking. +/// Bester Produktstart für den Direktor nach erwartbarem **Netto-Gewinn pro Minute**. +/// Erwarteter Erlös/Stück nutzt dieselbe 60%-100%-Bandlogik wie beim Verkauf: +/// `base = sell_cost * market_percent / 100` +/// `quality = clamp((2*knowledge_char + knowledge_dir)/3, 0..100)` +/// `revenue_piece = base * (0.6 + 0.4 * quality/100)` +/// Stückkosten wie im DirectorWorker (`6 + category`, Headroom-Rabatt). +/// Steuerabzug auf Profitanteil über `$3` (kumulativer Steuerprozentsatz für die Branch-Region). +/// Rankinggröße `worth` = `net_piece / production_time`. /// /// `worth_percent`: mit Fahrzeug `MAX` über Filialregionen; ohne Fahrzeug nur Direktor-Region. /// **Ein** Spielercharakter je User (`ORDER BY id DESC LIMIT 1`, `health > 0`). @@ -89,40 +91,96 @@ COALESCE(ftp.category, 1)::int AS product_category, fdb.region_id, 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 = fdb.id), 0) AS used_in_stock, ( ( - ftp.sell_cost - * ( - ( - CASE - WHEN EXISTS ( - SELECT 1 FROM falukant_data.vehicle v - WHERE v.falukant_user_id = fdu.id - ) - THEN bw.max_worth_pct - ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) - END - ) - + ( - 2.0 * COALESCE(fdk_character.knowledge, 0)::float8 - + COALESCE(fdk_director.knowledge, 0)::float8 - ) - / 3.0 - ) - / 100.0 + ( + ( + ftp.sell_cost + * ( + CASE + WHEN EXISTS ( + SELECT 1 FROM falukant_data.vehicle v + WHERE v.falukant_user_id = fdu.id + ) + THEN bw.max_worth_pct + ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) + END + ) / 100.0 + ) * ( + 0.6 + 0.4 * ( + LEAST( + 100.0, + GREATEST( + 0.0, + (2.0 * COALESCE(fdk_character.knowledge, 0)::float8 + + COALESCE(fdk_director.knowledge, 0)::float8) / 3.0 + ) + ) / 100.0 + ) + ) + ) - ( - ( - 6.0 + GREATEST(COALESCE(ftp.category, 1)::float8, 1.0) - ) * ( - 1.0 - LEAST( - GREATEST( - COALESCE(fdu.certificate, 1)::float8 - - GREATEST(COALESCE(ftp.category, 1)::float8, 1.0), - 0.0 - ) * 0.035, - 0.14 - ) - ) - ) - ) / (300.0 * NULLIF(ftp.production_time::float8, 0.0)) + GREATEST( + ( + ( + ( + ( + ftp.sell_cost + * ( + CASE + WHEN EXISTS ( + SELECT 1 FROM falukant_data.vehicle v + WHERE v.falukant_user_id = fdu.id + ) + THEN bw.max_worth_pct + ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) + END + ) / 100.0 + ) * ( + 0.6 + 0.4 * ( + LEAST( + 100.0, + GREATEST( + 0.0, + (2.0 * COALESCE(fdk_character.knowledge, 0)::float8 + + COALESCE(fdk_director.knowledge, 0)::float8) / 3.0 + ) + ) / 100.0 + ) + ) + ) + - ( + ( + 6.0 + GREATEST(COALESCE(ftp.category, 1)::float8, 1.0) + ) * ( + 1.0 - LEAST( + GREATEST( + COALESCE(fdu.certificate, 1)::float8 + - GREATEST(COALESCE(ftp.category, 1)::float8, 1.0), + 0.0 + ) * 0.035, + 0.14 + ) + ) + ) + ), + 0.0 + ) * (COALESCE($3::float8, 0.0) / 100.0) + ) + + ( + ( + 6.0 + GREATEST(COALESCE(ftp.category, 1)::float8, 1.0) + ) * ( + 1.0 - LEAST( + GREATEST( + COALESCE(fdu.certificate, 1)::float8 + - GREATEST(COALESCE(ftp.category, 1)::float8, 1.0), + 0.0 + ) * 0.035, + 0.14 + ) + ) + ) + ) + ) / NULLIF(ftp.production_time::float8, 0.0) ) AS worth, fdb.id AS branch_id, (SELECT COUNT(id) FROM falukant_data.production WHERE branch_id = fdb.id) AS running_productions, COALESCE((SELECT SUM(COALESCE(fdp.quantity, 0)) quantity FROM falukant_data.production fdp WHERE fdp.branch_id = fdb.id), 0) AS running_productions_quantity @@ -162,114 +220,39 @@ WHERE fdd.id = $1 AND fdb.id = $2 ORDER BY worth DESC LIMIT 1; /// Debug-Variante zur Nachvollziehbarkeit der Direktoren-Entscheidung. /// Liefert die Top-5 Kandidaten inkl. Teilmetriken, die in `worth` einfließen. pub const QUERY_GET_BEST_PRODUCTION_DEBUG: &str = r#" +WITH candidates AS ( SELECT ftp.id::text AS product_id, ftp.label_tr, COALESCE(ftp.category, 1)::int::text AS product_category, COALESCE(ftp.production_time, 0)::int::text AS production_time, ROUND(ftp.sell_cost::numeric, 2)::text AS sell_cost, - ROUND( - ( - CASE - WHEN EXISTS ( - SELECT 1 FROM falukant_data.vehicle v - WHERE v.falukant_user_id = fdu.id - ) - THEN bw.max_worth_pct - ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) - END - )::numeric, - 2 - )::text AS market_percent, - COALESCE(fdk_character.knowledge, 0)::int::text AS knowledge_character, - COALESCE(fdk_director.knowledge, 0)::int::text AS knowledge_director, - ROUND( - ( - ( - CASE - WHEN EXISTS ( - SELECT 1 FROM falukant_data.vehicle v - WHERE v.falukant_user_id = fdu.id - ) - THEN bw.max_worth_pct - ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) - END + ( + CASE + WHEN EXISTS ( + SELECT 1 FROM falukant_data.vehicle v + WHERE v.falukant_user_id = fdu.id ) - + (2.0 * COALESCE(fdk_character.knowledge, 0)::float8 + COALESCE(fdk_director.knowledge, 0)::float8) / 3.0 - )::numeric, - 2 - )::text AS effective_percent, - ROUND( + THEN bw.max_worth_pct + ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) + END + ) AS market_percent_val, + COALESCE(fdk_character.knowledge, 0)::float8 AS knowledge_char_val, + COALESCE(fdk_director.knowledge, 0)::float8 AS knowledge_dir_val, + ( ( - ( - 6.0 + GREATEST(COALESCE(ftp.category, 1)::float8, 1.0) - ) * ( - 1.0 - LEAST( - GREATEST( - COALESCE(fdu.certificate, 1)::float8 - - GREATEST(COALESCE(ftp.category, 1)::float8, 1.0), - 0.0 - ) * 0.035, - 0.14 - ) + 6.0 + GREATEST(COALESCE(ftp.category, 1)::float8, 1.0) + ) * ( + 1.0 - LEAST( + GREATEST( + COALESCE(fdu.certificate, 1)::float8 + - GREATEST(COALESCE(ftp.category, 1)::float8, 1.0), + 0.0 + ) * 0.035, + 0.14 ) - )::numeric, - 4 - )::text AS one_piece_cost, - ROUND( - ( - ftp.sell_cost - * ( - ( - CASE - WHEN EXISTS ( - SELECT 1 FROM falukant_data.vehicle v - WHERE v.falukant_user_id = fdu.id - ) - THEN bw.max_worth_pct - ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) - END - ) - + (2.0 * COALESCE(fdk_character.knowledge, 0)::float8 + COALESCE(fdk_director.knowledge, 0)::float8) / 3.0 - ) / 100.0 - )::numeric, - 4 - )::text AS revenue_piece, - ROUND( - ( - ( - ftp.sell_cost - * ( - ( - CASE - WHEN EXISTS ( - SELECT 1 FROM falukant_data.vehicle v - WHERE v.falukant_user_id = fdu.id - ) - THEN bw.max_worth_pct - ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0) - END - ) - + (2.0 * COALESCE(fdk_character.knowledge, 0)::float8 + COALESCE(fdk_director.knowledge, 0)::float8) / 3.0 - ) / 100.0 - - ( - ( - 6.0 + GREATEST(COALESCE(ftp.category, 1)::float8, 1.0) - ) * ( - 1.0 - LEAST( - GREATEST( - COALESCE(fdu.certificate, 1)::float8 - - GREATEST(COALESCE(ftp.category, 1)::float8, 1.0), - 0.0 - ) * 0.035, - 0.14 - ) - ) - ) - ) / (300.0 * NULLIF(ftp.production_time::float8, 0.0)) - )::numeric, - 8 - )::text AS worth + ) + ) AS one_piece_cost_val FROM falukant_data.director fdd JOIN falukant_data.character fdc ON fdc.id = fdd.director_character_id JOIN falukant_data.falukant_user fdu ON fdd.employer_user_id = fdu.id @@ -301,6 +284,23 @@ LEFT JOIN falukant_data.town_product_worth fdtpw_local JOIN falukant_data.knowledge fdk_character ON fdk_character.product_id = ftp.id AND fdk_character.character_id = user_character.id JOIN falukant_data.knowledge fdk_director ON fdk_director.product_id = ftp.id AND fdk_director.character_id = fdd.director_character_id WHERE fdd.id = $1 AND fdb.id = $2 +) +SELECT + product_id, + label_tr, + product_category, + production_time, + sell_cost, + ROUND(market_percent_val::numeric, 2)::text AS market_percent, + ROUND(knowledge_char_val::numeric, 0)::int::text AS knowledge_character, + ROUND(knowledge_dir_val::numeric, 0)::int::text AS knowledge_director, + ROUND(LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0))::numeric, 2)::text AS effective_percent, + ROUND(one_piece_cost_val::numeric, 4)::text AS one_piece_cost, + ROUND((sell_cost::float8 * market_percent_val / 100.0 * (0.6 + 0.4 * (LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0)) / 100.0)))::numeric, 4)::text AS revenue_piece, + ROUND((GREATEST((sell_cost::float8 * market_percent_val / 100.0 * (0.6 + 0.4 * (LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0)) / 100.0)) - one_piece_cost_val), 0.0) * (COALESCE($3::float8, 0.0) / 100.0))::numeric, 4)::text AS tax_piece, + ROUND(((sell_cost::float8 * market_percent_val / 100.0 * (0.6 + 0.4 * (LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0)) / 100.0))) - (GREATEST((sell_cost::float8 * market_percent_val / 100.0 * (0.6 + 0.4 * (LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0)) / 100.0)) - one_piece_cost_val), 0.0) * (COALESCE($3::float8, 0.0) / 100.0)) - one_piece_cost_val)::numeric, 4)::text AS net_piece, + ROUND(COALESCE($3::numeric, 0), 2)::text AS tax_percent, + ROUND((((sell_cost::float8 * market_percent_val / 100.0 * (0.6 + 0.4 * (LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0)) / 100.0))) - (GREATEST((sell_cost::float8 * market_percent_val / 100.0 * (0.6 + 0.4 * (LEAST(100.0, GREATEST(0.0, (2.0 * knowledge_char_val + knowledge_dir_val) / 3.0)) / 100.0)) - one_piece_cost_val), 0.0) * (COALESCE($3::float8, 0.0) / 100.0)) - one_piece_cost_val) / NULLIF(production_time::float8, 0.0))::numeric, 8)::text AS worth ORDER BY worth DESC LIMIT 5; "#;