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
298 pub(super) fn tick_food_data(&self) {
300 let world = self.get_world();
301 let difficulty = world.level_data.read().data().difficulty;
302 let natural_regen = world.get_game_rule(&NATURAL_HEALTH_REGENERATION);
303 let current_health = self.get_health();
304 let max_health = self.get_max_health();
305
306 let mut food = self.food_data.lock();
307 let result = food.tick(difficulty, natural_regen, current_health, max_health);
308
309 match result {
310 FoodTickResult::Heal { amount, exhaustion } => {
311 food.add_exhaustion(exhaustion);
312
313 drop(food);
314 self.heal(amount);
315 }
316 FoodTickResult::Starve => {
317 drop(food);
318
319 self.hurt(
320 &world,
321 &DamageSource::environment(&vanilla_damage_types::STARVE),
322 1.0,
323 );
324 }
325 FoodTickResult::None => {}
326 }
327 }
328
329 pub fn cause_food_exhaustion(&self, amount: f32) {
331 if !self.abilities.lock().invulnerable {
332 self.food_data.lock().add_exhaustion(amount);
333 }
334 }
335
336 pub fn is_hurt(&self) -> bool {
338 let health = self.get_health();
339 health > 0.0 && health < self.get_max_health()
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn player_tick_processes_food_exhaustion_once() {
349 use std::sync::Arc;
350
351 use crate::entity::Entity;
352 use crate::test_support::{TestPlayerBuilder, fresh_test_world};
353
354 let world_fixture = fresh_test_world("player_tick_food_exhaustion");
355 let world = &world_fixture.world;
356 let player = TestPlayerBuilder::new(Arc::clone(world), "TestPlayer", 1).build();
357 {
358 let mut food = player.food_data.lock();
359 food.saturation_level = 3.0;
360 food.add_exhaustion(8.5);
361 }
362
363 Arc::clone(&player).tick();
364
365 let food = player.food_data.lock();
366 assert_eq!(food.exhaustion_level.to_bits(), 4.5_f32.to_bits());
367 assert_eq!(food.saturation_level.to_bits(), 2.0_f32.to_bits());
368 }
369
370 #[test]
371 fn exhaustion_drains_saturation_then_food() {
372 let mut food = FoodData::new();
373 food.saturation_level = 3.0;
374 food.add_exhaustion(4.5);
375
376 let _ = food.tick(Difficulty::Normal, false, 20.0, 20.0);
378 assert!((food.saturation_level - 2.0).abs() < f32::EPSILON);
379 assert_eq!(food.food_level, MAX_FOOD_LEVEL);
380
381 food.saturation_level = 0.0;
383 food.exhaustion_level = 5.0;
384 let _ = food.tick(Difficulty::Normal, false, 20.0, 20.0);
385 assert_eq!(food.food_level, MAX_FOOD_LEVEL - 1);
386 }
387
388 #[test]
392 fn add_food_applies_absolute_saturation_without_recomputing_it() {
393 let mut food = FoodData::new();
394 food.food_level = 10;
395 food.saturation_level = 0.0;
396
397 food.add_food(4, 2.4);
399
400 assert_eq!(food.food_level, 14);
401 assert!((food.saturation_level - 2.4).abs() < f32::EPSILON);
402 }
403
404 #[test]
406 fn fast_regen() {
407 let mut food = FoodData::new(); let mut result = FoodTickResult::None;
410 for _ in 0..HEALTH_TICK_COUNT_SATURATED {
411 result = food.tick(Difficulty::Normal, true, 15.0, 20.0);
412 }
413
414 match result {
415 FoodTickResult::Heal { amount, exhaustion } => {
416 assert!((exhaustion - 5.0).abs() < f32::EPSILON);
417 assert!((amount - 5.0 / 6.0).abs() < f32::EPSILON);
418 }
419 other => panic!("Expected Heal, got {other:?}"),
420 }
421 }
422
423 #[test]
425 fn slow_regen() {
426 let mut food = FoodData::new();
427 food.food_level = 18;
428 food.saturation_level = 0.0;
429
430 let mut result = FoodTickResult::None;
431 for _ in 0..HEALTH_TICK_COUNT {
432 result = food.tick(Difficulty::Normal, true, 10.0, 20.0);
433 }
434
435 match result {
436 FoodTickResult::Heal { amount, exhaustion } => {
437 assert!((amount - 1.0).abs() < f32::EPSILON);
438 assert!((exhaustion - EXHAUSTION_HEAL).abs() < f32::EPSILON);
439 }
440 other => panic!("Expected Heal, got {other:?}"),
441 }
442 }
443
444 #[test]
449 fn starvation_by_difficulty() {
450 let run = |diff, health| {
451 let mut food = FoodData::new();
452 food.food_level = 0;
453 food.saturation_level = 0.0;
454 let mut result = FoodTickResult::None;
455 for _ in 0..HEALTH_TICK_COUNT {
456 result = food.tick(diff, false, health, 20.0);
457 }
458 result
459 };
460
461 assert_eq!(run(Difficulty::Hard, 1.0), FoodTickResult::Starve);
462 assert_eq!(run(Difficulty::Normal, 1.0), FoodTickResult::None);
463 assert_eq!(run(Difficulty::Normal, 2.0), FoodTickResult::Starve);
464 assert_eq!(run(Difficulty::Easy, 10.0), FoodTickResult::None);
465 assert_eq!(run(Difficulty::Easy, 11.0), FoodTickResult::Starve);
466 }
467
468 #[test]
471 fn no_regen_when_dead() {
472 let mut food = FoodData::new(); let mut result = FoodTickResult::None;
475 for _ in 0..HEALTH_TICK_COUNT_SATURATED {
476 result = food.tick(Difficulty::Normal, true, 0.0, 20.0);
477 }
478
479 assert_eq!(result, FoodTickResult::None);
480 }
481
482 #[test]
484 fn peaceful_never_drains_food() {
485 let mut food = FoodData::new();
486 food.saturation_level = 0.0;
487 food.exhaustion_level = 5.0;
488
489 let _ = food.tick(Difficulty::Peaceful, false, 20.0, 20.0);
490
491 assert_eq!(food.food_level, MAX_FOOD_LEVEL);
492 }
493}