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