diff --git a/docs/FALUKANT_DIRECTOR_PRODUCTION_COST.md b/docs/FALUKANT_DIRECTOR_PRODUCTION_COST.md index 7d27d6c..4cdc497 100644 --- a/docs/FALUKANT_DIRECTOR_PRODUCTION_COST.md +++ b/docs/FALUKANT_DIRECTOR_PRODUCTION_COST.md @@ -1,40 +1,30 @@ # Director: Stückkosten Produktion (Balancing) -## Problem +## Modell -Die DB wählt das „beste“ Produkt über `QUERY_GET_BEST_PRODUCTION` (Worth-Formel mit `- 6 * ftp.category`), während der Daemon früher **nur** `certificate * 6` pro Stück abgebucht hat – **ohne** die Produktklasse. Dadurch konnten Stückkosten und Worth-Ranking auseinanderlaufen. +- **`falukant_user.certificate`** begrenzt nur, **welche** Produkte wählbar sind (`ftp.category <= certificate` in `QUERY_GET_BEST_PRODUCTION`). Es gibt **keine** höheren Stückkosten nur wegen eines höheren Zertifikats. +- Die **Stückkosten** hängen von der **Produktklasse** (`falukant_type.product.category`) und einer **Basis** ab; optional **Headroom-Rabatt**, wenn das Zertifikat über der Klasse des produzierten Guts liegt. -## Aktuelle Formel (Rust, `DirectorWorker::calc_one_piece_cost`) +## Formel (Rust, `DirectorWorker`) ``` -raw = certificate × PRODUCTION_COST_PER_CERT_LEVEL - + product_category × PRODUCTION_COST_PER_PRODUCT_CATEGORY -effektiv = raw × (1 − headroom_discount) +raw = PRODUCTION_COST_BASE + product_category × PRODUCTION_COST_PER_PRODUCT_CATEGORY +headroom = max(0, certificate − product_category) +discount = min(headroom × PRODUCTION_HEADROOM_DISCOUNT_PER_STEP, PRODUCTION_HEADROOM_DISCOUNT_CAP) +effektiv = raw × (1 − discount) ``` -- `product_category` kommt aus `QUERY_GET_BEST_PRODUCTION` (`ftp.category`). -- **Headroom** = `max(0, certificate − product_category)` - Wenn du **nicht** am Klassenlimit produzierst (Zertifikat höher als nötig für das Produkt), gibt es einen kleinen Rabatt (Effizienz / Reserve). - -Konstanten in `src/worker/director.rs` (anpassen zum Feintuning): - | Konstante | Standard | Bedeutung | |-----------|----------|-----------| -| `PRODUCTION_COST_PER_CERT_LEVEL` | `6.0` | wie früher `×6` pro Zertifikatsstufe | -| `PRODUCTION_COST_PER_PRODUCT_CATEGORY` | `1.0` | Material pro Produktklasse | +| `PRODUCTION_COST_BASE` | `6.0` | fixer Basisanteil pro Stück | +| `PRODUCTION_COST_PER_PRODUCT_CATEGORY` | `1.0` | Material / Komplexität je Produktklasse | | `PRODUCTION_HEADROOM_DISCOUNT_PER_STEP` | `0.035` | Rabatt pro Headroom-Stufe | | `PRODUCTION_HEADROOM_DISCOUNT_CAP` | `0.14` | maximaler Gesamtrabatt | -## Spielerfortschritt - -- **Höheres Zertifikat** allein erhöht die Basis-Stückkosten linear – entspricht **besserer** Produktpalette (`ftp.category <= certificate` in SQL). -- **Höhere Produktklasse** erhöht `raw` über `product_category` (bessere Ware = mehr Material). -- **Zertifikat über der Produktklasse** (Headroom) senkt die effektiven Kosten – Belohnung, wenn du nicht immer nur am Limit produzierst. - ## Worth in SQL -Die Worth-Zeile in `QUERY_GET_BEST_PRODUCTION` sollte bei größeren Formeländerungen **mit** angepasst werden, damit der Director weiterhin sinnvoll sortiert. +`QUERY_GET_BEST_PRODUCTION` sortiert nach einer Worth-Zeile (u. a. `- 6 * ftp.category`). Bei größeren Formeländerungen Worth **mit** anpassen, damit der Director weiterhin sinnvoll sortiert. ## Parallelproduktionen -`MAX_PARALLEL_PRODUCTIONS` (aktuell 2) bestimmt, wie viele Linien pro Tick Geld binden – unabhängig von der Stückkostenformel; bei Liquiditätsproblemen ggf. auf `1` setzen. +`MAX_PARALLEL_PRODUCTIONS` (aktuell 2) bestimmt, wie viele Linien pro Tick Geld binden — unabhängig von der Stückkostenformel. diff --git a/src/worker/director.rs b/src/worker/director.rs index 837f791..1dc55f3 100644 --- a/src/worker/director.rs +++ b/src/worker/director.rs @@ -31,7 +31,7 @@ use crate::worker::sql::{ QUERY_GET_BRANCH_REGION, QUERY_GET_AVERAGE_WORTH, QUERY_UPDATE_INVENTORY_QTY, - QUERY_GET_PRODUCT_COST, + QUERY_GET_PRODUCT_CATEGORY_AND_USER_CERTIFICATE, QUERY_GET_USER_OFFICES, QUERY_CUMULATIVE_TAX_NO_EXEMPT, QUERY_CUMULATIVE_TAX_WITH_EXEMPT, @@ -51,6 +51,7 @@ struct ProductionPlan { falukant_user_id: i32, money: f64, certificate: i32, + product_category: i32, branch_id: i32, product_id: i32, region_id: i32, @@ -94,6 +95,14 @@ pub struct DirectorWorker { // Maximale Anzahl paralleler Produktionen pro Branch const MAX_PARALLEL_PRODUCTIONS: i32 = 2; +// Stückkosten: abhängig von der **Produktklasse**, nicht vom Zertifikat als Linearfaktor. +// Höheres Zertifikat erweitert nur die Produktpalette (`ftp.category <= certificate`); optional +// Headroom-Rabatt, wenn Zertifikat über der Klasse des gewählten Produkts liegt. +const PRODUCTION_COST_BASE: f64 = 6.0; +const PRODUCTION_COST_PER_PRODUCT_CATEGORY: f64 = 1.0; +const PRODUCTION_HEADROOM_DISCOUNT_PER_STEP: f64 = 0.035; +const PRODUCTION_HEADROOM_DISCOUNT_CAP: f64 = 0.14; + // ...existing code... // Verfügbare Transportmittel für eine Route (source_region -> target_region) @@ -284,6 +293,10 @@ impl DirectorWorker { // Pflichtfelder: ohne diese können wir keinen sinnvollen Plan erstellen. let falukant_user_id: i32 = row.get("falukant_user_id")?.parse().ok()?; let certificate: i32 = row.get("certificate")?.parse().ok()?; + let product_category: i32 = row + .get("product_category") + .and_then(|v| v.parse::().ok()) + .unwrap_or(1); let branch_id: i32 = row.get("branch_id")?.parse().ok()?; let product_id: i32 = row.get("product_id")?.parse().ok()?; @@ -318,6 +331,7 @@ impl DirectorWorker { falukant_user_id, money, certificate, + product_category, branch_id, product_id, region_id, @@ -396,8 +410,19 @@ impl DirectorWorker { } + /// Stück-Einstand: Basis + Material je Produktklasse; Zertifikat **erhöht** die Kosten nicht. + /// Headroom (`certificate > product_category`) senkt leicht (Erfahrung/Reserve). fn calc_one_piece_cost(plan: &ProductionPlan) -> f64 { - (plan.certificate * 6) as f64 + Self::piece_production_cost(plan.product_category, plan.certificate) + } + + fn piece_production_cost(product_category: i32, certificate: i32) -> f64 { + let cat = product_category.max(1); + let cert = certificate.max(1); + let raw = PRODUCTION_COST_BASE + (cat as f64) * PRODUCTION_COST_PER_PRODUCT_CATEGORY; + let headroom = (cert - cat).max(0); + let discount = ((headroom as f64) * PRODUCTION_HEADROOM_DISCOUNT_PER_STEP).min(PRODUCTION_HEADROOM_DISCOUNT_CAP); + raw * (1.0 - discount) } fn calc_max_money_production(plan: &ProductionPlan, one_piece_cost: f64) -> i32 { @@ -641,15 +666,25 @@ impl DirectorWorker { min_price + (max_price - min_price) * (knowledge_factor / 100.0) } - // 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", QUERY_GET_PRODUCT_COST)?; - let rows = conn.execute("get_product_cost", &[&product_id])?; - if let Some(row) = rows.first() - && let Some(sc) = row.get("sell_cost") - && let Ok(v) = sc.parse::() - { - return Ok(v); + /// Stückkosten für Steuer-/Marge beim Verkauf — gleiche Logik wie bei Produktionsstart. + fn resolve_production_piece_cost( + conn: &mut DbConnection, + product_id: i32, + falukant_user_id: i32, + fallback: f64, + ) -> Result { + conn.prepare("get_cat_cert", QUERY_GET_PRODUCT_CATEGORY_AND_USER_CERTIFICATE)?; + let rows = conn.execute("get_cat_cert", &[&product_id, &falukant_user_id])?; + if let Some(row) = rows.first() { + let cat = row + .get("category") + .and_then(|v| v.parse::().ok()) + .unwrap_or(1); + let cert = row + .get("certificate") + .and_then(|v| v.parse::().ok()) + .unwrap_or(1); + return Ok(Self::piece_production_cost(cat, cert)); } Ok(fallback) } @@ -733,7 +768,12 @@ impl DirectorWorker { let piece_price = Self::compute_piece_sell_price(item); let sell_price = piece_price * item.quantity as f64; - let one_piece_cost = Self::resolve_one_piece_cost(conn, item.product_id, item.sell_cost)?; + let one_piece_cost = Self::resolve_production_piece_cost( + conn, + item.product_id, + item.user_id, + item.sell_cost, + )?; let cumulative_tax_percent = Self::get_cumulative_tax_percent(conn, item.branch_id, item.user_id)?; let revenue_cents = (sell_price * 100.0).round() as i64; diff --git a/src/worker/sql.rs b/src/worker/sql.rs index 9903ab6..f7cc075 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -48,11 +48,22 @@ INSERT INTO falukant_log.notification (user_id, tr, shown, created_at, updated_a VALUES ($1, $2, FALSE, NOW(), NOW()); "#; -// Product pricing +// Product pricing (nur sell_cost; für Produktions-Stückkosten siehe QUERY_GET_PRODUCT_CATEGORY_AND_USER_CERTIFICATE) +#[allow(dead_code)] pub const QUERY_GET_PRODUCT_COST: &str = r#" SELECT sell_cost FROM falukant_type.product WHERE id = $1; "#; +/// Produktklasse + Spieler-Zertifikat für Stückkosten (kein „teurer wegen höherem Zertifikat“). +pub const QUERY_GET_PRODUCT_CATEGORY_AND_USER_CERTIFICATE: &str = r#" + SELECT COALESCE(p.category, 1)::int AS category, + COALESCE(u.certificate, 1)::int AS certificate + FROM falukant_type.product p + CROSS JOIN falukant_data.falukant_user u + WHERE p.id = $1::int + AND u.id = $2::int; +"#; + 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 @@ -63,7 +74,8 @@ WHERE current_time BETWEEN '08:00:00' AND '17:00:00'; "#; pub const QUERY_GET_BEST_PRODUCTION: &str = r#" -SELECT fdu.id falukant_user_id, CAST(fdu.money AS text) AS money, fdu.certificate, ftp.id product_id, ftp.label_tr, fdb.region_id, +SELECT fdu.id falukant_user_id, CAST(fdu.money AS text) AS money, fdu.certificate, ftp.id product_id, ftp.label_tr, +COALESCE(ftp.category, 1)::int AS product_category, fdb.region_id, (SELECT SUM(quantity) FROM falukant_data.stock fds WHERE fds.branch_id = fdb.id) 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 = fdb.id), 0) AS used_in_stock, (ftp.sell_cost * (fdtpw.worth_percent + (fdk_character.knowledge * 2 + fdk_director.knowledge) / 3) / 100 - 6 * ftp.category) / (300.0 * ftp.production_time) AS worth,