Skip to content
Snippets Groups Projects
hunger.rs 5.79 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 fn get_town_descriptor(&self) -> String {
		let percent = self.sustenance as f32 / TOWN_FOOD_TARGET as f32;

		if percent > 1.0 {
			String::from("The townsfolk have never looked healthier. They are bustling and joyous, brimming with energy. You'll have one of whatever they've been having!")
		} else if percent > 0.75 {
			String::from("The atmosphere is generally lively and upbeat, but as you glance around you notice a few worried faces.")
		} else if percent > 0.55 {
			String::from("You find yourself unable to strike up a conversation with any of the local townsfolk. There's a certain desperation about the people you see, but you can't quite place why. In the background, you do not hear the usual clinking of crockery and cups.")
		} else if percent > 0.15 {
			String::from("The tavern is looking quite empty, with only a few elderly or especially scruffy patrons within sight. None have a beer or food in front of them; instead they count the rings in the wooden tables.")
		} else if percent > 0.001 {
			String::from("Every wretched soul whose path you have crossed to reach the tavern has had the same hungry look in their eyes. The atmosphere is tense, as if at any moment the whole town could erupt into chaos. You decide to make your stay short.")
		} else {
			String::from("You look around tavern and see only the emaciated corpses of the former townsfolk. Checking one pile of corpses, you notice some covered in several bite marks. Are those the marks of human teeth? You dare not think too much about it.")
		}
	}
}

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>,
) {
}