use bevy::math::Vec3Swizzles; use bevy::prelude::*; use fake::faker::company::en::{Bs, BsAdj, BsNoun, BsVerb}; use fake::faker::lorem::en::{Paragraph, Paragraphs}; use fake::Fake; use ldtk_rust::{EntityInstance, FieldInstance}; use num_traits::AsPrimitive; use serde_json::Value; use crate::states::Player; use crate::world::utils::entity_to_worldspace; #[derive(Copy, Clone, PartialEq, Debug)] pub enum EncounterType { MildField, Bridge, Monastery, Docks, Desert, GrassyRoad, } impl TryFrom<String> for EncounterType { type Error = &'static str; fn try_from(value: String) -> Result<Self, Self::Error> { match &*value { "MildField" => Ok(Self::MildField), "Bridge" => Ok(Self::Bridge), "Monastery" => Ok(Self::Monastery), "Docks" => Ok(Self::Docks), "Desert" => Ok(Self::Desert), "GrassyRoad" => Ok(Self::GrassyRoad), _ => Err("Invalid encounter type"), } } } #[derive(Copy, Clone, PartialEq, Debug)] pub struct EncounterZone { pub zone_type: EncounterType, pub area: Rect, } impl EncounterZone { pub fn new( zone_type: EncounterType, cx: impl AsPrimitive<f32>, cy: impl AsPrimitive<f32>, width: impl AsPrimitive<f32>, height: impl AsPrimitive<f32>, ) -> Self { let cx = cx.as_(); let cy = cy.as_(); let width = width.as_(); let height = height.as_(); EncounterZone { zone_type, area: Rect::new( cx - (width / 2.0), cy - (height / 2.0), cx + (width / 2.0), cy + (height / 2.0), ), } } pub fn contains(&self, point: Vec2) -> bool { self.area.contains(point) } } #[derive(Default, Resource, Debug)] pub struct WorldZones(pub Vec<EncounterZone>); impl WorldZones { pub fn get_container(&self, point: Vec2) -> Option<EncounterZone> { self.0.iter().find(|e| e.contains(point)).copied() } pub fn from_entities(height: i64, value: Vec<&EntityInstance>) -> Self { value .iter() .filter_map(|instance| { if instance.identifier == String::from("DangerZone") { let (cx, cy) = entity_to_worldspace(height, instance); match instance .field_instances .iter() .find(|field| field.identifier == String::from("zone_type")) { Some(v) => match &v.value { Some(Value::String(zone_type)) => { EncounterType::try_from(zone_type.clone()).ok().map(|ze| { EncounterZone::new(ze, cx, cy, instance.width, instance.height) }) } _ => None, }, _ => None, } } else { None } }) .collect::<Vec<EncounterZone>>() .into() } } impl From<Vec<EncounterZone>> for WorldZones { fn from(value: Vec<EncounterZone>) -> Self { Self(value) } } #[derive(Clone, Debug)] pub enum EncounterOutcome { GainResource { resource_type: String, amount: (usize, usize), }, LoseResource { resource_type: String, amount: (usize, usize), }, Ambush { description: String, strength: usize, defence: usize, victory: Vec<EncounterOutcome>, defeat: Vec<EncounterOutcome>, }, } #[derive(Clone, Debug)] pub struct EncounterOption { pub label: String, pub outcome: Vec<EncounterOutcome>, } #[derive(Clone, Debug)] pub struct Encounter { pub title: String, pub description: String, pub options: Vec<EncounterOption>, } pub fn gen_encounter() -> Encounter { Encounter { title: Bs().fake(), description: Paragraph(1..3).fake(), options: vec![ EncounterOption { label: BsVerb().fake(), outcome: vec![EncounterOutcome::GainResource { resource_type: format!( "{} {}", BsAdj().fake::<String>(), BsNoun().fake::<String>() ), amount: { let b = fastrand::usize(2..5); let inc = fastrand::usize(2..5); (b, b + inc) }, }], }, EncounterOption { label: BsVerb().fake(), outcome: vec![EncounterOutcome::LoseResource { resource_type: format!( "{} {}", BsAdj().fake::<String>(), BsNoun().fake::<String>() ), amount: { let b = fastrand::usize(2..5); let inc = fastrand::usize(2..5); (b, b + inc) }, }], }, ], } } #[derive(Clone, Default, Resource, Debug)] pub enum EncounterState { #[default] NoEncounter, Choice(Encounter), Consequence(Encounter, EncounterOption), } impl EncounterState { pub fn is_in_encounter(&self) -> bool { !matches!(self, Self::NoEncounter) } } pub fn notify_new_zone( zones: Res<WorldZones>, player_query: Query<&Transform, With<Player>>, mut last_zone: Local<Option<EncounterZone>>, mut encounter_state: ResMut<EncounterState>, ) { for position in &player_query { if let Some(zone) = zones.get_container(position.translation.xy()) { match *last_zone { Some(existing_zone) => { if zone != existing_zone { log::info!("New Zone: {:?}", zone.zone_type); *last_zone = Some(zone); *encounter_state = EncounterState::Choice(gen_encounter()); } } None => { log::info!("New Zone: {:?}", zone.zone_type); *last_zone = Some(zone); *encounter_state = EncounterState::Choice(gen_encounter()); } } } else if last_zone.is_some() { *last_zone = None; } } }