/* * Copyright (C) 2025 Jonni Liljamo * * This file is licensed under MIT, see LICENSE for more information. */ use std::collections::HashMap; use avian3d::prelude::*; use bevy::{asset::LoadState, prelude::*}; use bevy_dice::{DicePlugin, DicePluginConfig, Die, DieResult, DieVariant, ThrowDie}; use bevy_egui::{EguiContextPass, EguiContexts, EguiPlugin, egui}; use bevy_embedded_assets::EmbeddedAssetPlugin; #[cfg(feature = "debug")] use bevy_inspector_egui::quick::WorldInspectorPlugin; #[cfg(feature = "wasm")] extern crate console_error_panic_hook; mod toast; fn main() { #[cfg(feature = "wasm")] std::panic::set_hook(Box::new(console_error_panic_hook::hook)); let mut app = App::new(); app.add_plugins(EmbeddedAssetPlugin::default()); app.add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { #[cfg(feature = "wasm")] canvas: Some("#bevy-dice-demo".into()), #[cfg(feature = "wasm")] fit_canvas_to_parent: true, #[cfg(feature = "wasm")] prevent_default_event_handling: false, ..Default::default() }), ..Default::default() })); app.add_plugins(( PhysicsPlugins::default(), #[cfg(feature = "debug")] PhysicsDebugPlugin::default(), )); app.add_plugins(( EguiPlugin { enable_multipass_for_primary_context: true, }, #[cfg(feature = "debug")] WorldInspectorPlugin::default(), )); #[cfg(feature = "debug")] app.insert_resource(UiDebugOptions { enabled: true, ..Default::default() }); app.add_plugins(DicePlugin).add_plugins(toast::ToastPlugin); app.init_resource::(); app.init_state::() .add_systems(Startup, load_assets) .add_systems(Update, wait_for_assets_to_load) .add_systems(OnEnter(ExampleState::Running), setup); app.init_resource::() .add_systems(EguiContextPass, ui); app.add_systems(Update, rotate_light); app.add_observer(despawn_dice).add_observer(show_die_result); app.run(); } #[derive(States, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] enum ExampleState { #[default] LoadingAssets, Running, } fn load_assets(mut commands: Commands, asset_server: Res) { commands.insert_resource(ExampleAssets { dice: asset_server.load("embedded://ext/Dice/Dice_Textured.glb"), }); } fn wait_for_assets_to_load( mut commands: Commands, asset_server: Res, r_assets: Res, ) { if let Some(LoadState::Loaded) = asset_server.get_load_state(&r_assets.dice) { commands.set_state(ExampleState::Running) } } fn setup( mut commands: Commands, mut r_meshes: ResMut>, mut r_materials: ResMut>, r_assets: Res, ) { commands.spawn(( RigidBody::Static, Collider::cylinder(4.0, 0.1), Mesh3d(r_meshes.add(Cylinder::new(4.0, 0.1))), MeshMaterial3d(r_materials.add(Color::WHITE)), )); commands.spawn(( PointLight { shadows_enabled: true, ..default() }, Transform::from_xyz(4.0, 8.0, 0.0), )); commands.spawn(( Camera3d::default(), Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Dir3::Y), )); commands.insert_resource(DicePluginConfig { gltf_handle: r_assets.dice.clone(), variant_mesh_names: HashMap::from([ (DieVariant::D4, "d4".into()), (DieVariant::D6, "d6".into()), (DieVariant::D8, "d8".into()), (DieVariant::D10, "d10".into()), (DieVariant::D10P, "d10_Percent".into()), (DieVariant::D12, "d12".into()), (DieVariant::D20, "d20".into()), ]), override_mesh_rotations: HashMap::from([(DieVariant::D6, Vec3::new(90.0, 90.0, 0.0))]), override_face_positions: HashMap::from([( DieVariant::D10P, HashMap::from([ (10, 30), (20, 40), (30, 10), (40, 20), (50, 70), (60, 80), (70, 50), (80, 60), (90, 90), (100, 100), ]), )]), }); } #[derive(Resource, Default)] struct ExampleAssets { dice: Handle, } #[derive(Resource)] struct UIState { selected_variant: DieVariant, die_id: u32, } impl Default for UIState { fn default() -> Self { Self { selected_variant: DieVariant::D6, die_id: 0, } } } fn ui(mut commands: Commands, mut contexts: EguiContexts, mut r_ui_state: ResMut) { egui::Window::new("Demo").show(contexts.ctx_mut(), |ui| { egui::ComboBox::from_label("") .selected_text(format!("{:?}", r_ui_state.selected_variant)) .show_ui(ui, |ui| { for variant in DieVariant::VALUES { ui.selectable_value( &mut r_ui_state.selected_variant, variant, format!("{:?}", variant), ); } }); if ui.button("Throw").clicked() { commands.trigger(ThrowDie { id: r_ui_state.die_id, variant: r_ui_state.selected_variant, angular_velocity: None, rotation: None, }); r_ui_state.die_id += 1; } if ui.button("Despawn Dice").clicked() { commands.trigger(DespawnDice); } }); } #[derive(Event)] struct DespawnDice; fn despawn_dice( _trigger: Trigger, mut commands: Commands, q_dice: Query>, ) { for die_entity in q_dice { commands.entity(die_entity).despawn(); } } fn show_die_result(trigger: Trigger, mut commands: Commands) { commands.trigger(toast::ShowResultToast(trigger.id, trigger.result)); } fn rotate_light(mut q_lights: Query<&mut Transform, With>, r_time: Res