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