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