mirror of
https://git.ngram.ca/OpenJam/rc-servers
synced 2026-08-23 23:08:52 +00:00
Implement item shop #70
This commit is contained in:
@@ -14542,6 +14542,7 @@
|
|||||||
"uri": "sqlite:../data/robocraft/rc_archive.db?mode=rw"
|
"uri": "sqlite:../data/robocraft/rc_archive.db?mode=rw"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"shop": {},
|
||||||
"settings": {
|
"settings": {
|
||||||
"banners": [
|
"banners": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,21 +30,21 @@ impl ItemShopBundle {
|
|||||||
|
|
||||||
fn dump(&self, writer: &mut dyn Write) -> std::io::Result<usize> {
|
fn dump(&self, writer: &mut dyn Write) -> std::io::Result<usize> {
|
||||||
let sku_bytes = self.sku.as_bytes();
|
let sku_bytes = self.sku.as_bytes();
|
||||||
let mut total_len = writer.write(&oj_rc_core::data::encode_7_bit_i32(sku_bytes.len() as i32))?;
|
let mut total_len = writer.write(&crate::data::encode_7_bit_i32(sku_bytes.len() as i32))?;
|
||||||
total_len += writer.write(sku_bytes)?;
|
total_len += writer.write(sku_bytes)?;
|
||||||
|
|
||||||
let bundle_name_key_bytes = self.bundle_name_key.as_bytes();
|
let bundle_name_key_bytes = self.bundle_name_key.as_bytes();
|
||||||
total_len += writer.write(&oj_rc_core::data::encode_7_bit_i32(bundle_name_key_bytes.len() as i32))?;
|
total_len += writer.write(&crate::data::encode_7_bit_i32(bundle_name_key_bytes.len() as i32))?;
|
||||||
total_len += writer.write(bundle_name_key_bytes)?;
|
total_len += writer.write(bundle_name_key_bytes)?;
|
||||||
|
|
||||||
let sprite_bytes = self.sprite.as_bytes();
|
let sprite_bytes = self.sprite.as_bytes();
|
||||||
total_len += writer.write(&oj_rc_core::data::encode_7_bit_i32(sprite_bytes.len() as i32))?;
|
total_len += writer.write(&crate::data::encode_7_bit_i32(sprite_bytes.len() as i32))?;
|
||||||
total_len += writer.write(sprite_bytes)?;
|
total_len += writer.write(sprite_bytes)?;
|
||||||
|
|
||||||
total_len += writer.write(&[self.is_sprite_full_size as u8])?;
|
total_len += writer.write(&[self.is_sprite_full_size as u8])?;
|
||||||
|
|
||||||
let currency_bytes = self.currency.as_str().as_bytes();
|
let currency_bytes = self.currency.as_str().as_bytes();
|
||||||
total_len += writer.write(&oj_rc_core::data::encode_7_bit_i32(currency_bytes.len() as i32))?;
|
total_len += writer.write(&crate::data::encode_7_bit_i32(currency_bytes.len() as i32))?;
|
||||||
total_len += writer.write(currency_bytes)?;
|
total_len += writer.write(currency_bytes)?;
|
||||||
|
|
||||||
total_len += writer.write(&self.price.to_le_bytes())?;
|
total_len += writer.write(&self.price.to_le_bytes())?;
|
||||||
@@ -64,7 +64,7 @@ impl ItemShopBundle {
|
|||||||
Ok(total_len)
|
Ok(total_len)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_transmissible_vec(items: Vec<Self>) -> Typed {
|
pub fn as_transmissible_vec<C>(items: Vec<Self>) -> Typed<C> {
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
let mut writer = std::io::Cursor::new(&mut buf);
|
let mut writer = std::io::Cursor::new(&mut buf);
|
||||||
writer.write_all(&(items.len() as i32).to_le_bytes()).unwrap();
|
writer.write_all(&(items.len() as i32).to_le_bytes()).unwrap();
|
||||||
@@ -17,6 +17,7 @@ pub mod robot_data;
|
|||||||
pub mod lobby;
|
pub mod lobby;
|
||||||
pub mod battle_arena_config;
|
pub mod battle_arena_config;
|
||||||
pub mod game_result;
|
pub mod game_result;
|
||||||
|
pub mod item_shop_bundle;
|
||||||
|
|
||||||
pub mod error_codes;
|
pub mod error_codes;
|
||||||
|
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ fn default_game_modes() -> GameModes {
|
|||||||
respawn_heal_duration: 10.0,
|
respawn_heal_duration: 10.0,
|
||||||
respawn_full_heal_duration: 0.5,
|
respawn_full_heal_duration: 0.5,
|
||||||
kill_limit: 0,
|
kill_limit: 0,
|
||||||
game_time_m: 20,
|
game_time_m: 2,
|
||||||
},
|
},
|
||||||
elimination: GameMode {
|
elimination: GameMode {
|
||||||
respawn_heal_duration: 10.0,
|
respawn_heal_duration: 10.0,
|
||||||
@@ -461,7 +461,7 @@ fn default_rotation() -> GameEventSequence {
|
|||||||
GameEventSequence {
|
GameEventSequence {
|
||||||
strategy: GameRotationStrategy::Sequence,
|
strategy: GameRotationStrategy::Sequence,
|
||||||
modes: vec![
|
modes: vec![
|
||||||
/*GameEvents {
|
GameEvents {
|
||||||
singleplayer: GameEvent {
|
singleplayer: GameEvent {
|
||||||
map: GameMap::Earth1,
|
map: GameMap::Earth1,
|
||||||
visibility: GameVisibility::Good,
|
visibility: GameVisibility::Good,
|
||||||
@@ -490,8 +490,8 @@ fn default_rotation() -> GameEventSequence {
|
|||||||
auto_heal: true,
|
auto_heal: true,
|
||||||
},
|
},
|
||||||
duration_s: 30, // 30 seconds
|
duration_s: 30, // 30 seconds
|
||||||
},*/
|
},
|
||||||
GameEvents {
|
/*GameEvents {
|
||||||
singleplayer: GameEvent {
|
singleplayer: GameEvent {
|
||||||
map: GameMap::Neptune1,
|
map: GameMap::Neptune1,
|
||||||
visibility: GameVisibility::Good,
|
visibility: GameVisibility::Good,
|
||||||
@@ -610,7 +610,7 @@ fn default_rotation() -> GameEventSequence {
|
|||||||
auto_heal: true,
|
auto_heal: true,
|
||||||
},
|
},
|
||||||
duration_s: 5*60,
|
duration_s: 5*60,
|
||||||
},
|
},*/
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use polariton::serdes::TypePrefix;
|
|||||||
|
|
||||||
use crate::persist::config::SelfValidator;
|
use crate::persist::config::SelfValidator;
|
||||||
|
|
||||||
use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig, FactoryConfig};
|
use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig, FactoryConfig, ItemShopConfig};
|
||||||
|
|
||||||
const CUBE_CONFIG_FILENAME: &str = "config.json";
|
const CUBE_CONFIG_FILENAME: &str = "config.json";
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ pub struct CubeConfig {
|
|||||||
battle: BattleConfig,
|
battle: BattleConfig,
|
||||||
chat: ChatConfig,
|
chat: ChatConfig,
|
||||||
factory: FactoryConfig,
|
factory: FactoryConfig,
|
||||||
|
shop: ItemShopConfig,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,4 +504,8 @@ impl <C: Clone + Send> super::ConfigProvider<C> for CubeConfig {
|
|||||||
fn tdm_settings(&self) -> super::TeamDeathMatchSettings {
|
fn tdm_settings(&self) -> super::TeamDeathMatchSettings {
|
||||||
self.battle.multiplayer.team_death_match.clone().into()
|
self.battle.multiplayer.team_death_match.clone().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shop_entries(&self) -> super::ShopEntriesResolver {
|
||||||
|
super::ShopEntriesResolver::new(self.shop.items.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ mod cubes_json;
|
|||||||
pub use cubes_json::CubeConfig;
|
pub use cubes_json::CubeConfig;
|
||||||
|
|
||||||
mod traits;
|
mod traits;
|
||||||
pub use traits::{ConfigProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings};
|
pub use traits::{ConfigProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings, ShopEntriesResolver, ShopAction, ShopGain};
|
||||||
|
|
||||||
mod validation;
|
mod validation;
|
||||||
pub use validation::{SelfValidator, ValidationInfo, ValidationMessage};
|
pub use validation::{SelfValidator, ValidationInfo, ValidationMessage};
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ pub trait ConfigProvider<C: Clone> {
|
|||||||
fn ba_settings(&self) -> BattleArenaResolver;
|
fn ba_settings(&self) -> BattleArenaResolver;
|
||||||
fn pit_settings(&self) -> PitSettings;
|
fn pit_settings(&self) -> PitSettings;
|
||||||
fn tdm_settings(&self) -> TeamDeathMatchSettings;
|
fn tdm_settings(&self) -> TeamDeathMatchSettings;
|
||||||
|
fn shop_entries(&self) -> ShopEntriesResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DevMessageProvider<C: Clone> {
|
pub struct DevMessageProvider<C: Clone> {
|
||||||
@@ -428,3 +429,60 @@ pub struct TeamDeathMatchSettings {
|
|||||||
pub respawn_time_seconds: u64,
|
pub respawn_time_seconds: u64,
|
||||||
pub self_destruct_is_kill: bool,
|
pub self_destruct_is_kill: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ShopEntriesResolver {
|
||||||
|
items: Vec<crate::persist::item_shop::ItemBundle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn item_shop_sku(i: usize) -> String {
|
||||||
|
format!("item-shop-bundle-{}", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShopEntriesResolver {
|
||||||
|
pub async fn resolve_entries<C>(&self, user: &dyn crate::persist::user::User<C>) -> Typed<C> {
|
||||||
|
let unlocked_cubes = user.unlocked_parts().await;
|
||||||
|
crate::data::item_shop_bundle::ItemShopBundle::as_transmissible_vec(
|
||||||
|
self.items.iter().enumerate()
|
||||||
|
.map(|(i, entry)| entry.as_data(item_shop_sku(i), &unlocked_cubes))
|
||||||
|
.collect()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sku -> purchase details
|
||||||
|
pub async fn resolve_transactions(&self) -> std::collections::HashMap<String, super::ShopAction> {
|
||||||
|
let mut map = std::collections::HashMap::with_capacity(self.items.len());
|
||||||
|
let now = chrono::Utc::now().timestamp();
|
||||||
|
for (i, entry) in self.items.iter().enumerate() {
|
||||||
|
let sku = super::traits::item_shop_sku(i);
|
||||||
|
let actual_price = if now > entry.discount_until { entry.discount_price } else { entry.price };
|
||||||
|
map.insert(sku, super::ShopAction {
|
||||||
|
cost_free: if matches!(entry.currency, crate::persist::item_shop::Currency::Robits) { actual_price } else { 0 },
|
||||||
|
cost_paid: if matches!(entry.currency, crate::persist::item_shop::Currency::CosmeticCredits) { actual_price } else { 0 },
|
||||||
|
gives: entry.gives.clone().into_iter().map(|x| x.into()).collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn new(items: Vec<crate::persist::item_shop::ItemBundle>) -> Self {
|
||||||
|
Self {
|
||||||
|
items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ShopAction {
|
||||||
|
pub cost_free: i32,
|
||||||
|
pub cost_paid: i32,
|
||||||
|
pub gives: Vec<ShopGain>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ShopGain {
|
||||||
|
Cube(u32),
|
||||||
|
Experience(i64),
|
||||||
|
FreeCurrency(i64),
|
||||||
|
PaidCurrency(i64),
|
||||||
|
TechPoints(i32)
|
||||||
|
}
|
||||||
|
|||||||
328
rc_core/src/persist/item_shop.rs
Normal file
328
rc_core/src/persist/item_shop.rs
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ItemShopConfig {
|
||||||
|
#[serde(default = "default_items")]
|
||||||
|
pub items: Vec<ItemBundle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::config::SelfValidator for ItemShopConfig {
|
||||||
|
type Context = crate::ConfigImpl;
|
||||||
|
fn validate(&self, info: &mut super::config::ValidationInfo, _ctx: &Self::Context) -> bool {
|
||||||
|
let daily_count = self.items.iter().filter(|x| matches!(x.recurrence, Recurrence::Daily)).count();
|
||||||
|
if daily_count < 6 {
|
||||||
|
info.warn(crate::persist::config::ValidationMessage {
|
||||||
|
path: vec!["items".to_owned()],
|
||||||
|
message: "Less than 6 daily items in shop so there will be blank slots".to_owned(),
|
||||||
|
});
|
||||||
|
} else if daily_count > 6 {
|
||||||
|
info.warn(crate::persist::config::ValidationMessage {
|
||||||
|
path: vec!["items".to_owned()],
|
||||||
|
message: "More than 6 daily items in shop so some will never be shown".to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let weekly_count = self.items.iter().filter(|x| matches!(x.recurrence, Recurrence::Weekly)).count();
|
||||||
|
if weekly_count < 3 {
|
||||||
|
info.warn(crate::persist::config::ValidationMessage {
|
||||||
|
path: vec!["items".to_owned()],
|
||||||
|
message: "Less than 3 weekly items in shop so there will be blank slots".to_owned(),
|
||||||
|
});
|
||||||
|
} else if weekly_count > 3 {
|
||||||
|
info.warn(crate::persist::config::ValidationMessage {
|
||||||
|
path: vec!["items".to_owned()],
|
||||||
|
message: "More than 3 weekly items in shop so some will never be shown".to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ItemBundle {
|
||||||
|
//pub sku: String,
|
||||||
|
#[serde(alias = "localized_key")]
|
||||||
|
pub localised_key: String,
|
||||||
|
pub sprite: String,
|
||||||
|
pub is_sprite_full_size: bool,
|
||||||
|
pub category: ShopCategory,
|
||||||
|
pub currency: Currency,
|
||||||
|
pub price: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub discount_until: i64, // seconds since unix epoch
|
||||||
|
#[serde(default)]
|
||||||
|
pub discount_price: i32,
|
||||||
|
pub recurrence: Recurrence,
|
||||||
|
//pub owns_required_cube: bool,
|
||||||
|
pub is_limited_edition: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub required_cubes: Vec<u32>,
|
||||||
|
pub gives: Vec<ItemPurchase>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemBundle {
|
||||||
|
pub fn as_data(&self, sku: String, unlocked_cubes: &[u32]) -> crate::data::item_shop_bundle::ItemShopBundle {
|
||||||
|
let mut contains_all = true;
|
||||||
|
for req in self.required_cubes.iter() {
|
||||||
|
contains_all &= unlocked_cubes.contains(req);
|
||||||
|
}
|
||||||
|
crate::data::item_shop_bundle::ItemShopBundle {
|
||||||
|
sku,
|
||||||
|
bundle_name_key: self.localised_key.clone(),
|
||||||
|
sprite: self.sprite.clone(),
|
||||||
|
is_sprite_full_size: self.is_sprite_full_size,
|
||||||
|
category: self.category.into(),
|
||||||
|
currency: self.currency.into(),
|
||||||
|
price: self.price,
|
||||||
|
discount_time: self.discount_until,
|
||||||
|
discount_price: self.discount_price,
|
||||||
|
recurrence: self.recurrence.into(),
|
||||||
|
owns_required_cube: contains_all,
|
||||||
|
is_limited_edition: self.is_limited_edition,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum ShopCategory {
|
||||||
|
Cube,
|
||||||
|
GarageBaySkin,
|
||||||
|
Bundle,
|
||||||
|
DeathEffect,
|
||||||
|
SpawnEffect,
|
||||||
|
Emotigram,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<ShopCategory> for crate::data::item_shop_bundle::ItemShopCategory {
|
||||||
|
fn from(value: ShopCategory) -> Self {
|
||||||
|
match value {
|
||||||
|
ShopCategory::Cube => Self::Cube,
|
||||||
|
ShopCategory::GarageBaySkin => Self::GarageBaySkin,
|
||||||
|
ShopCategory::Bundle => Self::Bundle,
|
||||||
|
ShopCategory::DeathEffect => Self::DeathEffect,
|
||||||
|
ShopCategory::SpawnEffect => Self::SpawnEffect,
|
||||||
|
ShopCategory::Emotigram => Self::Emotigram,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Recurrence {
|
||||||
|
Daily,
|
||||||
|
Weekly,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<Recurrence> for crate::data::item_shop_bundle::ItemShopRecurrence {
|
||||||
|
fn from(value: Recurrence) -> Self {
|
||||||
|
match value {
|
||||||
|
Recurrence::Daily => Self::Daily,
|
||||||
|
Recurrence::Weekly => Self::Weekly,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Currency {
|
||||||
|
Robits,
|
||||||
|
CosmeticCredits,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<Currency> for crate::data::item_shop_bundle::CurrencyType {
|
||||||
|
fn from(value: Currency) -> Self {
|
||||||
|
match value {
|
||||||
|
Currency::Robits => Self::Robits,
|
||||||
|
Currency::CosmeticCredits => Self::CosmeticCredits,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ItemPurchase {
|
||||||
|
Cube {
|
||||||
|
item_id: u32,
|
||||||
|
},
|
||||||
|
Experience {
|
||||||
|
#[serde(alias = "experience", alias = "exp")]
|
||||||
|
xp: i64,
|
||||||
|
},
|
||||||
|
FreeCurrency {
|
||||||
|
#[serde(alias = "robits")]
|
||||||
|
free_currency: i64,
|
||||||
|
},
|
||||||
|
PaidCurrency {
|
||||||
|
#[serde(alias = "cc", alias = "cosmetic_credits")]
|
||||||
|
paid_currency: i64,
|
||||||
|
},
|
||||||
|
TechPoints {
|
||||||
|
#[serde(alias = "tp")]
|
||||||
|
tech_points: i64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<ItemPurchase> for crate::persist::config::ShopGain {
|
||||||
|
fn from(value: ItemPurchase) -> Self {
|
||||||
|
match value {
|
||||||
|
ItemPurchase::Cube { item_id } => crate::persist::config::ShopGain::Cube(item_id),
|
||||||
|
ItemPurchase::Experience { xp } => crate::persist::config::ShopGain::Experience(xp),
|
||||||
|
ItemPurchase::FreeCurrency { free_currency } => crate::persist::config::ShopGain::FreeCurrency(free_currency),
|
||||||
|
ItemPurchase::PaidCurrency { paid_currency } => crate::persist::config::ShopGain::PaidCurrency(paid_currency),
|
||||||
|
ItemPurchase::TechPoints { tech_points } => crate::persist::config::ShopGain::PaidCurrency(tech_points),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_items() -> Vec<ItemBundle> {
|
||||||
|
vec![
|
||||||
|
// weekly (top row of 3)
|
||||||
|
ItemBundle {
|
||||||
|
//sku: "buy cc 100".to_owned(),
|
||||||
|
localised_key: "strRealMoneyStoreName_CosmeticCredits1".to_owned(),
|
||||||
|
sprite: "ItemShop_CosmeticCredits".to_owned(),
|
||||||
|
is_sprite_full_size: false,
|
||||||
|
category: ShopCategory::Bundle,
|
||||||
|
currency: Currency::Robits,
|
||||||
|
price: 100_000,
|
||||||
|
discount_until: 0,
|
||||||
|
discount_price: 100_000,
|
||||||
|
recurrence: Recurrence::Weekly,
|
||||||
|
//owns_required_cube: true,
|
||||||
|
is_limited_edition: false,
|
||||||
|
required_cubes: vec![],
|
||||||
|
gives: vec![
|
||||||
|
ItemPurchase::PaidCurrency { paid_currency: 100 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
ItemBundle {
|
||||||
|
//sku: "buy robopass 1 1".to_owned(),
|
||||||
|
localised_key: "strRealMoneyStoreName_RoboPass".to_owned(),
|
||||||
|
sprite: "Store_RoboPass_Season2".to_owned(),
|
||||||
|
is_sprite_full_size: true,
|
||||||
|
category: ShopCategory::Bundle,
|
||||||
|
currency: Currency::CosmeticCredits,
|
||||||
|
price: 10_000_000,
|
||||||
|
discount_until: 0,
|
||||||
|
discount_price: 10_000_000,
|
||||||
|
recurrence: Recurrence::Weekly,
|
||||||
|
//owns_required_cube: false,
|
||||||
|
is_limited_edition: true,
|
||||||
|
required_cubes: vec![],
|
||||||
|
gives: vec![],
|
||||||
|
},
|
||||||
|
ItemBundle {
|
||||||
|
//sku: "buy robit 100000".to_owned(),
|
||||||
|
localised_key: "strRealMoneyStoreName_RobitsBundle2".to_owned(),
|
||||||
|
sprite: "ItemShop_Robits".to_owned(),
|
||||||
|
is_sprite_full_size: false,
|
||||||
|
category: ShopCategory::Bundle,
|
||||||
|
currency: Currency::CosmeticCredits,
|
||||||
|
price: 100,
|
||||||
|
discount_until: 0,
|
||||||
|
discount_price: 100,
|
||||||
|
recurrence: Recurrence::Weekly,
|
||||||
|
//owns_required_cube: true,
|
||||||
|
is_limited_edition: false,
|
||||||
|
required_cubes: vec![],
|
||||||
|
gives: vec![
|
||||||
|
ItemPurchase::FreeCurrency { free_currency: 100_000 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// daily (lower row or 6)
|
||||||
|
ItemBundle {
|
||||||
|
//sku: "buy robopass 1 1".to_owned(),
|
||||||
|
localised_key: "strRoboPassSeason02".to_owned(),
|
||||||
|
sprite: "Store_RoboPass".to_owned(),
|
||||||
|
is_sprite_full_size: true,
|
||||||
|
category: ShopCategory::Bundle,
|
||||||
|
currency: Currency::Robits,
|
||||||
|
price: 10_000,
|
||||||
|
discount_until: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(),
|
||||||
|
discount_price: 1_000,
|
||||||
|
recurrence: Recurrence::Daily,
|
||||||
|
//owns_required_cube: false,
|
||||||
|
is_limited_edition: false,
|
||||||
|
required_cubes: vec![],
|
||||||
|
gives: vec![],
|
||||||
|
},
|
||||||
|
ItemBundle {
|
||||||
|
//sku: "buy robopass 1 2".to_owned(),
|
||||||
|
localised_key: "strRoboPassSeason02".to_owned(),
|
||||||
|
sprite: "Store_RoboPass".to_owned(),
|
||||||
|
is_sprite_full_size: true,
|
||||||
|
category: ShopCategory::Cube,
|
||||||
|
currency: Currency::Robits,
|
||||||
|
price: 10_000,
|
||||||
|
discount_until: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(),
|
||||||
|
discount_price: 2_000,
|
||||||
|
recurrence: Recurrence::Daily,
|
||||||
|
//owns_required_cube: false,
|
||||||
|
is_limited_edition: false,
|
||||||
|
required_cubes: vec![],
|
||||||
|
gives: vec![],
|
||||||
|
},
|
||||||
|
ItemBundle {
|
||||||
|
//sku: "buy robopass 1 3".to_owned(),
|
||||||
|
localised_key: "strRoboPassSeason02".to_owned(),
|
||||||
|
sprite: "Store_RoboPass".to_owned(),
|
||||||
|
is_sprite_full_size: true,
|
||||||
|
category: ShopCategory::Cube,
|
||||||
|
currency: Currency::Robits,
|
||||||
|
price: 10_000,
|
||||||
|
discount_until: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(),
|
||||||
|
discount_price: 3_000,
|
||||||
|
recurrence: Recurrence::Daily,
|
||||||
|
//owns_required_cube: false,
|
||||||
|
is_limited_edition: false,
|
||||||
|
required_cubes: vec![],
|
||||||
|
gives: vec![],
|
||||||
|
},
|
||||||
|
ItemBundle {
|
||||||
|
//sku: "buy robopass 1 4".to_owned(),
|
||||||
|
localised_key: "strRoboPassSeason02".to_owned(),
|
||||||
|
sprite: "Store_RoboPass".to_owned(),
|
||||||
|
is_sprite_full_size: true,
|
||||||
|
category: ShopCategory::Cube,
|
||||||
|
currency: Currency::Robits,
|
||||||
|
price: 10_000,
|
||||||
|
discount_until: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(),
|
||||||
|
discount_price: 4_000,
|
||||||
|
recurrence: Recurrence::Daily,
|
||||||
|
//owns_required_cube: false,
|
||||||
|
is_limited_edition: false,
|
||||||
|
required_cubes: vec![],
|
||||||
|
gives: vec![],
|
||||||
|
},
|
||||||
|
ItemBundle {
|
||||||
|
//sku: "buy robopass 1 5".to_owned(),
|
||||||
|
localised_key: "strRoboPassSeason02".to_owned(),
|
||||||
|
sprite: "Store_RoboPass".to_owned(),
|
||||||
|
is_sprite_full_size: true,
|
||||||
|
category: ShopCategory::Cube,
|
||||||
|
currency: Currency::Robits,
|
||||||
|
price: 10_000,
|
||||||
|
discount_until: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(),
|
||||||
|
discount_price: 5_000,
|
||||||
|
recurrence: Recurrence::Daily,
|
||||||
|
//owns_required_cube: false,
|
||||||
|
is_limited_edition: false,
|
||||||
|
required_cubes: vec![],
|
||||||
|
gives: vec![],
|
||||||
|
},
|
||||||
|
ItemBundle {
|
||||||
|
//sku: "buy robopass 1 6".to_owned(),
|
||||||
|
localised_key: "strRoboPassSeason02".to_owned(),
|
||||||
|
sprite: "Store_RoboPass".to_owned(),
|
||||||
|
is_sprite_full_size: true,
|
||||||
|
category: ShopCategory::Cube,
|
||||||
|
currency: Currency::Robits,
|
||||||
|
price: 10_000,
|
||||||
|
discount_until: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(),
|
||||||
|
discount_price: 6_000,
|
||||||
|
recurrence: Recurrence::Daily,
|
||||||
|
//owns_required_cube: false,
|
||||||
|
is_limited_edition: false,
|
||||||
|
required_cubes: vec![],
|
||||||
|
gives: vec![],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -41,6 +41,9 @@ pub use multiplayer::{MultiplayerConfig, NetworkConf};
|
|||||||
mod maps;
|
mod maps;
|
||||||
pub use maps::{MapsConfig, MapConfig};
|
pub use maps::{MapsConfig, MapConfig};
|
||||||
|
|
||||||
|
mod item_shop;
|
||||||
|
pub use item_shop::{ItemShopConfig, ItemBundle};
|
||||||
|
|
||||||
const VALID_ROBOT: &[u8] = &[64,
|
const VALID_ROBOT: &[u8] = &[64,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use argon2::PasswordVerifier;
|
use argon2::PasswordVerifier;
|
||||||
|
use oj_rc_database::sea_orm::IntoActiveModel;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
|
|
||||||
use crate::persist::config::ConfigProvider;
|
use crate::persist::config::ConfigProvider;
|
||||||
@@ -666,6 +667,19 @@ impl UserData {
|
|||||||
}
|
}
|
||||||
).await?
|
).await?
|
||||||
},
|
},
|
||||||
|
super::CurrencyOp::AddSub(to_addsub) => {
|
||||||
|
self.db.update_user_aux_by_user_id_and_descriptor_custom(
|
||||||
|
self.account.id,
|
||||||
|
desc.clone(),
|
||||||
|
move |model| {
|
||||||
|
use oj_rc_database::sea_orm::IntoActiveModel;
|
||||||
|
let new_currency = (model.data.parse::<u64>().unwrap_or_default() as i64) + to_addsub;
|
||||||
|
let mut am = model.to_owned().into_active_model();
|
||||||
|
am.data = oj_rc_database::sea_orm::ActiveValue::Set(new_currency.clamp(0, i64::MAX).to_string());
|
||||||
|
Some(am)
|
||||||
|
}
|
||||||
|
).await?
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let num: u64 = if let Some(model) = model_opt {
|
let num: u64 = if let Some(model) = model_opt {
|
||||||
model.data.parse().unwrap_or_default()
|
model.data.parse().unwrap_or_default()
|
||||||
@@ -708,6 +722,58 @@ impl <C: Clone> super::User<C> for UserData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn unlock_parts(&self, parts: &[u32]) -> Result<(), polariton_server::operations::SimpleOpError> {
|
||||||
|
let parts_row = self.db.user_aux_by_user_id_and_descriptor(self.account.id, oj_rc_database::schema::user_aux::Descriptor::UnlockedParts).await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to retrieve UnlockedParts to unlock parts for user {}: {}", self.account.id, e);
|
||||||
|
polariton_server::operations::SimpleOpError::with_message(
|
||||||
|
crate::data::error_codes::WebServicesError::DatabaseError as i16,
|
||||||
|
format!("Failed to retrieve UnlockedParts to unlock parts: {}", e),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
log::error!("Failed to find UnlockedParts to unlock parts for user {}", self.account.id);
|
||||||
|
polariton_server::operations::SimpleOpError::with_message(
|
||||||
|
crate::data::error_codes::WebServicesError::DatabaseError as i16,
|
||||||
|
"Failed to find UnlockedParts to unlock parts".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let mut unlocked_parts = serde_json::from_str::<super::inventory::UnlockedParts>(&parts_row.data)
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to deserialize UnlockedParts to unlock parts for user {}: {}", self.account.id, e);
|
||||||
|
polariton_server::operations::SimpleOpError::with_message(
|
||||||
|
crate::data::error_codes::WebServicesError::DatabaseError as i16,
|
||||||
|
format!("Failed to deserialize UnlockedParts to unlock parts: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
for new_part in parts {
|
||||||
|
unlocked_parts.unlocked.push(*new_part);
|
||||||
|
}
|
||||||
|
let mut parts_row = parts_row.into_active_model();
|
||||||
|
let parts_json = serde_json::to_string(&unlocked_parts)
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to serialize UnlockedParts to unlock parts for user {}: {}", self.account.id, e);
|
||||||
|
polariton_server::operations::SimpleOpError::with_message(
|
||||||
|
crate::data::error_codes::WebServicesError::DatabaseError as i16,
|
||||||
|
format!("Failed to serialize UnlockedParts to unlock parts: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
parts_row.data = oj_rc_database::sea_orm::ActiveValue::Set(parts_json);
|
||||||
|
self.db.update_user_aux_by_user_id_and_descriptor(
|
||||||
|
parts_row,
|
||||||
|
self.account.id,
|
||||||
|
oj_rc_database::schema::user_aux::Descriptor::UnlockedParts
|
||||||
|
).await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to update UnlockedParts to unlock parts for user {}: {}", self.account.id, e);
|
||||||
|
polariton_server::operations::SimpleOpError::with_message(
|
||||||
|
crate::data::error_codes::WebServicesError::DatabaseError as i16,
|
||||||
|
format!("Failed to update UnlockedParts to unlock parts: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn selected_garage(&self) -> (String, u32) {
|
async fn selected_garage(&self) -> (String, u32) {
|
||||||
match self.db.garage_selected(self.account.id).await {
|
match self.db.garage_selected(self.account.id).await {
|
||||||
Ok(Some(selected)) => (super::i64_as_uuid_str(selected.uuid), selected.slot as u32),
|
Ok(Some(selected)) => (super::i64_as_uuid_str(selected.uuid), selected.slot as u32),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ mod initial_data;
|
|||||||
pub use initial_data::{setup_new_user, register_new_user};
|
pub use initial_data::{setup_new_user, register_new_user};
|
||||||
|
|
||||||
mod inventory;
|
mod inventory;
|
||||||
pub use inventory::UnlockedParts;
|
pub use inventory::{UnlockedParts, UnlockOverride};
|
||||||
|
|
||||||
mod traits;
|
mod traits;
|
||||||
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser};
|
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser};
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ pub trait UserAuthenticator {
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait User<C>: ChatUser + SocialUser + LobbyUser + MultiplayerUser + SingleplayerUser + IntercomUser + CommonUser {
|
pub trait User<C>: ChatUser + SocialUser + LobbyUser + MultiplayerUser + SingleplayerUser + IntercomUser + CommonUser {
|
||||||
async fn unlocked_parts(&self) -> Vec<u32>;
|
async fn unlocked_parts(&self) -> Vec<u32>;
|
||||||
|
async fn unlock_parts(&self, parts: &[u32]) -> Result<(), polariton_server::operations::SimpleOpError>;
|
||||||
async fn selected_garage(&self) -> (String, u32);
|
async fn selected_garage(&self) -> (String, u32);
|
||||||
async fn select_garage(&self, slot: i32) -> Result<(), i16>;
|
async fn select_garage(&self, slot: i32) -> Result<(), i16>;
|
||||||
async fn all_slots(&self) -> UserSlots<C>;
|
async fn all_slots(&self) -> UserSlots<C>;
|
||||||
@@ -417,6 +418,7 @@ pub enum CurrencyOp {
|
|||||||
Get,
|
Get,
|
||||||
Add(u64),
|
Add(u64),
|
||||||
Sub(u64),
|
Sub(u64),
|
||||||
|
AddSub(i64),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub mod customisation_info;
|
|||||||
pub use oj_rc_core::data::garage_bay;
|
pub use oj_rc_core::data::garage_bay;
|
||||||
pub mod custom_games;
|
pub mod custom_games;
|
||||||
//pub use oj_rc_core::data::tech_tree;
|
//pub use oj_rc_core::data::tech_tree;
|
||||||
pub mod item_shop_bundle;
|
//pub use oj_rc_core::data::item_shop_bundle;
|
||||||
pub mod player_robopass_season;
|
pub mod player_robopass_season;
|
||||||
//pub use oj_rc_core::data::weapon_upgrade;
|
//pub use oj_rc_core::data::weapon_upgrade;
|
||||||
pub mod player_rank;
|
pub mod player_rank;
|
||||||
|
|||||||
@@ -1,144 +1,31 @@
|
|||||||
use polariton_server::operations::SimpleFunc;
|
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
|
||||||
use polariton::operation::ParameterTable;
|
use polariton::operation::ParameterTable;
|
||||||
|
|
||||||
use crate::data::item_shop_bundle::*;
|
const CODE: u8 = 188;
|
||||||
|
|
||||||
const PARAM_KEY: u8 = 65;
|
const PARAM_KEY: u8 = 65;
|
||||||
|
|
||||||
pub(super) fn item_bundle_provider() -> SimpleFunc<188, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result<ParameterTable, i16>) + Sync + Sync> {
|
pub(super) struct ItemBundleRetriever {
|
||||||
SimpleFunc::new(|params, _| {
|
resolver: oj_rc_core::persist::config::ShopEntriesResolver,
|
||||||
let mut params = params.to_dict();
|
}
|
||||||
params.insert(PARAM_KEY, ItemShopBundle::as_transmissible_vec(vec![
|
|
||||||
// weekly (top row of 3)
|
#[async_trait::async_trait]
|
||||||
ItemShopBundle {
|
impl SimpleOperation<()> for ItemBundleRetriever {
|
||||||
sku: "buy cc 100".to_owned(),
|
type User = crate::UserTy;
|
||||||
bundle_name_key: "strRealMoneyStoreName_CosmeticCredits1".to_owned(),
|
const CODE: u8 = CODE;
|
||||||
sprite: "ItemShop_CosmeticCredits".to_owned(),
|
|
||||||
is_sprite_full_size: false,
|
async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result<ParameterTable, SimpleOpError> {
|
||||||
category: ItemShopCategory::Bundle,
|
//let mut params = ParameterTable::<C>::with_capacity(2);
|
||||||
currency: CurrencyType::Robits,
|
let user_info = user.user()?;
|
||||||
price: 100_000,
|
let entries = self.resolver.resolve_entries(user_info.as_ref().as_ref()).await;
|
||||||
discount_time: 0,
|
params.insert(PARAM_KEY, entries);
|
||||||
discount_price: 100_000,
|
Ok(params)
|
||||||
recurrence: ItemShopRecurrence::Weekly,
|
}
|
||||||
owns_required_cube: true,
|
}
|
||||||
is_limited_edition: false,
|
|
||||||
},
|
pub(super) fn item_bundle_provider(conf: &oj_rc_core::ConfigImpl) -> SimpleOpImpl<(), crate::UserTy, ItemBundleRetriever> {
|
||||||
ItemShopBundle {
|
SimpleOpImpl::new(ItemBundleRetriever {
|
||||||
sku: "buy robopass 1 1".to_owned(),
|
resolver: <oj_rc_core::ConfigImpl as oj_rc_core::ConfigProvider<()>>::shop_entries(conf),
|
||||||
bundle_name_key: "strRealMoneyStoreName_RoboPass".to_owned(),
|
|
||||||
sprite: "Store_RoboPass_Season2".to_owned(),
|
|
||||||
is_sprite_full_size: true,
|
|
||||||
category: ItemShopCategory::Bundle,
|
|
||||||
currency: CurrencyType::CosmeticCredits,
|
|
||||||
price: 10_000_000,
|
|
||||||
discount_time: 0,
|
|
||||||
discount_price: 10_000_000,
|
|
||||||
recurrence: ItemShopRecurrence::Weekly,
|
|
||||||
owns_required_cube: false,
|
|
||||||
is_limited_edition: true,
|
|
||||||
},
|
|
||||||
ItemShopBundle {
|
|
||||||
sku: "buy robit 100000".to_owned(),
|
|
||||||
bundle_name_key: "strRealMoneyStoreName_RobitsBundle2".to_owned(),
|
|
||||||
sprite: "ItemShop_Robits".to_owned(),
|
|
||||||
is_sprite_full_size: false,
|
|
||||||
category: ItemShopCategory::Bundle,
|
|
||||||
currency: CurrencyType::CosmeticCredits,
|
|
||||||
price: 100,
|
|
||||||
discount_time: 0,
|
|
||||||
discount_price: 100,
|
|
||||||
recurrence: ItemShopRecurrence::Weekly,
|
|
||||||
owns_required_cube: true,
|
|
||||||
is_limited_edition: false,
|
|
||||||
},
|
|
||||||
// daily (lower row or 6)
|
|
||||||
ItemShopBundle {
|
|
||||||
sku: "buy robopass 1 1".to_owned(),
|
|
||||||
bundle_name_key: "strRoboPassSeason02".to_owned(),
|
|
||||||
sprite: "Store_RoboPass".to_owned(),
|
|
||||||
is_sprite_full_size: true,
|
|
||||||
category: ItemShopCategory::Bundle,
|
|
||||||
currency: CurrencyType::Robits,
|
|
||||||
price: 10_000,
|
|
||||||
discount_time: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(),
|
|
||||||
discount_price: 1_000,
|
|
||||||
recurrence: ItemShopRecurrence::Daily,
|
|
||||||
owns_required_cube: false,
|
|
||||||
is_limited_edition: false,
|
|
||||||
},
|
|
||||||
ItemShopBundle {
|
|
||||||
sku: "buy robopass 1 2".to_owned(),
|
|
||||||
bundle_name_key: "strRoboPassSeason02".to_owned(),
|
|
||||||
sprite: "Store_RoboPass".to_owned(),
|
|
||||||
is_sprite_full_size: true,
|
|
||||||
category: ItemShopCategory::Cube,
|
|
||||||
currency: CurrencyType::Robits,
|
|
||||||
price: 10_000,
|
|
||||||
discount_time: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(),
|
|
||||||
discount_price: 2_000,
|
|
||||||
recurrence: ItemShopRecurrence::Daily,
|
|
||||||
owns_required_cube: false,
|
|
||||||
is_limited_edition: false,
|
|
||||||
},
|
|
||||||
ItemShopBundle {
|
|
||||||
sku: "buy robopass 1 3".to_owned(),
|
|
||||||
bundle_name_key: "strRoboPassSeason02".to_owned(),
|
|
||||||
sprite: "Store_RoboPass".to_owned(),
|
|
||||||
is_sprite_full_size: true,
|
|
||||||
category: ItemShopCategory::Cube,
|
|
||||||
currency: CurrencyType::Robits,
|
|
||||||
price: 10_000,
|
|
||||||
discount_time: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(),
|
|
||||||
discount_price: 3_000,
|
|
||||||
recurrence: ItemShopRecurrence::Daily,
|
|
||||||
owns_required_cube: false,
|
|
||||||
is_limited_edition: false,
|
|
||||||
},
|
|
||||||
ItemShopBundle {
|
|
||||||
sku: "buy robopass 1 4".to_owned(),
|
|
||||||
bundle_name_key: "strRoboPassSeason02".to_owned(),
|
|
||||||
sprite: "Store_RoboPass".to_owned(),
|
|
||||||
is_sprite_full_size: true,
|
|
||||||
category: ItemShopCategory::Cube,
|
|
||||||
currency: CurrencyType::Robits,
|
|
||||||
price: 10_000,
|
|
||||||
discount_time: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(),
|
|
||||||
discount_price: 4_000,
|
|
||||||
recurrence: ItemShopRecurrence::Daily,
|
|
||||||
owns_required_cube: false,
|
|
||||||
is_limited_edition: false,
|
|
||||||
},
|
|
||||||
ItemShopBundle {
|
|
||||||
sku: "buy robopass 1 5".to_owned(),
|
|
||||||
bundle_name_key: "strRoboPassSeason02".to_owned(),
|
|
||||||
sprite: "Store_RoboPass".to_owned(),
|
|
||||||
is_sprite_full_size: true,
|
|
||||||
category: ItemShopCategory::Cube,
|
|
||||||
currency: CurrencyType::Robits,
|
|
||||||
price: 10_000,
|
|
||||||
discount_time: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(),
|
|
||||||
discount_price: 5_000,
|
|
||||||
recurrence: ItemShopRecurrence::Daily,
|
|
||||||
owns_required_cube: false,
|
|
||||||
is_limited_edition: false,
|
|
||||||
},
|
|
||||||
ItemShopBundle {
|
|
||||||
sku: "buy robopass 1 6".to_owned(),
|
|
||||||
bundle_name_key: "strRoboPassSeason02".to_owned(),
|
|
||||||
sprite: "Store_RoboPass".to_owned(),
|
|
||||||
is_sprite_full_size: true,
|
|
||||||
category: ItemShopCategory::Cube,
|
|
||||||
currency: CurrencyType::Robits,
|
|
||||||
price: 10_000,
|
|
||||||
discount_time: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(),
|
|
||||||
discount_price: 6_000,
|
|
||||||
recurrence: ItemShopRecurrence::Daily,
|
|
||||||
owns_required_cube: false,
|
|
||||||
is_limited_edition: false,
|
|
||||||
},
|
|
||||||
]));
|
|
||||||
Ok(params.into())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
98
rc_services_room/src/operations/item_shop_purchase.rs
Normal file
98
rc_services_room/src/operations/item_shop_purchase.rs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
|
||||||
|
use polariton::operation::{ParameterTable, Typed, Arr};
|
||||||
|
|
||||||
|
const CODE: u8 = 189;
|
||||||
|
|
||||||
|
const SKU_PARAM_KEY: u8 = 32; // str; in
|
||||||
|
const CURRENCY_PARAM_KEY: u8 = 53; // str; in
|
||||||
|
const PRICE_PARAM_KEY: u8 = 65; // int; in
|
||||||
|
const NEW_CUBES_PARAM_KEY: u8 = 72; // arr of str; out
|
||||||
|
|
||||||
|
pub(super) struct ItemBundleBuyer {
|
||||||
|
resolver: oj_rc_core::persist::config::ShopEntriesResolver,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl SimpleOperation<()> for ItemBundleBuyer {
|
||||||
|
type User = crate::UserTy;
|
||||||
|
const CODE: u8 = CODE;
|
||||||
|
|
||||||
|
async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result<ParameterTable, SimpleOpError> {
|
||||||
|
if let Some(Typed::Str(sku)) = params.remove(&SKU_PARAM_KEY) {
|
||||||
|
if let Some(Typed::Str(currency)) = params.remove(&CURRENCY_PARAM_KEY) {
|
||||||
|
if let Some(Typed::Int(price)) = params.remove(&PRICE_PARAM_KEY) {
|
||||||
|
let user_info = user.user()?;
|
||||||
|
let transactions = self.resolver.resolve_transactions().await;
|
||||||
|
// TODO refactor this into oj_rc_core in one of the User traits
|
||||||
|
if let Some(transaction) = transactions.get(&sku.string) {
|
||||||
|
let mut is_ok = false;
|
||||||
|
if currency.string == "Robits" && transaction.cost_free == price {
|
||||||
|
let ty = oj_rc_core::persist::user::CurrencyType::Free;
|
||||||
|
let op = oj_rc_core::persist::user::CurrencyOp::AddSub(-price as _);
|
||||||
|
user_info.currency(ty, op).await?;
|
||||||
|
is_ok = true;
|
||||||
|
} else if currency.string == "CosmeticCredits" && transaction.cost_paid == price {
|
||||||
|
let ty = oj_rc_core::persist::user::CurrencyType::Paid;
|
||||||
|
let op = oj_rc_core::persist::user::CurrencyOp::AddSub(-price as _);
|
||||||
|
user_info.currency(ty, op).await?;
|
||||||
|
is_ok = true;
|
||||||
|
}
|
||||||
|
if is_ok {
|
||||||
|
let mut new_cubes = Vec::new();
|
||||||
|
for award in transaction.gives.iter() {
|
||||||
|
match award {
|
||||||
|
oj_rc_core::persist::config::ShopGain::Cube(x) => {
|
||||||
|
new_cubes.push(*x);
|
||||||
|
},
|
||||||
|
oj_rc_core::persist::config::ShopGain::Experience(xp) => {
|
||||||
|
user_info.currency(
|
||||||
|
oj_rc_core::persist::user::CurrencyType::Experience,
|
||||||
|
oj_rc_core::persist::user::CurrencyOp::AddSub(*xp as _),
|
||||||
|
).await?;
|
||||||
|
},
|
||||||
|
oj_rc_core::persist::config::ShopGain::FreeCurrency(c) => {
|
||||||
|
user_info.currency(
|
||||||
|
oj_rc_core::persist::user::CurrencyType::Free,
|
||||||
|
oj_rc_core::persist::user::CurrencyOp::AddSub(*c as _),
|
||||||
|
).await?;
|
||||||
|
},
|
||||||
|
oj_rc_core::persist::config::ShopGain::PaidCurrency(c) => {
|
||||||
|
user_info.currency(
|
||||||
|
oj_rc_core::persist::user::CurrencyType::Paid,
|
||||||
|
oj_rc_core::persist::user::CurrencyOp::AddSub(*c as _),
|
||||||
|
).await?;
|
||||||
|
},
|
||||||
|
oj_rc_core::persist::config::ShopGain::TechPoints(tp) => {
|
||||||
|
user_info.currency(
|
||||||
|
oj_rc_core::persist::user::CurrencyType::TechPoints,
|
||||||
|
oj_rc_core::persist::user::CurrencyOp::AddSub(*tp as _),
|
||||||
|
).await?;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let new_cubes_len = new_cubes.len();
|
||||||
|
if !new_cubes.is_empty() {
|
||||||
|
user_info.unlock_parts(&new_cubes).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_cubes_typed: Vec<_> = new_cubes.into_iter().map(|x| Typed::Str(hex::encode((x as i32).to_be_bytes()).into())).collect();
|
||||||
|
//params.insert(NEW_CUBES_PARAM_KEY, Typed::Arr(new_cubes_i32.into()));
|
||||||
|
params.insert(NEW_CUBES_PARAM_KEY, Typed::Arr(Arr {
|
||||||
|
ty: polariton::serdes::TypePrefix::Str,
|
||||||
|
items: new_cubes_typed,
|
||||||
|
}));
|
||||||
|
log::info!("SKU {} purchased (ok? {}) {} ops received, {} cubes unlocked", sku.string, is_ok, transaction.gives.len(), new_cubes_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn item_purchase_provider(conf: &oj_rc_core::ConfigImpl) -> SimpleOpImpl<(), crate::UserTy, ItemBundleBuyer> {
|
||||||
|
SimpleOpImpl::new(ItemBundleBuyer {
|
||||||
|
resolver: <oj_rc_core::ConfigImpl as oj_rc_core::ConfigProvider<()>>::shop_entries(conf),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -99,6 +99,7 @@ mod garage_slot_name;
|
|||||||
mod garage_slot_copy;
|
mod garage_slot_copy;
|
||||||
mod steam_promo;
|
mod steam_promo;
|
||||||
mod campaign_save_result;
|
mod campaign_save_result;
|
||||||
|
mod item_shop_purchase;
|
||||||
|
|
||||||
use polariton_server::operations::OperationsHandler;
|
use polariton_server::operations::OperationsHandler;
|
||||||
|
|
||||||
@@ -151,7 +152,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler<crate::UserTy>
|
|||||||
.add(game_event_params::event_system_params_provider(&init_ctx.cubes))
|
.add(game_event_params::event_system_params_provider(&init_ctx.cubes))
|
||||||
.add(garage_bay_uuid::garage_id_provider())
|
.add(garage_bay_uuid::garage_id_provider())
|
||||||
.add(tech_tree_data::tech_tree_layout_provider(&init_ctx.cubes))
|
.add(tech_tree_data::tech_tree_layout_provider(&init_ctx.cubes))
|
||||||
.add(item_shop_bundles::item_bundle_provider())
|
.add(item_shop_bundles::item_bundle_provider(&init_ctx.cubes))
|
||||||
.add(robot_customisations::bay_customisations_provider())
|
.add(robot_customisations::bay_customisations_provider())
|
||||||
.add(player_data::player_data_provider())
|
.add(player_data::player_data_provider())
|
||||||
.add(player_robopass::player_robopass_season_provider())
|
.add(player_robopass::player_robopass_season_provider())
|
||||||
@@ -223,4 +224,5 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler<crate::UserTy>
|
|||||||
.add(polariton_server::operations::Ack::<90, _>::default()) // TODO handle SubmitCRFRatingRequest instead of ignoring it
|
.add(polariton_server::operations::Ack::<90, _>::default()) // TODO handle SubmitCRFRatingRequest instead of ignoring it
|
||||||
//.add(polariton_server::operations::Ack::<97, _>::default()) // TODO handle UpdateShopRobotOffsetRequest instead of ignoring it (this seems to break newly-uploaded vehicles for now)
|
//.add(polariton_server::operations::Ack::<97, _>::default()) // TODO handle UpdateShopRobotOffsetRequest instead of ignoring it (this seems to break newly-uploaded vehicles for now)
|
||||||
.add(steam_promo::steam_promos_provider())
|
.add(steam_promo::steam_promos_provider())
|
||||||
|
.add(item_shop_purchase::item_purchase_provider(&init_ctx.cubes))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user