give the shop a typed error instead of string literals

ShopError variants are matchable; the message is the Display, rendered
at the log site, not baked into the error type.
This commit is contained in:
WiseDev 2026-08-28 23:26:44 +03:00
parent a509529aef
commit 849c090e28

View file

@ -22,6 +22,28 @@ fn gold() -> LogicDataRef {
fn diamonds() -> LogicDataRef { fn diamonds() -> LogicDataRef {
LogicDataRef::by_name(table::RESOURCES, RESOURCE_DIAMONDS) LogicDataRef::by_name(table::RESOURCES, RESOURCE_DIAMONDS)
} }
#[derive(Debug, thiserror::Error)]
pub enum ShopError {
#[error("not a card")]
NotACard,
#[error("not on sale")]
NotOnSale,
#[error("buy limit reached")]
BuyLimitReached,
#[error("nothing sells that")]
NotSold,
#[error("chest not sold")]
ChestNotSold,
#[error("no market price")]
NoMarketPrice,
#[error("price is not a resource")]
PriceNotResource,
#[error("not enough funds")]
CannotAfford,
#[error("unsupported currency")]
UnsupportedCurrency,
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct TurnResult { pub struct TurnResult {
pub claims: Vec<LogicClaimRewardCommand>, pub claims: Vec<LogicClaimRewardCommand>,
@ -92,11 +114,11 @@ impl HomeMode {
_ => LogicDataRef::treasure_chest_for_arena(CHEST_FREE, arena), _ => LogicDataRef::treasure_chest_for_arena(CHEST_FREE, arena),
} }
} }
fn market_cost(&self, give: &ShopEntry) -> Result<ShopEntry, &'static str> { fn market_cost(&self, give: &ShopEntry) -> Result<ShopEntry, ShopError> {
if let Some(chest) = give.data.as_treasure_chest() { if let Some(chest) = give.data.as_treasure_chest() {
let price = chest.shop_price(); let price = chest.shop_price();
if price < 1 { if price < 1 {
return Err("that chest is not sold in the shop"); return Err(ShopError::ChestNotSold);
} }
return Ok(ShopEntry::new(diamonds(), price)); return Ok(ShopEntry::new(diamonds(), price));
} }
@ -106,27 +128,27 @@ impl HomeMode {
LogicGlobals::resource_diamond_cost(give.count), LogicGlobals::resource_diamond_cost(give.count),
)); ));
} }
Err("that item has no market price") Err(ShopError::NoMarketPrice)
} }
fn pay(&mut self, cost: &ShopEntry) -> Result<(), &'static str> { fn pay(&mut self, cost: &ShopEntry) -> Result<(), ShopError> {
if !cost.is_resource() { if !cost.is_resource() {
return Err("the price must be a resource"); return Err(ShopError::PriceNotResource);
} }
match cost.data.name() { match cost.data.name() {
RESOURCE_GOLD if self.logic.spend_gold(cost.count) => Ok(()), RESOURCE_GOLD if self.logic.spend_gold(cost.count) => Ok(()),
RESOURCE_DIAMONDS if self.logic.spend_diamonds(cost.count) => Ok(()), RESOURCE_DIAMONDS if self.logic.spend_diamonds(cost.count) => Ok(()),
RESOURCE_GOLD | RESOURCE_DIAMONDS => Err("the player cannot afford it"), RESOURCE_GOLD | RESOURCE_DIAMONDS => Err(ShopError::CannotAfford),
_ => Err("only gold and diamonds are accepted"), _ => Err(ShopError::UnsupportedCurrency),
} }
} }
fn buy_card(&mut self, card: &LogicDataRef) -> Result<Purchase, &'static str> { fn buy_card(&mut self, card: &LogicDataRef) -> Result<Purchase, ShopError> {
let spell = card.as_spell().ok_or("that is not a card")?; let spell = card.as_spell().ok_or(ShopError::NotACard)?;
let buy_times = self let buy_times = self
.logic .logic
.shop_buy_times(card) .shop_buy_times(card)
.ok_or("that card is not on sale right now")?; .ok_or(ShopError::NotOnSale)?;
if buy_times >= spell.shop_buy_limit() { if buy_times >= spell.shop_buy_limit() {
return Err("the buy limit for that card is reached"); return Err(ShopError::BuyLimitReached);
} }
let cost = ShopEntry::new(gold(), spell.cost_in_shop(buy_times)); let cost = ShopEntry::new(gold(), spell.cost_in_shop(buy_times));
self.pay(&cost)?; self.pay(&cost)?;
@ -143,14 +165,14 @@ impl HomeMode {
data: &LogicDataRef, data: &LogicDataRef,
count: i32, count: i32,
shop: &ShopCatalog, shop: &ShopCatalog,
) -> Result<Purchase, &'static str> { ) -> Result<Purchase, ShopError> {
if data.as_spell().is_some() { if data.as_spell().is_some() {
return self.buy_card(data); 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(ShopError::NotSold)?
.clone(); .clone();
let cost = match &offer.cost { let cost = match &offer.cost {
Some(cost) => cost.clone(), Some(cost) => cost.clone(),
@ -161,7 +183,7 @@ impl HomeMode {
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(ShopError::UnsupportedCurrency),
} }
} }
Ok(Purchase { Ok(Purchase {
@ -221,11 +243,11 @@ impl HomeMode {
result.changed = true; result.changed = true;
result.purchases.push(purchase); result.purchases.push(purchase);
} }
Err(reason) => { Err(error) => {
tracing::warn!( tracing::warn!(
command = command.command_type(), command = command.command_type(),
item = %data, item = %data,
reason, reason = %error,
"shop purchase rejected" "shop purchase rejected"
); );
} }