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