use bevy::{
ecs::message::{MessageReader, MessageWriter},
prelude::*,
};
use issun_core::mechanics::evolution::EvolutionInput;
use issun_core::mechanics::Mechanic;
use super::types::{
BevyEventEmitter, EnvironmentComponent, EvolutionConfigResource, EvolutionEventWrapper,
EvolutionStateComponent, EvolutionTick,
};
pub fn evolution_system<M>(
mut ticks: MessageReader<EvolutionTick>,
config: Res<EvolutionConfigResource>,
mut query: Query<(&mut EvolutionStateComponent, Option<&EnvironmentComponent>)>,
mut evolution_events: MessageWriter<EvolutionEventWrapper>,
) where
M: Mechanic<
Config = issun_core::mechanics::evolution::EvolutionConfig,
State = issun_core::mechanics::evolution::EvolutionState,
Input = EvolutionInput,
Event = issun_core::mechanics::evolution::EvolutionEvent,
> + Send
+ Sync
+ 'static,
{
for tick in ticks.read() {
let Ok((mut state_component, env_component)) = query.get_mut(tick.entity) else {
warn!(
"EvolutionTick: entity {:?} does not exist or has no EvolutionStateComponent",
tick.entity
);
continue;
};
let mut state = state_component.to_evolution_state();
let environment = tick
.custom_environment
.as_ref()
.map(|e| e.to_core_environment())
.or_else(|| env_component.map(|c| c.to_core_environment()))
.unwrap_or_default();
let time_delta = tick.time_delta.unwrap_or(config.config.time_delta);
let input = EvolutionInput {
time_delta,
environment,
};
let mut emitter = BevyEventEmitter::new(tick.entity, &mut evolution_events);
M::step(&config.config, &mut state, input, &mut emitter);
state_component.from_evolution_state(&state);
}
}
pub fn auto_evolution_system<M>(
time: Res<Time>,
config: Res<EvolutionConfigResource>,
mut query: Query<(
Entity,
&mut EvolutionStateComponent,
Option<&EnvironmentComponent>,
)>,
mut evolution_events: MessageWriter<EvolutionEventWrapper>,
) where
M: Mechanic<
Config = issun_core::mechanics::evolution::EvolutionConfig,
State = issun_core::mechanics::evolution::EvolutionState,
Input = EvolutionInput,
Event = issun_core::mechanics::evolution::EvolutionEvent,
> + Send
+ Sync
+ 'static,
{
let time_delta = time.delta_secs();
if time_delta <= 0.0 {
return;
}
for (entity, mut state_component, env_component) in query.iter_mut() {
let mut state = state_component.to_evolution_state();
let environment = env_component
.map(|c| c.to_core_environment())
.unwrap_or_default();
let input = EvolutionInput {
time_delta,
environment,
};
let mut emitter = BevyEventEmitter::new(entity, &mut evolution_events);
M::step(&config.config, &mut state, input, &mut emitter);
state_component.from_evolution_state(&state);
}
}
pub fn log_evolution_events(mut evolution_events: MessageReader<EvolutionEventWrapper>) {
use issun_core::mechanics::evolution::EvolutionEvent;
for wrapper in evolution_events.read() {
match &wrapper.event {
EvolutionEvent::ValueChanged {
old_value,
new_value,
delta,
} => {
debug!(
"Entity {:?}: Value changed from {:.2} to {:.2} (delta: {:.2})",
wrapper.entity, old_value, new_value, delta
);
}
EvolutionEvent::MinimumReached { final_value } => {
info!(
"Entity {:?}: Reached minimum (value: {:.2})",
wrapper.entity, final_value
);
}
EvolutionEvent::MaximumReached { final_value } => {
info!(
"Entity {:?}: Reached maximum (value: {:.2})",
wrapper.entity, final_value
);
}
EvolutionEvent::ThresholdCrossed {
threshold,
direction,
} => {
info!(
"Entity {:?}: Crossed threshold {:.0}% ({:?})",
wrapper.entity,
threshold * 100.0,
direction
);
}
EvolutionEvent::StatusChanged {
old_status,
new_status,
} => {
info!(
"Entity {:?}: Status changed from {:?} to {:?}",
wrapper.entity, old_status, new_status
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugins::evolution::types::SubjectType;
use issun_core::mechanics::evolution::prelude::*;
type TestEvolution = SimpleDecay;
#[test]
fn test_evolution_system_integration() {
let mut app = App::new();
app.add_plugins(bevy::MinimalPlugins);
app.insert_resource(EvolutionConfigResource::new(
issun_core::mechanics::evolution::EvolutionConfig {
base_rate: 10.0,
time_delta: 1.0,
},
));
app.add_message::<EvolutionTick>();
app.add_message::<EvolutionEventWrapper>();
app.add_systems(Update, evolution_system::<TestEvolution>);
let entity = app
.world_mut()
.spawn(EvolutionStateComponent::new(
100.0,
0.0,
100.0,
SubjectType::Food,
))
.id();
app.world_mut().write_message(EvolutionTick::new(entity));
app.update();
let state = app.world().get::<EvolutionStateComponent>(entity).unwrap();
assert_eq!(state.value, 90.0); }
#[test]
fn test_evolution_system_with_environment() {
let mut app = App::new();
app.add_plugins(bevy::MinimalPlugins);
app.insert_resource(EvolutionConfigResource::new(
issun_core::mechanics::evolution::EvolutionConfig {
base_rate: 10.0,
time_delta: 1.0,
},
));
app.add_message::<EvolutionTick>();
app.add_message::<EvolutionEventWrapper>();
app.add_systems(Update, evolution_system::<TestEvolution>);
let entity = app
.world_mut()
.spawn((
EvolutionStateComponent::new(100.0, 0.0, 100.0, SubjectType::Food),
EnvironmentComponent::new(30.0, 0.8),
))
.id();
app.world_mut().write_message(EvolutionTick::new(entity));
app.update();
let state = app.world().get::<EvolutionStateComponent>(entity).unwrap();
assert_eq!(state.value, 90.0);
}
#[test]
fn test_auto_evolution_system() {
let mut app = App::new();
app.add_plugins(bevy::MinimalPlugins);
app.insert_resource(EvolutionConfigResource::new(
issun_core::mechanics::evolution::EvolutionConfig {
base_rate: 5.0,
time_delta: 1.0,
},
));
app.add_message::<EvolutionEventWrapper>();
app.add_systems(Update, auto_evolution_system::<TestEvolution>);
let entity = app
.world_mut()
.spawn(EvolutionStateComponent::new(
100.0,
0.0,
100.0,
SubjectType::Resource,
))
.id();
app.update();
app.update();
let state = app.world().get::<EvolutionStateComponent>(entity).unwrap();
assert!(state.value < 100.0);
}
#[test]
fn test_evolution_to_minimum() {
let mut app = App::new();
app.add_plugins(bevy::MinimalPlugins);
app.insert_resource(EvolutionConfigResource::new(
issun_core::mechanics::evolution::EvolutionConfig {
base_rate: 200.0, time_delta: 1.0,
},
));
app.add_message::<EvolutionTick>();
app.add_message::<EvolutionEventWrapper>();
app.add_systems(Update, evolution_system::<TestEvolution>);
let entity = app
.world_mut()
.spawn(EvolutionStateComponent::new(
50.0,
0.0,
100.0,
SubjectType::Food,
))
.id();
app.world_mut().write_message(EvolutionTick::new(entity));
app.update();
let state = app.world().get::<EvolutionStateComponent>(entity).unwrap();
assert_eq!(state.value, 0.0);
use crate::plugins::evolution::types::EvolutionStatus;
assert_eq!(state.status, EvolutionStatus::Depleted);
}
#[test]
fn test_evolution_growth() {
use issun_core::mechanics::evolution::prelude::SimpleGrowth;
let mut app = App::new();
app.add_plugins(bevy::MinimalPlugins);
app.insert_resource(EvolutionConfigResource::new(
issun_core::mechanics::evolution::EvolutionConfig {
base_rate: 10.0,
time_delta: 1.0,
},
));
app.add_message::<EvolutionTick>();
app.add_message::<EvolutionEventWrapper>();
app.add_systems(Update, evolution_system::<SimpleGrowth>);
let entity = app
.world_mut()
.spawn(EvolutionStateComponent::new(
10.0,
0.0,
100.0,
SubjectType::Plant,
))
.id();
app.world_mut().write_message(EvolutionTick::new(entity));
app.update();
let state = app.world().get::<EvolutionStateComponent>(entity).unwrap();
assert_eq!(state.value, 20.0); }
}