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