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 {
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)]
pub struct TurnResult {
pub claims: Vec<LogicClaimRewardCommand>,
@ -92,11 +114,11 @@ impl HomeMode {
_ => 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() {
let price = chest.shop_price();
if price < 1 {
return Err("that chest is not sold in the shop");
return Err(ShopError::ChestNotSold);
}
return Ok(ShopEntry::new(diamonds(), price));
}
@ -106,27 +128,27 @@ impl HomeMode {
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() {
return Err("the price must be a resource");
return Err(ShopError::PriceNotResource);
}
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"),
RESOURCE_GOLD | RESOURCE_DIAMONDS => Err(ShopError::CannotAfford),
_ => Err(ShopError::UnsupportedCurrency),
}
}
fn buy_card(&mut self, card: &LogicDataRef) -> Result<Purchase, &'static str> {
let spell = card.as_spell().ok_or("that is not a card")?;
fn buy_card(&mut self, card: &LogicDataRef) -> Result<Purchase, ShopError> {
let spell = card.as_spell().ok_or(ShopError::NotACard)?;
let buy_times = self
.logic
.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() {
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));
self.pay(&cost)?;
@ -143,14 +165,14 @@ impl HomeMode {
data: &LogicDataRef,
count: i32,
shop: &ShopCatalog,
) -> Result<Purchase, &'static str> {
) -> Result<Purchase, ShopError> {
if data.as_spell().is_some() {
return self.buy_card(data);
}
let offer = shop
.offer_for(data)
.filter(|offer| offer.give.count == count)
.ok_or("nothing in the shop sells that")?
.ok_or(ShopError::NotSold)?
.clone();
let cost = match &offer.cost {
Some(cost) => cost.clone(),
@ -161,7 +183,7 @@ impl HomeMode {
match offer.give.data.name() {
RESOURCE_GOLD => self.logic.add_gold(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 {
@ -221,11 +243,11 @@ impl HomeMode {
result.changed = true;
result.purchases.push(purchase);
}
Err(reason) => {
Err(error) => {
tracing::warn!(
command = command.command_type(),
item = %data,
reason,
reason = %error,
"shop purchase rejected"
);
}