Add transport raid functionality to Falukant daemon: Introduced falukant_transport_raid module with SQL queries for managing transport raids, including candidate transports and user reputation updates. Updated documentation to reflect new WebSocket events and integrated the module into the UndergroundWorker for seamless processing of transport-related events.

This commit is contained in:
Torsten Schulz (local)
2026-03-23 14:25:21 +01:00
parent df143e2531
commit 3b25f8c3a0
7 changed files with 677 additions and 0 deletions

View File

@@ -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**.

View File

@@ -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`

View File

@@ -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'
);

View File

@@ -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,
&params_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", &[&region_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", &[&region_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<i32, DbError> {
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<i32, DbError> {
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<Vec<StockInfo>, 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::<i32>().ok())
.unwrap_or(default)
}

View File

@@ -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;

View File

@@ -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;
"#;

View File

@@ -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 {