configs/
kill_faucets.rs

1//! BAL-038 — daily anti-idle cap shared by every kill-driven faucet.
2//!
3//! Any repeatable reward caused by a physical enemy death runs its own persisted
4//! daily counter, its own diminishing chance and its own finite hard cap. The
5//! point is bounded: a canonical 60-minute session stays fully useful while an
6//! unattended 20-hour farm terminates.
7//!
8//! Counters are INDEPENDENT — one family hitting its cap never blocks another.
9//! Two shapes share the mechanism:
10//!
11//! * non-Cookie families keep their base chance flat until the granted amount
12//!   reaches `D`, then decay linearly to zero at `2D`;
13//! * direct Cookies keep their own fitted hyperbola (BAL-008) up to the same
14//!   finite cap.
15//!
16//! Rewards that are not kill drops — first clears, chapter-clear bundles,
17//! quests, AFK, Ratings, ads, dungeon clear bundles and purchases — are outside
18//! this cap entirely.
19
20use essences::currency::CurrencyId;
21use essences::kill_faucets::KillFaucetFamily;
22use schemars::JsonSchema;
23use serde::{Deserialize, Serialize};
24use tsify_next::Tsify;
25
26/// Authored `D_family` for the families whose target does not move with the
27/// chapter. [`KillFaucetFamily::CoreEssence`] is absent by design: its `D` is
28/// `R_value(ch)` from the Core Essence chapter band.
29///
30/// `D` is the target amount for one canonical 60-minute active session in the
31/// current progression band, and simultaneously the basis of the cap. Runtime
32/// never derives it from observed kills/hour, session length or cohort
33/// activity, and performs no adaptive normalization — a future kill family must
34/// arrive with its own authored table.
35#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
36#[tsify(from_wasm_abi, into_wasm_abi)]
37pub struct KillFaucetSettings {
38    #[schemars(title = "D для прямых Cookies за 60 минут")]
39    pub direct_cookies_d: f64,
40
41    #[schemars(
42        title = "Валюта прямых Cookies",
43        schema_with = "schema_loader::currency_link_id_schema"
44    )]
45    pub direct_cookie_currency_id: CurrencyId,
46
47    /// Every eligible ordinary campaign-mob death resolves this many INDEPENDENT
48    /// tickets in sequence. Each success grants one Cookie, advances the daily
49    /// `x`, and is shown to the player separately.
50    #[schemars(title = "Тикетов Cookie за одно убийство")]
51    pub direct_cookie_tickets_per_kill: u8,
52
53    /// `p0` — the chance on the first eligible ticket of the day.
54    #[schemars(title = "Cookie: шанс первого тикета дня (p0)")]
55    pub direct_cookie_p0: f64,
56
57    /// `b` — the number of Cookies after which the chance halves to `p0/2`.
58    /// The curve is `p(x) = min(1, p0 × b / (x + b))` with no probability floor;
59    /// the BAL-038 hard cap, not a floor, is what makes the day finite.
60    #[schemars(title = "Cookie: полураспад кривой (b)")]
61    pub direct_cookie_b: f64,
62
63    #[schemars(title = "D для копий законов за 60 минут")]
64    pub law_copies_d: f64,
65
66    #[schemars(
67        title = "D для камней экипировки за 60 минут",
68        description = "Trigger и Effect делят один счётчик: вид решается внутренним роллом 50/50 уже после выдачи."
69    )]
70    pub equipment_stones_d: f64,
71
72    #[schemars(
73        title = "D для камней артефактов с убийств за 60 минут",
74        description = "Только campaign mob + boss. Дроп за зачистку подземелья этот счётчик не тратит."
75    )]
76    pub artifact_stones_d: f64,
77
78    #[schemars(
79        title = "Валюта Skill Chapters faucet",
80        schema_with = "schema_loader::currency_link_id_schema"
81    )]
82    pub skill_chapter_currency_id: CurrencyId,
83
84    /// Exact daily campaign-boss budget while the Free Progress Pass road is
85    /// still active. Unlike `D` families, this is the final cap itself.
86    #[schemars(title = "Skill Chapters: дневной cap до завершения Pass")]
87    pub skill_chapter_cap_with_pass: i64,
88
89    /// Exact daily campaign-boss budget beginning with the next daily snapshot
90    /// after the final Progress Pass tier has been unlocked.
91    #[schemars(title = "Skill Chapters: дневной cap после Pass")]
92    pub skill_chapter_cap_after_pass: i64,
93
94    #[schemars(
95        title = "Валюта boss-only Gems faucet",
96        schema_with = "schema_loader::currency_link_id_schema"
97    )]
98    pub boss_gems_currency_id: CurrencyId,
99
100    /// Exact daily cap for Gems from campaign-boss deaths.
101    #[schemars(title = "Boss Gems: дневной cap")]
102    pub boss_gems_daily_cap: i64,
103}
104
105impl KillFaucetSettings {
106    /// Authored `D` for a family, or `None` for the band-driven Core Essence.
107    pub fn d_for(&self, family: KillFaucetFamily) -> Option<f64> {
108        match family {
109            KillFaucetFamily::DirectCookies => Some(self.direct_cookies_d),
110            KillFaucetFamily::LawCopies => Some(self.law_copies_d),
111            KillFaucetFamily::EquipmentStones => Some(self.equipment_stones_d),
112            KillFaucetFamily::ArtifactStones => Some(self.artifact_stones_d),
113            KillFaucetFamily::SkillChapters | KillFaucetFamily::BossGems => None,
114            KillFaucetFamily::CoreEssence => None,
115        }
116    }
117}
118
119impl KillFaucetSettings {
120    /// Direct-Cookie ticket chance at `granted` Cookies already dropped today.
121    ///
122    /// `min(1, p0 × b / (x + b))` — the same `a/(x+b)` shape with `a = p0 × b`
123    /// rewritten into parameters a designer can reason about. Deliberately has
124    /// no probability floor: the day ends at the BAL-038 cap, not at a floor.
125    pub fn direct_cookie_chance(&self, granted: i64) -> f64 {
126        let x = granted.max(0) as f64;
127        let b = self.direct_cookie_b;
128        if b <= 0.0 {
129            return 0.0;
130        }
131        (self.direct_cookie_p0 * b / (x + b)).clamp(0.0, 1.0)
132    }
133}