use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use std::any::TypeId;
#[derive(Debug, Clone, Default)]
pub enum InputCondition {
#[default]
Always,
Never,
InState(StateCondition),
NotInState(StateCondition),
ResourceExists(ResourceCondition),
ResourceAbsent(ResourceCondition),
Custom(CustomConditionId),
All(Vec<InputCondition>),
Any(Vec<InputCondition>),
Not(Box<InputCondition>),
}
impl InputCondition {
#[must_use]
pub fn always() -> Self {
Self::Always
}
#[must_use]
pub fn never() -> Self {
Self::Never
}
#[must_use]
pub fn in_state<S: States>(state: S) -> Self {
Self::InState(StateCondition::new::<S>(state))
}
#[must_use]
pub fn not_in_state<S: States>(state: S) -> Self {
Self::NotInState(StateCondition::new::<S>(state))
}
#[must_use]
pub fn resource_exists<R: Resource>() -> Self {
Self::ResourceExists(ResourceCondition::new::<R>())
}
#[must_use]
pub fn resource_absent<R: Resource>() -> Self {
Self::ResourceAbsent(ResourceCondition::new::<R>())
}
#[must_use]
pub fn custom(id: impl Into<String>) -> Self {
Self::Custom(CustomConditionId(id.into()))
}
#[must_use]
pub fn and(self, other: Self) -> Self {
match (self, other) {
(Self::Always, other) | (other, Self::Always) => other,
(Self::Never, _) | (_, Self::Never) => Self::Never,
(Self::All(mut a), Self::All(b)) => {
a.extend(b);
Self::All(a)
}
(Self::All(mut a), other) => {
a.push(other);
Self::All(a)
}
(other, Self::All(mut a)) => {
a.insert(0, other);
Self::All(a)
}
(a, b) => Self::All(vec![a, b]),
}
}
#[must_use]
pub fn or(self, other: Self) -> Self {
match (self, other) {
(Self::Always, _) | (_, Self::Always) => Self::Always,
(Self::Never, other) | (other, Self::Never) => other,
(Self::Any(mut a), Self::Any(b)) => {
a.extend(b);
Self::Any(a)
}
(Self::Any(mut a), other) => {
a.push(other);
Self::Any(a)
}
(other, Self::Any(mut a)) => {
a.insert(0, other);
Self::Any(a)
}
(a, b) => Self::Any(vec![a, b]),
}
}
#[must_use]
pub fn negated(self) -> Self {
match self {
Self::Always => Self::Never,
Self::Never => Self::Always,
Self::Not(inner) => *inner,
other => Self::Not(Box::new(other)),
}
}
}
impl std::ops::Not for InputCondition {
type Output = Self;
fn not(self) -> Self::Output {
self.negated()
}
}
#[derive(Debug, Clone)]
pub struct StateCondition {
#[expect(dead_code, reason = "stored for future state comparison functionality")]
state_type_id: TypeId,
#[expect(dead_code, reason = "stored for future state comparison functionality")]
check_fn: fn(&World) -> bool,
state_name: &'static str,
}
impl StateCondition {
pub fn new<S: States>(_expected: S) -> Self {
Self {
state_type_id: TypeId::of::<S>(),
check_fn: {
|_world| {
true
}
},
state_name: std::any::type_name::<S>(),
}
}
#[must_use]
pub fn state_name(&self) -> &'static str {
self.state_name
}
}
#[derive(Debug, Clone)]
pub struct ResourceCondition {
resource_type_id: TypeId,
resource_name: &'static str,
}
impl ResourceCondition {
#[must_use]
pub fn new<R: Resource>() -> Self {
Self {
resource_type_id: TypeId::of::<R>(),
resource_name: std::any::type_name::<R>(),
}
}
#[must_use]
pub fn check(&self, world: &World) -> bool {
world.contains_resource_by_id(
world
.components()
.get_resource_id(self.resource_type_id)
.unwrap(),
)
}
#[must_use]
pub fn resource_name(&self) -> &'static str {
self.resource_name
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CustomConditionId(pub String);
impl From<&str> for CustomConditionId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl From<String> for CustomConditionId {
fn from(s: String) -> Self {
Self(s)
}
}
#[derive(Debug)]
pub struct ConditionContext<'w> {
world: &'w World,
custom_results: &'w CustomConditionResults,
}
impl<'w> ConditionContext<'w> {
#[must_use]
pub fn new(world: &'w World, custom_results: &'w CustomConditionResults) -> Self {
Self {
world,
custom_results,
}
}
#[must_use]
pub fn evaluate(&self, condition: &InputCondition) -> bool {
match condition {
InputCondition::Always => true,
InputCondition::Never => false,
InputCondition::InState(_state_cond) => {
true
}
InputCondition::NotInState(state_cond) => {
!self.evaluate(&InputCondition::InState(state_cond.clone()))
}
InputCondition::ResourceExists(res_cond) => res_cond.check(self.world),
InputCondition::ResourceAbsent(res_cond) => !res_cond.check(self.world),
InputCondition::Custom(id) => self.custom_results.get(id).unwrap_or(false),
InputCondition::All(conditions) => conditions.iter().all(|c| self.evaluate(c)),
InputCondition::Any(conditions) => conditions.iter().any(|c| self.evaluate(c)),
InputCondition::Not(inner) => !self.evaluate(inner),
}
}
}
#[derive(Resource, Debug, Default)]
pub struct CustomConditionResults {
results: std::collections::HashMap<CustomConditionId, bool>,
}
impl CustomConditionResults {
pub fn set(&mut self, id: impl Into<CustomConditionId>, value: bool) {
self.results.insert(id.into(), value);
}
#[must_use]
pub fn get(&self, id: &CustomConditionId) -> Option<bool> {
self.results.get(id).copied()
}
pub fn clear(&mut self) {
self.results.clear();
}
}
#[derive(Debug, Clone)]
pub struct ConditionalBinding<B> {
pub binding: B,
pub condition: InputCondition,
}
impl<B> ConditionalBinding<B> {
#[must_use]
pub fn new(binding: B, condition: InputCondition) -> Self {
Self { binding, condition }
}
#[must_use]
pub fn always(binding: B) -> Self {
Self::new(binding, InputCondition::Always)
}
#[must_use]
pub fn when(mut self, condition: InputCondition) -> Self {
self.condition = self.condition.and(condition);
self
}
}
pub struct ConditionsPlugin;
impl Plugin for ConditionsPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<CustomConditionResults>();
}
}
pub trait Conditionable: Sized {
fn when(self, condition: InputCondition) -> ConditionalBinding<Self>;
fn when_in_state<S: States>(self, state: S) -> ConditionalBinding<Self> {
self.when(InputCondition::in_state(state))
}
fn when_not_in_state<S: States>(self, state: S) -> ConditionalBinding<Self> {
self.when(InputCondition::not_in_state(state))
}
}
impl<T> Conditionable for T {
fn when(self, condition: InputCondition) -> ConditionalBinding<Self> {
ConditionalBinding::new(self, condition)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_condition_always() {
let cond = InputCondition::always();
assert!(matches!(cond, InputCondition::Always));
}
#[test]
fn test_condition_never() {
let cond = InputCondition::never();
assert!(matches!(cond, InputCondition::Never));
}
#[test]
fn test_condition_and() {
let cond = InputCondition::always().and(InputCondition::always());
assert!(matches!(cond, InputCondition::Always));
let cond = InputCondition::always().and(InputCondition::never());
assert!(matches!(cond, InputCondition::Never));
let cond = InputCondition::never().and(InputCondition::always());
assert!(matches!(cond, InputCondition::Never));
}
#[test]
fn test_condition_or() {
let cond = InputCondition::always().or(InputCondition::never());
assert!(matches!(cond, InputCondition::Always));
let cond = InputCondition::never().or(InputCondition::always());
assert!(matches!(cond, InputCondition::Always));
let cond = InputCondition::never().or(InputCondition::never());
assert!(matches!(cond, InputCondition::Never));
}
#[test]
fn test_condition_not() {
use std::ops::Not;
let cond = InputCondition::always().not();
assert!(matches!(cond, InputCondition::Never));
let cond = InputCondition::never().not();
assert!(matches!(cond, InputCondition::Always));
let cond = InputCondition::custom("test").not().not();
assert!(matches!(cond, InputCondition::Custom(_)));
}
#[test]
fn test_custom_condition_results() {
let mut results = CustomConditionResults::default();
results.set("can_jump", true);
results.set("is_grounded", false);
assert_eq!(
results.get(&CustomConditionId("can_jump".into())),
Some(true)
);
assert_eq!(
results.get(&CustomConditionId("is_grounded".into())),
Some(false)
);
assert_eq!(results.get(&CustomConditionId("unknown".into())), None);
}
#[test]
fn test_conditional_binding() {
let binding = "action_binding".when(InputCondition::always());
assert!(matches!(binding.condition, InputCondition::Always));
let binding = "action_binding"
.when(InputCondition::custom("can_jump"))
.when(InputCondition::custom("is_grounded"));
assert!(matches!(binding.condition, InputCondition::All(_)));
}
}