use std::collections::HashMap;
use nalgebra_glm::{Vec2, Vec3, Vec4};
#[cfg(feature = "audio")]
use crate::ecs::audio::components::{AudioBus, AudioSource};
#[cfg(feature = "audio")]
use crate::ecs::audio::resources::{audio_engine_get_sound, audio_engine_is_initialized};
use crate::ecs::camera::components::Projection;
use crate::ecs::cutscene::components::{
Cutscene, CutsceneAction, CutsceneShot, camera_look_rotation, sample_camera_path,
};
use crate::ecs::cutscene::resources::CutsceneOverlay;
use crate::ecs::primitives::Visibility;
use crate::ecs::text::components::{TextAlignment, VerticalAlignment};
use crate::ecs::transform::commands::mark_local_transform_dirty;
use crate::ecs::ui::builder::UiTreeBuilder;
use crate::ecs::ui::components::{TextOverflow, UiCanvasData};
use crate::ecs::ui::state::{UiBase, UiStateTrait};
use crate::ecs::ui::units::{Ab, Rl};
use crate::ecs::ui::widgets::world_canvas::{ui_canvas_clear, ui_canvas_rect};
use crate::ecs::ui::widgets::world_layout::{ui_set_text, ui_set_visible};
use crate::ecs::window::resources::{window_scale_factor, window_viewport_size};
use crate::ecs::world::commands::despawn_recursive_immediate;
#[cfg(feature = "audio")]
use crate::ecs::world::commands::spawn_entities;
#[cfg(feature = "audio")]
use crate::ecs::world::{AUDIO_SOURCE, GLOBAL_TRANSFORM, LOCAL_TRANSFORM, NAME};
use crate::ecs::world::{Entity, World};
use crate::render::wgpu::passes::geometry::UiLayer;
#[derive(Default)]
struct FrameOverlay {
fade_color: Vec3,
fade_alpha: f32,
fade_start: f32,
letterbox: f32,
letterbox_start: f32,
title: Option<String>,
title_alpha: f32,
speaker: Option<String>,
dialogue: Option<String>,
focus: Option<(f32, f32)>,
focus_start: f32,
grade: Option<(f32, f32, f32)>,
grade_start: f32,
}
enum CameraSource {
Shot(CutsceneShot),
Follow {
actor: String,
eye_offset: Vec3,
look_height: f32,
field_of_view_degrees: f32,
},
LookAt {
actor: String,
look_height: f32,
damping: f32,
},
}
pub fn advance_cutscene_system(world: &mut World) {
let playing = world.resources.cutscene.playing;
let holding = world.resources.cutscene.holding;
if !playing && !holding {
if let Some(overlay) = world.resources.cutscene.overlay {
reset_overlay(world, overlay);
}
clear_post_effects(world);
stop_cutscene_audio(world);
world.resources.cutscene.camera_smoothed = None;
return;
}
if world.resources.cutscene.active.is_none() {
world.resources.cutscene.playing = false;
world.resources.cutscene.holding = false;
return;
}
let overlay = match world.resources.cutscene.overlay {
Some(overlay) => overlay,
None => {
let overlay = build_overlay(world);
world.resources.cutscene.overlay = Some(overlay);
overlay
}
};
let delta = if playing {
world.resources.window.timing.delta_time
} else {
0.0
};
let cutscene = world.resources.cutscene.active.take().unwrap();
let bindings = world.resources.cutscene.bindings.clone();
let camera = world.resources.cutscene.camera;
let speed = world.resources.cutscene.speed;
let looping = world.resources.cutscene.looping;
let characters_per_second = world.resources.cutscene.dialogue_characters_per_second;
let duration = cutscene.duration();
let cursor = world.resources.cutscene.marker_cursor;
let mut time = world.resources.cutscene.time + delta * speed;
let mut wrapped = false;
let mut just_finished = false;
if playing && time >= duration {
if looping && duration > 0.0 {
time %= duration;
wrapped = true;
} else {
time = duration;
just_finished = true;
}
}
if playing {
if wrapped {
fire_discrete_events(world, &cutscene, cursor, duration);
fire_discrete_events(world, &cutscene, -1.0, time);
} else {
fire_discrete_events(world, &cutscene, cursor, time);
}
world.resources.cutscene.marker_cursor = time;
}
let frame = evaluate_timeline(
world,
&cutscene,
&bindings,
camera,
time,
delta,
characters_per_second,
);
update_overlay(world, overlay, &frame);
reap_cutscene_audio(world, delta);
world.resources.cutscene.active = Some(cutscene);
world.resources.cutscene.time = time;
if just_finished {
if let Some(next) = world.resources.cutscene.queue.pop_front() {
world.resources.cutscene.active = Some(next);
world.resources.cutscene.time = 0.0;
world.resources.cutscene.marker_cursor = -1.0;
world.resources.cutscene.camera_smoothed = None;
world.resources.cutscene.playing = true;
world.resources.cutscene.finished = false;
world.resources.cutscene.holding = false;
} else {
world.resources.cutscene.playing = false;
world.resources.cutscene.finished = true;
world.resources.cutscene.holding = world.resources.cutscene.hold_on_finish;
}
}
}
fn fire_discrete_events(world: &mut World, cutscene: &Cutscene, low: f32, high: f32) {
let mut triggers: Vec<String> = Vec::new();
let mut sounds: Vec<(String, f32)> = Vec::new();
let mut musics: Vec<(String, f32)> = Vec::new();
for event in &cutscene.events {
if event.start > low && event.start <= high {
match &event.action {
CutsceneAction::Trigger { id } => triggers.push(id.clone()),
CutsceneAction::Sound { clip, volume } => sounds.push((clip.clone(), *volume)),
CutsceneAction::Music { track, volume } => musics.push((track.clone(), *volume)),
_ => {}
}
}
}
world.resources.cutscene.fired_markers.extend(triggers);
for (clip, volume) in sounds {
fire_sound(world, &clip, volume);
}
for (track, volume) in musics {
fire_music(world, &track, volume);
}
}
#[cfg(feature = "audio")]
const SOUND_ONESHOT_LIFETIME_SECONDS: f32 = 8.0;
#[cfg(feature = "audio")]
fn fire_sound(world: &mut World, clip: &str, volume: f32) {
if !audio_engine_is_initialized(&world.resources.audio)
|| audio_engine_get_sound(&world.resources.audio, clip).is_none()
{
return;
}
let entity = spawn_entities(
world,
NAME | LOCAL_TRANSFORM | GLOBAL_TRANSFORM | AUDIO_SOURCE,
1,
)[0];
world.core.set_audio_source(
entity,
AudioSource::new(clip.to_string())
.with_volume(volume)
.with_bus(AudioBus::Sfx)
.playing(),
);
world
.resources
.cutscene
.sound_oneshots
.push((entity, SOUND_ONESHOT_LIFETIME_SECONDS));
}
#[cfg(not(feature = "audio"))]
fn fire_sound(_world: &mut World, _clip: &str, _volume: f32) {}
#[cfg(feature = "audio")]
fn fire_music(world: &mut World, track: &str, volume: f32) {
if !audio_engine_is_initialized(&world.resources.audio)
|| audio_engine_get_sound(&world.resources.audio, track).is_none()
{
return;
}
if world.resources.cutscene.music_track.as_deref() == Some(track) {
return;
}
let entity = match world.resources.cutscene.music_entity {
Some(entity) => entity,
None => {
let entity = spawn_entities(
world,
NAME | LOCAL_TRANSFORM | GLOBAL_TRANSFORM | AUDIO_SOURCE,
1,
)[0];
world.resources.cutscene.music_entity = Some(entity);
entity
}
};
world.core.set_audio_source(
entity,
AudioSource::new(track.to_string())
.with_volume(volume)
.with_looping(true)
.with_bus(AudioBus::Music)
.playing(),
);
world.resources.cutscene.music_track = Some(track.to_string());
}
#[cfg(not(feature = "audio"))]
fn fire_music(_world: &mut World, _track: &str, _volume: f32) {}
fn reap_cutscene_audio(world: &mut World, delta: f32) {
let entries = std::mem::take(&mut world.resources.cutscene.sound_oneshots);
let mut survivors = Vec::with_capacity(entries.len());
for (entity, remaining) in entries {
let next = remaining - delta;
if next <= 0.0 {
despawn_recursive_immediate(world, entity);
} else {
survivors.push((entity, next));
}
}
world.resources.cutscene.sound_oneshots = survivors;
}
fn stop_cutscene_audio(world: &mut World) {
if let Some(entity) = world.resources.cutscene.music_entity.take() {
despawn_recursive_immediate(world, entity);
}
world.resources.cutscene.music_track = None;
let entries = std::mem::take(&mut world.resources.cutscene.sound_oneshots);
for (entity, _) in entries {
despawn_recursive_immediate(world, entity);
}
}
fn evaluate_timeline(
world: &mut World,
cutscene: &Cutscene,
bindings: &HashMap<String, Entity>,
camera: Option<Entity>,
time: f32,
delta: f32,
characters_per_second: f32,
) -> FrameOverlay {
let mut frame = FrameOverlay::default();
let mut camera_source: Option<CameraSource> = None;
let mut camera_start = f32::NEG_INFINITY;
let mut shake_offset = Vec3::zeros();
let mut handheld_eye = Vec3::zeros();
let mut handheld_target = Vec3::zeros();
let mut animations: HashMap<String, (String, bool, f32, f32, f32)> = HashMap::new();
for event in &cutscene.events {
if time < event.start {
continue;
}
let progress = event.eased_progress(time);
match &event.action {
CutsceneAction::CameraMove { from, to } => {
if event.start >= camera_start {
camera_start = event.start;
camera_source = Some(CameraSource::Shot(from.interpolate(*to, progress)));
}
}
CutsceneAction::CameraFollow {
actor,
eye_offset,
look_height,
field_of_view_degrees,
} => {
if event.start >= camera_start {
camera_start = event.start;
camera_source = Some(CameraSource::Follow {
actor: actor.clone(),
eye_offset: *eye_offset,
look_height: *look_height,
field_of_view_degrees: *field_of_view_degrees,
});
}
}
CutsceneAction::CameraLookAt {
actor,
look_height,
damping,
} => {
if event.start >= camera_start {
camera_start = event.start;
camera_source = Some(CameraSource::LookAt {
actor: actor.clone(),
look_height: *look_height,
damping: *damping,
});
}
}
CutsceneAction::CameraPath { waypoints } => {
if event.start >= camera_start {
camera_start = event.start;
camera_source =
Some(CameraSource::Shot(sample_camera_path(waypoints, progress)));
}
}
CutsceneAction::CameraShake {
amplitude,
frequency,
} => {
if time < event.end() {
let falloff = 1.0 - progress;
let magnitude = amplitude * falloff;
let phase = time * frequency;
shake_offset += Vec3::new(
(phase * 1.0).sin() * magnitude,
(phase * 1.7 + 1.3).sin() * magnitude,
(phase * 0.7 + 3.1).sin() * magnitude * 0.5,
);
}
}
CutsceneAction::Handheld {
position_amplitude,
target_amplitude,
frequency,
} => {
if time < event.end() {
let phase = time * frequency;
handheld_eye += Vec3::new(
phase.sin() * position_amplitude,
(phase * 0.8 + 1.7).sin() * position_amplitude * 0.7,
(phase * 1.3 + 4.2).sin() * position_amplitude,
);
handheld_target += Vec3::new(
(phase * 1.1 + 2.0).sin() * target_amplitude,
(phase * 0.9 + 0.5).sin() * target_amplitude,
(phase * 1.4 + 5.0).sin() * target_amplitude,
);
}
}
CutsceneAction::ActorMove { actor, from, to } => {
if let Some(&entity) = bindings.get(actor) {
let position = from + (to - from) * progress;
apply_actor_translation(world, entity, position);
}
}
CutsceneAction::ActorTurn {
actor,
from_yaw_radians,
to_yaw_radians,
} => {
if let Some(&entity) = bindings.get(actor) {
let yaw = from_yaw_radians + (to_yaw_radians - from_yaw_radians) * progress;
apply_actor_yaw(world, entity, yaw);
}
}
CutsceneAction::ActorVisible { actor, visible } => {
if let Some(&entity) = bindings.get(actor) {
apply_actor_visibility(world, entity, *visible);
}
}
CutsceneAction::ActorAnimation {
actor,
clip,
looping,
speed,
blend,
} => {
let replace = animations
.get(actor)
.is_none_or(|(_, _, _, _, start)| event.start >= *start);
if replace {
animations.insert(
actor.clone(),
(clip.clone(), *looping, *speed, *blend, event.start),
);
}
}
CutsceneAction::Dialogue { speaker, line } => {
if time < event.end() {
let line = localize(world, line);
frame.speaker = speaker.clone();
frame.dialogue =
Some(typewriter(&line, time - event.start, characters_per_second));
}
}
CutsceneAction::Title { line } => {
if time < event.end() {
frame.title = Some(localize(world, line));
frame.title_alpha = title_alpha(time - event.start, event.duration);
}
}
CutsceneAction::Grade {
exposure_ev,
saturation,
contrast,
} => {
if event.start >= frame.grade_start {
frame.grade_start = event.start;
let exposure = exposure_ev.0 + (exposure_ev.1 - exposure_ev.0) * progress;
let saturation = saturation.0 + (saturation.1 - saturation.0) * progress;
let contrast = contrast.0 + (contrast.1 - contrast.0) * progress;
frame.grade = Some((exposure, saturation, contrast));
}
}
CutsceneAction::Trigger { .. }
| CutsceneAction::Sound { .. }
| CutsceneAction::Music { .. } => {}
CutsceneAction::Fade { from, to, color } => {
if event.start >= frame.fade_start {
frame.fade_start = event.start;
frame.fade_alpha = from + (to - from) * progress;
frame.fade_color = *color;
}
}
CutsceneAction::Letterbox { from, to } => {
if event.start >= frame.letterbox_start {
frame.letterbox_start = event.start;
frame.letterbox = from + (to - from) * progress;
}
}
CutsceneAction::FocusPull {
from_distance,
to_distance,
range,
} => {
if event.start >= frame.focus_start {
frame.focus_start = event.start;
let distance = from_distance + (to_distance - from_distance) * progress;
frame.focus = Some((distance, *range));
}
}
}
}
let mut camera_damping = 0.0;
let camera_shot = camera_source.map(|source| match source {
CameraSource::Shot(shot) => shot,
CameraSource::Follow {
actor,
eye_offset,
look_height,
field_of_view_degrees,
} => {
let focus = actor_position(world, bindings, &actor);
CutsceneShot {
eye: focus + eye_offset,
target: focus + Vec3::new(0.0, look_height, 0.0),
field_of_view_degrees,
roll_degrees: 0.0,
}
}
CameraSource::LookAt {
actor,
look_height,
damping,
} => {
camera_damping = damping;
let focus = actor_position(world, bindings, &actor);
let eye = camera
.and_then(|entity| world.core.get_local_transform(entity))
.map(|transform| transform.translation)
.unwrap_or_default();
let field_of_view_degrees = camera
.and_then(|entity| world.core.get_camera(entity))
.and_then(|component| match &component.projection {
Projection::Perspective(perspective) => {
Some(perspective.y_fov_rad.to_degrees())
}
_ => None,
})
.unwrap_or(45.0);
CutsceneShot {
eye,
target: focus + Vec3::new(0.0, look_height, 0.0),
field_of_view_degrees,
roll_degrees: 0.0,
}
}
});
let camera_shot = camera_shot.map(|shot| {
if camera_damping > 0.0 {
let smoothed = match world.resources.cutscene.camera_smoothed {
Some(previous) => previous.interpolate(shot, (camera_damping * delta).min(1.0)),
None => shot,
};
world.resources.cutscene.camera_smoothed = Some(smoothed);
smoothed
} else {
world.resources.cutscene.camera_smoothed = None;
shot
}
});
if let (Some(camera), Some(mut shot)) = (camera, camera_shot) {
shot.eye += shake_offset + handheld_eye;
shot.target += shake_offset + handheld_target;
apply_camera_shot(world, camera, shot);
}
for (actor, (clip, looping, speed, blend, _)) in &animations {
if let Some(&entity) = bindings.get(actor) {
apply_actor_animation(world, entity, clip, *looping, *speed, *blend);
}
}
frame
}
fn actor_position(world: &World, bindings: &HashMap<String, Entity>, actor: &str) -> Vec3 {
bindings
.get(actor)
.and_then(|entity| world.core.get_local_transform(*entity))
.map(|transform| transform.translation)
.unwrap_or_default()
}
fn localize(world: &World, line: &str) -> String {
world
.resources
.cutscene
.localization
.get(line)
.cloned()
.unwrap_or_else(|| line.to_string())
}
fn apply_actor_animation(
world: &mut World,
entity: Entity,
clip: &str,
looping: bool,
speed: f32,
blend: f32,
) {
let clip_index = world.core.get_animation_player(entity).and_then(|player| {
player
.clips
.iter()
.position(|candidate| candidate.name == clip)
});
if let Some(index) = clip_index
&& let Some(player) = world.core.get_animation_player_mut(entity)
{
player.blend_to(index, blend);
player.looping = looping;
player.speed = speed;
player.playing = true;
}
}
fn typewriter(line: &str, elapsed: f32, characters_per_second: f32) -> String {
if characters_per_second <= 0.0 {
return line.to_string();
}
let total = line.chars().count();
let revealed = ((elapsed.max(0.0) * characters_per_second).floor() as usize).min(total);
line.chars().take(revealed).collect()
}
fn apply_camera_shot(world: &mut World, camera: Entity, shot: CutsceneShot) {
if let Some(transform) = world.core.get_local_transform_mut(camera) {
transform.translation = shot.eye;
transform.rotation = camera_look_rotation(shot.eye, shot.target, shot.roll_degrees);
}
if let Some(camera_component) = world.core.get_camera_mut(camera)
&& let Projection::Perspective(perspective) = &mut camera_component.projection
{
perspective.y_fov_rad = shot.field_of_view_degrees.to_radians();
}
mark_local_transform_dirty(world, camera);
}
fn apply_actor_translation(world: &mut World, entity: Entity, position: Vec3) {
if let Some(transform) = world.core.get_local_transform_mut(entity) {
transform.translation = position;
mark_local_transform_dirty(world, entity);
}
}
fn apply_actor_yaw(world: &mut World, entity: Entity, yaw_radians: f32) {
if let Some(transform) = world.core.get_local_transform_mut(entity) {
transform.rotation = nalgebra_glm::quat_angle_axis(yaw_radians, &Vec3::y());
mark_local_transform_dirty(world, entity);
}
}
fn apply_actor_visibility(world: &mut World, entity: Entity, visible: bool) {
if let Some(visibility) = world.core.get_visibility_mut(entity) {
visibility.visible = visible;
} else {
world
.core
.add_components(entity, crate::ecs::world::VISIBILITY);
world.core.set_visibility(entity, Visibility { visible });
}
}
fn title_alpha(local_time: f32, duration: f32) -> f32 {
if duration <= 0.0 {
return 1.0;
}
let fade = (duration * 0.35).min(0.7);
if fade <= 0.0 {
return 1.0;
}
if local_time < fade {
(local_time / fade).clamp(0.0, 1.0)
} else if local_time > duration - fade {
((duration - local_time) / fade).clamp(0.0, 1.0)
} else {
1.0
}
}
fn update_overlay(world: &mut World, overlay: CutsceneOverlay, frame: &FrameOverlay) {
apply_post_effects(world, frame);
let scale = window_scale_factor(world).max(0.01);
let (physical_width, physical_height) = window_viewport_size(world).unwrap_or((1280, 720));
let size = Vec2::new(
physical_width as f32 / scale,
physical_height as f32 / scale,
);
ui_canvas_clear(world, overlay.canvas);
let bar_height = size.y * 0.12 * frame.letterbox.clamp(0.0, 1.0);
if bar_height > 0.5 {
let black = Vec4::new(0.0, 0.0, 0.0, 1.0);
ui_canvas_rect(
world,
overlay.canvas,
Vec2::zeros(),
Vec2::new(size.x, bar_height),
black,
0.0,
);
ui_canvas_rect(
world,
overlay.canvas,
Vec2::new(0.0, size.y - bar_height),
Vec2::new(size.x, bar_height),
black,
0.0,
);
}
if frame.fade_alpha > 0.001 {
let color = Vec4::new(
frame.fade_color.x,
frame.fade_color.y,
frame.fade_color.z,
frame.fade_alpha.clamp(0.0, 1.0),
);
ui_canvas_rect(world, overlay.canvas, Vec2::zeros(), size, color, 0.0);
}
match &frame.title {
Some(line) => {
ui_set_text(world, overlay.title, line);
set_node_alpha(
world,
overlay.title,
Vec4::new(1.0, 0.96, 0.88, frame.title_alpha),
);
ui_set_visible(world, overlay.title, true);
}
None => ui_set_visible(world, overlay.title, false),
}
match &frame.dialogue {
Some(line) => {
ui_set_visible(world, overlay.dialogue_panel, true);
ui_set_text(world, overlay.dialogue, line);
set_node_alpha(world, overlay.dialogue, Vec4::new(0.96, 0.96, 1.0, 1.0));
ui_set_visible(world, overlay.dialogue, true);
match &frame.speaker {
Some(speaker) => {
ui_set_text(world, overlay.speaker, speaker);
set_node_alpha(world, overlay.speaker, Vec4::new(1.0, 0.82, 0.4, 1.0));
ui_set_visible(world, overlay.speaker, true);
}
None => ui_set_visible(world, overlay.speaker, false),
}
}
None => {
ui_set_visible(world, overlay.dialogue_panel, false);
ui_set_visible(world, overlay.dialogue, false);
ui_set_visible(world, overlay.speaker, false);
}
}
}
fn reset_overlay(world: &mut World, overlay: CutsceneOverlay) {
ui_canvas_clear(world, overlay.canvas);
ui_set_visible(world, overlay.title, false);
ui_set_visible(world, overlay.speaker, false);
ui_set_visible(world, overlay.dialogue, false);
ui_set_visible(world, overlay.dialogue_panel, false);
}
fn apply_post_effects(world: &mut World, frame: &FrameOverlay) {
let settings = &mut world.resources.render_settings;
match frame.focus {
Some((distance, range)) => {
settings.depth_of_field.enabled = true;
settings.depth_of_field.focus_distance = distance;
settings.depth_of_field.focus_range = range;
}
None => settings.depth_of_field.enabled = false,
}
if let Some((exposure_ev, saturation, contrast)) = frame.grade {
settings.color_grading.exposure_compensation_ev = exposure_ev;
settings.color_grading.saturation = saturation;
settings.color_grading.contrast = contrast;
}
}
fn clear_post_effects(world: &mut World) {
let settings = &mut world.resources.render_settings;
settings.depth_of_field.enabled = false;
settings.color_grading.exposure_compensation_ev = 0.0;
settings.color_grading.saturation = 1.0;
settings.color_grading.contrast = 1.0;
}
fn set_node_alpha(world: &mut World, entity: Entity, color: Vec4) {
if let Some(node_color) = world.ui.get_ui_node_color_mut(entity) {
node_color.colors[UiBase::INDEX] = Some(color);
node_color.computed_color = color;
}
}
fn build_overlay(world: &mut World) -> CutsceneOverlay {
world.resources.retained_ui.enabled = true;
let outline = Vec4::new(0.0, 0.0, 0.0, 0.85);
let mut tree = UiTreeBuilder::new(world);
let canvas = tree
.add_node()
.boundary(Ab(Vec2::zeros()), Rl(Vec2::new(100.0, 100.0)))
.with_layer(UiLayer::Tooltips)
.with_clip()
.entity();
let title = tree
.add_node()
.boundary(Ab(Vec2::zeros()), Rl(Vec2::new(100.0, 100.0)))
.with_text("", 58.0)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_text_outline(outline, 0.4)
.color_raw::<UiBase>(Vec4::new(1.0, 0.96, 0.88, 1.0))
.with_layer(UiLayer::Popups)
.with_z_index(10)
.entity();
let dialogue_panel = tree
.add_node()
.boundary(Rl(Vec2::new(8.0, 72.0)), Rl(Vec2::new(92.0, 88.0)))
.rect(12.0)
.color_raw::<UiBase>(Vec4::new(0.03, 0.04, 0.06, 0.62))
.with_layer(UiLayer::Popups)
.with_z_index(5)
.entity();
let speaker = tree
.add_node()
.boundary(Rl(Vec2::new(0.0, 68.0)), Rl(Vec2::new(100.0, 74.0)))
.with_text("", 22.0)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_text_outline(outline, 0.4)
.color_raw::<UiBase>(Vec4::new(1.0, 0.82, 0.4, 1.0))
.with_layer(UiLayer::Popups)
.with_z_index(10)
.entity();
let dialogue = tree
.add_node()
.boundary(Rl(Vec2::new(12.0, 74.0)), Rl(Vec2::new(88.0, 86.0)))
.with_text("", 28.0)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_text_overflow(TextOverflow::Wrap)
.with_text_outline(outline, 0.4)
.color_raw::<UiBase>(Vec4::new(0.96, 0.96, 1.0, 1.0))
.with_layer(UiLayer::Popups)
.with_z_index(10)
.entity();
tree.finish();
world.ui.set_ui_canvas(
canvas,
UiCanvasData {
commands: Vec::new(),
hit_test_enabled: false,
command_bounds: Vec::new(),
},
);
let overlay = CutsceneOverlay {
canvas,
title,
speaker,
dialogue,
dialogue_panel,
};
reset_overlay(world, overlay);
overlay
}