src/items.rs 8.5 K · 322 lines · raw · history

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