fix chest shop prices and gate the free chest
rounding is banded and the 5s band biases +3, so 22 goes to 25 not 20. numbers match the csv for arena 1 and 2. free chest end timestamp needs migration 0002.
This commit is contained in:
parent
a2bf58a627
commit
f218a45ed2
10 changed files with 129 additions and 11 deletions
|
|
@ -38,6 +38,7 @@ impl HomeMode {
|
||||||
build_avatar(profile),
|
build_avatar(profile),
|
||||||
current_timestamp,
|
current_timestamp,
|
||||||
);
|
);
|
||||||
|
logic.restore_free_chest_timer(profile.free_chest_end_timestamp);
|
||||||
let cycle = ShopCycle::at(current_timestamp);
|
let cycle = ShopCycle::at(current_timestamp);
|
||||||
logic.set_shop(cycle.seed, cycle.seconds_to_cycle, cycle.weekday_index);
|
logic.set_shop(cycle.seed, cycle.seconds_to_cycle, cycle.weekday_index);
|
||||||
Self { logic }
|
Self { logic }
|
||||||
|
|
@ -253,6 +254,7 @@ impl HomeMode {
|
||||||
count: spell.count,
|
count: spell.count,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
profile.free_chest_end_timestamp = self.logic.free_chest_end_timestamp();
|
||||||
profile.chest_id_counter = home.chest_id_counter;
|
profile.chest_id_counter = home.chest_id_counter;
|
||||||
profile.free_chest_collect_count = home.free_chest_collect_count;
|
profile.free_chest_collect_count = home.free_chest_collect_count;
|
||||||
profile.crowns_towards_crown_chest = home.crowns_towards_crown_chest;
|
profile.crowns_towards_crown_chest = home.crowns_towards_crown_chest;
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ fn profile_from_row(
|
||||||
chest_id_counter: row.get("chest_id_counter"),
|
chest_id_counter: row.get("chest_id_counter"),
|
||||||
free_chest_collect_count: row.get("free_chest_collect_count"),
|
free_chest_collect_count: row.get("free_chest_collect_count"),
|
||||||
crowns_towards_crown_chest: row.get("crowns_towards_crown_chest"),
|
crowns_towards_crown_chest: row.get("crowns_towards_crown_chest"),
|
||||||
|
free_chest_end_timestamp: row.get("free_chest_end_timestamp"),
|
||||||
chests,
|
chests,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,10 +216,11 @@ impl ProfileStore {
|
||||||
gold_resource_table, gold_resource_instance, gold_resource_name,
|
gold_resource_table, gold_resource_instance, gold_resource_name,
|
||||||
chest_slot_count, battle_count, win_count, lose_count,
|
chest_slot_count, battle_count, win_count, lose_count,
|
||||||
npc_win_count, npc_lose_count, three_crown_wins, tutorials_finished, updated_at,
|
npc_win_count, npc_lose_count, three_crown_wins, tutorials_finished, updated_at,
|
||||||
chest_id_counter, free_chest_collect_count, crowns_towards_crown_chest
|
chest_id_counter, free_chest_collect_count, crowns_towards_crown_chest,
|
||||||
|
free_chest_end_timestamp
|
||||||
)
|
)
|
||||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16,
|
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16,
|
||||||
$17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)
|
$17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29)
|
||||||
on conflict (account_high, account_low) do update set
|
on conflict (account_high, account_low) do update set
|
||||||
name = excluded.name,
|
name = excluded.name,
|
||||||
name_set_by_user = excluded.name_set_by_user,
|
name_set_by_user = excluded.name_set_by_user,
|
||||||
|
|
@ -245,7 +247,8 @@ impl ProfileStore {
|
||||||
updated_at = excluded.updated_at,
|
updated_at = excluded.updated_at,
|
||||||
chest_id_counter = excluded.chest_id_counter,
|
chest_id_counter = excluded.chest_id_counter,
|
||||||
free_chest_collect_count = excluded.free_chest_collect_count,
|
free_chest_collect_count = excluded.free_chest_collect_count,
|
||||||
crowns_towards_crown_chest = excluded.crowns_towards_crown_chest",
|
crowns_towards_crown_chest = excluded.crowns_towards_crown_chest,
|
||||||
|
free_chest_end_timestamp = excluded.free_chest_end_timestamp",
|
||||||
)
|
)
|
||||||
.bind(account.high)
|
.bind(account.high)
|
||||||
.bind(account.low)
|
.bind(account.low)
|
||||||
|
|
@ -275,6 +278,7 @@ impl ProfileStore {
|
||||||
.bind(profile.chest_id_counter)
|
.bind(profile.chest_id_counter)
|
||||||
.bind(profile.free_chest_collect_count)
|
.bind(profile.free_chest_collect_count)
|
||||||
.bind(profile.crowns_towards_crown_chest)
|
.bind(profile.crowns_towards_crown_chest)
|
||||||
|
.bind(profile.free_chest_end_timestamp)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
storage::sqlx::query(
|
storage::sqlx::query(
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ pub struct PlayerProfile {
|
||||||
pub chest_id_counter: i32,
|
pub chest_id_counter: i32,
|
||||||
pub free_chest_collect_count: i32,
|
pub free_chest_collect_count: i32,
|
||||||
pub crowns_towards_crown_chest: i32,
|
pub crowns_towards_crown_chest: i32,
|
||||||
|
pub free_chest_end_timestamp: i32,
|
||||||
pub chests: Vec<StoredChest>,
|
pub chests: Vec<StoredChest>,
|
||||||
}
|
}
|
||||||
impl PlayerProfile {
|
impl PlayerProfile {
|
||||||
|
|
@ -88,6 +89,7 @@ impl PlayerProfile {
|
||||||
chest_id_counter: 0,
|
chest_id_counter: 0,
|
||||||
free_chest_collect_count: 1,
|
free_chest_collect_count: 1,
|
||||||
crowns_towards_crown_chest: 0,
|
crowns_towards_crown_chest: 0,
|
||||||
|
free_chest_end_timestamp: 0,
|
||||||
chests: Vec::new(),
|
chests: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let pass_token = std::env::args().nth(4).filter(|token| token != "-");
|
let pass_token = std::env::args().nth(4).filter(|token| token != "-");
|
||||||
let claim_free_chest = std::env::args().any(|arg| arg == "--claim-free-chest");
|
let claim_free_chest = std::env::args().any(|arg| arg == "--claim-free-chest");
|
||||||
let desync = std::env::args().any(|arg| arg == "--desync");
|
let desync = std::env::args().any(|arg| arg == "--desync");
|
||||||
|
let buy_chest = std::env::args()
|
||||||
|
.skip_while(|arg| arg != "--buy-chest")
|
||||||
|
.nth(1);
|
||||||
let csv_root = std::env::var("SCROLL_CSV_ROOT").unwrap_or_else(|_| "assets".to_owned());
|
let csv_root = std::env::var("SCROLL_CSV_ROOT").unwrap_or_else(|_| "assets".to_owned());
|
||||||
match LogicDataTables::load_from_dir(&csv_root) {
|
match LogicDataTables::load_from_dir(&csv_root) {
|
||||||
Ok(tables) => {
|
Ok(tables) => {
|
||||||
|
|
@ -175,12 +178,36 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!(" random seed {}", home.random_seed);
|
println!(" random seed {}", home.random_seed);
|
||||||
if claim_free_chest {
|
let checksum = home.home.chest_id_counter
|
||||||
|
+ ((home.home.spell_collection.spells.len() as i32) << 16)
|
||||||
|
+ i32::from(desync);
|
||||||
|
if let Some(name) = buy_chest.as_deref() {
|
||||||
|
let chest = logic::LogicDataRef::by_name(logic::table::TREASURE_CHESTS, name);
|
||||||
|
match chest.as_treasure_chest() {
|
||||||
|
None => println!("!! no treasure chest named {name}"),
|
||||||
|
Some(data) => {
|
||||||
|
println!(
|
||||||
|
" buying {name} for {} diamonds",
|
||||||
|
data.shop_price()
|
||||||
|
);
|
||||||
let turn = EndClientTurnMessage {
|
let turn = EndClientTurnMessage {
|
||||||
tick: 0,
|
tick: 0,
|
||||||
checksum: home.home.chest_id_counter
|
checksum,
|
||||||
+ ((home.home.spell_collection.spells.len() as i32) << 16)
|
commands: vec![Box::new(logic::LogicBuyChestCommand {
|
||||||
+ i32::from(desync),
|
header: LogicCommandHeader::for_account(home.avatar.account_id),
|
||||||
|
unused: 1,
|
||||||
|
chest,
|
||||||
|
})],
|
||||||
|
trailing: None,
|
||||||
|
};
|
||||||
|
probe.send(&turn).await?;
|
||||||
|
println!("-> 14102 EndClientTurnMessage [LogicBuyChestCommand]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if claim_free_chest {
|
||||||
|
let turn = EndClientTurnMessage {
|
||||||
|
tick: 0,
|
||||||
|
checksum,
|
||||||
commands: vec![Box::new(LogicCollectFreeChestCommand {
|
commands: vec![Box::new(LogicCollectFreeChestCommand {
|
||||||
header: LogicCommandHeader::for_account(home.avatar.account_id),
|
header: LogicCommandHeader::for_account(home.avatar.account_id),
|
||||||
})],
|
})],
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,10 @@ impl Execute for LogicCollectFreeChestCommand {
|
||||||
if mode.avatar().arena.is_none() {
|
if mode.avatar().arena.is_none() {
|
||||||
return CommandOutcome::Rejected("the avatar has no arena");
|
return CommandOutcome::Rejected("the avatar has no arena");
|
||||||
}
|
}
|
||||||
|
if !mode.free_chest_ready() {
|
||||||
|
return CommandOutcome::Rejected("the free chest is still on cooldown");
|
||||||
|
}
|
||||||
|
mode.start_free_chest_timer();
|
||||||
mode.begin_claim();
|
mode.begin_claim();
|
||||||
CommandOutcome::ClaimStarted {
|
CommandOutcome::ClaimStarted {
|
||||||
source: chest_source::FREE,
|
source: chest_source::FREE,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ pub const PRICE_EPIC_INCREASE: &str = "PRICE_EPIC_INCREASE_PER_ONE_BOUGHT_PERCEN
|
||||||
pub const BUY_LIMIT_COMMON: &str = "BUY_LIMIT_COMMON";
|
pub const BUY_LIMIT_COMMON: &str = "BUY_LIMIT_COMMON";
|
||||||
pub const BUY_LIMIT_RARE: &str = "BUY_LIMIT_RARE";
|
pub const BUY_LIMIT_RARE: &str = "BUY_LIMIT_RARE";
|
||||||
pub const BUY_LIMIT_EPIC: &str = "BUY_LIMIT_EPIC";
|
pub const BUY_LIMIT_EPIC: &str = "BUY_LIMIT_EPIC";
|
||||||
|
pub const FREE_CHEST_INTERVAL_HOURS: &str = "FREE_CHEST_INTERVAL_HOURS";
|
||||||
pub const RARITY_COMMON: &str = "Common";
|
pub const RARITY_COMMON: &str = "Common";
|
||||||
pub const RARITY_RARE: &str = "Rare";
|
pub const RARITY_RARE: &str = "Rare";
|
||||||
pub const RARITY_EPIC: &str = "Epic";
|
pub const RARITY_EPIC: &str = "Epic";
|
||||||
|
|
@ -34,6 +35,11 @@ impl LogicGlobals {
|
||||||
_ => PRICE_COMMON_INCREASE,
|
_ => PRICE_COMMON_INCREASE,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
pub fn free_chest_interval_seconds() -> i32 {
|
||||||
|
Self::number(FREE_CHEST_INTERVAL_HOURS)
|
||||||
|
.max(0)
|
||||||
|
.saturating_mul(3_600)
|
||||||
|
}
|
||||||
pub fn resource_diamond_cost(amount: i32) -> i32 {
|
pub fn resource_diamond_cost(amount: i32) -> i32 {
|
||||||
const STEPS: [(i32, &str); 7] = [
|
const STEPS: [(i32, &str); 7] = [
|
||||||
(1, "RESOURCE_DIAMOND_COST_1"),
|
(1, "RESOURCE_DIAMOND_COST_1"),
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,34 @@ impl LogicArenaData {
|
||||||
pub fn index(&self) -> i32 {
|
pub fn index(&self) -> i32 {
|
||||||
self.int("Arena")
|
self.int("Arena")
|
||||||
}
|
}
|
||||||
|
pub fn chest_shop_price_multiplier(&self) -> i32 {
|
||||||
|
self.int("ChestShopPriceMultiplier")
|
||||||
|
}
|
||||||
|
pub fn scaled_chest_price(&self, price: i32) -> i32 {
|
||||||
|
(self.chest_shop_price_multiplier() as i64 * price as i64 / 100) as i32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn clamp_low_first(value: i64, low: i64, high: i64) -> i64 {
|
||||||
|
if value > low {
|
||||||
|
if value < high {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
high
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
low
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn round_shop_price(total: i32) -> i32 {
|
||||||
|
if total > 500 {
|
||||||
|
(total + 50) / 100 * 100
|
||||||
|
} else if total >= 101 {
|
||||||
|
(total + 5) / 10 * 10
|
||||||
|
} else if total >= 21 {
|
||||||
|
(total + 3) / 5 * 5
|
||||||
|
} else {
|
||||||
|
total
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl LogicTreasureChestData {
|
impl LogicTreasureChestData {
|
||||||
pub fn base_chest(&self) -> Option<LogicTreasureChestData> {
|
pub fn base_chest(&self) -> Option<LogicTreasureChestData> {
|
||||||
|
|
@ -64,11 +92,27 @@ impl LogicTreasureChestData {
|
||||||
pub fn arena_data(&self) -> LogicDataRef {
|
pub fn arena_data(&self) -> LogicDataRef {
|
||||||
LogicDataRef::by_name(crate::data::tables::table::ARENAS, self.string("Arena"))
|
LogicDataRef::by_name(crate::data::tables::table::ARENAS, self.string("Arena"))
|
||||||
}
|
}
|
||||||
pub fn shop_price(&self) -> i32 {
|
fn price_row(&self) -> LogicTreasureChestData {
|
||||||
self.root().int("ShopPriceWithoutSpeedUp")
|
self.base_chest().unwrap_or_else(|| self.clone())
|
||||||
}
|
}
|
||||||
pub fn speed_up_cost(&self) -> i32 {
|
pub fn shop_price(&self) -> i32 {
|
||||||
self.root().int("SpeedUpCost")
|
let scaled = match self.arena_data().as_arena() {
|
||||||
|
Some(arena) => {
|
||||||
|
arena.scaled_chest_price(self.price_row().int("ShopPriceWithoutSpeedUp"))
|
||||||
|
}
|
||||||
|
None => self.price_row().int("ShopPriceWithoutSpeedUp"),
|
||||||
|
};
|
||||||
|
round_shop_price(scaled.saturating_add(self.speed_up_cost_at(self.unlock_seconds())))
|
||||||
|
}
|
||||||
|
pub fn speed_up_cost_at(&self, seconds_left: i32) -> i32 {
|
||||||
|
let total = self.unlock_seconds();
|
||||||
|
if seconds_left < 1 || total < 1 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let cost = self.price_row().int("SpeedUpCost") as i64;
|
||||||
|
let total = total as i64;
|
||||||
|
let quotient = (cost * seconds_left as i64 + total - 1) / total;
|
||||||
|
clamp_low_first(quotient, 1, cost) as i32
|
||||||
}
|
}
|
||||||
pub fn unlock_seconds(&self) -> i32 {
|
pub fn unlock_seconds(&self) -> i32 {
|
||||||
let root = self.root();
|
let root = self.root();
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,30 @@ impl LogicHomeMode {
|
||||||
pub fn begin_claim(&mut self) {
|
pub fn begin_claim(&mut self) {
|
||||||
self.claiming_reward = true;
|
self.claiming_reward = true;
|
||||||
}
|
}
|
||||||
|
pub fn free_chest_ready(&self) -> bool {
|
||||||
|
self.home.free_chest_timer.remaining_ticks <= 0
|
||||||
|
}
|
||||||
|
pub fn free_chest_end_timestamp(&self) -> i32 {
|
||||||
|
self.home.free_chest_timer.end_timestamp
|
||||||
|
}
|
||||||
|
pub fn start_free_chest_timer(&mut self) {
|
||||||
|
let seconds = crate::data::LogicGlobals::free_chest_interval_seconds();
|
||||||
|
self.home.free_chest_timer = LogicTimer::started(seconds, self.current_timestamp);
|
||||||
|
self.home.free_chest_collect_count = self.home.free_chest_collect_count.saturating_add(1);
|
||||||
|
}
|
||||||
|
pub fn restore_free_chest_timer(&mut self, end_timestamp: i32) {
|
||||||
|
let remaining = end_timestamp.saturating_sub(self.current_timestamp).max(0);
|
||||||
|
self.home.free_chest_timer = if remaining > 0 {
|
||||||
|
LogicTimer {
|
||||||
|
remaining_ticks: remaining.saturating_mul(TICKS_PER_SECOND),
|
||||||
|
total_ticks: crate::data::LogicGlobals::free_chest_interval_seconds()
|
||||||
|
.saturating_mul(TICKS_PER_SECOND),
|
||||||
|
end_timestamp,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LogicTimer::idle()
|
||||||
|
};
|
||||||
|
}
|
||||||
pub fn set_shop(&mut self, seed: i32, seconds_to_cycle: i32, weekday_index: i32) {
|
pub fn set_shop(&mut self, seed: i32, seconds_to_cycle: i32, weekday_index: i32) {
|
||||||
let arena = self.avatar.arena.clone();
|
let arena = self.avatar.arena.clone();
|
||||||
self.home.shop_seed = seed;
|
self.home.shop_seed = seed;
|
||||||
|
|
|
||||||
3
crates/storage/build.rs
Normal file
3
crates/storage/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=migrations");
|
||||||
|
}
|
||||||
2
crates/storage/migrations/0002_free_chest_timer.sql
Normal file
2
crates/storage/migrations/0002_free_chest_timer.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
alter table players
|
||||||
|
add column if not exists free_chest_end_timestamp integer not null default 0;
|
||||||
Loading…
Reference in a new issue