src/items.rs 6.7 K · 269 lines · raw · history

1 use bevy::prelude::*;
2
3 use crate::{
4 world::{TILE_SIZE, TilePos, WorldMap},
5 worldgen::WorldGenerated,
6 };
7
8 pub struct ItemsPlugin;
9
10 impl Plugin for ItemsPlugin {
11 fn build(&self, app: &mut App) {
12 app.add_systems(Startup, create_item_assets).add_systems(
13 Update,
14 (
15 clear_items.run_if(on_message::<WorldGenerated>),
16 show_carried,
17 ),
18 );
19 }
20 }
21
22 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23 pub enum ItemKind {
24 Wood,
25 }
26
27 impl ItemKind {
28 pub const ALL: [ItemKind; 1] = [ItemKind::Wood];
29
30 fn color(self) -> Color {
31 match self {
32 ItemKind::Wood => Color::srgb_u8(150, 104, 62),
33 }
34 }
35 }
36
37 #[derive(Component, Debug, Clone, Copy, PartialEq)]
38 #[require(Transform)]
39 pub struct Item {
40 pub kind: ItemKind,
41 pub count: u32,
42 }
43
44 #[derive(Component, Default, Debug, Clone, Copy, PartialEq)]
45 pub struct Carrying(pub Option<Item>);
46
47 #[derive(Component)]
48 #[require(Transform, Visibility)]
49 pub struct CarriedVisual;
50
51 #[derive(Resource, Default)]
52 pub struct ItemAssets {
53 mesh: Handle<Mesh>,
54 materials: Vec<(ItemKind, Handle<ColorMaterial>)>,
55 }
56
57 impl ItemAssets {
58 pub fn carried_visual(&self) -> impl Bundle {
59 (
60 CarriedVisual,
61 Mesh2d(self.mesh.clone()),
62 MeshMaterial2d(self.material(ItemKind::Wood)),
63 Transform::from_xyz(0.0, TILE_SIZE * 0.3, 0.1).with_scale(Vec3::splat(0.7)),
64 Visibility::Hidden,
65 )
66 }
67
68 fn material(&self, kind: ItemKind) -> Handle<ColorMaterial> {
69 self.materials
70 .iter()
71 .find(|(k, _)| *k == kind)
72 .map(|(_, m)| m.clone())
73 .unwrap_or_default()
74 }
75 }
76
77 fn create_item_assets(
78 mut commands: Commands,
79 mut meshes: ResMut<Assets<Mesh>>,
80 mut materials: ResMut<Assets<ColorMaterial>>,
81 ) {
82 commands.insert_resource(ItemAssets {
83 mesh: meshes.add(Rectangle::new(TILE_SIZE * 0.7, TILE_SIZE * 0.4)),
84 materials: ItemKind::ALL
85 .iter()
86 .map(|&kind| (kind, materials.add(kind.color())))
87 .collect(),
88 });
89 }
90
91 fn show_carried(
92 colonists: Query<(&Carrying, &Children), Changed<Carrying>>,
93 mut visuals: Query<&mut Visibility, With<CarriedVisual>>,
94 ) {
95 for (carrying, children) in &colonists {
96 for &child in children {
97 if let Ok(mut visibility) = visuals.get_mut(child) {
98 *visibility = if carrying.0.is_some() {
99 Visibility::Inherited
100 } else {
101 Visibility::Hidden
102 };
103 }
104 }
105 }
106 }
107
108 fn clear_items(mut commands: Commands, items: Query<Entity, With<Item>>) {
109 for entity in &items {
110 commands.entity(entity).despawn();
111 }
112 }
113
114 pub const STACK_LIMIT: u32 = 50;
115 pub const CARRY_CAPACITY: u32 = 25;
116
117 pub type ItemQuery<'w, 's> = Query<'w, 's, (Entity, &'static mut Item, &'static TilePos)>;
118
119 pub fn drop_items(
120 commands: &mut Commands,
121 items: &mut ItemQuery,
122 assets: &ItemAssets,
123 tile: UVec2,
124 kind: ItemKind,
125 count: u32,
126 ) {
127 if let Some((_, mut item, _)) = items
128 .iter_mut()
129 .find(|(_, item, pos)| pos.0 == tile && item.kind == kind)
130 {
131 item.count += count;
132 return;
133 }
134 commands.spawn((
135 Name::new(format!("{kind:?}")),
136 Item { kind, count },
137 TilePos(tile),
138 Mesh2d(assets.mesh.clone()),
139 MeshMaterial2d(assets.material(kind)),
140 Transform::from_translation(WorldMap::tile_center(tile).extend(0.7)),
141 ));
142 }
143
144 pub fn take_items(
145 commands: &mut Commands,
146 items: &mut ItemQuery,
147 tile: UVec2,
148 max: u32,
149 ) -> Option<Item> {
150 let (entity, mut item, _) = items.iter_mut().find(|(_, _, pos)| pos.0 == tile)?;
151 let taken = item.count.min(max);
152 item.count -= taken;
153 if item.count == 0 {
154 commands.entity(entity).despawn();
155 }
156 Some(Item {
157 kind: item.kind,
158 count: taken,
159 })
160 }
161
162 pub fn room_for<'a>(
163 items: impl Iterator<Item = (&'a Item, &'a TilePos)>,
164 tile: UVec2,
165 kind: ItemKind,
166 ) -> u32 {
167 let mut room = STACK_LIMIT;
168 for (item, _) in items.filter(|(_, pos)| pos.0 == tile) {
169 if item.kind != kind {
170 return 0;
171 }
172 room = room.saturating_sub(item.count);
173 }
174 room
175 }
176
177 pub fn stock<'a>(items: impl Iterator<Item = &'a Item>, kind: ItemKind) -> u32 {
178 items.filter(|i| i.kind == kind).map(|i| i.count).sum()
179 }
180
181 #[cfg(test)]
182 mod tests {
183 use super::*;
184
185 fn drop_system(mut commands: Commands, mut items: ItemQuery) {
186 let assets = ItemAssets::default();
187 drop_items(
188 &mut commands,
189 &mut items,
190 &assets,
191 UVec2::ONE,
192 ItemKind::Wood,
193 5,
194 );
195 }
196
197 #[test]
198 fn dropping_on_a_tile_with_the_same_item_stacks() {
199 let mut app = App::new();
200 app.add_systems(Update, drop_system);
201 app.update();
202 app.update();
203 let items: Vec<Item> = app
204 .world_mut()
205 .query::<&Item>()
206 .iter(app.world())
207 .copied()
208 .collect();
209 assert_eq!(
210 items,
211 vec![Item {
212 kind: ItemKind::Wood,
213 count: 10
214 }]
215 );
216 }
217
218 fn take_system(mut commands: Commands, mut items: ItemQuery) {
219 take_items(&mut commands, &mut items, UVec2::ONE, 4);
220 }
221
222 fn wood(count: u32) -> Item {
223 Item {
224 kind: ItemKind::Wood,
225 count,
226 }
227 }
228
229 #[test]
230 fn taking_removes_up_to_max_and_despawns_empty_stacks() {
231 let mut app = App::new();
232 let stack = app.world_mut().spawn((wood(6), TilePos(UVec2::ONE))).id();
233 app.add_systems(Update, take_system);
234 app.update();
235 assert_eq!(app.world().get::<Item>(stack), Some(&wood(2)));
236 app.update();
237 assert!(app.world().get_entity(stack).is_err());
238 }
239
240 #[test]
241 fn room_for_respects_stack_limit_and_other_kinds() {
242 let tile = UVec2::ONE;
243 let pos = TilePos(tile);
244 assert_eq!(
245 room_for(std::iter::empty(), tile, ItemKind::Wood),
246 STACK_LIMIT
247 );
248 let partial = [(wood(STACK_LIMIT - 5), pos)];
249 assert_eq!(
250 room_for(partial.iter().map(|(i, p)| (i, p)), tile, ItemKind::Wood),
251 5
252 );
253 }
254
255 #[test]
256 fn stock_sums_counts_of_one_kind() {
257 let items = [
258 Item {
259 kind: ItemKind::Wood,
260 count: 3,
261 },
262 Item {
263 kind: ItemKind::Wood,
264 count: 4,
265 },
266 ];
267 assert_eq!(stock(items.iter(), ItemKind::Wood), 7);
268 }
269 }