use bevy::prelude::*; use serde::{Deserialize, Serialize}; use crate::{ world::{TILE_SIZE, TilePos, WorldMap}, worldgen::WorldGenerated, }; pub struct ItemsPlugin; impl Plugin for ItemsPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, create_item_assets).add_systems( Update, ( clear_items.run_if(on_message::), show_carried, ), ); } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ItemKind { Wood, Stone, Iron, Gold, Berries, Meat, Vegetables, Meal, Carcass, Tools, } impl ItemKind { pub const ALL: [ItemKind; 10] = [ ItemKind::Wood, ItemKind::Stone, ItemKind::Iron, ItemKind::Gold, ItemKind::Berries, ItemKind::Meat, ItemKind::Vegetables, ItemKind::Meal, ItemKind::Carcass, ItemKind::Tools, ]; pub fn nutrition(self) -> f32 { match self { ItemKind::Berries | ItemKind::Meat | ItemKind::Vegetables => 0.1, ItemKind::Meal => 0.9, _ => 0.0, } } pub fn is_food(self) -> bool { self.nutrition() > 0.0 } pub fn is_raw_food(self) -> bool { self.is_food() && self != ItemKind::Meal } fn color(self) -> Color { match self { ItemKind::Wood => Color::srgb_u8(150, 104, 62), ItemKind::Stone => Color::srgb_u8(170, 170, 176), ItemKind::Iron => Color::srgb_u8(178, 104, 72), ItemKind::Gold => Color::srgb_u8(236, 196, 60), ItemKind::Berries => Color::srgb_u8(170, 40, 90), ItemKind::Meat => Color::srgb_u8(200, 80, 70), ItemKind::Vegetables => Color::srgb_u8(120, 170, 60), ItemKind::Meal => Color::srgb_u8(240, 210, 150), ItemKind::Carcass => Color::srgb_u8(120, 60, 50), ItemKind::Tools => Color::srgb_u8(90, 100, 120), } } } #[derive(Component, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[require(Transform)] pub struct Item { pub kind: ItemKind, pub count: u32, pub spoil: f32, } impl Item { pub fn new(kind: ItemKind, count: u32) -> Self { Self { kind, count, spoil: 0.0, } } pub fn merge(&mut self, other: Item) { let total = (self.count + other.count).max(1) as f32; self.spoil = (self.spoil * self.count as f32 + other.spoil * other.count as f32) / total; self.count += other.count; } pub fn with_count(self, count: u32) -> Self { Self { count, ..self } } } #[derive(Component, Default, Debug, Clone, Copy, PartialEq)] pub struct Carrying(pub Option); #[derive(Component)] #[require(Transform, Visibility)] pub struct CarriedVisual; #[derive(Resource, Default)] pub struct ItemAssets { mesh: Handle, materials: Vec<(ItemKind, Handle)>, } impl ItemAssets { pub fn carried_visual(&self) -> impl Bundle { ( CarriedVisual, Mesh2d(self.mesh.clone()), MeshMaterial2d(self.material(ItemKind::Wood)), Transform::from_xyz(0.0, TILE_SIZE * 0.3, 0.1).with_scale(Vec3::splat(0.7)), Visibility::Hidden, ) } fn material(&self, kind: ItemKind) -> Handle { self.materials .iter() .find(|(k, _)| *k == kind) .map(|(_, m)| m.clone()) .unwrap_or_default() } } fn create_item_assets( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { commands.insert_resource(ItemAssets { mesh: meshes.add(Rectangle::new(TILE_SIZE * 0.7, TILE_SIZE * 0.4)), materials: ItemKind::ALL .iter() .map(|&kind| (kind, materials.add(kind.color()))) .collect(), }); } fn show_carried( assets: Res, colonists: Query<(&Carrying, &Children), Changed>, mut visuals: Query<(&mut Visibility, &mut MeshMaterial2d), With>, ) { for (carrying, children) in &colonists { for &child in children { let Ok((mut visibility, mut material)) = visuals.get_mut(child) else { continue; }; match carrying.0 { Some(item) => { *visibility = Visibility::Inherited; material.0 = assets.material(item.kind); } None => *visibility = Visibility::Hidden, } } } } fn clear_items(mut commands: Commands, items: Query>) { for entity in &items { commands.entity(entity).despawn(); } } pub const STACK_LIMIT: u32 = 50; pub const CARRY_CAPACITY: u32 = 25; pub type ItemQuery<'w, 's> = Query<'w, 's, (Entity, &'static mut Item, &'static TilePos)>; pub fn drop_items( commands: &mut Commands, items: &mut ItemQuery, assets: &ItemAssets, tile: UVec2, dropped: Item, ) { let kind = dropped.kind; if let Some((_, mut item, _)) = items .iter_mut() .find(|(_, item, pos)| pos.0 == tile && item.kind == kind) { item.merge(dropped); return; } spawn_stack(commands, assets, tile, dropped); } pub fn spawn_stack(commands: &mut Commands, assets: &ItemAssets, tile: UVec2, item: Item) { commands.spawn(( Name::new(format!("{:?}", item.kind)), item, TilePos(tile), Mesh2d(assets.mesh.clone()), MeshMaterial2d(assets.material(item.kind)), Transform::from_translation(WorldMap::tile_center(tile).extend(0.7)), )); } pub fn take_items( commands: &mut Commands, items: &mut ItemQuery, tile: UVec2, max: u32, ) -> Option { let (entity, mut item, _) = items.iter_mut().find(|(_, _, pos)| pos.0 == tile)?; let taken = item.count.min(max); item.count -= taken; if item.count == 0 { commands.entity(entity).despawn(); } Some(item.with_count(taken)) } pub fn room_for<'a>( items: impl Iterator, tile: UVec2, kind: ItemKind, ) -> u32 { let mut room = STACK_LIMIT; for (item, _) in items.filter(|(_, pos)| pos.0 == tile) { if item.kind != kind { return 0; } room = room.saturating_sub(item.count); } room } pub fn stock<'a>(items: impl Iterator, kind: ItemKind) -> u32 { items.filter(|i| i.kind == kind).map(|i| i.count).sum() } #[cfg(test)] mod tests { use super::*; fn drop_system(mut commands: Commands, mut items: ItemQuery) { let assets = ItemAssets::default(); drop_items( &mut commands, &mut items, &assets, UVec2::ONE, Item::new(ItemKind::Wood, 5), ); } #[test] fn dropping_on_a_tile_with_the_same_item_stacks() { let mut app = App::new(); app.add_systems(Update, drop_system); app.update(); app.update(); let items: Vec = app .world_mut() .query::<&Item>() .iter(app.world()) .copied() .collect(); assert_eq!(items, vec![Item::new(ItemKind::Wood, 10)]); } fn take_system(mut commands: Commands, mut items: ItemQuery) { take_items(&mut commands, &mut items, UVec2::ONE, 4); } fn wood(count: u32) -> Item { Item::new(ItemKind::Wood, count) } #[test] fn taking_removes_up_to_max_and_despawns_empty_stacks() { let mut app = App::new(); let stack = app.world_mut().spawn((wood(6), TilePos(UVec2::ONE))).id(); app.add_systems(Update, take_system); app.update(); assert_eq!(app.world().get::(stack), Some(&wood(2))); app.update(); assert!(app.world().get_entity(stack).is_err()); } #[test] fn room_for_respects_stack_limit_and_other_kinds() { let tile = UVec2::ONE; let pos = TilePos(tile); assert_eq!( room_for(std::iter::empty(), tile, ItemKind::Wood), STACK_LIMIT ); let partial = [(wood(STACK_LIMIT - 5), pos)]; assert_eq!( room_for(partial.iter().map(|(i, p)| (i, p)), tile, ItemKind::Wood), 5 ); } #[test] fn stock_sums_counts_of_one_kind() { let items = [Item::new(ItemKind::Wood, 3), Item::new(ItemKind::Wood, 4)]; assert_eq!(stock(items.iter(), ItemKind::Wood), 7); } }