Skip to content
Snippets Groups Projects
hunger.rs 4.29 KiB
Newer Older
use std::collections::HashMap;

Louis's avatar
Louis committed
use bevy::prelude::{Component, Entity, EventReader, Mut, Query, With, Without};
use serde::{Deserialize, Serialize};

use crate::const_data::get_goods_from_name_checked;
use crate::states::Player;
use crate::world::travel::WorldTickEvent;
use crate::world::{TownName, TradeGood, TradingState};

#[derive(Clone, Debug, Default, Serialize, Deserialize, Component)]
pub struct HungerState {
	pub sustenance: usize,
	pub starvation_ticks: f32,
}

impl HungerState {
	pub fn initial_player() -> Self {
		Self {
			sustenance: PLAYER_FOOD_TARGET,
			starvation_ticks: 0.0,
		}
	}
	pub fn initial_town() -> Self {
		Self {
			sustenance: TOWN_FOOD_TARGET,
			starvation_ticks: 0.0,
		}
	}
}

pub struct StarvationMarker;

pub const PLAYER_FOOD_TARGET: usize = 25;
pub const TOWN_FOOD_TARGET: usize = 75;

pub fn process_player_hunger_ticks(
	world_ticks: EventReader<WorldTickEvent>,
	mut query: Query<(&mut TradingState, &mut HungerState), (With<Player>, Without<TownName>)>,
) {
	if !world_ticks.is_empty() {
		log::info!("Processing player hunger");
		process_hunger_state(PLAYER_FOOD_TARGET, 1, query.iter_mut());
	}
}

pub fn process_towns_hunger_ticks(
	world_ticks: EventReader<WorldTickEvent>,
	mut query: Query<(&mut TradingState, &mut HungerState), (With<TownName>, Without<Player>)>,
) {
	if !world_ticks.is_empty() {
		log::info!("Processing town hunger");
		process_hunger_state(TOWN_FOOD_TARGET, 5, query.iter_mut());
	}
}

/// Run system logic for processing hunger state. We extract this out to a separate function
/// so that we can parallelise player & town hunger processing as they are identical but have
/// different target values. The systems have disjoint query sets (second tuple in the Query params),
/// so will run in parallel despite mutably accessing the same components
fn process_hunger_state<'a>(
	target: usize,
	decay: usize,
	mut entries: impl Iterator<Item = (Mut<'a, TradingState>, Mut<'a, HungerState>)>,
) {
	for (mut trading_state, mut hunger_state) in &mut entries {
		// Tick down our sustenance every time we check our food state
		hunger_state.sustenance = hunger_state.sustenance.saturating_sub(decay);
		// We don't need to consume if we somehow exceed our target
		if hunger_state.sustenance >= target {
			continue;
		}

		// The amount of sustenance that we need to get from food in the inventory
		let mut deficit = target - hunger_state.sustenance;

		// A list of what items we have that satisfy the following conditions:
		// - Has a food_value of at least 1 (We aren't eating spare wheels over here)
		// - Has a food value _less_ than the deficit we're trying to fill. We won't eat foods
		//   that we can't take full advantage of
		let mut foodstuffs = trading_state
			.items
			.iter()
			.filter_map(|(name, amount)| {
				get_goods_from_name_checked(name)
					.filter(|good| good.food_value > 0 && good.food_value <= deficit)
					.map(|good| (name, good, amount))
			})
			.collect::<Vec<(&String, TradeGood, &usize)>>();

		// Sort by highest food value. We eat the most nourishing foods first
		foodstuffs.sort_by(|(_, tga, _), (_, tgb, _)| tga.food_value.cmp(&tgb.food_value));

		// Track what we're going to consume
		let mut to_consume = Vec::with_capacity(foodstuffs.len() / 4);
		'consumption_loop: for (name, trade_good, available_amount) in &foodstuffs {
			let mut available = **available_amount;
			let mut consume_amount = 0;

			'deficit_loop: while deficit > 0 && available > 0 {
				if trade_good.food_value > deficit {
					break 'deficit_loop;
				}
				available -= 1;
				consume_amount += 1;
				deficit -= trade_good.food_value;
			}

			if consume_amount > 0 {
				to_consume.push(((*name).clone(), consume_amount));
			}

			// If we've satisfied our deficit, or we no longer have foods we can consume to take
			// full effect of their food_value (no wastage)
			if deficit == 0 || trade_good.food_value > deficit {
				break 'consumption_loop;
			}
		}

		// Actually remove items we're consuming for food
		for (item_name, amount) in to_consume {
			trading_state.remove_items(item_name, amount);
		}

		// Restore hunger to max, less any deficit we haven't satisfied
		hunger_state.sustenance = target - deficit;
	}
}
Louis's avatar
Louis committed

pub fn handle_entity_starvation(
	food_query: Query<(Entity, &HungerState)>,
	player_query: Query<(), With<Player>>,
	town_query: Query<&TownName>,
) {
}