use crate::app_state::overworld::OverworldEntity;
use crate::app_state::overworld::character::components::PlayerControlled;
use crate::core::collision::Rect2DCollider;
use crate::core::danmaku::PlayPerformanceEvent;
use bevy::prelude::*;
use bevy_fact_rule_event::{
ActionHandlerRegistry, FactDatabase, FactEvent, FactEventId, FactValueDef, RuleActionDef,
RuleRegistry, RuleSetAsset,
};
#[derive(Component, Debug)]
pub struct TriggerZone {
pub id: String,
pub enter_event: String,
pub player_inside: bool,
}
impl TriggerZone {
pub fn new(id: impl Into<String>) -> Self {
let id = id.into();
let enter_event = format!("trigger_enter_{}", id);
Self {
id,
enter_event,
player_inside: false,
}
}
}
#[derive(Component)]
pub struct DemoTriggerSpawned;
#[derive(Resource, Default)]
pub struct LoadedRuleSets {
pub handles: Vec<Handle<RuleSetAsset>>,
pub initialized: bool,
}
pub fn spawn_demo_trigger_zone_system(
mut commands: Commands,
existing_triggers: Query<&DemoTriggerSpawned>,
player_query: Query<&Transform, Added<PlayerControlled>>,
) {
if !existing_triggers.is_empty() {
return;
}
let Ok(player_transform) = player_query.single() else {
return;
};
let trigger_pos = Vec3::new(
player_transform.translation.x + 48.0,
player_transform.translation.y,
0.0,
);
commands.spawn((
OverworldEntity(),
TriggerZone::new("demo_trigger"),
Rect2DCollider::new(Vec2::new(64.0, 64.0), Vec2::ZERO),
Transform::from_translation(trigger_pos),
Visibility::Hidden,
DemoTriggerSpawned,
Name::new("DemoTriggerZone"),
));
info!(
"FRE: Spawned demo trigger zone at ({:.1}, {:.1})",
trigger_pos.x, trigger_pos.y
);
info!("FRE: Walk into the cyan box (press F3 to see it) to trigger danmaku!");
}
#[allow(clippy::type_complexity)]
pub fn trigger_zone_detection_system(
mut triggers: Query<(&Transform, &Rect2DCollider, &mut TriggerZone)>,
player: Query<(&Transform, &Rect2DCollider, Entity), With<PlayerControlled>>,
mut event_writer: MessageWriter<FactEvent>,
) {
let Ok((player_transform, player_collider, player_entity)) = player.single() else {
return;
};
let player_pos = player_transform.translation.truncate() + player_collider.offset;
let player_half_size = player_collider.size * 0.5;
for (trigger_transform, trigger_collider, mut trigger) in triggers.iter_mut() {
let trigger_pos = trigger_transform.translation.truncate() + trigger_collider.offset;
let trigger_half_size = trigger_collider.size * 0.5;
let overlap = (player_pos.x - trigger_pos.x).abs()
< (player_half_size.x + trigger_half_size.x)
&& (player_pos.y - trigger_pos.y).abs() < (player_half_size.y + trigger_half_size.y);
if overlap && !trigger.player_inside {
trigger.player_inside = true;
info!("FRE: Player entered trigger zone '{}'", trigger.id);
event_writer.write(
FactEvent::with_entity(trigger.enter_event.clone(), player_entity)
.with_data("trigger_id", &trigger.id),
);
} else if !overlap && trigger.player_inside {
trigger.player_inside = false;
info!("FRE: Player exited trigger zone '{}'", trigger.id);
}
}
}
pub fn load_fre_rules_system(
asset_server: Res<AssetServer>,
mut loaded_rule_sets: ResMut<LoadedRuleSets>,
) {
if loaded_rule_sets.initialized {
return;
}
let handle: Handle<RuleSetAsset> = asset_server.load("overworld/rules/demo.rules.ron");
loaded_rule_sets.handles.push(handle);
loaded_rule_sets.initialized = true;
info!("FRE: Loading rules from overworld/rules/demo.rules.ron");
}
pub fn register_loaded_rules_system(
loaded_rule_sets: Res<LoadedRuleSets>,
rule_set_assets: Res<Assets<RuleSetAsset>>,
mut registry: ResMut<RuleRegistry>,
mut fact_db: ResMut<FactDatabase>,
mut registered: Local<bool>,
) {
if *registered || !loaded_rule_sets.initialized {
return;
}
for handle in &loaded_rule_sets.handles {
if let Some(rule_set) = rule_set_assets.get(handle) {
for (key, value) in rule_set.get_initial_facts() {
let fact_value = match value {
FactValueDef::Int(v) => bevy_fact_rule_event::FactValue::Int(*v),
FactValueDef::Float(v) => bevy_fact_rule_event::FactValue::Float(*v),
FactValueDef::Bool(v) => bevy_fact_rule_event::FactValue::Bool(*v),
FactValueDef::String(v) => bevy_fact_rule_event::FactValue::String(v.clone()),
};
fact_db.set(key.as_str(), fact_value);
info!("FRE: Set initial fact '{}' from RON", key);
}
rule_set.register_rules(&mut registry);
*registered = true;
info!("FRE: Rules registered from RON asset");
}
}
}
pub fn setup_action_handlers_system(mut handler_registry: ResMut<ActionHandlerRegistry>) {
handler_registry.register("SetPlayerHP", |action, _db, _commands| {
if let RuleActionDef::Custom { params, .. } = action
&& let Some(value_str) = params.get("value")
&& let Ok(hp) = value_str.parse::<usize>()
{
info!("FRE Action: SetPlayerHP requested with value {}", hp);
}
});
info!("FRE: Custom action handlers registered");
}
pub fn play_danmaku_on_trigger_system(
mut events: MessageReader<FactEvent>,
mut performance_writer: MessageWriter<PlayPerformanceEvent>,
player_query: Query<&Transform, With<PlayerControlled>>,
fact_db: Res<FactDatabase>,
) {
for event in events.read() {
if event.id == FactEventId::new("demo_visit_updated") {
let visit_count = fact_db.get_int_or("demo_area_visit_count", 0);
if visit_count == 3 {
let spawn_pos = player_query
.iter()
.next()
.map(|t| t.translation.truncate())
.unwrap_or(Vec2::ZERO);
info!(
"FRE: Playing danmaku performance at {:?} (3rd visit)",
spawn_pos
);
performance_writer.write(
PlayPerformanceEvent::new("overworld/danmaku/demo_attack_ow.performance.ron")
.at_position(spawn_pos),
);
} else {
info!(
"FRE: Trigger entered (visit count: {}), waiting for 3rd visit",
visit_count
);
}
}
}
}
pub fn log_fact_changes_system(mut events: MessageReader<FactEvent>, fact_db: Res<FactDatabase>) {
for event in events.read() {
if event.id == FactEventId::new("demo_visit_updated") {
let count = fact_db.get_int_or("demo_area_visit_count", 0);
info!("FRE: demo_area_visit_count = {}", count);
}
}
}
pub fn handle_chase_state_actions_system(
mut events: MessageReader<FactEvent>,
mut next_state: ResMut<NextState<crate::app_state::overworld::OverworldState>>,
fact_db: Res<FactDatabase>,
) {
for event in events.read() {
if event.id == FactEventId::new("demo_visit_updated") {
let visit_count = fact_db.get_int_or("demo_area_visit_count", 0);
if visit_count == 2 {
info!("FRE: Entering Chase state (2nd visit)");
next_state.set(crate::app_state::overworld::OverworldState::Chase);
}
else if visit_count == 5 {
info!("FRE: Exiting Chase state (5th visit)");
next_state.set(crate::app_state::overworld::OverworldState::Normal);
}
}
}
}