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 pub fn add_food(&mut self, nutrition: i32, saturation: f32) {
178 self.add(nutrition, saturation);
179 }
180
181 #[must_use]
185 pub fn tick(
186 &mut self,
187 difficulty: Difficulty,
188 natural_regen: bool,
189 current_health: f32,
190 max_health: f32,
191 ) -> FoodTickResult {
192 if self.exhaustion_level > EXHAUSTION_DROP {
193 self.exhaustion_level -= EXHAUSTION_DROP;
194
195 if self.saturation_level > 0.0 {
196 self.saturation_level = (self.saturation_level - 1.0).max(0.0);
197 } else if difficulty != Difficulty::Peaceful {
198 self.food_level = (self.food_level - 1).max(0);
199 }
200 }
201
202 let food = self.food_level;
203 let is_hurt = current_health > 0.0 && current_health < max_health;
204
205 if natural_regen && self.saturation_level > 0.0 && is_hurt && food >= MAX_FOOD_LEVEL {
207 self.tick_timer += 1;
208
209 if self.tick_timer >= HEALTH_TICK_COUNT_SATURATED {
210 let saturation_spent = self.saturation_level.min(EXHAUSTION_HEAL);
211 let heal_amount = saturation_spent / EXHAUSTION_HEAL;
212 self.tick_timer = 0;
213 return FoodTickResult::Heal {
214 amount: heal_amount,
215 exhaustion: saturation_spent,
216 };
217 }
218 }
219 else if natural_regen && food >= HEAL_LEVEL && is_hurt {
221 self.tick_timer += 1;
222
223 if self.tick_timer >= HEALTH_TICK_COUNT {
224 self.tick_timer = 0;
225 return FoodTickResult::Heal {
226 amount: 1.0,
227 exhaustion: EXHAUSTION_HEAL,
228 };
229 }
230 }
231 else if food <= 0 {
233 self.tick_timer += 1;
234
235 if self.tick_timer >= HEALTH_TICK_COUNT {
236 self.tick_timer = 0;
237
238 if current_health > 10.0
239 || difficulty == Difficulty::Hard
240 || (current_health > 1.0 && difficulty == Difficulty::Normal)
241 {
242 return FoodTickResult::Starve;
243 }
244 }
245 } else {
246 self.tick_timer = 0;
247 }
248
249 FoodTickResult::None
250 }
251}
252
253#[derive(Debug, Clone, Copy, PartialEq)]
255pub enum FoodTickResult {
256 None,
258 Heal {
260 amount: f32,
262 exhaustion: f32,
264 },
265 Starve,
267}
268
269impl Player {
270 pub(super) fn tick_regeneration(&self) {
272 let world = self.get_world();
273 let difficulty = world.level_data.read().data().difficulty;
274 let natural_regen = world.get_game_rule(&NATURAL_HEALTH_REGENERATION);
275 let tick = self.tick_count();
276
277 if difficulty == Difficulty::Peaceful && natural_regen {
278 if tick % 20 == 0 {
279 if self.is_hurt() {
280 self.heal(1.0);
281 }
282
283 let mut food = self.food_data.lock();
284 if food.saturation_level < food_constants::MAX_SATURATION {
285 food.saturation_level += 1.0;
286 }
287 }
288
289 if tick % 10 == 0 {
290 let mut food = self.food_data.lock();
291 if food.needs_food() {
292 food.food_level += 1;
293 }
294 }
295 }
296
297 let current_health = self.get_health();
298 let max_health = self.get_max_health();
299
300 let mut food = self.food_data.lock();
301 let result = food.tick(difficulty, natural_regen, current_health, max_health);
302
303 match result {
304 FoodTickResult::Heal { amount, exhaustion } => {
305 food.add_exhaustion(exhaustion);
306
307 drop(food);
308 self.heal(amount);
309 }
310 FoodTickResult::Starve => {
311 drop(food);
312
313 self.hurt(
314 &world,
315 &DamageSource::environment(&vanilla_damage_types::STARVE),
316 1.0,
317 );
318 }
319 FoodTickResult::None => {}
320 }
321 }
322
323 pub fn cause_food_exhaustion(&self, amount: f32) {
325 if !self.abilities.lock().invulnerable {
326 self.food_data.lock().add_exhaustion(amount);
327 }
328 }
329
330 pub fn is_hurt(&self) -> bool {
332 let health = self.get_health();
333 health > 0.0 && health < self.get_max_health()
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn exhaustion_drains_saturation_then_food() {
343 let mut food = FoodData::new();
344 food.saturation_level = 3.0;
345 food.add_exhaustion(4.5);
346
347 let _ = food.tick(Difficulty::Normal, false, 20.0, 20.0);
349 assert!((food.saturation_level - 2.0).abs() < f32::EPSILON);
350 assert_eq!(food.food_level, MAX_FOOD_LEVEL);
351
352 food.saturation_level = 0.0;
354 food.exhaustion_level = 5.0;
355 let _ = food.tick(Difficulty::Normal, false, 20.0, 20.0);
356 assert_eq!(food.food_level, MAX_FOOD_LEVEL - 1);
357 }
358
359 #[test]
363 fn add_food_applies_absolute_saturation_without_recomputing_it() {
364 let mut food = FoodData::new();
365 food.food_level = 10;
366 food.saturation_level = 0.0;
367
368 food.add_food(4, 2.4);
370
371 assert_eq!(food.food_level, 14);
372 assert!((food.saturation_level - 2.4).abs() < f32::EPSILON);
373 }
374
375 #[test]
377 fn fast_regen() {
378 let mut food = FoodData::new(); let mut result = FoodTickResult::None;
381 for _ in 0..HEALTH_TICK_COUNT_SATURATED {
382 result = food.tick(Difficulty::Normal, true, 15.0, 20.0);
383 }
384
385 match result {
386 FoodTickResult::Heal { amount, exhaustion } => {
387 assert!((exhaustion - 5.0).abs() < f32::EPSILON);
388 assert!((amount - 5.0 / 6.0).abs() < f32::EPSILON);
389 }
390 other => panic!("Expected Heal, got {other:?}"),
391 }
392 }
393
394 #[test]
396 fn slow_regen() {
397 let mut food = FoodData::new();
398 food.food_level = 18;
399 food.saturation_level = 0.0;
400
401 let mut result = FoodTickResult::None;
402 for _ in 0..HEALTH_TICK_COUNT {
403 result = food.tick(Difficulty::Normal, true, 10.0, 20.0);
404 }
405
406 match result {
407 FoodTickResult::Heal { amount, exhaustion } => {
408 assert!((amount - 1.0).abs() < f32::EPSILON);
409 assert!((exhaustion - EXHAUSTION_HEAL).abs() < f32::EPSILON);
410 }
411 other => panic!("Expected Heal, got {other:?}"),
412 }
413 }
414
415 #[test]
420 fn starvation_by_difficulty() {
421 let run = |diff, health| {
422 let mut food = FoodData::new();
423 food.food_level = 0;
424 food.saturation_level = 0.0;
425 let mut result = FoodTickResult::None;
426 for _ in 0..HEALTH_TICK_COUNT {
427 result = food.tick(diff, false, health, 20.0);
428 }
429 result
430 };
431
432 assert_eq!(run(Difficulty::Hard, 1.0), FoodTickResult::Starve);
433 assert_eq!(run(Difficulty::Normal, 1.0), FoodTickResult::None);
434 assert_eq!(run(Difficulty::Normal, 2.0), FoodTickResult::Starve);
435 assert_eq!(run(Difficulty::Easy, 10.0), FoodTickResult::None);
436 assert_eq!(run(Difficulty::Easy, 11.0), FoodTickResult::Starve);
437 }
438
439 #[test]
442 fn no_regen_when_dead() {
443 let mut food = FoodData::new(); let mut result = FoodTickResult::None;
446 for _ in 0..HEALTH_TICK_COUNT_SATURATED {
447 result = food.tick(Difficulty::Normal, true, 0.0, 20.0);
448 }
449
450 assert_eq!(result, FoodTickResult::None);
451 }
452
453 #[test]
455 fn peaceful_never_drains_food() {
456 let mut food = FoodData::new();
457 food.saturation_level = 0.0;
458 food.exhaustion_level = 5.0;
459
460 let _ = food.tick(Difficulty::Peaceful, false, 20.0, 20.0);
461
462 assert_eq!(food.food_level, MAX_FOOD_LEVEL);
463 }
464}