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