use crate::ecs::EditorWorld;
use crate::systems::shell::{CutsceneCommand, EditorShellContext};
use nightshade::ecs::camera::components::Projection;
use nightshade::ecs::cutscene::Cutscene;
#[cfg(not(target_arch = "wasm32"))]
use nightshade::ecs::cutscene::CutsceneAction;
use nightshade::ecs::cutscene::components::{
CutsceneShot, camera_look_rotation, sample_camera_path,
};
use nightshade::ecs::lines::components::{Line, Lines};
use nightshade::ecs::primitives::EasingFunction;
use nightshade::ecs::world::{GLOBAL_TRANSFORM, LINES, VISIBILITY};
use nightshade::prelude::*;
const MARKER_SIZE: f32 = 0.5;
const MARKER_MATERIAL: &str = "editor_cutscene_waypoint";
const CURVE_COLOR: Vec4 = Vec4::new(1.0, 0.55, 0.15, 1.0);
const SEGMENTS_PER_SPAN: usize = 16;
pub fn apply_commands(
editor_world: &mut EditorWorld,
world: &mut World,
context: &mut EditorShellContext,
) {
let commands = std::mem::take(&mut context.cutscene_commands);
for command in commands {
match command {
CutsceneCommand::AddPoint => add_point(editor_world, world),
CutsceneCommand::Clear => clear(editor_world, world),
CutsceneCommand::Save(name) => save(editor_world, &name),
CutsceneCommand::Load(name) => load(editor_world, world, &name),
CutsceneCommand::Preview => preview(editor_world, world),
CutsceneCommand::Stop => stop_preview(editor_world, world),
}
}
}
pub fn sync(editor_world: &mut EditorWorld, world: &mut World) {
let waypoints = editor_world.resources.cutscene_edit.waypoints.clone();
for (index, marker) in waypoints.iter().enumerate() {
if let Some(transform) = world.core.get_local_transform(*marker) {
let eye = transform.translation;
let forward =
nalgebra_glm::quat_rotate_vec3(&transform.rotation, &Vec3::new(0.0, 0.0, -1.0));
if let Some(shot) = editor_world.resources.cutscene_edit.shots.get_mut(index) {
shot.eye = eye;
shot.target = eye + forward;
}
}
}
let lines_entity = ensure_curve_lines(editor_world, world);
let lines = build_curve_lines(&editor_world.resources.cutscene_edit.shots);
world.core.set_lines(lines_entity, Lines::new(lines));
if editor_world.resources.cutscene_edit.previewing {
advance_cutscene_system(world);
if cutscene_finished(world) {
stop_preview(editor_world, world);
}
}
}
fn add_point(editor_world: &mut EditorWorld, world: &mut World) {
let Some(camera) = editor_world.resources.camera.camera_entity else {
return;
};
let Some(transform) = world.core.get_global_transform(camera) else {
return;
};
let eye = transform.translation();
let forward = nalgebra_glm::normalize(&transform.forward_vector());
let field_of_view_degrees = world
.core
.get_camera(camera)
.and_then(|component| match &component.projection {
Projection::Perspective(perspective) => Some(perspective.y_fov_rad.to_degrees()),
_ => None,
})
.unwrap_or(45.0);
let shot = CutsceneShot {
eye,
target: eye + forward,
field_of_view_degrees,
roll_degrees: 0.0,
};
let rotation = camera_look_rotation(shot.eye, shot.target, 0.0);
let marker = spawn_marker(editor_world, world, eye, rotation);
editor_world.resources.cutscene_edit.shots.push(shot);
editor_world.resources.cutscene_edit.waypoints.push(marker);
}
fn clear(editor_world: &mut EditorWorld, world: &mut World) {
let markers = std::mem::take(&mut editor_world.resources.cutscene_edit.waypoints);
for marker in markers {
editor_world
.resources
.editor_scene
.unregister_scaffolding(marker);
despawn_recursive_immediate(world, marker);
}
editor_world.resources.cutscene_edit.shots.clear();
}
fn preview(editor_world: &mut EditorWorld, world: &mut World) {
let shots = editor_world.resources.cutscene_edit.shots.clone();
if shots.len() < 2 {
return;
}
let camera = ensure_preview_camera(editor_world, world);
set_cutscene_camera(world, camera);
editor_world.resources.cutscene_edit.restore_camera = world.resources.active_camera;
world.resources.active_camera = Some(camera);
play_cutscene(world, build_cutscene(&shots));
editor_world.resources.cutscene_edit.previewing = true;
}
fn stop_preview(editor_world: &mut EditorWorld, world: &mut World) {
if !editor_world.resources.cutscene_edit.previewing {
return;
}
stop_cutscene(world);
advance_cutscene_system(world);
let restore = editor_world
.resources
.cutscene_edit
.restore_camera
.take()
.or(editor_world.resources.camera.camera_entity);
if let Some(camera) = restore {
world.resources.active_camera = Some(camera);
}
editor_world.resources.cutscene_edit.previewing = false;
}
fn build_cutscene(shots: &[CutsceneShot]) -> Cutscene {
let duration = (shots.len() as f32 * 1.5).max(2.0);
Cutscene::new("Editor Path").camera_path(
0.0,
duration,
EasingFunction::SineInOut,
shots.to_vec(),
)
}
#[cfg(not(target_arch = "wasm32"))]
fn extract_path(cutscene: &Cutscene) -> Vec<CutsceneShot> {
cutscene
.events
.iter()
.find_map(|event| match &event.action {
CutsceneAction::CameraPath { waypoints } => Some(waypoints.clone()),
_ => None,
})
.unwrap_or_default()
}
#[cfg(not(target_arch = "wasm32"))]
fn save(editor_world: &EditorWorld, name: &str) {
let cutscene = build_cutscene(&editor_world.resources.cutscene_edit.shots);
let path = format!("{name}.cutscene.json");
match serde_json::to_string_pretty(&cutscene) {
Ok(text) => match std::fs::write(&path, text) {
Ok(()) => tracing::info!("Saved cutscene to {path}"),
Err(error) => tracing::error!("Failed to write {path}: {error}"),
},
Err(error) => tracing::error!("Failed to serialize cutscene: {error}"),
}
}
#[cfg(target_arch = "wasm32")]
fn save(_editor_world: &EditorWorld, _name: &str) {
tracing::warn!("Saving cutscenes is not supported on this target");
}
#[cfg(not(target_arch = "wasm32"))]
fn load(editor_world: &mut EditorWorld, world: &mut World, name: &str) {
let path = format!("{name}.cutscene.json");
let text = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(error) => {
tracing::error!("Failed to read {path}: {error}");
return;
}
};
let cutscene = match serde_json::from_str::<Cutscene>(&text) {
Ok(cutscene) => cutscene,
Err(error) => {
tracing::error!("Failed to parse {path}: {error}");
return;
}
};
clear(editor_world, world);
for shot in extract_path(&cutscene) {
let rotation = camera_look_rotation(shot.eye, shot.target, shot.roll_degrees);
let marker = spawn_marker(editor_world, world, shot.eye, rotation);
editor_world.resources.cutscene_edit.shots.push(shot);
editor_world.resources.cutscene_edit.waypoints.push(marker);
}
}
#[cfg(target_arch = "wasm32")]
fn load(_editor_world: &mut EditorWorld, _world: &mut World, _name: &str) {
tracing::warn!("Loading cutscenes is not supported on this target");
}
fn spawn_marker(
editor_world: &mut EditorWorld,
world: &mut World,
position: Vec3,
rotation: Quat,
) -> Entity {
ensure_marker_material(world);
let marker = spawn_mesh(
world,
"Cube",
position,
Vec3::new(MARKER_SIZE, MARKER_SIZE, MARKER_SIZE),
);
world
.core
.set_material_ref(marker, MaterialRef::new(MARKER_MATERIAL));
if let Some(transform) = world.core.get_local_transform_mut(marker) {
transform.translation = position;
transform.rotation = rotation;
}
mark_local_transform_dirty(world, marker);
editor_world
.resources
.editor_scene
.register_scaffolding(marker);
marker
}
fn ensure_preview_camera(editor_world: &mut EditorWorld, world: &mut World) -> Entity {
if let Some(camera) = editor_world.resources.cutscene_edit.preview_camera
&& world
.core
.entity_has_components(camera, nightshade::ecs::world::CAMERA)
{
return camera;
}
let camera = spawn_camera(world, Vec3::zeros(), "Cutscene Preview".to_string());
if let Some(component) = world.core.get_camera_mut(camera) {
component.smoothing = None;
}
editor_world
.resources
.editor_scene
.register_scaffolding(camera);
editor_world.resources.cutscene_edit.preview_camera = Some(camera);
camera
}
fn ensure_curve_lines(editor_world: &mut EditorWorld, world: &mut World) -> Entity {
if let Some(entity) = editor_world.resources.cutscene_edit.curve_lines
&& world.core.entity_has_components(entity, LINES)
{
return entity;
}
let entity = spawn_entities(world, LINES | VISIBILITY | GLOBAL_TRANSFORM, 1)[0];
world.core.set_lines(entity, Lines::default());
world
.core
.set_visibility(entity, Visibility { visible: true });
world
.core
.set_global_transform(entity, GlobalTransform::default());
editor_world
.resources
.editor_scene
.register_scaffolding(entity);
editor_world.resources.cutscene_edit.curve_lines = Some(entity);
entity
}
fn build_curve_lines(shots: &[CutsceneShot]) -> Vec<Line> {
if shots.len() < 2 {
return Vec::new();
}
let total = SEGMENTS_PER_SPAN * (shots.len() - 1);
let mut lines = Vec::with_capacity(total);
let mut previous = sample_camera_path(shots, 0.0).eye;
for index in 1..=total {
let progress = index as f32 / total as f32;
let point = sample_camera_path(shots, progress).eye;
lines.push(Line {
start: previous,
end: point,
color: CURVE_COLOR,
});
previous = point;
}
lines
}
fn ensure_marker_material(world: &mut World) {
if world
.resources
.assets
.material_registry
.registry
.name_to_index
.contains_key(MARKER_MATERIAL)
{
return;
}
material_registry_insert(
&mut world.resources.assets.material_registry,
MARKER_MATERIAL.to_string(),
Material {
base_color: [1.0, 0.55, 0.15, 1.0],
emissive_factor: [0.5, 0.25, 0.05],
emissive_strength: 2.0,
unlit: true,
..Default::default()
},
);
if let Some(&index) = world
.resources
.assets
.material_registry
.registry
.name_to_index
.get(MARKER_MATERIAL)
{
registry_add_reference(
&mut world.resources.assets.material_registry.registry,
index,
);
}
}