// A little restuarant manager // Customer > Has Thirst that they want to fullfil // Worker > Wants to fulfill orders to increase profits of business // High-level, we have the following: // Agent - Shared behaviour between Customer and Worker // Customer - Has Thirst, wants to satisfy it somehow // Worker - Wants to increase income of business // Customer has Actions: // - GoToServingDesk, MakeOrder, ConsumeOrder, ConsumeInventory // Worker has Actions: // - GoToServingDesk, TakeOrder, MakeProduct, MoveProduct, HandOverOrder /* Sequence Diagram for the full flow of actions (paste into https://sequencediagram.org/) Customer->Order Desk: GoToOrderDesk Order Desk->Worker: RequestWorker Worker->Order Desk: GoToOrderDesk Customer->Order Desk: PlaceOrder Worker->Order Desk: TakeOrder Customer->Order Desk: WaitForOrder Worker->Lemonade Maker: GoToLemonadeMaker Lemonade Maker->Worker: MakeLemonade Worker->Order Desk: FinishOrder Customer->Order Desk: PickupLemonade Customer->Customer: DrinkLemonade */ use std::collections::{HashMap, VecDeque}; use bevy::{color::palettes::css::*, prelude::*}; use bevy_dogoap::prelude::*; fn main() { let mut app = App::new(); // Customer components + actions register_components!( app, vec![ Thirst, CarryingItem, PlacedOrder, OrderReady, AtOrderDesk, ShouldGoToOrderDesk ] ); register_actions!( app, vec![ DrinkLemonade, PickupLemonade, WaitForOrder, PlaceOrder, GoToOrderDesk ] ); // Worker components + actions register_components!( app, vec![Energy, AtLemonadeMaker, ServedOrder, ShouldGoToOrderDesk] ); register_actions!( app, vec![Rest, ServeOrder, ProduceLemonade, GoToLemonadeMaker] ); app.add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { canvas: Some("#example-canvas".into()), ..default() }), ..default() })) .add_plugins(DogoapPlugin) .add_systems(Startup, setup) .add_systems(Update, (draw_state_debug, draw_ui)) // Systems that always affects needs .add_systems(FixedUpdate, update_thirst) // Systems that handle actions .add_systems( FixedUpdate, ( handle_pickup_lemonade, handle_drink_lemonade, handle_place_order, handle_wait_for_order, handle_go_to_order_desk, handle_move_to, handle_call_worker_to_empty_order_desk, ), ) .run(); } // LocalFields for customer #[derive(Component, Clone, DatumComponent)] struct Thirst(f64); #[derive(Component, Clone, EnumComponent)] struct CarryingItem(Item); #[derive(Component, Clone, DatumComponent)] struct PlacedOrder(bool); #[derive(Component, Clone, DatumComponent)] struct OrderReady(bool); #[derive(Component, Clone, DatumComponent)] struct AtOrderDesk(bool); #[derive(Component, Clone, DatumComponent)] struct ShouldGoToOrderDesk(bool); // Actions for customer #[derive(Component, Clone, Default, ActionComponent)] struct DrinkLemonade; #[derive(Component, Clone, Default, ActionComponent)] struct PickupLemonade; #[derive(Component, Clone, Default, ActionComponent)] struct WaitForOrder; #[derive(Component, Clone, Default, ActionComponent)] struct PlaceOrder; #[derive(Component, Clone, Default, ActionComponent)] struct GoToOrderDesk; // DatumComponents for worker #[derive(Component, Clone, DatumComponent)] struct Energy(f64); #[derive(Component, Clone, DatumComponent)] struct ServedOrder(bool); #[derive(Component, Clone, DatumComponent)] struct AtLemonadeMaker(bool); // Actions for worker #[derive(Component, Clone, Default, ActionComponent)] struct Rest; #[derive(Component, Clone, Default, ActionComponent)] struct ServeOrder; #[derive(Component, Clone, Default, ActionComponent)] struct ProduceLemonade; #[derive(Component, Clone, Default, ActionComponent)] struct GoToLemonadeMaker; // Markers #[derive(Component)] struct Agent; #[derive(Component, Default)] struct Customer { order: Option, } #[derive(Component)] struct Worker; #[derive(Clone, Default, Copy, Reflect)] enum Item { #[default] Nothing, // Actual items: Lemonade, } #[derive(Component)] struct LemonadeMaker; #[derive(Component)] struct Order { items_to_produce: VecDeque, items: Vec, owner: Entity, } #[derive(Component, Default)] struct OrderDesk { assigned_customer: Option, assigned_worker: Option, can_take_order: bool, // set to true when both customer and worker present current_order: Option, } #[derive(Component)] struct MoveTo(Vec3); #[derive(Component)] struct StateDebugText; fn setup(mut commands: Commands) { // Spawn customers for _i in 0..1 { let goal = Goal::from_reqs(&[Thirst::is_less(1.0)]); // Requires us to carry a lemonade, results in us having 10 less thirst + carrying Nothing let drink_lemonade_action = DrinkLemonade::new() .add_precondition(CarryingItem::is(Item::Lemonade)) .add_mutator(CarryingItem::set(Item::Nothing)) .add_mutator(Thirst::decrease(10.0)); // Requires us to not be carrying nothing, and leads to us having a lemonade let pickup_lemonade_action = PickupLemonade::new() .add_precondition(CarryingItem::is(Item::Nothing)) .add_precondition(OrderReady::is(true)) .add_precondition(AtOrderDesk::is(true)) .add_mutator(CarryingItem::set(Item::Lemonade)); // Requires us to having placed an order, order not yet ready and we're at the order desk let wait_for_order_action = WaitForOrder::new() .add_precondition(PlacedOrder::is(true)) .add_precondition(OrderReady::is(false)) .add_precondition(AtOrderDesk::is(true)) .add_mutator(OrderReady::set(true)); // Requires us to not having placed an order previously, and we're at the ordering desk let place_order_action = PlaceOrder::new() .add_precondition(PlacedOrder::is(false)) .add_precondition(AtOrderDesk::is(true)) .add_mutator(PlacedOrder::set(true)); let go_to_order_desk_action = GoToOrderDesk::new() .add_precondition(AtOrderDesk::is(false)) .add_mutator(AtOrderDesk::set(true)); let (mut planner, components) = create_planner!({ actions: [ (DrinkLemonade, drink_lemonade_action), (PickupLemonade, pickup_lemonade_action), (WaitForOrder, wait_for_order_action), (PlaceOrder, place_order_action), (GoToOrderDesk, go_to_order_desk_action), ], state: [ Thirst(0.0), CarryingItem(Item::Nothing), PlacedOrder(false), OrderReady(false), AtOrderDesk(false), ], goals: [goal], }); planner.remove_goal_on_no_plan_found = false; // Don't remove the goal planner.always_plan = true; // Re-calculate our plan whenever we can planner.current_goal = Some(goal.clone()); commands .spawn(( Agent, Name::new("Customer"), Customer::default(), planner, components, TransformBundle::from(Transform::from_xyz(-200.0, -100.0, 1.0)), )) .with_children(|subcommands| { subcommands.spawn(( Text2dBundle { transform: Transform::from_translation(Vec3::new(-70.0, 0.0, 10.0)), text: Text::from_section( "", TextStyle { font_size: 12.0, ..default() }, ) .with_justify(JustifyText::Left), text_anchor: bevy::sprite::Anchor::TopLeft, ..default() }, StateDebugText, )); }); } // Spawn worker for _i in 0..1 { // Now for the worker // Final outcome for the worker is increasing the amount of money, always // We trick the agent into performing our actions forever by having a: // ServedOrder DatumComponent that the agent wants to set to true, // but at runtime it can never actually get there. // In order to set ServedOrder to true, the agent needs to run ServeOrder // let goal = Goal::from_reqs(&[Energy::is_more(1.0), ServedOrder::is(true)]); let goal = Goal::from_reqs(&[AtOrderDesk::is(true)]); let serve_order_action = ServeOrder::new() .add_precondition(CarryingItem::is(Item::Lemonade)) .add_precondition(AtOrderDesk::is(true)) .add_mutator(ServedOrder::set(true)); let produce_lemonade_action = ProduceLemonade::new() .add_precondition(CarryingItem::is(Item::Nothing)) .add_precondition(AtLemonadeMaker::is(true)) .add_mutator(CarryingItem::set(Item::Lemonade)); let go_to_lemonade_maker_action = GoToLemonadeMaker::new() .add_precondition(AtLemonadeMaker::is(false)) .add_mutator(AtLemonadeMaker::set(true)); let rest_action = Rest::new() .add_precondition(Energy::is_less(10.0)) .add_mutator(Energy::increase(50.0)); let go_to_order_desk_action = GoToOrderDesk::new() .add_precondition(AtOrderDesk::is(false)) .add_precondition(ShouldGoToOrderDesk::is(true)) .add_mutator(AtOrderDesk::set(true)); let (planner, components) = create_planner!({ actions: [ (Rest, rest_action), (ServeOrder, serve_order_action), (ProduceLemonade, produce_lemonade_action), (GoToLemonadeMaker, go_to_lemonade_maker_action), (GoToOrderDesk, go_to_order_desk_action), ], state: [ Energy(50.0), ServedOrder(false), AtLemonadeMaker(false), AtOrderDesk(false), CarryingItem(Item::Nothing), ShouldGoToOrderDesk(false), ], goals: [goal], }); commands .spawn(( Agent, Name::new("Worker"), Worker, planner, components, TransformBundle::from(Transform::from_xyz(0.0, 0.0, 1.0)), )) .with_children(|subcommands| { subcommands.spawn(( Text2dBundle { transform: Transform::from_translation(Vec3::new(10.0, -10.0, 10.0)), text: Text::from_section( "", TextStyle { font_size: 12.0, ..default() }, ) .with_justify(JustifyText::Left), text_anchor: bevy::sprite::Anchor::TopLeft, ..default() }, StateDebugText, )); }); } commands .spawn(( Name::new("Lemonade Maker"), LemonadeMaker, TransformBundle::from(Transform::from_xyz(100.0, 0.0, 1.0)), )) .with_children(|subcommands| { subcommands.spawn(( Text2dBundle { transform: Transform::from_translation(Vec3::new(0.0, 25.0, 10.0)), text: Text::from_section( "Lemonade Maker", TextStyle { font_size: 12.0, ..default() }, ) .with_justify(JustifyText::Left), text_anchor: bevy::sprite::Anchor::TopLeft, ..default() }, StateDebugText, )); }); commands .spawn(( Name::new("Order Desk"), OrderDesk::default(), TransformBundle::from(Transform::from_xyz(-100.0, 0.0, 1.0)), )) .with_children(|subcommands| { subcommands.spawn(( Text2dBundle { transform: Transform::from_translation(Vec3::new(0.0, 50.0, 10.0)), text: Text::from_section( "Order Desk", TextStyle { font_size: 12.0, ..default() }, ) .with_justify(JustifyText::Left), text_anchor: bevy::sprite::Anchor::TopLeft, ..default() }, StateDebugText, )); }); commands.spawn(Camera2dBundle::default()); } fn handle_call_worker_to_empty_order_desk( mut commands: Commands, mut q_order_desks: Query<(&mut OrderDesk, &Transform)>, mut q_workers: Query< (Entity, &mut ShouldGoToOrderDesk, &Transform), (With, Without), >, ) { for (mut order_desk, t_order_desk) in q_order_desks.iter_mut() { if order_desk.assigned_customer.is_some() && order_desk.assigned_worker.is_none() { // This order desk needs a worker! let (mut worker, mut should_go, t_worker) = q_workers.iter_mut().next().expect("no workers"); should_go.0 = true; order_desk.assigned_worker = Some(worker); } } } fn handle_move_to( mut commands: Commands, time: Res