essences/
currency.rs

1use crate::prelude::*;
2use event_system::script::types::ESCurrencyUnit;
3
4#[declare]
5pub type CurrencyId = uuid::Uuid;
6
7// Wire-compat note: the variant order is part of the bincode format because
8// `serde` encodes unit variants as their declaration index. Pre-existing
9// variants keep their original positions; all new variants are appended after.
10//
11// The `Serialize` impl below masquerades new variants as `BundleClaim` for
12// serde channels (bincode wire + JSON state patches) so a backend release can
13// ship without re-releasing the Unity/Python client. Server-side analytics
14// uses `Debug`, not `Serialize`, so the canonical variant still reaches the
15// dashboard. Once all clients have shipped knowledge of the new variants, the
16// custom impl can be deleted and `Serialize` re-added to the derive list.
17#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
18#[tsify(from_wasm_abi, into_wasm_abi)]
19pub enum CurrencySource {
20    EntityDeath,
21    ItemSell,
22    AutoItemSell,
23    QuestClaim,
24    QuestsTrackReward,
25    VassalTaskCompletion,
26    ReferralLvlUp,
27    ReferralDailyReward,
28    GiftClaim,
29    ArenaTicketBuy,
30    ClaimVassalReward,
31    ClaimSuzerainReward,
32    ArenaMatchmakingRefresh,
33    GachaLevelUp,
34    PetCaseLevelUp,
35    // BundleClaim stays at its original index — also serves as the
36    // `#[serde(default)]` fallback for legacy serialized state without a
37    // recorded bundle origin.
38    #[default]
39    BundleClaim,
40    AdReward,
41    Cheat,
42    // Appended in this PR. Bundles are containers; the analytics signal lives
43    // in the source that decided to grant the bundle, not in the claim
44    // mechanism. Always append new variants — never insert in the middle.
45    MailReward,
46    DungeonReward,
47    OfferBuy,
48    ChapterReward,
49    NewUserGrant,
50    PvpArenaReward,
51    PvpVassalReward,
52    AfkReward,
53}
54
55impl serde::Serialize for CurrencySource {
56    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
57    where
58        S: serde::Serializer,
59    {
60        // Wire-compat shim. Pre-existing variants serialize as themselves
61        // (matching what `#[derive(Serialize)]` would have produced — same
62        // declaration-index, same name). New variants masquerade as
63        // `BundleClaim` so a pre-PR client (whose `CurrencySource` enum has
64        // only the original 18 variants) can still bincode-decode the message.
65        // Server-side analytics uses `Debug`, which keeps the canonical name.
66        let (idx, name) = match self {
67            Self::EntityDeath => (0u32, "EntityDeath"),
68            Self::ItemSell => (1, "ItemSell"),
69            Self::AutoItemSell => (2, "AutoItemSell"),
70            Self::QuestClaim => (3, "QuestClaim"),
71            Self::QuestsTrackReward => (4, "QuestsTrackReward"),
72            Self::VassalTaskCompletion => (5, "VassalTaskCompletion"),
73            Self::ReferralLvlUp => (6, "ReferralLvlUp"),
74            Self::ReferralDailyReward => (7, "ReferralDailyReward"),
75            Self::GiftClaim => (8, "GiftClaim"),
76            Self::ArenaTicketBuy => (9, "ArenaTicketBuy"),
77            Self::ClaimVassalReward => (10, "ClaimVassalReward"),
78            Self::ClaimSuzerainReward => (11, "ClaimSuzerainReward"),
79            Self::ArenaMatchmakingRefresh => (12, "ArenaMatchmakingRefresh"),
80            Self::GachaLevelUp => (13, "GachaLevelUp"),
81            Self::PetCaseLevelUp => (14, "PetCaseLevelUp"),
82            Self::BundleClaim => (15, "BundleClaim"),
83            Self::AdReward => (16, "AdReward"),
84            Self::Cheat => (17, "Cheat"),
85            // Backend-only variants — collapse to BundleClaim on the wire.
86            Self::MailReward
87            | Self::DungeonReward
88            | Self::OfferBuy
89            | Self::ChapterReward
90            | Self::NewUserGrant
91            | Self::PvpArenaReward
92            | Self::PvpVassalReward
93            | Self::AfkReward => (15, "BundleClaim"),
94        };
95        serializer.serialize_unit_variant("CurrencySource", idx, name)
96    }
97}
98
99#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
100#[tsify(from_wasm_abi, into_wasm_abi)]
101pub enum CurrencyConsumer {
102    AbilityCaseOpen,
103    AbilityCaseSlotUpgrade,
104    ItemCaseOpen,
105    GiftSend,
106    DungeonRaid,
107    DungeonFightEnd,
108    ClassChange,
109    OfferBuy,
110    ItemCaseUpgrade,
111    CaseUpgradeSpeedUp,
112    CaseUpgradeSkip,
113    ArenaFight,
114    ArenaMatchmakingRefresh,
115    ArenaTicketBuy,
116    SkinBuy,
117    AfkInstantReward,
118    StatueRoll,
119    TalentUpgrade,
120    TalentUpgradeSkip,
121    PetCaseOpen,
122}
123
124#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify, CustomType)]
125pub struct Currency {
126    #[schemars(schema_with = "id_schema")]
127    pub id: CurrencyId,
128    #[schemars(title = "Название")]
129    pub name: i18n::I18nString,
130    #[schemars(title = "Описание")]
131    pub description: i18n::I18nString,
132    #[schemars(title = "URL картинки", schema_with = "webp_url_schema")]
133    pub icon_url: String,
134    #[schemars(title = "Иконка", schema_with = "asset_currency_icon_schema")]
135    pub icon_path: String,
136}
137
138#[derive(
139    Clone,
140    Debug,
141    Serialize,
142    Deserialize,
143    PartialEq,
144    Eq,
145    Hash,
146    JsonSchema,
147    Tsify,
148    CustomType,
149    Default,
150)]
151pub struct CurrencyUnit {
152    #[schemars(schema_with = "currency_link_id_schema")]
153    pub currency_id: CurrencyId,
154    #[schemars(title = "Количество")]
155    pub amount: i64,
156}
157
158impl PartialOrd for CurrencyUnit {
159    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
160        Some(self.cmp(other))
161    }
162}
163
164impl Ord for CurrencyUnit {
165    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
166        match self.currency_id.cmp(&other.currency_id) {
167            std::cmp::Ordering::Equal => self.amount.cmp(&other.amount),
168            ordering => ordering,
169        }
170    }
171}
172
173pub fn increase_currencies(
174    currencies_to_increase: &mut Vec<CurrencyUnit>,
175    currencies_to_add: &[CurrencyUnit],
176) {
177    for add_currency in currencies_to_add {
178        if let Some(unit) = currencies_to_increase
179            .iter_mut()
180            .find(|unit| unit.currency_id == add_currency.currency_id)
181        {
182            unit.amount += add_currency.amount;
183        } else {
184            currencies_to_increase.push(add_currency.clone());
185        }
186    }
187}
188
189pub fn decrease_currencies(
190    currencies_to_decrease: &mut [CurrencyUnit],
191    currencies_to_subtract: &[CurrencyUnit],
192) -> anyhow::Result<()> {
193    for subtract_currency in currencies_to_subtract {
194        if let Some(unit) = currencies_to_decrease
195            .iter_mut()
196            .find(|unit| unit.currency_id == subtract_currency.currency_id)
197        {
198            if unit.amount < subtract_currency.amount {
199                anyhow::bail!(
200                    "Required currency {subtract_currency:?} is bigger than available {unit:?}"
201                )
202            }
203
204            unit.amount -= subtract_currency.amount;
205        } else {
206            anyhow::bail!("Given currency {subtract_currency:?} isn't present in currencies")
207        }
208    }
209    Ok(())
210}
211
212pub fn force_decrease_currencies(
213    currencies_to_decrease: &mut Vec<CurrencyUnit>,
214    currencies_to_subtract: &[CurrencyUnit],
215) -> anyhow::Result<()> {
216    for subtract_currency in currencies_to_subtract {
217        if let Some(unit) = currencies_to_decrease
218            .iter_mut()
219            .find(|unit| unit.currency_id == subtract_currency.currency_id)
220        {
221            unit.amount -= subtract_currency.amount;
222        }
223    }
224
225    currencies_to_decrease.retain(|unit| unit.amount != 0);
226
227    Ok(())
228}
229
230pub fn check_can_decrease_currencies(
231    available_currencies: &[CurrencyUnit],
232    required_currencies: &[CurrencyUnit],
233) -> bool {
234    for subtract_currency in required_currencies {
235        if let Some(unit) = available_currencies
236            .iter()
237            .find(|unit| unit.currency_id == subtract_currency.currency_id)
238        {
239            if unit.amount < subtract_currency.amount {
240                return false;
241            }
242        } else {
243            return false;
244        }
245    }
246    true
247}
248
249pub fn from_es_currencies(es_currencies: &[ESCurrencyUnit]) -> Vec<CurrencyUnit> {
250    let mut result = vec![];
251    for es_currency in es_currencies.iter() {
252        result.push(CurrencyUnit {
253            currency_id: es_currency.currency_id,
254            amount: es_currency.amount,
255        });
256    }
257    result
258}