Skip to content
Snippets Groups Projects
encounters.rs 4.99 KiB
Newer Older
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;