/*
* 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::{gltf::GltfMesh, prelude::*};
use bevy_rand::{global::GlobalEntropy, plugin::EntropyPlugin, prelude::WyRand};
use rand::prelude::Rng;
mod die;
pub use die::Die;
mod face;
pub(crate) use face::Face;
pub struct DicePlugin;
impl Plugin for DicePlugin {
fn build(&self, app: &mut App) {
app.register_type::<DicePluginConfig>()
.register_type::<DieVariant>()
.register_type::<Die>()
.register_type::<Face>()
.add_plugins(EntropyPlugin::<WyRand>::new())
.add_observer(throw_die)
.add_observer(die::die_stopped)
.add_systems(
Update,
(
die::die_wait_for_stop,
#[cfg(feature = "debug")]
face::draw_face_gizmos,
),
);
}
}
#[derive(Resource, Reflect)]
pub struct DicePluginConfig {
/// Gltf handle to get the meshes from.
pub gltf_handle: Handle<Gltf>,
/// Mapping of variants to Gltf mesh names.
pub variant_mesh_names: HashMap<DieVariant, String>,
/// Mesh rotation overrides, used to correct the mesh rotation relative to
/// the face positions.
pub override_mesh_rotations: HashMap<DieVariant, Vec3>,
/// Face position overrides, used to completely remap what value is in what
/// face position.
///
/// Can also be used to just override the values of faces.
pub override_face_positions: HashMap<DieVariant, HashMap<u8, u8>>,
}
#[derive(Reflect, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DieVariant {
D4,
D6,
D8,
D10,
D10P,
D12,
D20,
}
impl DieVariant {
pub const VALUES: [Self; 7] = [
Self::D4,
Self::D6,
Self::D8,
Self::D10,
Self::D10P,
Self::D12,
Self::D20,
];
fn faces(&self) -> Vec<(Face, Transform)> {
match self {
Self::D4 => face::D4_FACE_CONFIGS.to_vec(),
Self::D6 => face::D6_FACE_CONFIGS.to_vec(),
Self::D8 => face::D8_FACE_CONFIGS.to_vec(),
Self::D10 => face::D10_FACE_CONFIGS.to_vec(),
Self::D10P => face::D10P_FACE_CONFIGS.to_vec(),
Self::D12 => face::D12_FACE_CONFIGS.to_vec(),
Self::D20 => face::D20_FACE_CONFIGS.to_vec(),
}
}
}
#[derive(Event)]
pub struct ThrowDie {
/// Die variant.
pub variant: DieVariant,
/// Initial angular velocity, random if not set.
pub angular_velocity: Option<Vec3>,
/// Initial rotation, random if not set.
pub rotation: Option<Quat>,
}
fn throw_die(
trigger: Trigger<ThrowDie>,
mut commands: Commands,
mut rng: GlobalEntropy<WyRand>,
r_gltfs: Res<Assets<Gltf>>,
r_gltf_meshes: Res<Assets<GltfMesh>>,
r_meshes: Res<Assets<Mesh>>,
r_config: Res<DicePluginConfig>,
) {
let gltf = r_gltfs.get(&r_config.gltf_handle).unwrap();
let gltf_mesh = r_gltf_meshes
.get(&gltf.named_meshes[&*r_config.variant_mesh_names[&trigger.variant]])
.unwrap();
let mesh = r_meshes.get(&gltf_mesh.primitives[0].mesh).unwrap();
let angular_velocity = match trigger.angular_velocity {
Some(v) => AngularVelocity(v),
None => AngularVelocity(Vec3 {
x: rng.random_range(0u32..=10u32) as f32,
y: rng.random_range(0u32..=10u32) as f32,
z: rng.random_range(0u32..=10u32) as f32,
}),
};
let mesh_rotation = match r_config.override_mesh_rotations.get(&trigger.variant) {
Some(rotation) => Quat::from_euler(
EulerRot::XYZ,
rotation.x.to_radians(),
rotation.y.to_radians(),
rotation.z.to_radians(),
),
None => Quat::default(),
};
let mut die = commands.spawn((
Visibility::Inherited,
// Place the mesh in a child to allow correcting the rotation.
children![(
Mesh3d(gltf_mesh.primitives[0].mesh.clone()),
MeshMaterial3d(gltf_mesh.primitives[0].material.clone().unwrap()),
Transform {
rotation: mesh_rotation,
..Default::default()
},
)],
Transform {
translation: Vec3::new(0.0, 4.0, 0.0),
//rotation: Quat::from_rng(rng.as_mut()),
..Default::default()
},
RigidBody::Dynamic,
Collider::convex_hull_from_mesh(mesh).unwrap(),
angular_velocity,
Die::default(),
));
die.with_children(|parent| {
if let Some(overrides) = r_config.override_face_positions.get(&trigger.variant) {
let defaults = trigger.variant.faces();
for default in defaults {
if let Some(new_value) = overrides.get(&default.0.value) {
parent.spawn((Face::new(*new_value), default.1));
} else {
parent.spawn(default.clone());
}
}
} else {
for face in trigger.variant.faces() {
parent.spawn(face);
}
}
});
}