generate the daily card shop server side
prices come from the csv globals now, a json cost is just an override. DATABASE_URL defaults to postgres:///scroll when unset.
This commit is contained in:
parent
ad5a1613e3
commit
8a745d634c
19 changed files with 449 additions and 80 deletions
|
|
@ -1,12 +1,11 @@
|
||||||
{
|
{
|
||||||
"offers": [
|
"offers": [
|
||||||
{ "id": 1, "give": "Gold#1000", "cost": "Diamonds#10" },
|
{ "id": 1, "give": "Gold#1000" },
|
||||||
{ "id": 2, "give": "Gold#10000", "cost": "Diamonds#80" },
|
{ "id": 2, "give": "Gold#10000" },
|
||||||
{ "id": 3, "give": "Diamonds#500", "cost": "Gold#20000" },
|
{ "id": 3, "give": "Gold#100000" },
|
||||||
{ "id": 4, "give": "Knight#10", "cost": "Gold#500" },
|
{ "id": 4, "give": "chest:Silver#1" },
|
||||||
{ "id": 5, "give": "Witch#4", "cost": "Gold#2000" },
|
{ "id": 5, "give": "chest:Gold#1" },
|
||||||
{ "id": 6, "give": "chest:Silver#1", "cost": "Diamonds#5" },
|
{ "id": 6, "give": "chest:Magic#1" },
|
||||||
{ "id": 7, "give": "chest:Gold#1", "cost": "Diamonds#20" },
|
{ "id": 7, "give": "Diamonds#500", "cost": "Gold#20000" }
|
||||||
{ "id": 8, "give": "chest:Magic#1", "cost": "Diamonds#230" }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ use service_rpc::{
|
||||||
use crate::config::AuthConfig;
|
use crate::config::AuthConfig;
|
||||||
use crate::store::{unavailable, AccountStore};
|
use crate::store::{unavailable, AccountStore};
|
||||||
use crate::time::{days_between, format_utc, unix_millis, unix_seconds};
|
use crate::time::{days_between, format_utc, unix_millis, unix_seconds};
|
||||||
pub fn missing_database() -> std::io::Error {
|
fn connect_failed(error: storage::StorageError) -> std::io::Error {
|
||||||
std::io::Error::other(
|
std::io::Error::other(format!(
|
||||||
"DATABASE_URL is not set, point it at a postgres database (postgres:///scroll)",
|
"{error}\ncreate the database with `createdb scroll`, or point DATABASE_URL somewhere else"
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
pub const ERROR_CODE_GENERIC: i32 = 1;
|
pub const ERROR_CODE_GENERIC: i32 = 1;
|
||||||
pub const ERROR_CODE_PATCH: i32 = 7;
|
pub const ERROR_CODE_PATCH: i32 = 7;
|
||||||
|
|
@ -21,10 +21,12 @@ pub struct AuthService {
|
||||||
}
|
}
|
||||||
impl AuthService {
|
impl AuthService {
|
||||||
pub async fn bootstrap(config: AuthConfig) -> std::io::Result<Arc<Self>> {
|
pub async fn bootstrap(config: AuthConfig) -> std::io::Result<Arc<Self>> {
|
||||||
let database = config.database.as_ref().ok_or_else(missing_database)?;
|
let database = config.database.clone().unwrap_or_default();
|
||||||
let store = AccountStore::open(database).await?;
|
let store = AccountStore::open(&database)
|
||||||
|
.await
|
||||||
|
.map_err(connect_failed)?;
|
||||||
let accounts = store.count().await?;
|
let accounts = store.count().await?;
|
||||||
tracing::info!(accounts, "account store ready");
|
tracing::info!(accounts, url = %database.url, "account store ready");
|
||||||
Ok(Arc::new(Self { config, store }))
|
Ok(Arc::new(Self { config, store }))
|
||||||
}
|
}
|
||||||
pub fn store(&self) -> &AccountStore {
|
pub fn store(&self) -> &AccountStore {
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,25 @@ use logic::{
|
||||||
LogicClaimRewardCommand, LogicDataRef, LogicHomeMode, LogicTimer, OutOfSyncMessage,
|
LogicClaimRewardCommand, LogicDataRef, LogicHomeMode, LogicTimer, OutOfSyncMessage,
|
||||||
OwnHomeDataMessage,
|
OwnHomeDataMessage,
|
||||||
};
|
};
|
||||||
|
use logic::{table, LogicGlobals};
|
||||||
use service_rpc::AccountRef;
|
use service_rpc::AccountRef;
|
||||||
use tokio::sync::{Mutex, RwLock};
|
use tokio::sync::{Mutex, RwLock};
|
||||||
use crate::config::ChestRewardConfig;
|
use crate::config::ChestRewardConfig;
|
||||||
use crate::home::{build_avatar, build_home};
|
use crate::home::{build_avatar, build_home};
|
||||||
use crate::rewards::RewardRoller;
|
use crate::rewards::RewardRoller;
|
||||||
use crate::shop::{ShopCatalog, ShopOffer};
|
use crate::shop::{Purchase, ShopCatalog, ShopCycle, ShopEntry};
|
||||||
use crate::store::{OwnedCard, PlayerProfile, StoredChest};
|
use crate::store::{OwnedCard, PlayerProfile, StoredChest};
|
||||||
pub const MAX_FAST_FORWARD_TICKS: i32 = 20 * 60 * 60;
|
pub const MAX_FAST_FORWARD_TICKS: i32 = 20 * 60 * 60;
|
||||||
|
fn gold() -> LogicDataRef {
|
||||||
|
LogicDataRef::by_name(table::RESOURCES, RESOURCE_GOLD)
|
||||||
|
}
|
||||||
|
fn diamonds() -> LogicDataRef {
|
||||||
|
LogicDataRef::by_name(table::RESOURCES, RESOURCE_DIAMONDS)
|
||||||
|
}
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct TurnResult {
|
pub struct TurnResult {
|
||||||
pub claims: Vec<LogicClaimRewardCommand>,
|
pub claims: Vec<LogicClaimRewardCommand>,
|
||||||
pub purchases: Vec<ShopOffer>,
|
pub purchases: Vec<Purchase>,
|
||||||
pub out_of_sync: Option<OutOfSyncMessage>,
|
pub out_of_sync: Option<OutOfSyncMessage>,
|
||||||
pub changed: bool,
|
pub changed: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -26,13 +33,26 @@ pub struct HomeMode {
|
||||||
}
|
}
|
||||||
impl HomeMode {
|
impl HomeMode {
|
||||||
pub fn from_profile(profile: &PlayerProfile, current_timestamp: i32) -> Self {
|
pub fn from_profile(profile: &PlayerProfile, current_timestamp: i32) -> Self {
|
||||||
Self {
|
let mut logic = LogicHomeMode::new(
|
||||||
logic: LogicHomeMode::new(
|
build_home(profile),
|
||||||
build_home(profile),
|
build_avatar(profile),
|
||||||
build_avatar(profile),
|
current_timestamp,
|
||||||
current_timestamp,
|
);
|
||||||
),
|
let cycle = ShopCycle::at(current_timestamp);
|
||||||
|
logic.set_shop(cycle.seed, cycle.seconds_to_cycle, cycle.weekday_index);
|
||||||
|
Self { logic }
|
||||||
|
}
|
||||||
|
pub fn shop_cycle(&self) -> ShopCycle {
|
||||||
|
ShopCycle::at(self.logic.current_timestamp())
|
||||||
|
}
|
||||||
|
pub fn refresh_shop(&mut self) -> Option<ShopCycle> {
|
||||||
|
let cycle = self.shop_cycle();
|
||||||
|
if self.logic.home().shop_seed == cycle.seed {
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
|
self.logic
|
||||||
|
.set_shop(cycle.seed, cycle.seconds_to_cycle, cycle.weekday_index);
|
||||||
|
Some(cycle)
|
||||||
}
|
}
|
||||||
pub fn logic(&self) -> &LogicHomeMode {
|
pub fn logic(&self) -> &LogicHomeMode {
|
||||||
&self.logic
|
&self.logic
|
||||||
|
|
@ -44,37 +64,83 @@ impl HomeMode {
|
||||||
random_seed,
|
random_seed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn market_cost(&self, give: &ShopEntry) -> Result<ShopEntry, &'static str> {
|
||||||
|
if let Some(chest) = give.data.as_treasure_chest() {
|
||||||
|
let price = chest.int("ShopPriceWithoutSpeedUp");
|
||||||
|
if price < 1 {
|
||||||
|
return Err("that chest is not sold in the shop");
|
||||||
|
}
|
||||||
|
return Ok(ShopEntry::new(diamonds(), price));
|
||||||
|
}
|
||||||
|
if give.is_resource() {
|
||||||
|
return Ok(ShopEntry::new(
|
||||||
|
diamonds(),
|
||||||
|
LogicGlobals::resource_diamond_cost(give.count),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err("that item has no market price")
|
||||||
|
}
|
||||||
|
fn pay(&mut self, cost: &ShopEntry) -> Result<(), &'static str> {
|
||||||
|
if !cost.is_resource() {
|
||||||
|
return Err("the price must be a resource");
|
||||||
|
}
|
||||||
|
match cost.data.name() {
|
||||||
|
RESOURCE_GOLD if self.logic.spend_gold(cost.count) => Ok(()),
|
||||||
|
RESOURCE_DIAMONDS if self.logic.spend_diamonds(cost.count) => Ok(()),
|
||||||
|
RESOURCE_GOLD | RESOURCE_DIAMONDS => Err("the player cannot afford it"),
|
||||||
|
_ => Err("only gold and diamonds are accepted"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn buy_card(&mut self, card: &LogicDataRef) -> Result<Purchase, &'static str> {
|
||||||
|
let spell = card.as_spell().ok_or("that is not a card")?;
|
||||||
|
let buy_times = self
|
||||||
|
.logic
|
||||||
|
.shop_buy_times(card)
|
||||||
|
.ok_or("that card is not on sale right now")?;
|
||||||
|
if buy_times >= spell.shop_buy_limit() {
|
||||||
|
return Err("the buy limit for that card is reached");
|
||||||
|
}
|
||||||
|
let cost = ShopEntry::new(gold(), spell.cost_in_shop(buy_times));
|
||||||
|
self.pay(&cost)?;
|
||||||
|
self.logic.increase_sold_spells(card);
|
||||||
|
self.logic.grant_card(card.clone(), 1);
|
||||||
|
Ok(Purchase {
|
||||||
|
offer_id: 0,
|
||||||
|
give: ShopEntry::new(card.clone(), 1),
|
||||||
|
cost,
|
||||||
|
})
|
||||||
|
}
|
||||||
fn purchase(
|
fn purchase(
|
||||||
&mut self,
|
&mut self,
|
||||||
data: &LogicDataRef,
|
data: &LogicDataRef,
|
||||||
count: i32,
|
count: i32,
|
||||||
shop: &ShopCatalog,
|
shop: &ShopCatalog,
|
||||||
) -> Result<ShopOffer, &'static str> {
|
) -> Result<Purchase, &'static str> {
|
||||||
|
if data.as_spell().is_some() {
|
||||||
|
return self.buy_card(data);
|
||||||
|
}
|
||||||
let offer = shop
|
let offer = shop
|
||||||
.offer_for(data)
|
.offer_for(data)
|
||||||
.filter(|offer| offer.give.count == count)
|
.filter(|offer| offer.give.count == count)
|
||||||
.ok_or("nothing in the shop sells that")?
|
.ok_or("nothing in the shop sells that")?
|
||||||
.clone();
|
.clone();
|
||||||
if !offer.cost.is_resource() {
|
let cost = match &offer.cost {
|
||||||
return Err("the price must be a resource");
|
Some(cost) => cost.clone(),
|
||||||
}
|
None => self.market_cost(&offer.give)?,
|
||||||
match offer.cost.data.name() {
|
};
|
||||||
RESOURCE_GOLD if self.logic.spend_gold(offer.cost.count) => {}
|
self.pay(&cost)?;
|
||||||
RESOURCE_DIAMONDS if self.logic.spend_diamonds(offer.cost.count) => {}
|
|
||||||
RESOURCE_GOLD | RESOURCE_DIAMONDS => return Err("the player cannot afford it"),
|
|
||||||
_ => return Err("only gold and diamonds are accepted"),
|
|
||||||
}
|
|
||||||
if offer.give.is_resource() {
|
if offer.give.is_resource() {
|
||||||
match offer.give.data.name() {
|
match offer.give.data.name() {
|
||||||
RESOURCE_GOLD => self.logic.add_gold(offer.give.count),
|
RESOURCE_GOLD => self.logic.add_gold(offer.give.count),
|
||||||
RESOURCE_DIAMONDS => self.logic.add_diamonds(offer.give.count),
|
RESOURCE_DIAMONDS => self.logic.add_diamonds(offer.give.count),
|
||||||
_ => return Err("only gold and diamonds can be sold"),
|
_ => return Err("only gold and diamonds can be sold"),
|
||||||
}
|
}
|
||||||
} else if offer.give.is_card() {
|
|
||||||
self.logic
|
|
||||||
.grant_card(offer.give.data.clone(), offer.give.count);
|
|
||||||
}
|
}
|
||||||
Ok(offer)
|
Ok(Purchase {
|
||||||
|
offer_id: offer.id,
|
||||||
|
give: offer.give,
|
||||||
|
cost,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
pub fn end_client_turn(
|
pub fn end_client_turn(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -113,12 +179,12 @@ impl HomeMode {
|
||||||
}
|
}
|
||||||
CommandOutcome::PurchaseRequested { data, count } => {
|
CommandOutcome::PurchaseRequested { data, count } => {
|
||||||
match self.purchase(&data, count, shop) {
|
match self.purchase(&data, count, shop) {
|
||||||
Ok(offer) => {
|
Ok(purchase) => {
|
||||||
if offer.give.is_chest() {
|
if purchase.give.is_chest() {
|
||||||
pending.push((chest_source::PURCHASED, 0));
|
pending.push((chest_source::PURCHASED, 0));
|
||||||
}
|
}
|
||||||
result.changed = true;
|
result.changed = true;
|
||||||
result.purchases.push(offer);
|
result.purchases.push(purchase);
|
||||||
}
|
}
|
||||||
Err(reason) => {
|
Err(reason) => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use logic::{
|
use logic::{
|
||||||
EndClientTurnMessage, LogicCommandManager, LogicDataRef, OutOfSyncMessage, OwnHomeDataMessage,
|
AvailableServerCommandMessage, EndClientTurnMessage, LogicCommandManager, LogicDataRef,
|
||||||
|
LogicShopSeedChangedCommand, OutOfSyncMessage, OwnHomeDataMessage,
|
||||||
};
|
};
|
||||||
use service_rpc::{
|
use service_rpc::{
|
||||||
AccountRef, GameApi, GameRequest, GameResponse, HomeRequestKind, RpcError, RpcResult,
|
AccountRef, GameApi, GameRequest, GameResponse, HomeRequestKind, RpcError, RpcResult,
|
||||||
|
|
@ -11,8 +12,8 @@ use crate::catalog::Catalog;
|
||||||
use crate::config::GameConfig;
|
use crate::config::GameConfig;
|
||||||
use crate::home_mode::{available_server_command, HomeModeRegistry};
|
use crate::home_mode::{available_server_command, HomeModeRegistry};
|
||||||
use crate::rewards::RewardRoller;
|
use crate::rewards::RewardRoller;
|
||||||
use crate::shop::ShopCatalog;
|
use crate::shop::{ShopCatalog, ShopCycle};
|
||||||
use crate::store::{missing_database, unavailable, PlayerProfile, ProfileStore};
|
use crate::store::{connect_failed, unavailable, PlayerProfile, ProfileStore};
|
||||||
use crate::time::unix_seconds;
|
use crate::time::unix_seconds;
|
||||||
pub struct GameService {
|
pub struct GameService {
|
||||||
config: GameConfig,
|
config: GameConfig,
|
||||||
|
|
@ -24,8 +25,10 @@ pub struct GameService {
|
||||||
impl GameService {
|
impl GameService {
|
||||||
pub async fn bootstrap(config: GameConfig) -> std::io::Result<Arc<Self>> {
|
pub async fn bootstrap(config: GameConfig) -> std::io::Result<Arc<Self>> {
|
||||||
let catalog = Arc::new(Catalog::load(config.csv_root.as_deref()));
|
let catalog = Arc::new(Catalog::load(config.csv_root.as_deref()));
|
||||||
let database = config.database.as_ref().ok_or_else(missing_database)?;
|
let database = config.database.clone().unwrap_or_default();
|
||||||
let profiles = ProfileStore::open(database).await?;
|
let profiles = ProfileStore::open(&database)
|
||||||
|
.await
|
||||||
|
.map_err(connect_failed)?;
|
||||||
let profile_count = profiles.count().await?;
|
let profile_count = profiles.count().await?;
|
||||||
let shop = Arc::new(ShopCatalog::load(config.shop_path.as_deref()));
|
let shop = Arc::new(ShopCatalog::load(config.shop_path.as_deref()));
|
||||||
for offer in shop.describe() {
|
for offer in shop.describe() {
|
||||||
|
|
@ -110,11 +113,22 @@ impl GameApi for GameService {
|
||||||
.sessions
|
.sessions
|
||||||
.get_or_open(account, &profile, unix_seconds() as i32)
|
.get_or_open(account, &profile, unix_seconds() as i32)
|
||||||
.await;
|
.await;
|
||||||
let (home_data, checksum): (OwnHomeDataMessage, i32) = {
|
let (home_data, checksum, cycle, on_sale): (
|
||||||
|
OwnHomeDataMessage,
|
||||||
|
i32,
|
||||||
|
ShopCycle,
|
||||||
|
Vec<String>,
|
||||||
|
) = {
|
||||||
let home = session.lock().await;
|
let home = session.lock().await;
|
||||||
(
|
(
|
||||||
home.own_home_data(self.config.random_seed),
|
home.own_home_data(self.config.random_seed),
|
||||||
home.logic().checksum(),
|
home.logic().checksum(),
|
||||||
|
home.shop_cycle(),
|
||||||
|
home.logic()
|
||||||
|
.shop_spells()
|
||||||
|
.iter()
|
||||||
|
.map(|slot| slot.data.to_string())
|
||||||
|
.collect(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
|
|
@ -125,7 +139,24 @@ impl GameApi for GameService {
|
||||||
checksum,
|
checksum,
|
||||||
"serving own home data"
|
"serving own home data"
|
||||||
);
|
);
|
||||||
Ok(vec![encode(&home_data)?])
|
tracing::info!(
|
||||||
|
%account,
|
||||||
|
seed = cycle.seed,
|
||||||
|
weekday = cycle.weekday_index,
|
||||||
|
cards = ?on_sale,
|
||||||
|
"shop on sale"
|
||||||
|
);
|
||||||
|
let seed_changed = LogicShopSeedChangedCommand::new(
|
||||||
|
cycle.seed,
|
||||||
|
cycle.seconds_to_cycle,
|
||||||
|
cycle.weekday_index,
|
||||||
|
);
|
||||||
|
Ok(vec![
|
||||||
|
encode(&home_data)?,
|
||||||
|
encode(&AvailableServerCommandMessage {
|
||||||
|
command: Box::new(seed_changed),
|
||||||
|
})?,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
async fn client_capabilities(&self, account: AccountRef, ping_ms: i32) -> RpcResult<()> {
|
async fn client_capabilities(&self, account: AccountRef, ping_ms: i32) -> RpcResult<()> {
|
||||||
tracing::debug!(%account, ping_ms, "client capabilities");
|
tracing::debug!(%account, ping_ms, "client capabilities");
|
||||||
|
|
@ -171,16 +202,16 @@ impl GameApi for GameService {
|
||||||
);
|
);
|
||||||
replies.push(encode::<OutOfSyncMessage>(&out_of_sync)?);
|
replies.push(encode::<OutOfSyncMessage>(&out_of_sync)?);
|
||||||
}
|
}
|
||||||
for offer in &result.purchases {
|
for purchase in &result.purchases {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
%account,
|
%account,
|
||||||
offer = offer.id,
|
offer = purchase.offer_id,
|
||||||
cost = %offer.cost,
|
cost = %purchase.cost,
|
||||||
give = %offer.give,
|
give = %purchase.give,
|
||||||
"shop purchase"
|
"shop purchase"
|
||||||
);
|
);
|
||||||
if let Err(error) = self.profiles.record_purchase(account, offer).await {
|
if let Err(error) = self.profiles.record_purchase(account, purchase).await {
|
||||||
tracing::warn!(%account, offer = offer.id, %error, "could not record the purchase");
|
tracing::warn!(%account, %error, "could not record the purchase");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for claim in result.claims {
|
for claim in result.claims {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ pub const DEFAULT_SHOP: &str = include_str!("default_shop.json");
|
||||||
pub struct ShopOffer {
|
pub struct ShopOffer {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub give: ShopEntry,
|
pub give: ShopEntry,
|
||||||
pub cost: ShopEntry,
|
#[serde(default)]
|
||||||
|
pub cost: Option<ShopEntry>,
|
||||||
}
|
}
|
||||||
impl ShopOffer {
|
impl ShopOffer {
|
||||||
pub fn sells(&self, data: &logic::LogicDataRef) -> bool {
|
pub fn sells(&self, data: &logic::LogicDataRef) -> bool {
|
||||||
|
|
@ -67,7 +68,10 @@ impl ShopCatalog {
|
||||||
pub fn describe(&self) -> Vec<String> {
|
pub fn describe(&self) -> Vec<String> {
|
||||||
self.offers
|
self.offers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|offer| format!("{} = {} -> {}", offer.id, offer.cost, offer.give))
|
.map(|offer| match &offer.cost {
|
||||||
|
Some(cost) => format!("{} = {} -> {}", offer.id, cost, offer.give),
|
||||||
|
None => format!("{} = market price -> {}", offer.id, offer.give),
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
crates/game-service/src/shop/cycle.rs
Normal file
19
crates/game-service/src/shop/cycle.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
pub const SECONDS_PER_DAY: i32 = 86_400;
|
||||||
|
pub const WEEKDAYS: i32 = 7;
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ShopCycle {
|
||||||
|
pub seed: i32,
|
||||||
|
pub weekday_index: i32,
|
||||||
|
pub seconds_to_cycle: i32,
|
||||||
|
}
|
||||||
|
impl ShopCycle {
|
||||||
|
pub fn at(timestamp: i32) -> Self {
|
||||||
|
let day = timestamp.div_euclid(SECONDS_PER_DAY);
|
||||||
|
let seconds_into_day = timestamp.rem_euclid(SECONDS_PER_DAY);
|
||||||
|
Self {
|
||||||
|
seed: day.wrapping_mul(2_654_435_761u32 as i32) | 1,
|
||||||
|
weekday_index: day.rem_euclid(WEEKDAYS) + 1,
|
||||||
|
seconds_to_cycle: SECONDS_PER_DAY - seconds_into_day,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
{
|
{
|
||||||
"offers": [
|
"offers": [
|
||||||
{ "id": 1, "give": "Gold#1000", "cost": "Diamonds#10" },
|
{ "id": 1, "give": "Gold#1000" },
|
||||||
{ "id": 2, "give": "Gold#10000", "cost": "Diamonds#80" },
|
{ "id": 2, "give": "Gold#10000" },
|
||||||
{ "id": 3, "give": "Diamonds#500", "cost": "Gold#20000" },
|
{ "id": 3, "give": "Gold#100000" },
|
||||||
{ "id": 4, "give": "Knight#10", "cost": "Gold#500" },
|
{ "id": 4, "give": "chest:Silver#1" },
|
||||||
{ "id": 5, "give": "Witch#4", "cost": "Gold#2000" },
|
{ "id": 5, "give": "chest:Gold#1" },
|
||||||
{ "id": 6, "give": "chest:Silver#1", "cost": "Diamonds#5" },
|
{ "id": 6, "give": "chest:Magic#1" },
|
||||||
{ "id": 7, "give": "chest:Gold#1", "cost": "Diamonds#20" },
|
{ "id": 7, "give": "Diamonds#500", "cost": "Gold#20000" }
|
||||||
{ "id": 8, "give": "chest:Magic#1", "cost": "Diamonds#230" }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,12 @@
|
||||||
mod catalog;
|
mod catalog;
|
||||||
|
mod cycle;
|
||||||
mod entry;
|
mod entry;
|
||||||
pub use catalog::{ShopCatalog, ShopOffer, DEFAULT_SHOP};
|
pub use catalog::{ShopCatalog, ShopOffer, DEFAULT_SHOP};
|
||||||
|
pub use cycle::{ShopCycle, SECONDS_PER_DAY, WEEKDAYS};
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Purchase {
|
||||||
|
pub offer_id: i32,
|
||||||
|
pub give: ShopEntry,
|
||||||
|
pub cost: ShopEntry,
|
||||||
|
}
|
||||||
pub use entry::{EntryError, ShopEntry, LOOKUP, QUALIFIER, SEPARATOR};
|
pub use entry::{EntryError, ShopEntry, LOOKUP, QUALIFIER, SEPARATOR};
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ pub use profile::{
|
||||||
pub fn unavailable(error: StorageError) -> RpcError {
|
pub fn unavailable(error: StorageError) -> RpcError {
|
||||||
RpcError::Unavailable(error.to_string())
|
RpcError::Unavailable(error.to_string())
|
||||||
}
|
}
|
||||||
pub fn missing_database() -> std::io::Error {
|
pub fn connect_failed(error: StorageError) -> std::io::Error {
|
||||||
std::io::Error::other(
|
std::io::Error::other(format!(
|
||||||
"DATABASE_URL is not set, point it at a postgres database (postgres:///scroll)",
|
"{error}\ncreate the database with `createdb scroll`, or point DATABASE_URL somewhere else"
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use logic::LogicDataRef;
|
||||||
use service_rpc::AccountRef;
|
use service_rpc::AccountRef;
|
||||||
use storage::sqlx::{Postgres, QueryBuilder};
|
use storage::sqlx::{Postgres, QueryBuilder};
|
||||||
use storage::{Database, DatabaseConfig, PgRow, Result, Row};
|
use storage::{Database, DatabaseConfig, PgRow, Result, Row};
|
||||||
use crate::shop::ShopOffer;
|
use crate::shop::Purchase;
|
||||||
use crate::store::{
|
use crate::store::{
|
||||||
OwnedCard, PlayerProfile, StoredChest, CARD_HOLDER_COLLECTION, CARD_HOLDER_DECK,
|
OwnedCard, PlayerProfile, StoredChest, CARD_HOLDER_COLLECTION, CARD_HOLDER_DECK,
|
||||||
};
|
};
|
||||||
|
|
@ -341,9 +341,9 @@ impl ProfileStore {
|
||||||
.await?;
|
.await?;
|
||||||
Ok(total.max(0) as usize)
|
Ok(total.max(0) as usize)
|
||||||
}
|
}
|
||||||
pub async fn record_purchase(&self, account: AccountRef, offer: &ShopOffer) -> Result<()> {
|
pub async fn record_purchase(&self, account: AccountRef, purchase: &Purchase) -> Result<()> {
|
||||||
let (cost_table, cost_instance, _) = parts(&offer.cost.data);
|
let (cost_table, cost_instance, _) = parts(&purchase.cost.data);
|
||||||
let (give_table, give_instance, _) = parts(&offer.give.data);
|
let (give_table, give_instance, _) = parts(&purchase.give.data);
|
||||||
storage::sqlx::query(
|
storage::sqlx::query(
|
||||||
"insert into shop_purchases (
|
"insert into shop_purchases (
|
||||||
account_high, account_low, offer_id,
|
account_high, account_low, offer_id,
|
||||||
|
|
@ -354,13 +354,13 @@ impl ProfileStore {
|
||||||
)
|
)
|
||||||
.bind(account.high)
|
.bind(account.high)
|
||||||
.bind(account.low)
|
.bind(account.low)
|
||||||
.bind(offer.id)
|
.bind(purchase.offer_id)
|
||||||
.bind(cost_table)
|
.bind(cost_table)
|
||||||
.bind(cost_instance)
|
.bind(cost_instance)
|
||||||
.bind(offer.cost.count)
|
.bind(purchase.cost.count)
|
||||||
.bind(give_table)
|
.bind(give_table)
|
||||||
.bind(give_instance)
|
.bind(give_instance)
|
||||||
.bind(offer.give.count)
|
.bind(purchase.give.count)
|
||||||
.bind(unix_seconds())
|
.bind(unix_seconds())
|
||||||
.execute(self.database.pool())
|
.execute(self.database.pool())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
||||||
75
crates/logic/src/data/globals.rs
Normal file
75
crates/logic/src/data/globals.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
use crate::data::data_ref::LogicDataRef;
|
||||||
|
use crate::data::tables::table;
|
||||||
|
pub const PRICE_COMMON: &str = "PRICE_COMMON";
|
||||||
|
pub const PRICE_RARE: &str = "PRICE_RARE";
|
||||||
|
pub const PRICE_EPIC: &str = "PRICE_EPIC";
|
||||||
|
pub const PRICE_COMMON_INCREASE: &str = "PRICE_COMMON_INCREASE_PER_ONE_BOUGHT_PERCENT";
|
||||||
|
pub const PRICE_RARE_INCREASE: &str = "PRICE_RARE_INCREASE_PER_ONE_BOUGHT_PERCENT";
|
||||||
|
pub const PRICE_EPIC_INCREASE: &str = "PRICE_EPIC_INCREASE_PER_ONE_BOUGHT_PERCENT";
|
||||||
|
pub const BUY_LIMIT_COMMON: &str = "BUY_LIMIT_COMMON";
|
||||||
|
pub const BUY_LIMIT_RARE: &str = "BUY_LIMIT_RARE";
|
||||||
|
pub const BUY_LIMIT_EPIC: &str = "BUY_LIMIT_EPIC";
|
||||||
|
pub const RARITY_COMMON: &str = "Common";
|
||||||
|
pub const RARITY_RARE: &str = "Rare";
|
||||||
|
pub const RARITY_EPIC: &str = "Epic";
|
||||||
|
pub struct LogicGlobals;
|
||||||
|
impl LogicGlobals {
|
||||||
|
pub fn number(name: &str) -> i32 {
|
||||||
|
LogicDataRef::by_name(table::GLOBALS, name)
|
||||||
|
.data()
|
||||||
|
.map(|data| data.int("NumberValue"))
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
pub fn base_price(rarity: &str) -> i32 {
|
||||||
|
Self::number(match rarity {
|
||||||
|
RARITY_EPIC => PRICE_EPIC,
|
||||||
|
RARITY_RARE => PRICE_RARE,
|
||||||
|
_ => PRICE_COMMON,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn price_increase_percent(rarity: &str) -> i32 {
|
||||||
|
Self::number(match rarity {
|
||||||
|
RARITY_EPIC => PRICE_EPIC_INCREASE,
|
||||||
|
RARITY_RARE => PRICE_RARE_INCREASE,
|
||||||
|
_ => PRICE_COMMON_INCREASE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn resource_diamond_cost(amount: i32) -> i32 {
|
||||||
|
const STEPS: [(i32, &str); 7] = [
|
||||||
|
(1, "RESOURCE_DIAMOND_COST_1"),
|
||||||
|
(10, "RESOURCE_DIAMOND_COST_10"),
|
||||||
|
(100, "RESOURCE_DIAMOND_COST_100"),
|
||||||
|
(1_000, "RESOURCE_DIAMOND_COST_1000"),
|
||||||
|
(10_000, "RESOURCE_DIAMOND_COST_10000"),
|
||||||
|
(100_000, "RESOURCE_DIAMOND_COST_100000"),
|
||||||
|
(1_000_000, "RESOURCE_DIAMOND_COST_1000000"),
|
||||||
|
];
|
||||||
|
if amount <= STEPS[0].0 {
|
||||||
|
return Self::number(STEPS[0].1);
|
||||||
|
}
|
||||||
|
for window in STEPS.windows(2) {
|
||||||
|
let (low_amount, low_key) = window[0];
|
||||||
|
let (high_amount, high_key) = window[1];
|
||||||
|
if amount > high_amount {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let low = Self::number(low_key);
|
||||||
|
let high = Self::number(high_key);
|
||||||
|
let span = (high_amount - low_amount) as i64;
|
||||||
|
if span <= 0 {
|
||||||
|
return high;
|
||||||
|
}
|
||||||
|
let travelled = (amount - low_amount) as i64;
|
||||||
|
let interpolated = low as i64 + (high - low) as i64 * travelled / span;
|
||||||
|
return interpolated as i32;
|
||||||
|
}
|
||||||
|
Self::number(STEPS[STEPS.len() - 1].1)
|
||||||
|
}
|
||||||
|
pub fn buy_limit(rarity: &str) -> i32 {
|
||||||
|
Self::number(match rarity {
|
||||||
|
RARITY_EPIC => BUY_LIMIT_EPIC,
|
||||||
|
RARITY_RARE => BUY_LIMIT_RARE,
|
||||||
|
_ => BUY_LIMIT_COMMON,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
mod data_ref;
|
mod data_ref;
|
||||||
|
mod globals;
|
||||||
mod logic_data;
|
mod logic_data;
|
||||||
mod logic_data_table;
|
mod logic_data_table;
|
||||||
mod logic_data_tables;
|
mod logic_data_tables;
|
||||||
mod tables;
|
mod tables;
|
||||||
mod typed;
|
mod typed;
|
||||||
pub use data_ref::LogicDataRef;
|
pub use data_ref::LogicDataRef;
|
||||||
|
pub use globals::{LogicGlobals, RARITY_COMMON, RARITY_EPIC, RARITY_RARE};
|
||||||
pub use logic_data::LogicData;
|
pub use logic_data::LogicData;
|
||||||
pub use logic_data_table::LogicDataTable;
|
pub use logic_data_table::LogicDataTable;
|
||||||
pub use logic_data_tables::{
|
pub use logic_data_tables::{
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,11 @@ typed_data!(LogicResourceData);
|
||||||
typed_data!(LogicTreasureChestData);
|
typed_data!(LogicTreasureChestData);
|
||||||
typed_data!(LogicRarityData);
|
typed_data!(LogicRarityData);
|
||||||
typed_data!(LogicResourcePackData);
|
typed_data!(LogicResourcePackData);
|
||||||
|
impl LogicArenaData {
|
||||||
|
pub fn index(&self) -> i32 {
|
||||||
|
self.int("Arena")
|
||||||
|
}
|
||||||
|
}
|
||||||
impl LogicResourcePackData {
|
impl LogicResourcePackData {
|
||||||
pub fn resource(&self) -> LogicDataRef {
|
pub fn resource(&self) -> LogicDataRef {
|
||||||
LogicDataRef::by_name(
|
LogicDataRef::by_name(
|
||||||
|
|
@ -57,6 +62,34 @@ impl LogicSpellData {
|
||||||
pub fn rarity(&self) -> &str {
|
pub fn rarity(&self) -> &str {
|
||||||
self.string("Rarity")
|
self.string("Rarity")
|
||||||
}
|
}
|
||||||
|
pub fn unlock_arena_data(&self) -> LogicDataRef {
|
||||||
|
LogicDataRef::by_name(
|
||||||
|
crate::data::tables::table::ARENAS,
|
||||||
|
self.string("UnlockArena"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub fn is_unlocked_in_arena(&self, arena: &LogicDataRef) -> bool {
|
||||||
|
let required = self
|
||||||
|
.unlock_arena_data()
|
||||||
|
.as_arena()
|
||||||
|
.map(|a| a.index())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let reached = arena.as_arena().map(|a| a.index()).unwrap_or(0);
|
||||||
|
reached >= required
|
||||||
|
}
|
||||||
|
pub fn cost_in_shop(&self, buy_times: i32) -> i32 {
|
||||||
|
use crate::data::globals::LogicGlobals;
|
||||||
|
let rarity = self.rarity();
|
||||||
|
let percent = LogicGlobals::price_increase_percent(rarity);
|
||||||
|
let mut cost = LogicGlobals::base_price(rarity);
|
||||||
|
for _ in 0..buy_times.max(0) {
|
||||||
|
cost = cost.saturating_mul(percent) / 100;
|
||||||
|
}
|
||||||
|
cost
|
||||||
|
}
|
||||||
|
pub fn shop_buy_limit(&self) -> i32 {
|
||||||
|
crate::data::globals::LogicGlobals::buy_limit(self.rarity())
|
||||||
|
}
|
||||||
pub fn mana_cost(&self) -> i32 {
|
pub fn mana_cost(&self) -> i32 {
|
||||||
self.int("ManaCost")
|
self.int("ManaCost")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::commands::{CommandOutcome, LogicCommand, LogicReward};
|
use crate::commands::{CommandOutcome, LogicCommand, LogicReward};
|
||||||
use crate::data::{LogicDataRef, LogicRarityData};
|
use crate::data::{LogicDataRef, LogicRarityData};
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
ChestSource, LogicChest, LogicClientAvatar, LogicClientHome, LogicSpell, LogicTimer,
|
ChestSource, LogicChest, LogicClientAvatar, LogicClientHome, LogicDataSlot, LogicSpell,
|
||||||
TICKS_PER_SECOND,
|
LogicTimer, TICKS_PER_SECOND,
|
||||||
};
|
};
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct LogicHomeMode {
|
pub struct LogicHomeMode {
|
||||||
|
|
@ -108,6 +108,33 @@ impl LogicHomeMode {
|
||||||
pub fn begin_claim(&mut self) {
|
pub fn begin_claim(&mut self) {
|
||||||
self.claiming_reward = true;
|
self.claiming_reward = true;
|
||||||
}
|
}
|
||||||
|
pub fn set_shop(&mut self, seed: i32, seconds_to_cycle: i32, weekday_index: i32) {
|
||||||
|
let arena = self.avatar.arena.clone();
|
||||||
|
self.home.shop_seed = seed;
|
||||||
|
self.home.shop_weekday_index = weekday_index;
|
||||||
|
self.home.shop_timer = LogicTimer::started(seconds_to_cycle, self.current_timestamp);
|
||||||
|
self.home.shop_spells = crate::home::randomize_shop_items(seed, weekday_index, &arena);
|
||||||
|
}
|
||||||
|
pub fn shop_spells(&self) -> &[LogicDataSlot] {
|
||||||
|
&self.home.shop_spells
|
||||||
|
}
|
||||||
|
pub fn shop_buy_times(&self, card: &LogicDataRef) -> Option<i32> {
|
||||||
|
self.home
|
||||||
|
.shop_spells
|
||||||
|
.iter()
|
||||||
|
.find(|slot| &slot.data == card)
|
||||||
|
.map(|slot| slot.count)
|
||||||
|
}
|
||||||
|
pub fn increase_sold_spells(&mut self, card: &LogicDataRef) {
|
||||||
|
if let Some(slot) = self
|
||||||
|
.home
|
||||||
|
.shop_spells
|
||||||
|
.iter_mut()
|
||||||
|
.find(|slot| &slot.data == card)
|
||||||
|
{
|
||||||
|
slot.count = slot.count.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn diamonds(&self) -> i32 {
|
pub fn diamonds(&self) -> i32 {
|
||||||
self.avatar.diamonds
|
self.avatar.diamonds
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,7 @@
|
||||||
mod logic_home_mode;
|
mod logic_home_mode;
|
||||||
|
mod shop;
|
||||||
pub use logic_home_mode::LogicHomeMode;
|
pub use logic_home_mode::LogicHomeMode;
|
||||||
|
pub use shop::{
|
||||||
|
cards_on_sale, randomize_shop_items, LogicRandom, CARD_TABLES, EPIC_SUNDAY_WEEKDAY,
|
||||||
|
RARITIES_ON_SALE,
|
||||||
|
};
|
||||||
|
|
|
||||||
92
crates/logic/src/home/shop.rs
Normal file
92
crates/logic/src/home/shop.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
use crate::data::{table, LogicDataRef, LogicDataTables, RARITY_COMMON, RARITY_EPIC, RARITY_RARE};
|
||||||
|
use crate::model::LogicDataSlot;
|
||||||
|
pub const EPIC_SUNDAY_WEEKDAY: i32 = 1;
|
||||||
|
pub const RARITIES_ON_SALE: [&str; 3] = [RARITY_COMMON, RARITY_RARE, RARITY_EPIC];
|
||||||
|
pub const CARD_TABLES: [i32; 3] = [
|
||||||
|
table::SPELLS_CHARACTERS,
|
||||||
|
table::SPELLS_BUILDINGS,
|
||||||
|
table::SPELLS_OTHER,
|
||||||
|
];
|
||||||
|
pub struct LogicRandom {
|
||||||
|
seed: i32,
|
||||||
|
}
|
||||||
|
impl LogicRandom {
|
||||||
|
pub fn new(seed: i32) -> Self {
|
||||||
|
Self { seed }
|
||||||
|
}
|
||||||
|
pub fn seed(&self) -> i32 {
|
||||||
|
self.seed
|
||||||
|
}
|
||||||
|
pub fn next(&mut self, bound: i32) -> i32 {
|
||||||
|
if bound <= 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if self.seed == 0 {
|
||||||
|
self.seed = -1;
|
||||||
|
}
|
||||||
|
let mut state = self.seed;
|
||||||
|
state ^= state.wrapping_shl(13);
|
||||||
|
state ^= state >> 17;
|
||||||
|
state ^= state.wrapping_shl(5);
|
||||||
|
self.seed = state;
|
||||||
|
state.checked_abs().unwrap_or(0) % bound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn cards_on_sale(rarity: &str, arena: &LogicDataRef) -> Vec<LogicDataRef> {
|
||||||
|
let tables = LogicDataTables::instance();
|
||||||
|
let mut pool = Vec::new();
|
||||||
|
for table_index in CARD_TABLES {
|
||||||
|
let Some(rows) = tables.table(table_index) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for row in rows.iter() {
|
||||||
|
let candidate = LogicDataRef::from(std::sync::Arc::clone(row));
|
||||||
|
let Some(spell) = candidate.as_spell() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if spell.not_in_use() || spell.rarity() != rarity || !spell.is_unlocked_in_arena(arena)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pool.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pool
|
||||||
|
}
|
||||||
|
pub fn randomize_shop_items(
|
||||||
|
seed: i32,
|
||||||
|
weekday_index: i32,
|
||||||
|
arena: &LogicDataRef,
|
||||||
|
) -> Vec<LogicDataSlot> {
|
||||||
|
let picks = if weekday_index == EPIC_SUNDAY_WEEKDAY {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
let mut random = LogicRandom::new(seed);
|
||||||
|
let mut chosen: Vec<LogicDataRef> = Vec::new();
|
||||||
|
for rarity in RARITIES_ON_SALE {
|
||||||
|
let pool = cards_on_sale(rarity, arena);
|
||||||
|
if pool.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for _ in 0..picks {
|
||||||
|
let mut attempts = 0;
|
||||||
|
loop {
|
||||||
|
let candidate = pool[random.next(pool.len() as i32) as usize].clone();
|
||||||
|
if !chosen.contains(&candidate) {
|
||||||
|
chosen.push(candidate);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
attempts += 1;
|
||||||
|
if attempts >= pool.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chosen
|
||||||
|
.into_iter()
|
||||||
|
.map(|card| LogicDataSlot::new(card, 0))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
@ -15,12 +15,12 @@ pub use commands::{
|
||||||
};
|
};
|
||||||
pub use data::{
|
pub use data::{
|
||||||
table, DataError, LogicArenaData, LogicData, LogicDataRef, LogicDataTable,
|
table, DataError, LogicArenaData, LogicData, LogicDataRef, LogicDataTable,
|
||||||
LogicDataTableResource, LogicDataTables, LogicRarityData, LogicResourceData,
|
LogicDataTableResource, LogicDataTables, LogicGlobals, LogicRarityData, LogicResourceData,
|
||||||
LogicResourcePackData, LogicSpellData, LogicTreasureChestData, DATA_TABLE_RESOURCES,
|
LogicResourcePackData, LogicSpellData, LogicTreasureChestData, DATA_TABLE_RESOURCES,
|
||||||
TABLE_COUNT,
|
TABLE_COUNT,
|
||||||
};
|
};
|
||||||
pub use factory::{scroll_message_registry, LogicScrollMessageFactory};
|
pub use factory::{scroll_message_registry, LogicScrollMessageFactory};
|
||||||
pub use home::LogicHomeMode;
|
pub use home::{randomize_shop_items, LogicHomeMode, LogicRandom};
|
||||||
pub use messages::*;
|
pub use messages::*;
|
||||||
pub use model::*;
|
pub use model::*;
|
||||||
pub use titan::{GlobalId, LogicLong};
|
pub use titan::{GlobalId, LogicLong};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
mod error;
|
mod error;
|
||||||
mod pool;
|
mod pool;
|
||||||
pub use error::{Result, StorageError};
|
pub use error::{Result, StorageError};
|
||||||
pub use pool::{Database, DatabaseConfig};
|
pub use pool::{Database, DatabaseConfig, DEFAULT_URL};
|
||||||
pub use sqlx;
|
pub use sqlx;
|
||||||
pub use sqlx::postgres::PgRow;
|
pub use sqlx::postgres::PgRow;
|
||||||
pub use sqlx::{PgPool, Row};
|
pub use sqlx::{PgPool, Row};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use std::time::Duration;
|
||||||
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
|
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
|
||||||
use sqlx::{ConnectOptions, PgPool};
|
use sqlx::{ConnectOptions, PgPool};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
pub const DEFAULT_URL: &str = "postgres:///scroll";
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DatabaseConfig {
|
pub struct DatabaseConfig {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
|
@ -9,6 +10,11 @@ pub struct DatabaseConfig {
|
||||||
pub acquire_timeout: Duration,
|
pub acquire_timeout: Duration,
|
||||||
pub statement_log_threshold: Duration,
|
pub statement_log_threshold: Duration,
|
||||||
}
|
}
|
||||||
|
impl Default for DatabaseConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(DEFAULT_URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
impl DatabaseConfig {
|
impl DatabaseConfig {
|
||||||
pub fn new(url: impl Into<String>) -> Self {
|
pub fn new(url: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -21,7 +27,8 @@ impl DatabaseConfig {
|
||||||
pub fn from_env() -> Option<Self> {
|
pub fn from_env() -> Option<Self> {
|
||||||
let url = std::env::var("DATABASE_URL")
|
let url = std::env::var("DATABASE_URL")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|value| !value.trim().is_empty())?;
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.unwrap_or_else(|| DEFAULT_URL.to_owned());
|
||||||
let mut config = Self::new(url);
|
let mut config = Self::new(url);
|
||||||
if let Some(max) = std::env::var("DATABASE_MAX_CONNECTIONS")
|
if let Some(max) = std::env::var("DATABASE_MAX_CONNECTIONS")
|
||||||
.ok()
|
.ok()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue