/*
* Copyright (C) 2025 Jonni Liljamo <jonni@liljamo.com>
*
* 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;
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,
..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(),
));
app.add_plugins(DicePlugin);
app.init_resource::<ExampleAssets>();
app.init_state::<ExampleState>()
.add_systems(Startup, load_assets)
.add_systems(Update, wait_for_assets_to_load)
.add_systems(OnEnter(ExampleState::Running), setup);
app.init_resource::<UIState>()
.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<AssetServer>) {
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<AssetServer>,
r_assets: Res<ExampleAssets>,
) {
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<Assets<Mesh>>,
mut r_materials: ResMut<Assets<StandardMaterial>>,
r_assets: Res<ExampleAssets>,
) {
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<Gltf>,
}
#[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<UIState>) {
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<DespawnDice>,
mut commands: Commands,
q_dice: Query<Entity, With<Die>>,
) {
for die_entity in q_dice {
commands.entity(die_entity).despawn();
}
}
fn show_die_result(trigger: Trigger<DieResult>) {
info!("{}: {}", trigger.id, trigger.result);
}
fn rotate_light(mut q_lights: Query<&mut Transform, With<PointLight>>, r_time: Res<Time>) {
q_lights.iter_mut().for_each(|mut transform| {
transform.rotate_around(
Vec3::new(0.0, 0.0, 0.0),
Quat::from_euler(EulerRot::XYZ, 0.0, 0.5 * r_time.delta_secs(), 0.0),
)
});
}