Newer
Older
use bevy::prelude::{
BuildChildren, Color, Commands, Component, DespawnRecursiveExt, Entity, EventReader,
EventWriter, Mut, NodeBundle, Query, Res, TextBundle, Transform, TransformBundle, Val,
VisibilityBundle, With, Without,
};
use bevy::text::{Text, TextStyle};
use bevy::ui::{AlignItems, FlexDirection, JustifyContent, Style, UiRect};
use serde::{Deserialize, Serialize};
use crate::assets::AssetHandles;
use crate::const_data::get_goods_from_name_checked;
use crate::states::Player;
use crate::system::camera::ChaseCam;
use crate::system::utilities::format_ui_distance;
use crate::world::spawning::apply_skull_marker;
use crate::world::travel::WorldTickEvent;
use crate::world::{DistanceTravelled, TownName, TradeGood, TradingState, WorldLinked};
#[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 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,
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);
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// 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;
}
}
pub struct PlayerStarvedEvent {
distance_travelled: f32,
}
mut commands: Commands,
assets: Res<AssetHandles>,
food_query: Query<(Entity, &HungerState), Without<StarvationMarker>>,
player_query: Query<(&Transform, &DistanceTravelled), With<Player>>,
for (entity, hunger) in &food_query {
if hunger.sustenance == 0 {
if town_query.contains(entity) {
apply_skull_marker(&mut commands, &assets, entity);
commands.entity(entity).insert(StarvationMarker);
} else if let Ok((transform, distance_travelled)) = player_query.get(entity) {
commands.entity(entity).despawn_recursive();
let new_entity = commands
.spawn((
TransformBundle::from(*transform),
VisibilityBundle::default(),
ChaseCam,
WorldLinked,
))
.id();
apply_skull_marker(&mut commands, &assets, new_entity);
player_events.send(PlayerStarvedEvent {
distance_travelled: distance_travelled.0,
})
}
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
pub fn handle_player_starved(
mut commands: Commands,
assets: Res<AssetHandles>,
mut events: EventReader<PlayerStarvedEvent>,
) {
for event in events.iter() {
commands
.spawn((
NodeBundle {
style: Style {
margin: UiRect::all(Val::Auto),
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..Default::default()
},
..Default::default()
},
WorldLinked,
))
.with_children(|builder| {
builder.spawn(TextBundle {
text: Text::from_section(
"You Have Perished",
TextStyle {
font_size: 64.0,
color: Color::ANTIQUE_WHITE,
font: assets.font("compass_pro"),
},
),
..Default::default()
});
builder.spawn(TextBundle {
text: Text::from_section(
format!(
"You made it: {}",
format_ui_distance(event.distance_travelled)
),
TextStyle {
font_size: 48.0,
color: Color::ANTIQUE_WHITE,
font: assets.font("equipment_pro"),
},
),
..Default::default()
});
builder.spawn(TextBundle {
text: Text::from_section(
"Press Escape To Return To Menu",
TextStyle {
font_size: 32.0,
color: Color::ANTIQUE_WHITE,
font: assets.font("equipment_pro"),
},
),
..Default::default()
});
});
}
}