You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
martian-chess/src/display3d.rs

1569 lines
58 KiB
Rust

use crate::{
game::{Board, BoardIndex, Piece, Side},
prelude::*,
tweak::Tweaks,
};
use bevy::{
animation::RepeatAnimation,
core_pipeline::{
bloom::BloomSettings,
experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasSettings},
prepass::MotionVectorPrepass,
tonemapping::{DebandDither, Tonemapping},
Skybox,
},
input::{
mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel},
ButtonState,
},
pbr::{
ExtendedMaterial, MaterialExtension, ScreenSpaceAmbientOcclusionBundle,
ScreenSpaceAmbientOcclusionSettings,
},
render::{
render_resource::{AsBindGroup, ShaderRef, TextureViewDescriptor, TextureViewDimension},
view::ColorGrading,
},
utils::HashMap,
window::PrimaryWindow,
};
use tweaks::*;
pub(crate) struct Display3dPlugin;
impl Plugin for Display3dPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
TemporalAntiAliasPlugin,
MaterialPlugin::<DissolveMaterial>::default(),
))
.insert_resource(Msaa::Off)
.add_systems(
OnExit(GameState::Loading),
(
initialize,
fix_skybox.before(initialize),
update_tweaks.run_if(resource_exists::<tweak::GameTweaks>()),
),
)
.add_systems(
Update,
(
load_assets
.run_if(in_state(GameState::Loading))
.run_if(on_event::<AssetEvent<Tweaks>>()),
hydrate_camera.run_if(any_component_added::<Camera3d>),
set_piece_model.run_if(any_component_added::<Piece>),
set_piece_model.run_if(any_component_changed::<Piece>),
set_board_model.run_if(any_component_added::<game::BoardComponent>),
set_board_model.run_if(any_component_added::<TilesComponent>),
set_valid_move_model.run_if(any_component_added::<game::ValidMove>),
set_tile_hitbox.run_if(any_component_added::<game::Tile>),
set_piece_position.run_if(any_component_changed::<BoardIndex>),
set_piece_texture
.run_if(any_component_changed::<Side>)
.run_if(resource_exists::<tweak::GameTweaks>()),
select
.run_if(in_state(GameState::Play))
.run_if(in_state(DisplayState::Display3d))
.run_if(on_event::<MouseButtonInput>()),
pick_up.run_if(any_component_added::<game::Selected>),
put_down.run_if(any_component_removed::<game::Selected>()),
switch_sides
.run_if(in_state(GameState::Play))
.run_if(state_changed::<game::TurnState>()),
update_tweaks
.run_if(on_event::<AssetEvent<Tweaks>>())
.run_if(resource_exists::<tweak::GameTweaks>()),
scale_lighting.run_if(
any_component_added::<DirectionalLight>
.or_else(any_component_added::<SpotLight>)
.or_else(any_component_added::<PointLight>)
.or_else(on_event::<AssetEvent<Tweaks>>()),
),
setup_capture_piece.run_if(any_component_changed::<Handle<StandardMaterial>>),
capture_piece.run_if(any_with_component::<game::Captured>()),
skip_animation.run_if(|keys: Res<Input<KeyCode>>| -> bool {
keys.just_pressed(KeyCode::Return)
}),
),
)
.add_systems(
Update,
(
move_camera.run_if(on_event::<MouseMotion>()),
mouse_zoom.run_if(on_event::<MouseWheel>()),
gizmo_system,
selected_gizmo,
moves_gizmo,
debug_selected.run_if(any_with_component::<game::Selected>()),
)
.run_if(resource_exists::<debug::DebugEnabled>())
.run_if(in_state(GameState::Play))
.run_if(in_state(DisplayState::Display3d)),
)
.add_systems(OnExit(DisplayState::Display3d), deactivate::<Display3d>)
.add_systems(
OnEnter(GameState::Play),
(
activate::<Display3d>.run_if(in_state(DisplayState::Display3d)),
set_piece_texture.run_if(resource_exists::<tweak::GameTweaks>()),
update_tweaks.run_if(resource_exists::<tweak::GameTweaks>()),
opening_animation
.run_if(run_once())
.run_if(in_state(DisplayState::Display3d)),
),
);
}
}
#[derive(Debug, Component)]
pub(crate) struct Display3d;
#[derive(Debug, Component)]
struct TilesComponent;
#[derive(Debug, Resource, Clone)]
struct AssetsMap {
hitbox_shape: Handle<Mesh>,
hitbox_material: Handle<StandardMaterial>,
}
/// Load 3d models
/// This is kind of pulling double duty.
/// Both loads the GLTF file _and_ populates the ModelMap once that is loaded.
fn load_assets(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let hitbox_shape = meshes.add(shape::Box::new(1.0, 0.1, 1.0).into());
let hitbox_material = materials.add(StandardMaterial {
base_color: Color::NONE,
perceptual_roughness: 0.0,
reflectance: 0.0,
alpha_mode: AlphaMode::Blend,
..default()
});
commands.insert_resource(AssetsMap {
hitbox_shape,
hitbox_material,
});
}
/// Initialize the 3d board
fn initialize(mut commands: Commands, board: Res<game::Board>, assets: Res<AssetsMap>) {
info!("Initializing root");
commands
.spawn((
SpatialBundle {
visibility: Visibility::Hidden,
..default()
},
Display3d,
))
.with_children(|parent| {
info!("Intializing 3D Board!");
parent
.spawn((Display3d, game::BoardComponent, SceneBundle { ..default() }))
.with_children(|parent| {
// TEMP: Tiles
parent.spawn((Display3d, TilesComponent, SceneBundle { ..default() }));
// Hitboxes
game::tiles().for_each(|(index, tile)| {
let side = Board::side(index).expect("Spawn valid side");
parent.spawn((
Display3d,
index,
tile,
PbrBundle {
mesh: assets.hitbox_shape.clone(),
material: assets.hitbox_material.clone(),
visibility: Visibility::Hidden,
..default()
},
side,
game::Selectable,
));
});
// Valid move indicators
game::tiles().for_each(|(index, _)| {
let side = Board::side(index).expect("Spawn valid side");
parent.spawn((
Display3d,
index,
SceneBundle {
visibility: Visibility::Hidden,
transform: Transform::from_translation(board_translation(&index)),
..default()
},
side,
game::ValidMove,
));
});
// Pieces
board.pieces().iter().for_each(|(index, piece)| {
let side = Board::side(*index).expect("Spawn valid side");
parent.spawn((
side,
Display3d,
piece.clone(),
index.clone(),
SceneBundle { ..default() },
game::Selectable,
));
});
});
});
}
fn hydrate_camera(
events: Query<(&Name, Entity), Added<Camera>>,
gltfs: Res<Assets<Gltf>>,
state: Res<State<game::TurnState>>,
mut players: Query<&mut AnimationPlayer>,
mut commands: Commands,
tweaks_file: Res<tweak::GameTweaks>,
tweaks: Res<Assets<Tweaks>>,
) {
let tweak = tweaks
.get(tweaks_file.handle.clone())
.expect("Load tweakfile");
events
.iter()
.filter(|(name, _)| name.as_str() == "GameCam")
.for_each(|(_, entity)| {
info!("Initialize 3d camera");
let skybox_handle = tweak
.get_handle::<Image>("display3d_models_skybox_file")
.unwrap();
info!("Hydrating camera {:?}", entity);
// Populate the components for the camera
commands.entity(entity).insert((
Display3d,
DisplayState::Display3d,
Camera3dBundle {
camera: Camera {
is_active: true,
hdr: tweak.get::<bool>("display3d_hdr").unwrap(),
..default()
},
dither: DebandDither::Enabled,
color_grading: ColorGrading { ..default() },
..default()
},
BloomSettings { ..default() },
Skybox(skybox_handle.clone()),
EnvironmentMapLight {
diffuse_map: skybox_handle.clone(),
specular_map: skybox_handle.clone(),
},
UiCameraConfig { show_ui: true },
FogSettings { ..default() },
ScreenSpaceAmbientOcclusionBundle { ..default() },
TemporalAntiAliasSettings { ..default() },
MotionVectorPrepass { ..default() },
// Name::new("3D Camera"),
));
let assets_handle = tweak
.get_handle::<Gltf>("display3d_models_assets_file")
.unwrap();
let gltf = gltfs.get(assets_handle).expect("Load GLTF content");
// Set it to the default position by starting the initial animation
if let Ok(mut player) = players.get_mut(entity) {
debug!("Animations: {:?}", gltf.named_animations.keys());
// GameCamIntro1, GameCamIntro2, GameCamSide1>2, GameCamSide2>1
let animation = match state.get() {
game::TurnState(game::Side::A) => gltf.named_animations.get(
tweak
.get::<String>("display3d_models_animations_intro_a")
.unwrap()
.as_str(),
),
game::TurnState(game::Side::B) => gltf.named_animations.get(
tweak
.get::<String>("display3d_models_animations_intro_b")
.unwrap()
.as_str(),
),
}
.expect("Camera Startup");
player.play(animation.clone()).pause();
}
});
}
/// Update display3d tweaks in the game
/// Triggered on entering 3d state and when the tweakfile is updated.
fn update_tweaks(
mut camera_settings: Query<
(
Entity,
&mut FogSettings,
&mut ColorGrading,
&mut Tonemapping,
&mut BloomSettings,
),
With<Display3d>,
>,
tweaks_file: Res<tweak::GameTweaks>,
tweaks: Res<Assets<Tweaks>>,
// mut lights: Query<
// (
// Option<&mut SpotLight>,
// Option<&mut PointLight>,
// Option<&mut DirectionalLight>,
// ),
// Or<(With<SpotLight>, With<PointLight>, With<DirectionalLight>)>,
// >,
mut commands: Commands,
) {
if let Some(tweak) = tweaks.get(tweaks_file.handle.clone()) {
camera_settings.iter_mut().for_each(
|(entity, mut fog, mut color_grading, mut tonemapping, mut bloom)| {
*fog = tweak
.get::<TweakFogSettings>("display3d_fog")
.unwrap()
.into();
*color_grading = tweak
.get::<TweakColorGrading>("display3d_color_grading")
.unwrap()
.into();
*tonemapping = tweak
.get::<TweakTonemapping>("display3d_color_tonemapping")
.unwrap()
.into();
*bloom = tweak
.get::<TweakBloomSettings>("display3d_bloom")
.unwrap()
.into();
let quality_level = tweak.get::<TweakScreenSpaceAmbientOcclusionQualityLevel>(
"display3d_ssao_quality_level",
);
match quality_level {
Some(quality_level) => {
commands
.entity(entity)
.insert(ScreenSpaceAmbientOcclusionSettings {
quality_level: quality_level.into(),
});
commands.insert_resource(Msaa::Off);
}
None => {
commands
.entity(entity)
.remove::<ScreenSpaceAmbientOcclusionSettings>();
let msaa: Msaa = tweak.get::<TweakMsaa>("display3d_msaa").unwrap().into();
commands.insert_resource(msaa);
}
}
},
);
// lights
// .iter_mut()
// .for_each(|(mut spot, mut point, mut direction)| {
// // Depending on the light, set the scalar tweak
// });
}
}
fn fix_skybox(
mut images: ResMut<Assets<Image>>,
tweaks_file: Res<tweak::GameTweaks>,
tweaks: Res<Assets<Tweaks>>,
) {
let tweak = tweaks.get(tweaks_file.handle.clone()).unwrap();
let handle = tweak
.get_handle_unchecked::<Image>("display3d_models_skybox_file")
.unwrap();
let image = images.get_mut(handle).unwrap();
info!("Loaded skybox image");
// NOTE: PNGs do not have any metadata that could indicate they contain a cubemap texture,
// so they appear as one texture. The following code reconfigures the texture as necessary.
if image.texture_descriptor.array_layer_count() == 1 {
image.reinterpret_stacked_2d_as_array(
image.texture_descriptor.size.height / image.texture_descriptor.size.width,
);
image.texture_view_descriptor = Some(TextureViewDescriptor {
dimension: Some(TextureViewDimension::Cube),
..default()
});
}
}
/// Set the model for each piece based on the game::Piece::* marker
fn set_piece_model(
mut events: Query<
(&mut Handle<Scene>, &Piece),
(
Or<(Added<game::Piece>, Changed<game::Piece>)>,
With<Display3d>,
),
>,
gltfs: Res<Assets<Gltf>>,
tweaks: Res<Assets<Tweaks>>,
tweaks_file: Res<tweak::GameTweaks>,
) {
let tweak = tweaks
.get(tweaks_file.handle.clone())
.expect("Load tweakfile");
events.iter_mut().for_each(|(mut handle, piece)| {
let assets_handle = tweak
.get_handle::<Gltf>("display3d_models_assets_file")
.unwrap();
let gltf = gltfs.get(assets_handle).expect("Load GLTF content");
*handle = match piece {
game::Piece::Pawn => gltf.named_scenes.get(
tweak
.get::<String>("display3d_models_scenes_pawn")
.unwrap()
.as_str(),
),
game::Piece::Drone => gltf.named_scenes.get(
tweak
.get::<String>("display3d_models_scenes_drone")
.unwrap()
.as_str(),
),
game::Piece::Queen => gltf.named_scenes.get(
tweak
.get::<String>("display3d_models_scenes_queen")
.unwrap()
.as_str(),
),
}
.expect("Game board model")
.clone();
});
}
fn set_board_model(
mut boards: Query<
&mut Handle<Scene>,
(
With<game::BoardComponent>,
Without<TilesComponent>,
With<Display3d>,
),
>,
gltfs: Res<Assets<Gltf>>,
tweaks: Res<Assets<Tweaks>>,
tweaks_file: Res<tweak::GameTweaks>,
) {
let tweak = tweaks
.get(tweaks_file.handle.clone())
.expect("Load tweakfile");
boards.iter_mut().for_each(|mut handle| {
let assets_handle = tweak
.get_handle::<Gltf>("display3d_models_assets_file")
.unwrap();
let gltf = gltfs.get(assets_handle).expect("Load GLTF content");
*handle = gltf
.named_scenes
.get(
tweak
.get::<String>("display3d_models_scenes_board")
.unwrap()
.as_str(),
)
.expect("Game board model")
.clone();
});
}
/// Sets a piece location given it's board index
fn set_piece_position(
mut events: Query<
(&mut Transform, &BoardIndex),
(With<game::Piece>, With<Display3d>, Changed<BoardIndex>),
>,
) {
events.iter_mut().for_each(|(mut t, i)| {
t.translation = board_translation(i);
});
}
/// Given a board index returns the Vec3 location in space
fn board_translation(&BoardIndex { x, y }: &BoardIndex) -> Vec3 {
// Scale x down by 4 to account for -4..4 scaling
let x = x as i8 - 4;
// Mirror y axis because our board index is inverted...
let y = -(y as i8) + 1;
let x = if x < 0 {
x as f32 * 1.3 + 0.275 // 0.325
} else {
x as f32 * 1.3 + 1.05 // 1.0
};
let y = y as f32 * 1.3 + 0.65;
Vec3::new(x, 0.0, y)
}
fn capture_translation(side: &Side, num: usize) -> Vec3 {
info!("Side: {:?} Num: {:?}", side, num);
let x = 5.0 - ((num % 4) as f32 * 1.3);
let y = -1.3;
let z = 4.0 + ((num / 4) as f32 * 1.3);
match side {
Side::B => Vec3::new(-x, y, z),
Side::A => Vec3::new(x, y, -z),
}
}
fn gizmo_system(mut gizmos: Gizmos) {
for y in 0..4 {
for x in 0..8 {
gizmos.cuboid(
Transform::from_translation(board_translation(&BoardIndex { x, y }))
.with_scale(Vec3::new(1.0, 0.1, 1.0)),
Color::PURPLE,
)
}
}
}
/// TODO: This has bad feel, needs to be tuned
fn move_camera(
buttons: Res<Input<MouseButton>>,
mut events: EventReader<MouseMotion>,
mut camera: Query<&mut Transform, (With<Display3d>, With<Camera>)>,
) {
events.read().for_each(|MouseMotion { delta }| {
if buttons.pressed(MouseButton::Left) {
camera.iter_mut().for_each(|mut t| {
t.rotate_around(Vec3::ZERO, Quat::from_rotation_y(delta.x / 256.0));
t.rotate_around(Vec3::ZERO, Quat::from_rotation_x(delta.y / 256.0));
t.look_at(Vec3::ZERO, Vec3::Y);
});
}
});
}
fn mouse_zoom(
mut events: EventReader<MouseWheel>,
mut camera: Query<&mut Transform, (With<Display3d>, With<Camera>)>,
) {
events.read().for_each(|MouseWheel { unit, y, .. }| {
camera.iter_mut().for_each(|mut t| {
match unit {
MouseScrollUnit::Line => {
t.translation *= 1.0 - (*y / 4.0);
}
MouseScrollUnit::Pixel => {
t.translation *= 1.0 - (*y / 64.0);
}
}
t.look_at(Vec3::ZERO, Vec3::Y);
});
});
}
/// Set the Texture for a piece given it's position (left or right) on the bord.
/// Executed when Side is changed or upon entry to Display3d state
/// Getting this to run _after_ GLTF is loaded is a pain.
/// PERF: We are saving what to work on in a Vector which is bad.
/// CAVEAT: We are only exeucting this when a piece changes or state is changed.
fn set_piece_texture(
events: Query<
(Entity, &Piece, &Side),
(
With<game::Piece>,
With<Display3d>,
Or<(Changed<Side>, Added<Side>)>,
),
>,
gltfs: Res<Assets<Gltf>>,
children: Query<&Children>,
mut models: Query<(&Name, &mut Handle<StandardMaterial>)>,
tweaks: Res<Assets<Tweaks>>,
tweaks_file: Res<tweak::GameTweaks>,
) {
events.iter().for_each(|(entity, piece, side)| {
debug!("Checking piece texture for {:?}", entity);
let tweak = tweaks.get(tweaks_file.handle.clone()).unwrap();
let assets_handle = tweak
.get_handle::<Gltf>("display3d_models_assets_file")
.unwrap();
if let Some(gltf) = gltfs.get(assets_handle) {
// Why can't we just models.iter_many_mut(...).for_each(...)??
let mut stuff = models.iter_many_mut(children.iter_descendants(entity));
while let Some((n, mut m)) = stuff.fetch_next() {
debug!("Setting piece texture for {:?}", n);
match (*piece, side, n.as_str()) {
(Piece::Queen, Side::A, "Queen.0") => {
*m = gltf
.named_materials
.get(
tweak
.get::<String>("display3d_models_materials_queen_red")
.unwrap()
.as_str(),
)
.expect("Load Red Queen texture")
.clone()
}
(Piece::Queen, Side::A, "Queen.1") => {
*m = gltf
.named_materials
.get(
tweak
.get::<String>("display3d_models_materials_dots_red")
.unwrap()
.as_str(),
)
.expect("Load Red Dots texture")
.clone()
}
(Piece::Queen, Side::B, "Queen.0") => {
*m = gltf
.named_materials
.get(
tweak
.get::<String>("display3d_models_materials_queen_blue")
.unwrap()
.as_str(),
)
.expect("Load Blue Queen texture")
.clone()
}
(Piece::Queen, Side::B, "Queen.1") => {
*m = gltf
.named_materials
.get(
tweak
.get::<String>("display3d_models_materials_dots_blue")
.unwrap()
.as_str(),
)
.expect("Load Red Dots texture")
.clone()
}
(Piece::Drone, Side::A, "Drone.0") => {
*m = gltf
.named_materials
.get(
tweak
.get::<String>("display3d_models_materials_drone_red")
.unwrap()
.as_str(),
)
.expect("Load Red Drone texture")
.clone()
}
(Piece::Drone, Side::A, "Drone.1") => {
*m = gltf
.named_materials
.get(
tweak
.get::<String>("display3d_models_materials_dots_red")
.unwrap()
.as_str(),
)
.expect("Load Red Dots texture")
.clone()
}
(Piece::Drone, Side::B, "Drone.0") => {
*m = gltf
.named_materials
.get(
tweak
.get::<String>("display3d_models_materials_drone_blue")
.unwrap()
.as_str(),
)
.expect("Load Blue Drone texture")
.clone()
}
(Piece::Drone, Side::B, "Drone.1") => {
*m = gltf
.named_materials
.get(
tweak
.get::<String>("display3d_models_materials_dots_blue")
.unwrap()
.as_str(),
)
.expect("Load Blue Dots texture")
.clone()
}
(Piece::Pawn, Side::A, "Pawn.0") => {
*m = gltf
.named_materials
.get(
tweak
.get::<String>("display3d_models_materials_pawn_red")
.unwrap()
.as_str(),
)
.expect("Load Red Pawn texture")
.clone()
}
(Piece::Pawn, Side::A, "Pawn.1") => {
*m = gltf
.named_materials
.get(
tweak
.get::<String>("display3d_models_materials_dots_red")
.unwrap()
.as_str(),
)
.expect("Load Red Dots texture")
.clone()
}
(Piece::Pawn, Side::B, "Pawn.0") => {
*m = gltf
.named_materials
.get(
tweak
.get::<String>("display3d_models_materials_pawn_blue")
.unwrap()
.as_str(),
)
.expect("Load Blue Pawn texture")
.clone()
}
(Piece::Pawn, Side::B, "Pawn.1") => {
*m = gltf
.named_materials
.get(
tweak
.get::<String>("display3d_models_materials_dots_blue")
.unwrap()
.as_str(),
)
.expect("Load Blue Dots texture")
.clone()
}
_ => warn!("???"),
}
}
}
});
}
/// Select tiles and pieces in 3d
/// There is a bug where we are selecting multiple entities...
/// TODO: Selectable generalize picking pieces **and** hitboxes
fn select(
mut events: EventReader<MouseButtonInput>,
query: Query<(Entity, &Handle<Mesh>, &GlobalTransform)>,
meshes: Res<Assets<Mesh>>,
cameras: Query<(&Camera, &GlobalTransform)>,
windows: Query<&Window, With<PrimaryWindow>>,
selectable: Query<(Entity, &BoardIndex, &Side), (With<game::Selectable>, With<Display3d>)>,
selected: Query<Entity, With<game::Selected>>,
children: Query<&Children>,
mut selections: EventWriter<game::Selection>,
state: Res<State<game::TurnState>>,
) {
events
.read()
.filter(|ev| ev.state == ButtonState::Pressed)
.for_each(|_| {
windows.iter().for_each(|window| {
window.cursor_position().and_then(|pos| {
cameras.iter().for_each(|(camera, gt)| {
camera.viewport_to_world(gt, pos).and_then(|ray| {
query
.iter()
.filter_map(|(entity, handle, gt)| {
meshes.get(handle).map(|mesh| (entity, mesh, gt))
})
.for_each(|(entity, mesh, gt)| {
hit::intersects3d(&ray, mesh, &gt).and_then(|_hit| {
selectable
.iter()
.find_map(|(e, &board_index, &side)| {
// Check the side of the selection if no piece is selected
// Otherwise this is fine, select away
let side_check =
!selected.is_empty() || state.get().0 == side;
let hit_check = {
// This entity was hit (tile hitboxes)
let primary = entity == e;
// A child was hit (pieces)
let secondary = children
.iter_descendants(e)
.any(|child| child == entity);
primary || secondary
};
(side_check && hit_check).then_some(board_index)
})
.iter()
.for_each(|&board_index| {
info!("Board index selected: {:?}", board_index);
selections
.send(game::Selection(board_index.clone()));
});
Some(())
});
});
Some(())
});
});
Some(())
});
});
});
}
fn selected_gizmo(
selected: Query<&Transform, (With<game::Selected>, With<Display3d>)>,
mut gizmos: Gizmos,
) {
selected.iter().for_each(|transform| {
gizmos.cuboid(transform.clone(), Color::GREEN);
})
}
fn moves_gizmo(
selected: Query<&BoardIndex, (With<game::Piece>, With<game::Selected>, With<Display3d>)>,
board: Res<Board>,
mut gizmos: Gizmos,
) {
selected.iter().for_each(|idx| {
board
.valid_moves(*idx)
.iter()
.map(|i| Transform::from_translation(board_translation(i)))
.for_each(|t| gizmos.cuboid(t, Color::WHITE))
});
}
fn set_valid_move_model(
mut events: Query<
(&mut Handle<Scene>, &mut Visibility),
(With<Display3d>, Added<game::ValidMove>),
>,
gltfs: Res<Assets<Gltf>>,
tweaks: Res<Assets<Tweaks>>,
tweaks_file: Res<tweak::GameTweaks>,
) {
let tweak = tweaks
.get(tweaks_file.handle.clone())
.expect("Load tweakfile");
let assets_handle = tweak
.get_handle::<Gltf>("display3d_models_assets_file")
.unwrap();
if let Some(gltf) = gltfs.get(assets_handle) {
events.iter_mut().for_each(|(mut handle, mut visibility)| {
*handle = gltf
.named_scenes
.get(
tweak
.get::<String>("display3d_models_scenes_valid_move")
.unwrap()
.as_str(),
)
.unwrap()
.clone();
*visibility = Visibility::Hidden;
})
}
}
fn _play_valid_move_animation(_players: Query<&AnimationPlayer>) {
todo!();
}
fn pick_up(
mut events: Query<
(Entity, &game::Piece),
(With<game::Piece>, With<Display3d>, Added<game::Selected>),
>,
gltfs: Res<Assets<Gltf>>,
children: Query<&Children>,
mut players: Query<(&Name, &mut AnimationPlayer)>,
clips: Res<Assets<AnimationClip>>,
tweaks: Res<Assets<Tweaks>>,
tweaks_file: Res<tweak::GameTweaks>,
) {
let tweak = tweaks
.get(tweaks_file.handle.clone())
.expect("Load tweakfile");
events.iter_mut().for_each(|(entity, piece)| {
let assets_handle = tweak
.get_handle::<Gltf>("display3d_models_assets_file")
.unwrap();
let gltf = gltfs.get(assets_handle).expect("Load GLTF content");
info!("Pickup animation for {:?}", entity);
children.iter_descendants(entity).for_each(|child| {
info!(" Child: {:?}", child);
if let Ok((name, mut player)) = players.get_mut(child) {
info!("Picking up {:?} ({:?}) {:?}", name, entity, piece);
let pickup_animation =
format!("display3d_models_animations_pick_up_{:?}", piece).to_ascii_lowercase();
let pickup_handle = gltf
.named_animations
.get(
tweak
.get::<String>(pickup_animation.as_str())
.unwrap()
.as_str(),
)
.expect("Pickup Animation");
let idle_animation =
format!("display3d_models_animations_idle_{:?}", piece).to_ascii_lowercase();
let idle_handle = gltf
.named_animations
.get(
tweak
.get::<String>(idle_animation.as_str())
.unwrap()
.as_str(),
)
.expect("Idle animation");
if let Some(pickup_clip) = clips.get(pickup_handle) {
if let Some(idle_clip) = clips.get(idle_handle) {
if pickup_clip.compatible_with(name) && idle_clip.compatible_with(name) {
player
.start_with_transition(
pickup_handle.clone(),
Duration::from_secs_f32(0.75),
)
.start_with_transition(
idle_handle.clone(),
Duration::from_secs_f32(1.5),
)
.set_repeat(RepeatAnimation::Forever);
}
};
}
}
})
});
}
fn put_down(
mut events: RemovedComponents<game::Selected>,
mut query: Query<&game::Piece, (With<game::Piece>, With<game::Selectable>, With<Display3d>)>,
gltfs: Res<Assets<Gltf>>,
children: Query<&Children>,
mut players: Query<(&Name, &mut AnimationPlayer)>,
clips: Res<Assets<AnimationClip>>,
tweaks: Res<Assets<Tweaks>>,
tweaks_file: Res<tweak::GameTweaks>,
) {
let tweak = tweaks
.get(&tweaks_file.handle.clone())
.expect("Load tweakfile");
events.read().for_each(|entity| {
if let Ok(piece) = query.get_mut(entity) {
let assets_handle = tweak
.get_handle::<Gltf>("display3d_models_assets_file")
.unwrap();
let gltf = gltfs.get(assets_handle).expect("Load GLTF content");
children.iter_descendants(entity).for_each(|child| {
if let Ok((name, mut player)) = players.get_mut(child) {
info!("Putting down {:?}", entity);
let putdown_animation =
format!("display3d_models_animations_put_down_{:?}", piece)
.to_ascii_lowercase();
let putdown_handle = gltf
.named_animations
.get(
tweak
.get::<String>(putdown_animation.as_str())
.unwrap()
.as_str(),
)
.expect("PutDown Animation");
if let Some(putdown_clip) = clips.get(putdown_handle) {
if putdown_clip.compatible_with(name) {
player
.start_with_transition(
putdown_handle.clone(),
Duration::from_secs_f32(0.75),
)
.set_repeat(RepeatAnimation::Never);
}
}
}
})
}
})
}
fn set_tile_hitbox(
mut events: Query<(&mut Transform, &BoardIndex), (With<Display3d>, Added<game::Tile>)>,
) {
events.iter_mut().for_each(|(mut transform, index)| {
*transform = Transform::from_translation(board_translation(index));
});
}
fn opening_animation(mut players: Query<&mut AnimationPlayer, (With<Camera>, With<Display3d>)>) {
players.iter_mut().for_each(|mut player| {
info!("Playing intro camera animation");
player.resume()
});
}
// When called skips any running animations
fn skip_animation(
mut players: Query<&mut AnimationPlayer>,
clips: Res<Assets<AnimationClip>>,
time: Res<Time>,
) {
players.iter_mut().for_each(|mut p| {
if let Some(c) = clips.get(p.animation_clip()) {
// HACK: We should be able to skip to the end of an animation
// But implementation details means this is as close as we can get...
p.seek_to(c.duration() - (2.0 * time.delta_seconds()));
}
})
}
fn switch_sides(
mut players: Query<&mut AnimationPlayer, (With<Camera>, With<Display3d>)>,
gltfs: Res<Assets<Gltf>>,
state: Res<State<game::TurnState>>,
tweaks: Res<Assets<Tweaks>>,
tweaks_file: Res<tweak::GameTweaks>,
) {
let tweak = tweaks
.get(tweaks_file.handle.clone())
.expect("Load tweakfile");
let assets_handle = tweak
.get_handle::<Gltf>("display3d_models_assets_file")
.unwrap();
let gltf = gltfs.get(assets_handle).expect("Load GLTF content");
players.iter_mut().for_each(|mut player| {
let animation = match state.get() {
game::TurnState(game::Side::A) => gltf.named_animations.get(
tweak
.get::<String>("display3d_models_animations_turn_a")
.unwrap()
.as_str(),
),
game::TurnState(game::Side::B) => gltf.named_animations.get(
tweak
.get::<String>("display3d_models_animations_turn_b")
.unwrap()
.as_str(),
),
};
player.start_with_transition(
animation.expect("Camera Transition Animation").clone(),
Duration::from_secs_f32(1.00),
);
});
}
fn scale_lighting(
mut directional: Query<(
Entity,
&mut DirectionalLight,
Option<&Original<DirectionalLight>>,
)>,
mut spot: Query<(Entity, &mut SpotLight, Option<&Original<SpotLight>>)>,
mut point: Query<(Entity, &mut PointLight, Option<&Original<PointLight>>)>,
mut commands: Commands,
tweaks: Res<Assets<Tweaks>>,
tweaks_file: Res<tweak::GameTweaks>,
) {
let tweak = tweaks
.get(tweaks_file.handle.clone())
.expect("Load tweakfile");
let directional_tweak = tweak
.get::<f32>("display3d_lights_scaling_directional")
.expect("Directional lighting scalar");
directional
.iter_mut()
.for_each(|(entity, mut val, original)| {
debug!("Scaling directional light {:?}", entity);
if let Some(Original(v)) = original {
val.illuminance = v.illuminance * directional_tweak;
} else {
commands.entity(entity).insert(Original(val.clone()));
val.illuminance *= directional_tweak;
}
});
let spot_tweak = tweak
.get::<f32>("display3d_lights_scaling_spot")
.expect("Spot lighting scalar");
spot.iter_mut().for_each(|(entity, mut val, original)| {
debug!("Scaling spot light {:?}", entity);
if let Some(Original(v)) = original {
val.intensity = v.intensity * spot_tweak;
} else {
commands.entity(entity).insert(Original(val.clone()));
val.intensity *= spot_tweak;
}
});
let point_tweak = tweak
.get::<f32>("display3d_lights_scaling_point")
.expect("Point lighting scalar");
point.iter_mut().for_each(|(entity, mut val, original)| {
debug!("Scaling point light {:?}", entity);
if let Some(Original(v)) = original {
val.intensity = v.intensity * point_tweak;
} else {
commands.entity(entity).insert(Original(val.clone()));
val.intensity *= point_tweak;
}
});
}
pub(super) mod tweaks {
use bevy::{
core_pipeline::{bloom::BloomSettings, tonemapping::Tonemapping},
math::Vec3,
pbr::{FogFalloff, FogSettings, ScreenSpaceAmbientOcclusionQualityLevel},
prelude::*,
render::{
color::Color,
view::{ColorGrading, Msaa},
},
};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub enum TweakTonemapping {
None,
Reinhard,
ReinhardLuminance,
AcesFitted,
AgX,
SomewhatBoringDisplayTransform,
TonyMcMapface,
BlenderFilmic,
}
impl From<TweakTonemapping> for Tonemapping {
fn from(src: TweakTonemapping) -> Tonemapping {
match src {
TweakTonemapping::None => Tonemapping::None,
TweakTonemapping::Reinhard => Tonemapping::Reinhard,
TweakTonemapping::ReinhardLuminance => Tonemapping::ReinhardLuminance,
TweakTonemapping::AcesFitted => Tonemapping::AcesFitted,
TweakTonemapping::AgX => Tonemapping::AgX,
TweakTonemapping::SomewhatBoringDisplayTransform => {
Tonemapping::SomewhatBoringDisplayTransform
}
TweakTonemapping::TonyMcMapface => Tonemapping::TonyMcMapface,
TweakTonemapping::BlenderFilmic => Tonemapping::BlenderFilmic,
}
}
}
#[derive(Debug, Deserialize)]
pub struct TweakBloomSettings {
intensity: f32,
}
impl From<TweakBloomSettings> for BloomSettings {
fn from(src: TweakBloomSettings) -> BloomSettings {
BloomSettings {
intensity: src.intensity,
..default()
}
}
}
#[derive(Debug, Deserialize)]
pub enum TweakMsaa {
Off,
Sample2,
Sample4,
Sample8,
}
impl From<TweakMsaa> for Msaa {
fn from(src: TweakMsaa) -> Msaa {
match src {
TweakMsaa::Off => Msaa::Off,
TweakMsaa::Sample2 => Msaa::Sample2,
TweakMsaa::Sample4 => Msaa::Sample4,
TweakMsaa::Sample8 => Msaa::Sample8,
}
}
}
#[derive(Deserialize, Debug)]
pub enum TweakScreenSpaceAmbientOcclusionQualityLevel {
Low,
Medium,
High,
Ultra,
}
impl From<TweakScreenSpaceAmbientOcclusionQualityLevel>
for ScreenSpaceAmbientOcclusionQualityLevel
{
fn from(
src: TweakScreenSpaceAmbientOcclusionQualityLevel,
) -> ScreenSpaceAmbientOcclusionQualityLevel {
match src {
TweakScreenSpaceAmbientOcclusionQualityLevel::Low => {
ScreenSpaceAmbientOcclusionQualityLevel::Low
}
TweakScreenSpaceAmbientOcclusionQualityLevel::Medium => {
ScreenSpaceAmbientOcclusionQualityLevel::Medium
}
TweakScreenSpaceAmbientOcclusionQualityLevel::High => {
ScreenSpaceAmbientOcclusionQualityLevel::High
}
TweakScreenSpaceAmbientOcclusionQualityLevel::Ultra => {
ScreenSpaceAmbientOcclusionQualityLevel::Ultra
}
}
}
}
#[derive(Debug, Deserialize)]
pub struct TweakFogSettings {
pub color: Color,
pub directional_light_color: Color,
pub directional_light_exponent: f32,
pub falloff: TweakFogFalloff,
}
impl From<TweakFogSettings> for FogSettings {
fn from(src: TweakFogSettings) -> FogSettings {
FogSettings {
color: src.color,
directional_light_color: src.directional_light_color,
directional_light_exponent: src.directional_light_exponent,
falloff: src.falloff.into(),
}
}
}
#[derive(Debug, Deserialize)]
pub enum TweakFogFalloff {
Linear {
start: f32,
end: f32,
},
Exponential {
density: f32,
},
ExponentialSquared {
density: f32,
},
Atmospheric {
extinction: Vec3,
inscattering: Vec3,
},
}
impl From<TweakFogFalloff> for FogFalloff {
fn from(src: TweakFogFalloff) -> FogFalloff {
match src {
TweakFogFalloff::Linear { start, end } => FogFalloff::Linear { start, end },
TweakFogFalloff::Exponential { density } => FogFalloff::Exponential { density },
TweakFogFalloff::ExponentialSquared { density } => {
FogFalloff::ExponentialSquared { density }
}
TweakFogFalloff::Atmospheric {
extinction,
inscattering,
} => FogFalloff::Atmospheric {
extinction,
inscattering,
},
}
}
}
#[derive(Debug, Deserialize)]
pub struct TweakColorGrading {
pub exposure: f32,
pub gamma: f32,
pub pre_saturation: f32,
pub post_saturation: f32,
}
impl From<TweakColorGrading> for ColorGrading {
fn from(src: TweakColorGrading) -> ColorGrading {
ColorGrading {
exposure: src.exposure,
gamma: src.gamma,
pre_saturation: src.pre_saturation,
post_saturation: src.post_saturation,
}
}
}
}
/// Type expressing the extended material of standardMaterial + dissolveMaterial
type DissolveMaterial = ExtendedMaterial<StandardMaterial, DissolveExtension>;
/// Material extension for dissolving effect
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
struct DissolveExtension {
#[uniform(100)]
percentage: f32,
}
impl MaterialExtension for DissolveExtension {
fn fragment_shader() -> ShaderRef {
"shaders/dissolve.wgsl".into()
}
}
// Component for 'backing up' components which are temporarily not used
#[derive(Debug, Component, Clone)]
struct Backup<T: Component>(T);
/// Sets up all pieces to have an associated "dissolve" material ready for capture
fn setup_capture_piece(
// All entities with materials are candidates for this procedure
events: Query<
(Entity, &Handle<StandardMaterial>),
(
Added<Handle<StandardMaterial>>,
Changed<Handle<StandardMaterial>>,
),
>,
// Only process newly created pieces (we do not delete pieces at runtime)
query: Query<Entity, (With<Piece>, Added<Children>)>,
// Children of pieces are the actual meshes that need materials
parents: Query<&Parent>,
// Used to create DissovleMaterial
standard_materials: Res<Assets<StandardMaterial>>,
// Used to create Handle<DissolveMaterial>
mut dissolve_materials: ResMut<Assets<DissolveMaterial>>,
// Used to insert Backup(Handle<DissolveMaterial>);
mut commands: Commands,
// Cache dissolve textures that have already been created
mut cache: Local<HashMap<Handle<StandardMaterial>, Handle<DissolveMaterial>>>,
) {
events
.iter()
// Only process if this is a child of a piece
.filter(|(child, _)| query.iter_many(parents.iter_ancestors(*child)).count() > 0)
// Handle this entity (mesh)
.for_each(|(child, std_handle)| {
let dis_handle = match cache.get(std_handle) {
// We have not seen this material, so create a new dissolve material
None => {
// Extension we will add to existing gltf-sourced materials
let extension = DissolveExtension { percentage: 1.0 };
// Base material we will extend for the duration of the dissolve effect
let base = standard_materials
.get(std_handle)
.expect("Resolve material data")
.clone();
dissolve_materials.add(ExtendedMaterial { base, extension })
}
Some(dis_handle) => dis_handle.clone(),
};
// Insert this handle into the cache (may be redundant)
cache.insert(std_handle.clone(), dis_handle.clone());
// Add the dissolve handle as a Backup(T)
commands.entity(child).insert(Backup(dis_handle.clone()));
});
}
/// When a piece is captured...
/// 1. Play a cool "captured" animation and a neat sound
/// 2. Move the piece to the side of the board
/// 3. Play the same "captured" animation in reverse
/// The animation is like a 'beam me up scotty' sorta thing.
fn capture_piece(
events: Query<Entity, (With<Display3d>, Added<game::Captured>)>,
mut query: Query<
(&mut Visibility, &mut Transform, &Side),
(With<Display3d>, With<game::Captured>),
>,
mut state: Local<Option<game::CaptureFlow>>,
mut kids: Local<Vec<Entity>>,
mut prog: Local<f32>,
mut dissolve_materials: ResMut<Assets<DissolveMaterial>>,
object_standard_materials: Query<(
Entity,
&Handle<StandardMaterial>,
&Backup<Handle<DissolveMaterial>>,
)>,
object_dissolve_materials: Query<(
Entity,
&Handle<DissolveMaterial>,
&Backup<Handle<StandardMaterial>>,
)>,
children: Query<&Children>,
mut commands: Commands,
time: Res<Time>,
score: Res<game::Score>,
) {
let duration: f32 = 3.0;
match *state {
// State is None, so we need to initiate the animation by swapping the StadardMaterial for the DissolveMaterial
None => {
*state = events.iter().next().map(|entity| {
// Reset dissolve progress
*prog = 1.0;
// store the kids we want to process
*kids = object_standard_materials
.iter_many(children.iter_descendants(entity))
.map(|(child, std_handle, Backup(dis_handle))| {
// Swap standard and dissolve material
commands
.entity(child)
.insert(dis_handle.clone())
.insert(Backup(std_handle.clone()))
.remove::<Handle<StandardMaterial>>()
.remove::<Backup<Handle<DissolveMaterial>>>();
// Return child entity to be processed later in flow
child
})
.collect();
// Set the next state to start fading out
game::CaptureFlow::FadeOut(entity)
});
}
Some(s) => {
match s {
game::CaptureFlow::FadeOut(_entity) => {
// Play fade-out animation
{
// Calculate how much of the animation has passed
let delta = time.delta_seconds() / duration;
// Set progress to current - delta
*prog -= delta;
// If progress is less than zero
if *prog <= 0.0 {
// Set to exactly 0 for simplicity
*prog = 0.0;
// Move to next state now that animation is done
*state = s.next();
// This takes effect after updating all children
}
object_dissolve_materials.iter_many(kids.iter()).for_each(
|(_child, dis_handle, _)| {
let dissolve_material = dissolve_materials
.get_mut(dis_handle)
.expect("Get the dissolve material");
// Change the material's value to create animation
dissolve_material.extension.percentage = *prog;
debug!(
"Play fade out animation {:?} {:?}",
delta, dissolve_material.extension.percentage
);
},
);
}
}
game::CaptureFlow::Store(entity) => {
let (mut v, mut t, side) = query
.get_mut(entity)
.expect("Visibility and Transform of captured piece");
// Hide piece now that animation is done
*v = Visibility::Hidden;
// HACK: This is dirty. Why side, but score.captures(!side)?
info!(
"Capture translation: ({:?}, {:?}) {:?}",
side,
score.captures(!*side).saturating_sub(1),
t.translation
);
t.translation =
capture_translation(side, score.captures(!*side).saturating_sub(1));
*state = s.next();
}
game::CaptureFlow::FadeIn(entity) => {
let (mut v, _, _) = query
.get_mut(entity)
.expect("Visibility and Transform of captured piece");
// Show piece now that it is moved
*v = Visibility::Inherited;
// Calculate how much of the animation has passed
let delta = time.delta_seconds() / duration;
// Move the animation forward by delta
*prog += delta;
// If we have completed the animation
if *prog >= 1.0 {
// Move to next state now that animation is done
*state = s.next();
}
// Play fade-in animation
{
object_dissolve_materials.iter_many(kids.iter()).for_each(
|(child, dis_handle, Backup(std_handle))| {
let dissolve_material = dissolve_materials
.get_mut(dis_handle)
.expect("Get the dissolve material");
// Change the material's value to create animation
dissolve_material.extension.percentage = *prog;
debug!(
"Play fade in animation {:?} {:?}",
delta, dissolve_material.extension.percentage
);
// If we are done with the animation cleanup
if dissolve_material.extension.percentage >= 1.0 {
// Re-add the original material
commands
.entity(child)
.insert(std_handle.clone())
.insert(Backup(dis_handle.clone()))
.remove::<Handle<DissolveMaterial>>()
.remove::<Backup<Handle<StandardMaterial>>>();
}
},
);
}
}
}
}
}
}
fn debug_selected(
query: Query<(Entity, &BoardIndex, &Piece, &Side), With<game::Selected>>,
mut debug_info: ResMut<debug::DebugInfo>,
) {
query.iter().for_each(|(e, bi, p, s)| {
debug_info.set(
"Active".into(),
format!(
"\n>>ID: {:?}\n>>Piece: {:?}\n>>Side: {:?}\n>>Index: {:?}",
e, p, s, bi
),
);
});
}