diff --git a/assets/coin.png b/assets/coin.png new file mode 100644 index 0000000..b877135 Binary files /dev/null and b/assets/coin.png differ diff --git a/assets/raw_meat.png b/assets/raw_meat.png new file mode 100644 index 0000000..ea79948 Binary files /dev/null and b/assets/raw_meat.png differ diff --git a/src/entities/item/container.rs b/src/entities/item/container.rs new file mode 100644 index 0000000..9c8df04 --- /dev/null +++ b/src/entities/item/container.rs @@ -0,0 +1,54 @@ +use bevy::prelude::*; + +#[derive(Component, Debug)] +pub struct Container { + // Maximum weight this container can hold + pub max_weight: u32, + // Maximum number of items (None = unlimited) + pub max_items: Option, + // Current weight of contents + pub current_weight: u32, + // List of item entities contained + pub contents: Vec, // TODO - move away from Entity + // Whether the container is currently open/accessible + pub is_open: bool, +} + +impl Container { + pub fn new(max_weight: u32, max_items: Option) -> Self { + Self { + max_weight, + max_items, + current_weight: 0, + contents: Vec::new(), + is_open: true, + } + } + + pub fn can_fit(&self, weight: u32) -> bool { + self.current_weight + weight <= self.max_weight + && self + .max_items + .map_or(true, |max| self.contents.len() < max as usize) + } + + pub fn add_item(&mut self, item_entity: Entity, weight: u32) -> bool { + if self.can_fit(weight) { + self.contents.push(item_entity); + self.current_weight += weight; + true + } else { + false + } + } + + pub fn remove_item(&mut self, item_entity: Entity, weight: u32) -> bool { + if let Some(pos) = self.contents.iter().position(|&e| e == item_entity) { + self.contents.remove(pos); + self.current_weight = self.current_weight.saturating_sub(weight); + true + } else { + false + } + } +} diff --git a/src/entities/item/core.rs b/src/entities/item/core.rs new file mode 100644 index 0000000..fef4cbe --- /dev/null +++ b/src/entities/item/core.rs @@ -0,0 +1,219 @@ +use bevy::prelude::*; + +use crate::entities::item::decorations::ItemDecorations; +use crate::entities::item::material::Material; +use crate::entities::item::quality::Quality; +use crate::entities::item::types::ItemType; + +#[derive(Component, Debug)] +#[require(Transform, Visibility)] +pub struct Item { + // How worn/damaged the item is (0-255, where 255 is completely destroyed) + pub wear: u8, + // The quality of craftsmanship for the base item + pub quality: Quality, + // Time until stains fade (in game ticks/turns), + pub stain_time: u16, + // Temperature of the item (for hot/cold items) + pub temperature: i16, // Can be negative for very cold items +} + +impl Default for Item { + fn default() -> Self { + Self { + wear: 0, + quality: Quality::default(), + stain_time: 0, + temperature: 20, // Room temperature + } + } +} + +impl Item { + pub fn new() -> Self { + Self::default() + } + + // Calculate weight based on materials and decorations + pub fn calculate_weight( + &self, + item_type: &ItemType, + base_material: &Material, + decorations: Option<&ItemDecorations>, + ) -> u32 { + // Base weight from primary material + let base_volume = item_type.base_volume(); + let base_weight = base_material.density() * base_volume; + + // Additional weight from decorations + let decoration_weight = decorations + .map(|d| d.total_decoration_weight()) + .unwrap_or(0.0); + + let total_weight = base_weight + decoration_weight; + + // Convert to u32 (multiply by 1000 to get grams if density was in kg/unit) + (total_weight * 1000.0) as u32 + } + + // Calculate total value considering all factors + pub fn calculate_value( + &self, + item_type: &ItemType, + base_material: &Material, + decorations: Option<&ItemDecorations>, + ) -> u32 { + // Base value from item type and material + let base_value = item_type.base_value() + base_material.base_value(); + + // Apply quality multiplier to base + let quality_modified_value = base_value as f32 * self.quality.value_multiplier(); + + // Additional value from decorations + let decoration_value = decorations.map(|d| d.total_decoration_value()).unwrap_or(0); + + // Reduce value based on wear + let wear_multiplier = (255 - self.wear) as f32 / 255.0; + + let total_value = (quality_modified_value + decoration_value as f32) * wear_multiplier; + + total_value as u32 + } + + // Check if the item is destroyed due to wear + pub fn is_destroyed(&self) -> bool { + self.wear >= 255 + } + + // Apply wear to the item + pub fn add_wear(&mut self, wear_amount: u8) { + self.wear = self.wear.saturating_add(wear_amount); + } + + // Check if item has any stains + pub fn has_stains(&self) -> bool { + self.stain_time > 0 + } + + // Reduce stain time (call this periodically to fade stains) + pub fn reduce_stain_time(&mut self, amount: u16) { + self.stain_time = self.stain_time.saturating_sub(amount); + } + + // Add staining to the item + pub fn add_stain(&mut self, duration: u16) { + self.stain_time = self.stain_time.saturating_add(duration); + } +} + +// Bundle for creating item entities +#[derive(Bundle)] +pub struct ItemBundle { + pub item: Item, + pub item_type: ItemType, + pub base_material: Material, + pub decorations: ItemDecorations, + pub transform: Transform, + pub visibility: Visibility, + pub sprite: Sprite, +} + +// Component for items that have names (can be generated or custom) +#[derive(Component, Debug, Clone)] +pub struct ItemName { + // The base name of the item type + pub base_name: String, + // Custom name given by player or generated + pub custom_name: Option, +} + +impl ItemName { + pub fn new(base_name: String) -> Self { + Self { + base_name, + custom_name: None, + } + } + + // Generate full display name including materials and decorations + pub fn display_name( + &self, + item: &Item, + base_material: &Material, + decorations: Option<&ItemDecorations>, + ) -> String { + let mut parts = Vec::new(); + + // Quality prefix + let quality_name = item.quality.display_name(); + if !quality_name.is_empty() { + parts.push(quality_name.to_string()); + } + + // Base material + parts.push(base_material.name()); + + // Item name + parts.push(self.base_name.clone()); + + let mut result = parts.join(" "); + + // Add decorations to the description + if let Some(decorations) = decorations { + let mut decoration_parts = Vec::new(); + + // Gem encrustings + for gem in &decorations.gem_encrustings { + let quality_prefix = if gem.quality != Quality::Ordinary { + format!("{} ", gem.quality.display_name()) + } else { + String::new() + }; + decoration_parts.push(format!( + "encrusted with {}{}", + quality_prefix, + gem.material.name() + )); + } + + // Trim materials + for trim in &decorations.trim_materials { + let quality_prefix = if trim.quality != Quality::Ordinary { + format!("{} ", trim.quality.display_name()) + } else { + String::new() + }; + let trim_name = if quality_prefix.is_empty() { + trim.material.name() + } else { + format!("{} {}", quality_prefix, trim.material.name()) + }; + decoration_parts.push(format!("with {} trim", trim_name)); + } + + // Engravings + for engraving in &decorations.engravings { + let quality_prefix = if engraving.quality != Quality::Ordinary { + format!("{} ", engraving.quality.display_name()) + } else { + String::new() + }; + decoration_parts.push(format!( + "engraved with {}images of {}", + quality_prefix, engraving.subject + )); + } + + if !decoration_parts.is_empty() { + result = format!("{}, {}", result, decoration_parts.join(", ")); + } + } + + // Custom name + if let Some(custom) = &self.custom_name { + result = format!("\"{}\" {}", custom, result); + } + + result + } +} diff --git a/src/entities/item/decorations.rs b/src/entities/item/decorations.rs new file mode 100644 index 0000000..28a9f8c --- /dev/null +++ b/src/entities/item/decorations.rs @@ -0,0 +1,122 @@ +use bevy::prelude::*; + +use crate::entities::item::material::Material; +use crate::entities::item::quality::Quality; + +#[derive(Component, Debug, Clone)] +pub struct ItemDecorations { + // Gems encrusted on the item + pub gem_encrustings: Vec, + // Metal trim/bands + pub trim_materials: Vec, + // Engravings on the item + pub engravings: Vec, +} + +impl Default for ItemDecorations { + fn default() -> Self { + Self { + gem_encrustings: Vec::new(), + trim_materials: Vec::new(), + engravings: Vec::new(), + } + } +} + +impl ItemDecorations { + pub fn new() -> Self { + Self::default() + } + + // Calculate total additional weight from decorations + pub fn total_decoration_weight(&self) -> f32 { + let gem_weight: f32 = self + .gem_encrustings + .iter() + .map(|gem| gem.material.density() * gem.size) + .sum(); + + let trim_weight: f32 = self + .trim_materials + .iter() + .map(|trim| trim.material.density() * trim.amount) + .sum(); + + // Engravings don't add weight, they remove material + gem_weight + trim_weight + } + + // Calculate total additional value from decorations + pub fn total_decoration_value(&self) -> u32 { + let gem_value: f32 = self + .gem_encrustings + .iter() + .map(|gem| gem.material.base_value() as f32 * gem.size * gem.quality.value_multiplier()) + .sum(); + + let trim_value: f32 = self + .trim_materials + .iter() + .map(|trim| { + trim.material.base_value() as f32 * trim.amount * trim.quality.value_multiplier() + }) + .sum(); + + let engraving_value: f32 = self + .engravings + .iter() + .map(|engraving| engraving.base_value() * engraving.quality.value_multiplier()) + .sum(); + + (gem_value + trim_value + engraving_value) as u32 + } + + pub fn add_gem_encrusting(&mut self, material: Material, size: f32, quality: Quality) { + if let Material::Gem(_) = material { + self.gem_encrustings.push(GemEncrusting { + material, + size, + quality, + }); + } + } + + pub fn add_trim(&mut self, material: Material, amount: f32, quality: Quality) { + self.trim_materials.push(TrimMaterial { + material, + amount, + quality, + }); + } + + pub fn add_engraving(&mut self, subject: String, quality: Quality) { + self.engravings.push(Engraving { subject, quality }); + } +} + +#[derive(Debug, Clone)] +pub struct GemEncrusting { + pub material: Material, // Should be a gem, but using material to gain access to its wider functions + pub size: f32, // Size of the gem (affects weight and value) + pub quality: Quality, // Quality of the gem cut +} + +#[derive(Debug, Clone)] +pub struct TrimMaterial { + pub material: Material, // Usually metal for bands/trim + pub amount: f32, // Amount of material used + pub quality: Quality, // Quality of the trim work +} + +#[derive(Debug, Clone)] +pub struct Engraving { + pub subject: String, // What the engraving depicts + pub quality: Quality, // Quality of the engraving +} + +impl Engraving { + pub fn base_value(&self) -> f32 { + // Base value for engravings (they're pure craftsmanship) + 20.0 + } +} diff --git a/src/entities/item/material.rs b/src/entities/item/material.rs new file mode 100644 index 0000000..37a15e1 --- /dev/null +++ b/src/entities/item/material.rs @@ -0,0 +1,156 @@ +use bevy::prelude::*; + +#[derive(Component, Debug, Clone, PartialEq, Eq, Hash)] +pub enum Material { + NA, + Wood(WoodType), + Stone(StoneType), + Metal(MetalType), + Cloth(ClothType), + Leather(LeatherType), + Bone, + Shell, + Glass, + Ceramic, + Gem(GemType), + Wax, +} + +impl Material { + // Weight per unit volume (arbitrary units - adjust as needed) + pub fn density(&self) -> f32 { + match self { + Material::NA => 1.0, + Material::Wood(wood) => match wood { + WoodType::Oak => 0.7, + WoodType::Pine => 0.5, + WoodType::Birch => 0.6, + WoodType::Ironwood => 1.2, + }, + Material::Stone(stone) => match stone { + StoneType::Granite => 2.7, + StoneType::Marble => 2.6, + StoneType::Sandstone => 2.2, + StoneType::Obsidian => 2.4, + }, + Material::Metal(metal) => match metal { + MetalType::Iron => 7.9, + MetalType::Steel => 7.8, + MetalType::Copper => 8.9, + MetalType::Silver => 10.5, + MetalType::Gold => 19.3, + MetalType::Platinum => 21.5, + }, + Material::Cloth(_) => 0.3, + Material::Leather(_) => 0.8, + Material::Bone => 1.8, + Material::Shell => 2.0, + Material::Glass => 2.5, + Material::Ceramic => 2.3, + Material::Gem(gem) => match gem { + GemType::Diamond => 3.5, + GemType::Ruby => 4.0, + GemType::Emerald => 2.7, + GemType::Sapphire => 4.0, + }, + Material::Wax => 0.1, + } + } + + // Base value per unit (before quality modifiers) + pub fn base_value(&self) -> u32 { + match self { + Material::NA => 1, + Material::Wood(_) => 1, + Material::Stone(_) => 2, + Material::Metal(metal) => match metal { + MetalType::Iron => 5, + MetalType::Steel => 8, + MetalType::Copper => 3, + MetalType::Silver => 15, + MetalType::Gold => 30, + MetalType::Platinum => 40, + }, + Material::Cloth(_) => 2, + Material::Leather(_) => 3, + Material::Bone => 1, + Material::Shell => 2, + Material::Glass => 4, + Material::Ceramic => 3, + Material::Gem(gem) => match gem { + GemType::Diamond => 200, + GemType::Ruby => 150, + GemType::Emerald => 100, + GemType::Sapphire => 120, + }, + Material::Wax => 3, + } + } + + pub fn name(&self) -> String { + match self { + Material::NA => "N/A".to_string(), + Material::Wood(wood) => format!("{:?}", wood).to_lowercase(), + Material::Stone(stone) => format!("{:?}", stone).to_lowercase(), + Material::Metal(metal) => format!("{:?}", metal).to_lowercase(), + Material::Cloth(cloth) => format!("{:?}", cloth).to_lowercase(), + Material::Leather(leather) => { + format!("{} leather", format!("{:?}", leather).to_lowercase()) + } + Material::Bone => "bone".to_string(), + Material::Shell => "shell".to_string(), + Material::Glass => "glass".to_string(), + Material::Ceramic => "ceramic".to_string(), + Material::Gem(gem) => format!("{:?}", gem).to_lowercase(), + Material::Wax => "wax".to_string(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum WoodType { + Oak, + Pine, + Birch, + Ironwood, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum StoneType { + Granite, + Marble, + Sandstone, + Obsidian, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum MetalType { + Iron, + Steel, + Copper, + Silver, + Gold, + Platinum, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ClothType { + Cotton, + Silk, + Wool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum LeatherType { + Cow, + Pig, + Dragon, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum GemType { + Diamond, + Ruby, + Emerald, + Sapphire, +} diff --git a/src/entities/item/mod.rs b/src/entities/item/mod.rs new file mode 100644 index 0000000..867bbe5 --- /dev/null +++ b/src/entities/item/mod.rs @@ -0,0 +1,19 @@ +pub mod container; +pub mod core; +pub mod decorations; +pub mod material; +pub mod perishable; +pub mod prefabs; +pub mod quality; +pub mod systems; +pub mod types; + +pub use container::*; +pub use core::*; +pub use decorations::*; +pub use material::*; +pub use perishable::*; +pub use prefabs::*; +pub use quality::*; +pub use systems::*; +pub use types::*; diff --git a/src/entities/item/perishable.rs b/src/entities/item/perishable.rs new file mode 100644 index 0000000..5bb82c1 --- /dev/null +++ b/src/entities/item/perishable.rs @@ -0,0 +1,51 @@ +use bevy::prelude::*; + +use crate::entities::item::material::Material; +use crate::entities::item::types::ItemType; +use crate::entities::item::Item; + +#[derive(Component, Debug)] +pub struct Perishable { + // How much the item has decayed (0-255) + pub decay: u8, + // How fast this item decays per tick + pub decay_rate: u8, + // What happens when fully decayed + pub decay_result: DecayResult, +} + +#[derive(Debug, Clone)] +pub enum DecayResult { + Disappear, + Transform(ItemType, Material), // Transform into another item type with different material +} + +pub fn decay_system( + time: Res