diff --git a/docs/FALUKANT_TRANSPORT_RAID_DAEMON.md b/docs/FALUKANT_TRANSPORT_RAID_DAEMON.md new file mode 100644 index 0000000..f94e6fc --- /dev/null +++ b/docs/FALUKANT_TRANSPORT_RAID_DAEMON.md @@ -0,0 +1,41 @@ +# Falukant: Transportüberfälle & Wachen (Daemon) + +## Migration + +`migrations/008_falukant_transport_raid.sql`: + +- `falukant_data.transport.guard_count` (Standard `0`) +- Eintrag `falukant_type.underground.tr = 'raid_transport'` + +## Code + +| Komponente | Datei | +|------------|--------| +| Tick (ca. 60 s) | `src/worker/falukant_transport_raid.rs` → `run_tick` | +| Aufruf | `src/worker/underground.rs` (`UndergroundWorker::tick`, vor den 1-Tage-Jobs) | +| SQL | `src/worker/sql.rs` (`QUERY_RAID_*`) | + +## Logik (V1) + +1. Offene Untergrundaufträge `raid_transport` mit `result IS NULL` (ohne 24h-Wartezeit). +2. Parameter: `regionId`, `bandSize` (JSON, auch `region_id` / `band_size`). +3. Region muss `falukant_type.region.id IN (4,5)` und `label_tr <> 'town'` erfüllen. +4. Kandidaten-Transporte: noch unterwegs, `size >= 2`, Fracht, Route berührt die Region (`source` oder `target`), **nicht** vom Auftraggeber. +5. Begegnung: Zufall mit von Bandengröße und Ladungsgröße abhängiger Wahrscheinlichkeit. +6. Kampf: `raidPower` vs. `guardPower` (Wachen aus `transport.guard_count`) → `repelled` | `partial_success` | `major_success`. +7. Beute: Anteil der Menge, **nie** die volle Ladung; zusätzlicher Verlust beim Abtransport ins Lager (`lostDueToStorage`). +8. Einlagerung: bevorzugt Niederlassung **in derselben Region**, sonst erste Branch-ID des Users. +9. Opfer: reduzierte `reputation`, verkleinerte Transportmenge. + +## Events + +| Event | Empfänger | +|-------|-----------| +| `falukantTransportRaid` | Opfer (`reason`: `transport_raided`) | +| `falukantUndergroundUpdate` | Auftraggeber (`raid_*`) | +| `falukantUpdateStatus` | beide | +| `falukantBranchUpdate` | beide | + +## Backend / UI + +Projektseitig: API für `raid_transport`, `guardCount` am Transport, Formularfilter für Regionen. Dieses Dokument beschreibt nur den **Daemon**. diff --git a/docs/FALUKANT_UI_WEBSOCKET.md b/docs/FALUKANT_UI_WEBSOCKET.md index fa98188..23b67ce 100644 --- a/docs/FALUKANT_UI_WEBSOCKET.md +++ b/docs/FALUKANT_UI_WEBSOCKET.md @@ -190,6 +190,18 @@ Konkrete Routen stehen im **YourPart3**-Backend; das Frontend sollte eine zentra --- +### Transportüberfälle (`raid_transport`) + +| Event | Payload (Auszug) | +|-------|-------------------| +| `falukantTransportRaid` | `user_id` (Opfer), `reason`: `transport_raided` | +| `falukantUndergroundUpdate` | `user_id` (Auftraggeber), `reason`: `raid_repelled`, `raid_success`, `raid_partial_success`, `raid_loot_stored` | +| `falukantUpdateStatus` / `falukantBranchUpdate` | beide Seiten nach Überfall | + +Daemon: `src/worker/falukant_transport_raid.rs`, Doku: `docs/FALUKANT_TRANSPORT_RAID_DAEMON.md`. + +--- + ## 6. Bezug zum Code (YpDaemon) - Worker: `src/worker/falukant_family.rs` diff --git a/migrations/008_falukant_transport_raid.sql b/migrations/008_falukant_transport_raid.sql new file mode 100644 index 0000000..d6a8188 --- /dev/null +++ b/migrations/008_falukant_transport_raid.sql @@ -0,0 +1,14 @@ +-- Transportwachen + Untergrundtyp Überfall auf Transporte (Daemon-Auflösung). +-- Siehe docs/FALUKANT_TRANSPORT_RAID_DAEMON.md + +ALTER TABLE falukant_data.transport + ADD COLUMN IF NOT EXISTS guard_count INTEGER NOT NULL DEFAULT 0; + +COMMENT ON COLUMN falukant_data.transport.guard_count IS 'Mitgeschickte Wachen (reduzieren Überfallchance/Beute im Daemon)'; + +-- Untergrundtyp (falukant_type.underground: Spalte tr = Schlüssel in falukant_data.underground.underground_type_id) +INSERT INTO falukant_type.underground (tr, name) +SELECT 'raid_transport', 'Überfälle auf Transporte' +WHERE NOT EXISTS ( + SELECT 1 FROM falukant_type.underground WHERE tr = 'raid_transport' +); diff --git a/src/worker/falukant_transport_raid.rs b/src/worker/falukant_transport_raid.rs new file mode 100644 index 0000000..4be6dc5 --- /dev/null +++ b/src/worker/falukant_transport_raid.rs @@ -0,0 +1,505 @@ +//! Transportüberfälle (`raid_transport`) und Wachen (`transport.guard_count`). +//! Siehe `docs/FALUKANT_TRANSPORT_RAID_DAEMON.md`. +//! +//! Läuft im Underground-Tick (ca. 60 s). Kein 1-Tage-Wartezeit wie andere Untergrundjobs. + +use crate::db::{ConnectionPool, DbError, Row}; +use crate::message_broker::MessageBroker; +use rand::Rng; +use serde_json::{json, Value as Json}; + +use crate::worker::sql::{ + QUERY_GET_AVAILABLE_STOCKS, QUERY_INSERT_INVENTORY, QUERY_RAID_ACTIVE_UNDERGROUND, + QUERY_RAID_APP_USER_FOR_FALUKANT, QUERY_RAID_CANDIDATE_TRANSPORTS, + QUERY_RAID_FALUKANT_USER_FOR_CHARACTER, QUERY_RAID_NEAREST_BRANCH_FOR_USER, + QUERY_RAID_REGION_ALLOWED, QUERY_RAID_SCHEMA_READY, QUERY_RAID_SUBTRACT_REP_BY_USER, + QUERY_RAID_UPDATE_UNDERGROUND_RESULT, QUERY_UPDATE_TRANSPORT_SIZE, +}; + +/// Wird aus `UndergroundWorker::tick` aufgerufen. +pub fn run_tick(pool: &ConnectionPool, broker: &MessageBroker) -> Result<(), DbError> { + if !schema_ready(pool) { + return Ok(()); + } + + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("raid_active", QUERY_RAID_ACTIVE_UNDERGROUND) + .map_err(|e| DbError::new(format!("prepare raid_active: {e}")))?; + + let rows = conn + .execute("raid_active", &[]) + .map_err(|e| DbError::new(format!("exec raid_active: {e}")))?; + + drop(conn); + + for row in rows { + let ug_id = parse_i32(&row, "id", -1); + let performer_cid = parse_i32(&row, "performer_id", -1); + let params_str = row.get("parameters").cloned().unwrap_or_else(|| "{}".into()); + if ug_id < 0 || performer_cid < 0 { + continue; + } + + if let Err(e) = try_resolve_one_raid( + pool, + broker, + ug_id, + performer_cid, + ¶ms_str, + ) { + eprintln!("[falukant_transport_raid] underground_id={ug_id}: {e}"); + } + } + + Ok(()) +} + +fn schema_ready(pool: &ConnectionPool) -> bool { + let Ok(mut conn) = pool.get() else { + return false; + }; + if conn.prepare("rs", QUERY_RAID_SCHEMA_READY).is_err() { + return false; + } + let Ok(rows) = conn.execute("rs", &[]) else { + return false; + }; + rows.first() + .and_then(|r| r.get("ready")) + .map(|v| v == "t" || v == "true") + .unwrap_or(false) +} + +fn try_resolve_one_raid( + pool: &ConnectionPool, + broker: &MessageBroker, + underground_id: i32, + performer_character_id: i32, + params_json: &str, +) -> Result<(), DbError> { + let p: Json = serde_json::from_str(params_json).unwrap_or_else(|_| json!({})); + let region_id = p + .get("regionId") + .or_else(|| p.get("region_id")) + .and_then(|v| v.as_i64()) + .map(|x| x as i32) + .unwrap_or(-1); + let band_size = p + .get("bandSize") + .or_else(|| p.get("band_size")) + .and_then(|v| v.as_i64()) + .map(|x| x as i32) + .unwrap_or(3) + .clamp(1, 50); + + if region_id < 0 { + return finish_invalid( + pool, + broker, + underground_id, + performer_character_id, + "invalid_parameters", + json!({"message": "regionId required"}), + ); + } + + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare("raid_reg", QUERY_RAID_REGION_ALLOWED) + .map_err(|e| DbError::new(format!("prepare raid_reg: {e}")))?; + let ok = conn + .execute("raid_reg", &[®ion_id]) + .map_err(|e| DbError::new(format!("exec raid_reg: {e}")))?; + if ok.is_empty() { + return finish_invalid( + pool, + broker, + underground_id, + performer_character_id, + "invalid_region", + json!({"message": "region not allowed for raid_transport"}), + ); + } + + conn.prepare("raid_fu", QUERY_RAID_FALUKANT_USER_FOR_CHARACTER) + .map_err(|e| DbError::new(format!("prepare raid_fu: {e}")))?; + let fu_rows = conn + .execute("raid_fu", &[&performer_character_id]) + .map_err(|e| DbError::new(format!("exec raid_fu: {e}")))?; + let (performer_falukant_id, performer_app_uid) = fu_rows + .first() + .map(|r| { + ( + parse_i32(r, "falukant_user_id", -1), + parse_i32(r, "app_user_id", -1), + ) + }) + .unwrap_or((-1, -1)); + if performer_falukant_id < 0 { + return finish_invalid( + pool, + broker, + underground_id, + performer_character_id, + "performer_not_found", + json!({}), + ); + } + + drop(conn); + + conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare("raid_cand", QUERY_RAID_CANDIDATE_TRANSPORTS) + .map_err(|e| DbError::new(format!("prepare raid_cand: {e}")))?; + let candidates = conn + .execute("raid_cand", &[®ion_id, &performer_falukant_id]) + .map_err(|e| DbError::new(format!("exec raid_cand: {e}")))?; + + if candidates.is_empty() { + return Ok(()); + } + + let cand = &candidates[0]; + let transport_id = parse_i32(cand, "transport_id", -1); + let transport_size = parse_i32(cand, "transport_size", 0); + let product_id = parse_i32(cand, "product_id", -1); + let guard_count = parse_i32(cand, "guard_count", 0).max(0); + let victim_fu = parse_i32(cand, "victim_falukant_user_id", -1); + let vehicle_cap = parse_i32(cand, "vehicle_capacity", 1).max(1); + + if transport_id < 0 || product_id < 0 || victim_fu < 0 || transport_size < 2 { + return Ok(()); + } + + let mut rng = rand::thread_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) { + return Ok(()); + } + + // Kampf + let raid_power = band_size + rng.gen_range(0..=band_size); + let gmax = guard_count.max(1); + let guard_power = guard_count + rng.gen_range(0..=gmax); + + let outcome = if raid_power > (guard_power as f64 * 1.25) as i32 { + "major_success" + } else if raid_power > guard_power { + "partial_success" + } else { + "repelled" + }; + + if outcome == "repelled" { + let result = json!({ + "status": "completed", + "bandSize": band_size, + "regionId": region_id, + "attempts": 1, + "successes": 0, + "lastTargetTransportId": transport_id, + "lastLoot": Json::Null, + "lastOutcome": "repelled" + }); + save_result(pool, underground_id, &result)?; + publish_underground(broker, performer_app_uid, "raid_repelled"); + publish_status(broker, performer_app_uid); + return Ok(()); + } + + // Beuteanteil (nie Totalverlust) + let base_loot = match outcome { + "major_success" => rng.gen_range(0.35..0.60), + _ => rng.gen_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 mut to_store = (stolen as f64 * carry_loss).floor() as i32; + to_store = to_store.max(0).min(stolen); + + // Nächstgelegenes Lager + let (branch_id, _) = nearest_branch(pool, region_id, performer_falukant_id)?; + let (stored, lost_storage) = if branch_id > 0 && to_store > 0 { + let remaining = add_to_branch_inventory(pool, branch_id, product_id, to_store)?; + (to_store - remaining, remaining) + } else { + (0, to_store) + }; + + let new_size = transport_size - stolen; + if new_size < 1 { + return Err(DbError::new("raid: invalid new transport size")); + } + + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare("upd_sz", QUERY_UPDATE_TRANSPORT_SIZE) + .map_err(|e| DbError::new(format!("prepare upd_sz: {e}")))?; + conn.execute("upd_sz", &[&transport_id, &new_size]) + .map_err(|e| DbError::new(format!("exec upd_sz: {e}")))?; + + // Opfer: kleiner Rufabzug + conn.prepare("raid_rep", QUERY_RAID_SUBTRACT_REP_BY_USER) + .map_err(|e| DbError::new(format!("prepare raid_rep: {e}")))?; + let _ = conn.execute("raid_rep", &[&victim_fu, &2_i32]); + + drop(conn); + + let victim_app_uid = app_user_for_falukant(pool, victim_fu)?; + + let result = json!({ + "status": "completed", + "bandSize": band_size, + "regionId": region_id, + "attempts": 1, + "successes": 1, + "lastTargetTransportId": transport_id, + "lastLoot": { + "productId": product_id, + "stolenFromTransport": stolen, + "stored": stored, + "lostDueToStorage": lost_storage, + "branchId": branch_id + }, + "lastOutcome": outcome + }); + + save_result(pool, underground_id, &result)?; + + if victim_app_uid > 0 { + publish_transport_raid(broker, victim_app_uid, "transport_raided"); + publish_status(broker, victim_app_uid); + publish_branch(broker, victim_app_uid); + } + + let ug_reason = match outcome { + "major_success" => "raid_success", + _ => "raid_partial_success", + }; + publish_underground(broker, performer_app_uid, ug_reason); + if stored > 0 { + publish_underground(broker, performer_app_uid, "raid_loot_stored"); + } + publish_status(broker, performer_app_uid); + publish_branch(broker, performer_app_uid); + + Ok(()) +} + +fn finish_invalid( + pool: &ConnectionPool, + broker: &MessageBroker, + underground_id: i32, + performer_character_id: i32, + code: &str, + detail: Json, +) -> Result<(), DbError> { + let performer_app_uid = falukant_user_row(pool, performer_character_id) + .map(|(_, app)| app) + .unwrap_or(-1); + + let result = json!({ + "status": "error", + "code": code, + "detail": detail + }); + save_result(pool, underground_id, &result)?; + if performer_app_uid > 0 { + publish_underground(broker, performer_app_uid, "raid_repelled"); + publish_status(broker, performer_app_uid); + } + Ok(()) +} + +fn falukant_user_row(pool: &ConnectionPool, character_id: i32) -> Result<(i32, i32), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare("fu", QUERY_RAID_FALUKANT_USER_FOR_CHARACTER) + .map_err(|e| DbError::new(format!("prepare fu: {e}")))?; + let rows = conn + .execute("fu", &[&character_id]) + .map_err(|e| DbError::new(format!("exec fu: {e}")))?; + rows.first() + .map(|r| { + ( + parse_i32(r, "falukant_user_id", -1), + parse_i32(r, "app_user_id", -1), + ) + }) + .ok_or_else(|| DbError::new("character not found")) +} + +fn app_user_for_falukant(pool: &ConnectionPool, falukant_user_id: i32) -> Result { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare("au", QUERY_RAID_APP_USER_FOR_FALUKANT) + .map_err(|e| DbError::new(format!("prepare au: {e}")))?; + let rows = conn + .execute("au", &[&falukant_user_id]) + .map_err(|e| DbError::new(format!("exec au: {e}")))?; + Ok(rows + .first() + .map(|r| parse_i32(r, "user_id", -1)) + .unwrap_or(-1)) +} + +fn nearest_branch( + pool: &ConnectionPool, + raid_region_id: i32, + performer_falukant_id: i32, +) -> Result<(i32, i32), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare("nb", QUERY_RAID_NEAREST_BRANCH_FOR_USER) + .map_err(|e| DbError::new(format!("prepare nb: {e}")))?; + let rows = conn + .execute("nb", &[&raid_region_id, &performer_falukant_id]) + .map_err(|e| DbError::new(format!("exec nb: {e}")))?; + let branch_id = rows + .first() + .map(|r| parse_i32(r, "branch_id", -1)) + .unwrap_or(-1); + let reg = rows + .first() + .map(|r| parse_i32(r, "region_id", -1)) + .unwrap_or(-1); + Ok((branch_id, reg)) +} + +fn add_to_branch_inventory( + pool: &ConnectionPool, + branch_id: i32, + product_id: i32, + quantity: i32, +) -> Result { + use std::cmp::min; + + let stocks = load_stocks(pool, branch_id)?; + let mut remaining = quantity; + for s in stocks { + if remaining <= 0 { + break; + } + let free = s.total_capacity - s.filled; + if free <= 0 { + continue; + } + let to_store = min(remaining, free); + store_in_stock(pool, s.stock_id, product_id, to_store)?; + remaining -= to_store; + } + Ok(remaining) +} + +struct StockInfo { + stock_id: i32, + total_capacity: i32, + filled: i32, +} + +fn load_stocks(pool: &ConnectionPool, branch_id: i32) -> Result, DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare("gs", QUERY_GET_AVAILABLE_STOCKS) + .map_err(|e| DbError::new(format!("prepare gs: {e}")))?; + let rows = conn + .execute("gs", &[&branch_id]) + .map_err(|e| DbError::new(format!("exec gs: {e}")))?; + let mut out = Vec::new(); + for row in rows { + let stock_id = parse_i32(&row, "id", -1); + let total_capacity = parse_i32(&row, "total_capacity", 0); + let filled = parse_i32(&row, "filled", 0); + if stock_id >= 0 { + out.push(StockInfo { + stock_id, + total_capacity, + filled, + }); + } + } + Ok(out) +} + +fn store_in_stock(pool: &ConnectionPool, stock_id: i32, product_id: i32, quantity: i32) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare("ins", QUERY_INSERT_INVENTORY) + .map_err(|e| DbError::new(format!("prepare ins: {e}")))?; + let quality: i32 = 100; + conn.execute("ins", &[&stock_id, &product_id, &quantity, &quality]) + .map_err(|e| DbError::new(format!("exec ins: {e}")))?; + Ok(()) +} + +fn save_result(pool: &ConnectionPool, underground_id: i32, result: &Json) -> Result<(), DbError> { + let s = result.to_string(); + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare("ur", QUERY_RAID_UPDATE_UNDERGROUND_RESULT) + .map_err(|e| DbError::new(format!("prepare ur: {e}")))?; + conn.execute("ur", &[&underground_id, &s]) + .map_err(|e| DbError::new(format!("exec ur: {e}")))?; + Ok(()) +} + +fn publish_transport_raid(broker: &MessageBroker, user_id: i32, reason: &str) { + let m = format!( + r#"{{"event":"falukantTransportRaid","user_id":{},"reason":"{}"}}"#, + user_id, reason + ); + broker.publish(m); +} + +fn publish_underground(broker: &MessageBroker, user_id: i32, reason: &str) { + if user_id <= 0 { + return; + } + let m = format!( + r#"{{"event":"falukantUndergroundUpdate","user_id":{},"reason":"{}"}}"#, + user_id, reason + ); + broker.publish(m); +} + +fn publish_status(broker: &MessageBroker, user_id: i32) { + if user_id <= 0 { + return; + } + let m = format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id); + broker.publish(m); +} + +fn publish_branch(broker: &MessageBroker, user_id: i32) { + if user_id <= 0 { + return; + } + let m = format!(r#"{{"event":"falukantBranchUpdate","user_id":{}}}"#, user_id); + broker.publish(m); +} + +fn parse_i32(row: &Row, key: &str, default: i32) -> i32 { + row.get(key) + .and_then(|v| v.parse::().ok()) + .unwrap_or(default) +} diff --git a/src/worker/mod.rs b/src/worker/mod.rs index ca469af..773facd 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -15,6 +15,7 @@ mod falukant_family; mod falukant_certificate; mod falukant_servants; mod falukant_debtors; +mod falukant_transport_raid; mod sql; pub use base::Worker; diff --git a/src/worker/sql.rs b/src/worker/sql.rs index 652857d..26fc19d 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -276,6 +276,106 @@ UPDATE falukant_data.transport WHERE id = $1; "#; +// --- Falukant: Transportüberfälle (docs/FALUKANT_TRANSPORT_RAID_DAEMON.md) --- +pub const QUERY_RAID_SCHEMA_READY: &str = r#" +SELECT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'falukant_data' + AND table_name = 'transport' + AND column_name = 'guard_count' +) AS ready; +"#; + +pub const QUERY_RAID_ACTIVE_UNDERGROUND: &str = r#" +SELECT u.id, + u.performer_id, + COALESCE(u.parameters::text, '{}') AS parameters + FROM falukant_data.underground u + JOIN falukant_type.underground t ON t.tr = u.underground_type_id + WHERE u.result IS NULL + AND t.tr = 'raid_transport' + ORDER BY u.created_at ASC + LIMIT 100; +"#; + +pub const QUERY_RAID_REGION_ALLOWED: &str = r#" +SELECT r.id + FROM falukant_data.region r + JOIN falukant_type.region rt ON rt.id = r.region_type_id + WHERE r.id = $1::int + AND rt.id IN (4, 5) + AND COALESCE(rt.label_tr, '') <> 'town'; +"#; + +pub const QUERY_RAID_FALUKANT_USER_FOR_CHARACTER: &str = r#" +SELECT fu.id AS falukant_user_id, fu.user_id AS app_user_id + FROM falukant_data.character c + JOIN falukant_data.falukant_user fu ON fu.user_id = c.user_id + WHERE c.id = $1::int + LIMIT 1; +"#; + +/// Aktive (noch unterwegs) Transporte mit Fracht, Route berührt Region, nicht vom Auftraggeber. +pub const QUERY_RAID_CANDIDATE_TRANSPORTS: &str = r#" +SELECT + t.id AS transport_id, + t.size AS transport_size, + t.product_id, + COALESCE(t.guard_count, 0)::int AS guard_count, + v.falukant_user_id AS victim_falukant_user_id, + vt.capacity AS vehicle_capacity +FROM falukant_data.transport t +JOIN falukant_data.vehicle v ON v.id = t.vehicle_id +JOIN falukant_type.vehicle vt ON vt.id = v.vehicle_type_id +JOIN falukant_data.region_distance rd + ON ( + (rd.source_region_id = t.source_region_id AND rd.target_region_id = t.target_region_id) + OR (rd.source_region_id = t.target_region_id AND rd.target_region_id = t.source_region_id) + ) + AND (rd.transport_mode = vt.transport_mode OR rd.transport_mode IS NULL) +WHERE t.product_id IS NOT NULL + AND t.size > 0 + AND vt.speed > 0 + AND t.created_at + (rd.distance / vt.speed::double precision) * INTERVAL '1 minute' > NOW() + AND (t.source_region_id = $1::int OR t.target_region_id = $1::int) + AND v.falukant_user_id <> $2::int +ORDER BY RANDOM() +LIMIT 20; +"#; + +/// Bevorzugt eine Niederlassung in der Überfallregion, sonst kleinste branch_id. +pub const QUERY_RAID_NEAREST_BRANCH_FOR_USER: &str = r#" +SELECT b.id AS branch_id, b.region_id + FROM falukant_data.branch b + WHERE b.falukant_user_id = $2::int + ORDER BY CASE WHEN b.region_id = $1::int THEN 0 ELSE 1 END, b.id ASC + LIMIT 1; +"#; + +pub const QUERY_RAID_APP_USER_FOR_FALUKANT: &str = r#" +SELECT fu.user_id + FROM falukant_data.falukant_user fu + WHERE fu.id = $1::int + LIMIT 1; +"#; + +pub const QUERY_RAID_UPDATE_UNDERGROUND_RESULT: &str = r#" +UPDATE falukant_data.underground + SET result = $2::jsonb, + updated_at = NOW() + WHERE id = $1::int; +"#; + +pub const QUERY_RAID_SUBTRACT_REP_BY_USER: &str = r#" +UPDATE falukant_data.character c + SET reputation = GREATEST(0::numeric, COALESCE(c.reputation, 50::numeric) - $2::numeric), + updated_at = NOW() + FROM falukant_data.falukant_user fu + WHERE fu.id = $1::int + AND fu.user_id = c.user_id + AND c.health > 0; +"#; + pub const QUERY_GET_REGION_WORTH_FOR_PRODUCT: &str = r#" SELECT tpw.region_id, tpw.product_id, tpw.worth_percent FROM falukant_data.town_product_worth tpw JOIN falukant_data.branch b ON b.region_id = tpw.region_id WHERE b.falukant_user_id = $1 AND tpw.product_id = $2; "#; diff --git a/src/worker/underground.rs b/src/worker/underground.rs index c4d3467..4f3e57c 100644 --- a/src/worker/underground.rs +++ b/src/worker/underground.rs @@ -231,6 +231,10 @@ impl UndergroundWorker { } fn tick(pool: &ConnectionPool, broker: &MessageBroker) -> Result<(), DbError> { + if let Err(err) = super::falukant_transport_raid::run_tick(pool, broker) { + eprintln!("[UndergroundWorker] falukant_transport_raid::run_tick: {err}"); + } + let rows = Self::fetch_pending(pool)?; for row in rows {