1use steel_registry::vanilla_damage_types;
7use steel_registry::vanilla_game_rules::NATURAL_HEALTH_REGENERATION;
8use steel_utils::types::Difficulty;
9
10use crate::entity::LivingEntity;
11use crate::entity::damage::DamageSource;
12use crate::player::Player;
13
14pub mod food_constants {
16 pub const MAX_FOOD_LEVEL: i32 = 20;
18
19 pub const MAX_SATURATION: f32 = 20.0;
21
22 pub const DEFAULT_SATURATION: f32 = 5.0;
24
25 pub const SATURATION_FLOOR: f32 = 2.5;
27
28 pub const EXHAUSTION_DROP: f32 = 4.0;
30
31 pub const HEALTH_TICK_COUNT: i32 = 80;
33
34 pub const HEALTH_TICK_COUNT_SATURATED: i32 = 10;
36
37 pub const HEAL_LEVEL: i32 = 18;
39
40 pub const SPRINT_LEVEL: i32 = 6;
42
43 pub const FOOD_SATURATION_POOR: f32 = 0.1;
45
46 pub const FOOD_SATURATION_LOW: f32 = 0.3;
48
49 pub const FOOD_SATURATION_NORMAL: f32 = 0.6;
51
52 pub const FOOD_SATURATION_GOOD: f32 = 0.8;
54
55 pub const FOOD_SATURATION_MAX: f32 = 1.0;
57
58 pub const FOOD_SATURATION_SUPERNATURAL: f32 = 1.2;
60
61 pub const EXHAUSTION_HEAL: f32 = 6.0;
63
64 pub const EXHAUSTION_JUMP: f32 = 0.05;
66
67 pub const EXHAUSTION_SPRINT_JUMP: f32 = 0.2;
69
70 pub const EXHAUSTION_MINE: f32 = 0.005;
72
73 pub const EXHAUSTION_ATTACK: f32 = 0.1;
75
76 pub const EXHAUSTION_WALK: f32 = 0.0;
78
79 pub const EXHAUSTION_CROUCH: f32 = 0.0;
81
82 pub const EXHAUSTION_SPRINT: f32 = 0.1;
84
85 pub const EXHAUSTION_SWIM: f32 = 0.01;
87
88 pub const DEFAULT_FOOD_LEVEL: i32 = 20;
90
91 pub const MAX_EXHAUSTION: f32 = 40.0;
93}
94
95use food_constants::{
96 DEFAULT_FOOD_LEVEL, DEFAULT_SATURATION, EXHAUSTION_DROP, EXHAUSTION_HEAL, HEAL_LEVEL,
97 HEALTH_TICK_COUNT, HEALTH_TICK_COUNT_SATURATED, MAX_EXHAUSTION, MAX_FOOD_LEVEL, SPRINT_LEVEL,
98};
99
100#[must_use]
102pub fn saturation_by_modifier(nutrition: i32, modifier: f32) -> f32 {
103 nutrition as f32 * modifier * 2.0
104}
105
106#[derive(Debug, Clone)]
109pub struct FoodData {
110 pub food_level: i32,
112 pub saturation_level: f32,
114 pub exhaustion_level: f32,
116 pub tick_timer: i32,
118}
119
120impl Default for FoodData {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126impl FoodData {
127 #[must_use]
129 pub const fn new() -> Self {
130 Self {
131 food_level: DEFAULT_FOOD_LEVEL,
132 saturation_level: DEFAULT_SATURATION,
133 exhaustion_level: 0.0,
134 tick_timer: 0,
135 }
136 }
137
138 #[must_use]
141 pub const fn needs_food(&self) -> bool {
142 self.food_level < MAX_FOOD_LEVEL
143 }
144
145 #[must_use]
148 pub const fn has_enough_food(&self) -> bool {
149 self.food_level > SPRINT_LEVEL
150 }
151
152 pub fn add_exhaustion(&mut self, amount: f32) {
155 self.exhaustion_level = (self.exhaustion_level + amount).min(MAX_EXHAUSTION);
156 }
157
158 fn add(&mut self, food: i32, saturation: f32) {
160 self.food_level = (self.food_level + food).clamp(0, MAX_FOOD_LEVEL);
161 self.saturation_level =
162 (self.saturation_level + saturation).clamp(0.0, self.food_level as f32);
163 }
164
165 pub fn eat(&mut self, nutrition: i32, saturation_modifier: f32) {
167 self.add(
168 nutrition,
169 saturation_by_modifier(nutrition, saturation_modifier),
170 );
171 }
172
173 #[must_use]
177 pub fn tick(
178 &mut self,
179 difficulty: Difficulty,
180 natural_regen: bool,
181 current_health: f32,
182 max_health: f32,
183 ) -> FoodTickResult {
184 if self.exhaustion_level > EXHAUSTION_DROP {
185 self.exhaustion_level -= EXHAUSTION_DROP;
186
187 if self.saturation_level > 0.0 {
188 self.saturation_level = (self.saturation_level - 1.0).max(0.0);
189 } else if difficulty != Difficulty::Peaceful {
190 self.food_level = (self.food_level - 1).max(0);
191 }
192 }
193
194 let food = self.food_level;
195 let is_hurt = current_health > 0.0 && current_health < max_health;
196
197 if natural_regen && self.saturation_level > 0.0 && is_hurt && food >= MAX_FOOD_LEVEL {
199 self.tick_timer += 1;
200
201 if self.tick_timer >= HEALTH_TICK_COUNT_SATURATED {
202 let saturation_spent = self.saturation_level.min(EXHAUSTION_HEAL);
203 let heal_amount = saturation_spent / EXHAUSTION_HEAL;
204 self.tick_timer = 0;
205 return FoodTickResult::Heal {
206 amount: heal_amount,
207 exhaustion: saturation_spent,
208 };
209 }
210 }
211 else if natural_regen && food >= HEAL_LEVEL && is_hurt {
213 self.tick_timer += 1;
214
215 if self.tick_timer >= HEALTH_TICK_COUNT {
216 self.tick_timer = 0;
217 return FoodTickResult::Heal {
218 amount: 1.0,
219 exhaustion: EXHAUSTION_HEAL,
220 };
221 }
222 }
223 else if food <= 0 {
225 self.tick_timer += 1;
226
227 if self.tick_timer >= HEALTH_TICK_COUNT {
228 self.tick_timer = 0;
229
230 if current_health > 10.0
231 || difficulty == Difficulty::Hard
232 || (current_health > 1.0 && difficulty == Difficulty::Normal)
233 {
234 return FoodTickResult::Starve;
235 }
236 }
237 } else {
238 self.tick_timer = 0;
239 }
240
241 FoodTickResult::None
242 }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq)]
247pub enum FoodTickResult {
248 None,
250 Heal {
252 amount: f32,
254 exhaustion: f32,
256 },
257 Starve,
259}
260
261impl Player {
262 pub(super) fn tick_regeneration(&self) {
264 let world = self.get_world();
265 let difficulty = world.level_data.read().data().difficulty;
266 let natural_regen = world.get_game_rule(&NATURAL_HEALTH_REGENERATION);
267 let tick = self.tick_count();
268
269 if difficulty == Difficulty::Peaceful && natural_regen {
270 if tick % 20 == 0 {
271 if self.is_hurt() {
272 self.heal(1.0);
273 }
274
275 let mut food = self.food_data.lock();
276 if food.saturation_level < food_constants::MAX_SATURATION {
277 food.saturation_level += 1.0;
278 }
279 }
280
281 if tick % 10 == 0 {
282 let mut food = self.food_data.lock();
283 if food.needs_food() {
284 food.food_level += 1;
285 }
286 }
287 }
288
289 let current_health = self.get_health();
290 let max_health = self.get_max_health();
291
292 let mut food = self.food_data.lock();
293 let result = food.tick(difficulty, natural_regen, current_health, max_health);
294
295 match result {
296 FoodTickResult::Heal { amount, exhaustion } => {
297 food.add_exhaustion(exhaustion);
298
299 drop(food);
300 self.heal(amount);
301 }
302 FoodTickResult::Starve => {
303 drop(food);
304
305 self.hurt(
306 &world,
307 &DamageSource::environment(&vanilla_damage_types::STARVE),
308 1.0,
309 );
310 }
311 FoodTickResult::None => {}
312 }
313 }
314
315 pub fn cause_food_exhaustion(&self, amount: f32) {
317 if !self.abilities.lock().invulnerable {
318 self.food_data.lock().add_exhaustion(amount);
319 }
320 }
321
322 pub fn is_hurt(&self) -> bool {
324 let health = self.get_health();
325 health > 0.0 && health < self.get_max_health()
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn exhaustion_drains_saturation_then_food() {
335 let mut food = FoodData::new();
336 food.saturation_level = 3.0;
337 food.add_exhaustion(4.5);
338
339 let _ = food.tick(Difficulty::Normal, false, 20.0, 20.0);
341 assert!((food.saturation_level - 2.0).abs() < f32::EPSILON);
342 assert_eq!(food.food_level, MAX_FOOD_LEVEL);
343
344 food.saturation_level = 0.0;
346 food.exhaustion_level = 5.0;
347 let _ = food.tick(Difficulty::Normal, false, 20.0, 20.0);
348 assert_eq!(food.food_level, MAX_FOOD_LEVEL - 1);
349 }
350
351 #[test]
353 fn fast_regen() {
354 let mut food = FoodData::new(); let mut result = FoodTickResult::None;
357 for _ in 0..HEALTH_TICK_COUNT_SATURATED {
358 result = food.tick(Difficulty::Normal, true, 15.0, 20.0);
359 }
360
361 match result {
362 FoodTickResult::Heal { amount, exhaustion } => {
363 assert!((exhaustion - 5.0).abs() < f32::EPSILON);
364 assert!((amount - 5.0 / 6.0).abs() < f32::EPSILON);
365 }
366 other => panic!("Expected Heal, got {other:?}"),
367 }
368 }
369
370 #[test]
372 fn slow_regen() {
373 let mut food = FoodData::new();
374 food.food_level = 18;
375 food.saturation_level = 0.0;
376
377 let mut result = FoodTickResult::None;
378 for _ in 0..HEALTH_TICK_COUNT {
379 result = food.tick(Difficulty::Normal, true, 10.0, 20.0);
380 }
381
382 match result {
383 FoodTickResult::Heal { amount, exhaustion } => {
384 assert!((amount - 1.0).abs() < f32::EPSILON);
385 assert!((exhaustion - EXHAUSTION_HEAL).abs() < f32::EPSILON);
386 }
387 other => panic!("Expected Heal, got {other:?}"),
388 }
389 }
390
391 #[test]
396 fn starvation_by_difficulty() {
397 let run = |diff, health| {
398 let mut food = FoodData::new();
399 food.food_level = 0;
400 food.saturation_level = 0.0;
401 let mut result = FoodTickResult::None;
402 for _ in 0..HEALTH_TICK_COUNT {
403 result = food.tick(diff, false, health, 20.0);
404 }
405 result
406 };
407
408 assert_eq!(run(Difficulty::Hard, 1.0), FoodTickResult::Starve);
409 assert_eq!(run(Difficulty::Normal, 1.0), FoodTickResult::None);
410 assert_eq!(run(Difficulty::Normal, 2.0), FoodTickResult::Starve);
411 assert_eq!(run(Difficulty::Easy, 10.0), FoodTickResult::None);
412 assert_eq!(run(Difficulty::Easy, 11.0), FoodTickResult::Starve);
413 }
414
415 #[test]
418 fn no_regen_when_dead() {
419 let mut food = FoodData::new(); let mut result = FoodTickResult::None;
422 for _ in 0..HEALTH_TICK_COUNT_SATURATED {
423 result = food.tick(Difficulty::Normal, true, 0.0, 20.0);
424 }
425
426 assert_eq!(result, FoodTickResult::None);
427 }
428
429 #[test]
431 fn peaceful_never_drains_food() {
432 let mut food = FoodData::new();
433 food.saturation_level = 0.0;
434 food.exhaustion_level = 5.0;
435
436 let _ = food.tick(Difficulty::Peaceful, false, 20.0, 20.0);
437
438 assert_eq!(food.food_level, MAX_FOOD_LEVEL);
439 }
440}