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