use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Currency {
pub gold: u64,
pub silver: u64,
pub copper: u64,
}
impl Currency {
pub fn new(gold: u64, silver: u64, copper: u64) -> Self {
let mut c = Self { gold, silver, copper };
c.normalize();
c
}
pub fn from_copper(total: u64) -> Self {
let gold = total / 10_000;
let rem = total % 10_000;
let silver = rem / 100;
let copper = rem % 100;
Self { gold, silver, copper }
}
pub fn gold(amount: u64) -> Self {
Self { gold: amount, silver: 0, copper: 0 }
}
pub fn silver(amount: u64) -> Self {
Self::from_copper(amount * 100)
}
pub fn copper(amount: u64) -> Self {
Self::from_copper(amount)
}
pub fn zero() -> Self {
Self { gold: 0, silver: 0, copper: 0 }
}
pub fn normalize(&mut self) {
let carry_silver = self.copper / 100;
self.copper %= 100;
self.silver += carry_silver;
let carry_gold = self.silver / 100;
self.silver %= 100;
self.gold += carry_gold;
}
pub fn to_copper_total(&self) -> u64 {
self.gold * 10_000 + self.silver * 100 + self.copper
}
pub fn has_at_least(&self, amount: &Currency) -> bool {
self.to_copper_total() >= amount.to_copper_total()
}
pub fn try_subtract(&mut self, amount: &Currency) -> bool {
let total = self.to_copper_total();
let cost = amount.to_copper_total();
if total < cost {
return false;
}
*self = Self::from_copper(total - cost);
true
}
pub fn add(&mut self, amount: &Currency) {
let total = self.to_copper_total() + amount.to_copper_total();
*self = Self::from_copper(total);
}
pub fn multiply(&self, factor: u32) -> Self {
Self::from_copper(self.to_copper_total() * factor as u64)
}
pub fn scale(&self, factor: f32) -> Self {
let scaled = (self.to_copper_total() as f32 * factor).round() as u64;
Self::from_copper(scaled)
}
pub fn is_zero(&self) -> bool {
self.to_copper_total() == 0
}
}
impl Default for Currency {
fn default() -> Self {
Self::zero()
}
}
impl std::fmt::Display for Currency {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}g {}s {}c", self.gold, self.silver, self.copper)
}
}
impl PartialOrd for Currency {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.to_copper_total().partial_cmp(&other.to_copper_total())
}
}
impl Ord for Currency {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.to_copper_total().cmp(&other.to_copper_total())
}
}
#[derive(Debug, Clone)]
pub struct PriceModifier {
pub base_price: u64,
pub demand_factor: f32,
pub supply_factor: f32,
pub player_rep_factor: f32,
}
impl PriceModifier {
pub fn new(base_price: u64) -> Self {
Self {
base_price,
demand_factor: 1.0,
supply_factor: 1.0,
player_rep_factor: 1.0,
}
}
pub fn final_price(&self) -> u64 {
let base = self.base_price as f32;
let rep_discount = 1.0 + (1.0 - self.player_rep_factor.clamp(0.5, 1.5)) * 0.3;
let modified = base
* self.demand_factor.clamp(0.5, 3.0)
* self.supply_factor.clamp(0.25, 2.0)
* rep_discount;
modified.round().max(1.0) as u64
}
pub fn final_currency(&self) -> Currency {
Currency::from_copper(self.final_price())
}
}
#[derive(Debug, Clone)]
pub struct PlayerReputation {
pub faction_id: String,
pub value: f32,
}
impl PlayerReputation {
pub fn new(faction_id: impl Into<String>) -> Self {
Self { faction_id: faction_id.into(), value: 0.0 }
}
pub fn adjust(&mut self, delta: f32) {
self.value = (self.value + delta).clamp(-1.0, 1.0);
}
pub fn price_factor(&self) -> f32 {
1.0 - self.value * 0.3
}
pub fn label(&self) -> &'static str {
match self.value {
v if v >= 0.8 => "Exalted",
v if v >= 0.5 => "Revered",
v if v >= 0.2 => "Honored",
v if v >= -0.2 => "Neutral",
v if v >= -0.5 => "Unfriendly",
v if v >= -0.8 => "Hostile",
_ => "Hated",
}
}
}
#[derive(Debug, Clone)]
pub struct TaxSystem {
region_taxes: HashMap<String, f32>,
pub black_market_premium: f32,
}
impl TaxSystem {
pub fn new() -> Self {
let mut t = Self {
region_taxes: HashMap::new(),
black_market_premium: 1.35,
};
t.region_taxes.insert("capital".into(), 0.08);
t.region_taxes.insert("frontier".into(), 0.03);
t.region_taxes.insert("merchant_district".into(), 0.12);
t.region_taxes.insert("black_market".into(), 0.00);
t
}
pub fn set_region_tax(&mut self, region_id: impl Into<String>, rate: f32) {
self.region_taxes.insert(region_id.into(), rate.clamp(0.0, 0.5));
}
pub fn apply_tax(&self, base_price: u64, region_id: &str) -> u64 {
let tax_rate = self.region_taxes.get(region_id).copied().unwrap_or(0.05);
let taxed = base_price as f32 * (1.0 + tax_rate);
let black_market = if region_id == "black_market" {
taxed * self.black_market_premium
} else {
taxed
};
black_market.round() as u64
}
pub fn tax_amount(&self, base_price: u64, region_id: &str) -> u64 {
let final_price = self.apply_tax(base_price, region_id);
final_price.saturating_sub(base_price)
}
}
impl Default for TaxSystem {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ShopItem {
pub item_id: String,
pub stock: i32,
pub max_stock: i32,
pub price: Currency,
pub restock_rate: f32,
pub last_restock: f32,
}
impl ShopItem {
pub fn new(
item_id: impl Into<String>,
stock: i32,
max_stock: i32,
price: Currency,
restock_rate: f32,
) -> Self {
Self {
item_id: item_id.into(),
stock,
max_stock,
price,
restock_rate,
last_restock: 0.0,
}
}
pub fn buy_back_price(&self) -> Currency {
self.price.scale(0.40)
}
pub fn is_in_stock(&self) -> bool {
self.stock == -1 || self.stock > 0
}
pub fn is_unlimited(&self) -> bool {
self.stock == -1
}
}
#[derive(Debug, Clone)]
pub struct ShopInventory {
pub shop_id: String,
pub shop_name: String,
pub faction_id: String,
items: HashMap<String, ShopItem>,
pub region_id: String,
pub tax_system: TaxSystem,
}
impl ShopInventory {
pub fn new(
shop_id: impl Into<String>,
shop_name: impl Into<String>,
faction_id: impl Into<String>,
region_id: impl Into<String>,
) -> Self {
Self {
shop_id: shop_id.into(),
shop_name: shop_name.into(),
faction_id: faction_id.into(),
items: HashMap::new(),
region_id: region_id.into(),
tax_system: TaxSystem::new(),
}
}
pub fn add_item(&mut self, item: ShopItem) {
self.items.insert(item.item_id.clone(), item);
}
pub fn get_item(&self, item_id: &str) -> Option<&ShopItem> {
self.items.get(item_id)
}
pub fn all_items(&self) -> Vec<&ShopItem> {
self.items.values().collect()
}
pub fn buy(
&mut self,
item_id: &str,
qty: u32,
player_currency: &mut Currency,
player_rep: f32,
) -> Result<Currency, String> {
let item = self.items.get_mut(item_id)
.ok_or_else(|| format!("Item '{}' not found in shop", item_id))?;
if !item.is_unlimited() && (item.stock as u32) < qty {
return Err(format!("Not enough stock: have {}, need {}", item.stock, qty));
}
let rep_scale = 1.0 - player_rep.clamp(-1.0, 1.0) * 0.3;
let unit_price = item.price.scale(rep_scale);
let total_cost = unit_price.multiply(qty);
let taxed_total_copper = self.tax_system.apply_tax(total_cost.to_copper_total(), &self.region_id);
let taxed_total = Currency::from_copper(taxed_total_copper);
if !player_currency.has_at_least(&taxed_total) {
return Err(format!("Insufficient funds: need {}, have {}", taxed_total, player_currency));
}
player_currency.try_subtract(&taxed_total);
if !item.is_unlimited() {
item.stock -= qty as i32;
}
Ok(taxed_total)
}
pub fn sell(
&mut self,
item_id: &str,
qty: u32,
player_currency: &mut Currency,
) -> Result<Currency, String> {
let item = self.items.get_mut(item_id)
.ok_or_else(|| format!("Shop does not buy '{}'", item_id))?;
if item.max_stock > 0 && item.stock >= item.max_stock {
return Err(format!("Shop is full for item '{}'", item_id));
}
let payout = item.buy_back_price().multiply(qty);
player_currency.add(&payout);
if item.max_stock > 0 {
item.stock = (item.stock + qty as i32).min(item.max_stock);
}
Ok(payout)
}
pub fn restock(&mut self, dt: f32, current_time: f32) {
for item in self.items.values_mut() {
if item.restock_rate <= 0.0 || item.max_stock <= 0 {
continue;
}
if item.stock >= item.max_stock {
continue;
}
let time_since = current_time - item.last_restock;
let units_to_add = (time_since * item.restock_rate).floor() as i32;
if units_to_add >= 1 {
item.stock = (item.stock + units_to_add).min(item.max_stock);
item.last_restock = current_time;
}
}
}
}
#[derive(Debug, Clone)]
pub struct TradeOffer {
pub id: u64,
pub from_items: Vec<(String, u32)>,
pub to_items: Vec<(String, u32)>,
pub gold_delta: i64,
pub expires_at: f32,
pub proposer_id: String,
pub receiver_id: String,
pub accepted: bool,
pub rejected: bool,
}
impl TradeOffer {
pub fn new(
id: u64,
proposer_id: impl Into<String>,
receiver_id: impl Into<String>,
from_items: Vec<(String, u32)>,
to_items: Vec<(String, u32)>,
gold_delta: i64,
expires_at: f32,
) -> Self {
Self {
id,
from_items,
to_items,
gold_delta,
expires_at,
proposer_id: proposer_id.into(),
receiver_id: receiver_id.into(),
accepted: false,
rejected: false,
}
}
pub fn is_expired(&self, current_time: f32) -> bool {
current_time > self.expires_at
}
pub fn is_pending(&self) -> bool {
!self.accepted && !self.rejected
}
}
const PRICE_HISTORY_SIZE: usize = 20;
#[derive(Debug, Clone)]
struct PriceRingBuffer {
values: [u64; PRICE_HISTORY_SIZE],
head: usize,
count: usize,
}
impl PriceRingBuffer {
fn new() -> Self {
Self { values: [0u64; PRICE_HISTORY_SIZE], head: 0, count: 0 }
}
fn push(&mut self, price: u64) {
self.values[self.head] = price;
self.head = (self.head + 1) % PRICE_HISTORY_SIZE;
if self.count < PRICE_HISTORY_SIZE {
self.count += 1;
}
}
fn moving_average(&self) -> f32 {
if self.count == 0 {
return 0.0;
}
let sum: u64 = self.values[..self.count].iter().sum();
sum as f32 / self.count as f32
}
fn last(&self) -> Option<u64> {
if self.count == 0 {
return None;
}
let last_idx = if self.head == 0 { PRICE_HISTORY_SIZE - 1 } else { self.head - 1 };
Some(self.values[last_idx])
}
}
#[derive(Debug, Clone)]
struct ItemEconomy {
recent_sales: f32,
recent_supply: f32,
equilibrium_price: u64,
base_price: u64,
price_history: PriceRingBuffer,
}
impl ItemEconomy {
fn new(base_price: u64) -> Self {
Self {
recent_sales: 0.0,
recent_supply: 1.0,
equilibrium_price: base_price,
base_price,
price_history: PriceRingBuffer::new(),
}
}
fn demand_factor(&self) -> f32 {
if self.recent_supply <= 0.0 {
return 3.0;
}
(self.recent_sales / self.recent_supply).clamp(0.25, 3.0)
}
fn update_price(&mut self) {
let factor = self.demand_factor();
let target = (self.base_price as f32 * factor).round() as u64;
let t = 0.10_f32;
let eq = self.equilibrium_price as f32;
let new_eq = eq + t * (target as f32 - eq);
self.equilibrium_price = new_eq.round().max(1.0) as u64;
self.price_history.push(self.equilibrium_price);
}
fn decay(&mut self, dt: f32) {
let decay_rate = 0.02_f32 * dt;
self.recent_sales = (self.recent_sales * (1.0 - decay_rate)).max(0.0);
self.recent_supply = (self.recent_supply * (1.0 - decay_rate * 0.5)).max(0.01);
}
}
#[derive(Debug, Clone)]
pub struct Economy {
items: HashMap<String, ItemEconomy>,
pub tax_system: TaxSystem,
update_accumulator: f32,
update_interval: f32,
}
impl Economy {
pub fn new() -> Self {
Self {
items: HashMap::new(),
tax_system: TaxSystem::new(),
update_accumulator: 0.0,
update_interval: 30.0,
}
}
pub fn register_item(&mut self, item_id: impl Into<String>, base_price_copper: u64) {
self.items.insert(item_id.into(), ItemEconomy::new(base_price_copper));
}
pub fn current_price(&self, item_id: &str) -> Option<u64> {
self.items.get(item_id).map(|e| e.equilibrium_price)
}
pub fn current_price_currency(&self, item_id: &str) -> Option<Currency> {
self.current_price(item_id).map(Currency::from_copper)
}
pub fn average_price(&self, item_id: &str) -> Option<f32> {
self.items.get(item_id).map(|e| e.price_history.moving_average())
}
pub fn demand_factor(&self, item_id: &str) -> f32 {
self.items.get(item_id).map(|e| e.demand_factor()).unwrap_or(1.0)
}
pub fn record_sale(&mut self, item_id: &str, qty: u32, _price_copper: u64) {
if let Some(item) = self.items.get_mut(item_id) {
item.recent_sales += qty as f32;
}
}
pub fn record_purchase(&mut self, item_id: &str, qty: u32) {
if let Some(item) = self.items.get_mut(item_id) {
item.recent_supply += qty as f32;
}
}
pub fn update_prices(&mut self, dt: f32) {
for item in self.items.values_mut() {
item.decay(dt);
}
self.update_accumulator += dt;
if self.update_accumulator >= self.update_interval {
self.update_accumulator = 0.0;
for item in self.items.values_mut() {
item.update_price();
}
}
}
pub fn price_modifier(&self, item_id: &str, player_rep_factor: f32) -> PriceModifier {
let item = self.items.get(item_id);
let base_price = item.map(|e| e.base_price).unwrap_or(100);
let demand_factor = item.map(|e| e.demand_factor()).unwrap_or(1.0);
let supply_factor = if let Some(e) = item {
if e.recent_sales > 0.0 {
(e.recent_supply / e.recent_sales).clamp(0.25, 2.0)
} else {
1.0
}
} else {
1.0
};
PriceModifier {
base_price,
demand_factor,
supply_factor,
player_rep_factor,
}
}
pub fn register_defaults(&mut self) {
self.register_item("iron_ingot", 200);
self.register_item("steel_ingot", 600);
self.register_item("coal", 30);
self.register_item("leather_strip", 50);
self.register_item("wooden_plank", 40);
self.register_item("iron_sword", 1500);
self.register_item("iron_shield", 1800);
self.register_item("steel_sword", 4500);
self.register_item("iron_helmet", 1200);
self.register_item("red_herb", 60);
self.register_item("blue_herb", 80);
self.register_item("clean_water", 10);
self.register_item("spring_water", 25);
self.register_item("golden_root", 250);
self.register_item("health_potion_minor", 500);
self.register_item("health_potion_major", 1500);
self.register_item("mana_potion", 700);
self.register_item("raw_meat", 80);
self.register_item("salt", 20);
self.register_item("roasted_meat", 180);
self.register_item("hearty_stew", 400);
self.register_item("magic_dust", 300);
self.register_item("fire_essence", 800);
self.register_item("light_rune", 600);
self.register_item("silver_ingot", 500);
self.register_item("gold_chain", 2000);
self.register_item("ruby_gem", 3000);
self.register_item("sapphire_gem", 3500);
self.register_item("silver_ring", 1200);
self.register_item("ruby_necklace", 8000);
}
}
impl Default for Economy {
fn default() -> Self {
let mut e = Self::new();
e.register_defaults();
e
}
}