1use core::f32::consts::TAU;
23use core::ops::Range;
24use core::time::Duration;
25
26use mirage_engine::prelude::*;
27
28const ELF_SOURCE: &str = "examples/assets/elf.glb";
30const ELF_ROOT: &str = "Elf";
32const ELF_HEIGHT: f32 = 1.6;
36
37const WINDOW_WIDTH: u32 = 1280;
40const WINDOW_HEIGHT: u32 = 720;
41
42const ELF_SPEED: f32 = 4.0;
44const WALK_CAP: f32 = 0.5;
47const WALK_THRESHOLD: f32 = 0.1;
49const TURN_RATE: f32 = TAU * 2.0;
51
52const JUMP_LAUNCH_SPEED: f32 = 4.5;
56const GRAVITY: f32 = 9.8;
59
60const IDLE_LOCOMOTION_FADE: Duration = Duration::from_millis(200);
61const ATTACK_ENTER_FADE: Duration = Duration::from_millis(80);
62const ATTACK_CHAIN_FADE: Duration = Duration::from_millis(60);
63const ATTACK_EXIT_FADE: Duration = Duration::from_millis(200);
64const ATTACK_CHAIN_ENTRY: f32 = 0.15;
67const ATTACK_RELEASE: f32 = 0.8;
70const HIT_ENTER_FADE: Duration = Duration::from_millis(50);
71const HIT_EXIT_FADE: Duration = Duration::from_millis(150);
72const DEATH_FADE: Duration = Duration::from_millis(150);
73const JUMP_ENTER_FADE: Duration = Duration::from_millis(100);
74const JUMP_EXIT_FADE: Duration = Duration::from_millis(150);
75const SIT_DOWN_FADE: Duration = Duration::from_millis(200);
76const STAND_UP_FADE: Duration = Duration::from_millis(150);
77const STAND_EXIT_FADE: Duration = Duration::from_millis(150);
78const DANCE_FADE: Duration = Duration::from_millis(200);
79
80const ELF_START: Vec3 = Vec3::new(-3.0, 0.0, 4.0);
81
82const HURT_PATCHES: [Vec3; 3] = [
84 Vec3::new(1.5, 0.0, -1.0),
85 Vec3::new(-1.5, 0.0, -3.5),
86 Vec3::new(3.0, 0.0, 2.0),
87];
88const HURT_RADIUS: f32 = 0.9;
89const FATAL_HITS: u32 = 3;
91
92const SEAT_POSITION: Vec3 = Vec3::new(-3.5, 0.0, -3.0);
98const SEAT_FOOTPRINT: f32 = 1.0;
99const SEAT_HEIGHT: f32 = 0.45;
100const SEATED_HEIGHT: f32 = 0.75;
102const SEAT_HEAD_HEIGHT: f32 = SEAT_HEIGHT + SEATED_HEIGHT;
104const SEAT_STAND_CLEARANCE: f32 = 0.05;
106const SEAT_SPOT: Vec3 = Vec3::new(
110 SEAT_POSITION.x,
111 0.0,
112 SEAT_POSITION.z + SEAT_FOOTPRINT * 0.5 + SEAT_STAND_CLEARANCE,
113);
114const SEAT_FACING: f32 = 0.0;
115const SEAT_INTERACT_RADIUS: f32 = 1.6;
118
119const SCRUBBED_ELF_POSITION: Vec3 = Vec3::new(3.5, 0.0, 4.0);
122const SCRUB_NEAR: f32 = 1.5;
124const SCRUB_FAR: f32 = 5.0;
126
127const LAMP_POST_POSITION: Vec3 = Vec3::new(-4.9, 0.0, -2.0);
129const LAMP_POST_HEIGHT: f32 = 2.2;
130const LAMP_POST_THICKNESS: f32 = 0.16;
131const LAMP_POST_COLOR: Color = Color::rgb(0.16, 0.14, 0.12);
132const LAMP_HEAD_SIZE: f32 = 0.34;
134const LAMP_HEAD_GAP: f32 = 0.06;
138const LAMP_LIGHT_COLOR: Color = Color::rgb(5.5, 4.2, 2.2);
141const LAMP_LIGHT_RANGE: f32 = 6.0;
142
143const SPOT_POSITION: Vec3 = Vec3::new(1.0, 6.0, -0.83);
146const SPOT_DIRECTION: Vec3 = Vec3::NEG_Y;
147const SPOT_COLOR: Color = Color::rgb(11.0, 9.8, 8.2);
150const SPOT_RANGE: f32 = 9.0;
151const SPOT_ANGLE: f32 = 0.85;
152const SPOT_FIXTURE_SIZE: f32 = 0.22;
154const SPOT_FIXTURE_COLOR: Color = Color::rgb(0.2, 0.2, 0.22);
155
156const BUTTERFLY_SOURCE: &str = "examples/assets/butterfly.glb";
158const BUTTERFLY_ROOT: &str = "Butterfly";
160const BUTTERFLY_CENTER: Vec3 = Vec3::new(-4.2, 0.0, -2.5);
163const BUTTERFLY_RADIUS: Vec2 = Vec2::new(1.8, 1.4);
166const BUTTERFLY_HEIGHT: f32 = LAMP_POST_HEIGHT;
168const BUTTERFLY_ANGULAR_SPEED: f32 = TAU / 14.0;
170const BUTTERFLY_LIGHT_COLOR: Color = Color::rgb(1.8, 5.5, 5.0);
172const BUTTERFLY_LIGHT_RANGE: f32 = 3.0;
173const BUTTERFLY_EMISSIVE: Color = Color::rgb(0.6, 1.8, 1.6);
176
177const GROUND_SIZE: f32 = 400.0;
178const GROUND_COLOR: Color = Color::rgb(0.24, 0.30, 0.22);
179const HURT_COLOR: Color = Color::rgb(0.75, 0.12, 0.10);
180const SEAT_COLOR: Color = Color::rgb(0.5, 0.42, 0.3);
181const SUN_DIRECTION: Vec3 = Vec3::new(-0.85, -0.18, -0.5);
183const SUN_COLOR: Color = Color::rgb(0.55, 0.32, 0.22);
184const SKY_ZENITH: Color = Color::rgb(0.06, 0.07, 0.2);
185const SKY_HORIZON: Color = Color::rgb(0.55, 0.35, 0.28);
186const SKY_NADIR: Color = Color::rgb(0.05, 0.05, 0.07);
187const SKY_LIGHT: f32 = 0.15;
190
191const CAMERA_BACK: f32 = 3.4;
193const CAMERA_UP: f32 = 1.7;
194const CAMERA_LOOK_HEIGHT: f32 = 0.8;
196const CAMERA_FOV: f32 = 50.0;
197const CAMERA_YAW_PER_DRAG: f32 = core::f32::consts::PI;
201const CAMERA_PITCH_PER_DRAG: f32 = core::f32::consts::FRAC_PI_3;
202const CAMERA_YAW_SCALE: f32 = CAMERA_YAW_PER_DRAG / WINDOW_WIDTH as f32;
203const CAMERA_PITCH_SCALE: f32 = CAMERA_PITCH_PER_DRAG / WINDOW_HEIGHT as f32;
204const CAMERA_PITCH_RANGE: Range<f32> = -0.4..0.9;
208
209const CONTROLS: [(&str, &str); 10] = [
211 ("mouse", "turns the camera"),
212 ("click", "locks the pointer"),
213 ("escape", "frees the pointer"),
214 ("wasd or arrows", "walk"),
215 ("left shift", "runs"),
216 ("f", "attacks, chains on a second press"),
217 ("space", "jumps"),
218 ("n", "dances while idle"),
219 ("e", "sits on the seat and stands back up"),
220 ("r", "starts a new elf"),
221];
222
223const PANEL_TEXT_COLOR: egui::Color32 = egui::Color32::from_gray(230);
225const PANEL_BACKDROP: u8 = 190;
227const PANEL_PADDING: i8 = 8;
229const PROMPT_SIZE: f32 = 15.0;
231const PROMPT_LIFT: f32 = 0.35;
233const PROMPT_PADDING: f32 = 4.0;
235
236meshes! { enum Shape { Plane, Cube, Elf, Butterfly } }
237
238#[derive(Catalog, Clone, Copy, Debug, Eq, Hash, PartialEq)]
240enum Sky {
241 Day,
242}
243
244impl Skyboxes for Sky {
245 fn build(&self, _assets: &Assets) -> SkyboxData {
246 match self {
247 Self::Day => SkyboxData::gradient(SKY_ZENITH, SKY_HORIZON, SKY_NADIR).lit_by(SKY_LIGHT),
248 }
249 }
250}
251
252#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
253struct Elf;
254
255#[derive(Clip, Clone, Debug, PartialEq, Eq, Hash)]
258enum ElfClip {
259 #[clip("idle")]
260 Idle,
261 #[clip("walk")]
262 Walk,
263 #[clip("jog")]
264 Jog,
265 #[clip("attack")]
266 Attack,
267 #[clip("hit")]
268 Hit,
269 #[clip("death")]
270 Death,
271 #[clip("sit_down")]
272 SitDown,
273 #[clip("sit")]
274 Sit,
275 #[clip("stand_up")]
276 StandUp,
277 #[clip("jump")]
278 Jump,
279 #[clip("dance")]
280 Dance,
281}
282
283impl Mesh<NoParts, ElfClip> for Elf {
284 fn build(&self, assets: &Assets) -> MeshData<NoParts, ElfClip> {
285 assets.model(ELF_ROOT)
286 }
287}
288
289#[derive(Default)]
291struct ElfInput {
292 speed: f32,
294 attack: bool,
295 hit: bool,
298 dying: bool,
301 jump: bool,
302 landed: bool,
305 dance: bool,
306 interact: bool,
309 near_seat: bool,
311}
312
313#[derive(Clone, Copy, Eq, PartialEq, Debug)]
314enum ElfState {
315 Idle,
316 Locomotion,
317 Attack,
318 Hit,
319 Death,
320 SitDown,
321 Sit,
322 StandUp,
323 Jump,
324 Dance,
325}
326
327impl ElfState {
328 fn seated(self) -> bool {
330 matches!(self, Self::SitDown | Self::Sit | Self::StandUp)
331 }
332
333 fn grounded(input: &ElfInput) -> Self {
336 match input.speed > WALK_THRESHOLD {
337 true => Self::Locomotion,
338 false => Self::Idle,
339 }
340 }
341}
342
343impl AnimationStates for ElfState {
344 type Clip = ElfClip;
345 type Input = ElfInput;
346
347 fn entry() -> Self {
348 Self::Idle
349 }
350
351 fn motion(&self, input: &ElfInput) -> Motion<ElfClip> {
352 match self {
353 Self::Idle => Motion::looping(ElfClip::Idle),
354 Self::Locomotion => {
355 Motion::blend(ElfClip::Walk, ElfClip::Jog, input.speed).paced(input.speed)
356 }
357 Self::Attack => Motion::once(ElfClip::Attack),
358 Self::Hit => Motion::once(ElfClip::Hit),
359 Self::Death => Motion::once(ElfClip::Death),
360 Self::SitDown => Motion::once(ElfClip::SitDown),
361 Self::Sit => Motion::looping(ElfClip::Sit),
362 Self::StandUp => Motion::once(ElfClip::StandUp),
363 Self::Jump => Motion::once(ElfClip::Jump),
364 Self::Dance => Motion::looping(ElfClip::Dance),
365 }
366 }
367
368 fn next(&self, input: &ElfInput, at: Progress) -> Option<Transition<Self>> {
369 match (self, input) {
370 (Self::Death, _) => None,
371 (_, ElfInput { dying: true, .. }) => Some(Self::Death.fade(DEATH_FADE)),
372 (_, ElfInput { hit: true, .. }) if *self != Self::Hit => {
373 Some(Self::Hit.fade(HIT_ENTER_FADE))
374 }
375 (Self::Hit, _) if at.ended() => Some(ElfState::grounded(input).fade(HIT_EXIT_FADE)),
376 (Self::Attack, ElfInput { attack: true, .. }) if at.past(ATTACK_RELEASE) => Some(
377 Self::Attack
378 .restarted()
379 .entering_at(ATTACK_CHAIN_ENTRY)
380 .fade(ATTACK_CHAIN_FADE),
381 ),
382 (Self::Attack, _) if at.past(ATTACK_RELEASE) => {
383 Some(ElfState::grounded(input).fade(ATTACK_EXIT_FADE))
384 }
385 (Self::SitDown | Self::Sit | Self::StandUp, i) if i.speed > WALK_THRESHOLD => {
386 Some(Self::Locomotion.fade(STAND_EXIT_FADE))
387 }
388 (Self::SitDown | Self::Sit | Self::StandUp, ElfInput { attack: true, .. }) => {
389 Some(Self::Attack.fade(ATTACK_ENTER_FADE))
390 }
391 (Self::SitDown | Self::Sit | Self::StandUp, ElfInput { jump: true, .. }) => {
392 Some(Self::Jump.fade(JUMP_ENTER_FADE))
393 }
394 (Self::SitDown, _) if at.ended() => Some(Self::Sit.at_once()),
395 (Self::Sit, ElfInput { interact: true, .. }) => Some(Self::StandUp.fade(STAND_UP_FADE)),
396 (Self::StandUp, _) if at.ended() => Some(Self::Idle.fade(STAND_EXIT_FADE)),
397 (Self::Jump, ElfInput { landed: true, .. }) => {
398 Some(ElfState::grounded(input).fade(JUMP_EXIT_FADE))
399 }
400 (Self::Jump, _) if at.ended() => Some(ElfState::grounded(input).fade(JUMP_EXIT_FADE)),
401 (
402 Self::Idle | Self::Locomotion,
403 ElfInput {
404 interact: true,
405 near_seat: true,
406 ..
407 },
408 ) => Some(Self::SitDown.fade(SIT_DOWN_FADE)),
409 (Self::Idle | Self::Locomotion, ElfInput { attack: true, .. }) => {
410 Some(Self::Attack.fade(ATTACK_ENTER_FADE))
411 }
412 (Self::Idle | Self::Locomotion, ElfInput { jump: true, .. }) => {
413 Some(Self::Jump.fade(JUMP_ENTER_FADE))
414 }
415 (Self::Idle, ElfInput { dance: true, .. }) => Some(Self::Dance.fade(DANCE_FADE)),
416 (Self::Dance, i) if i.speed > WALK_THRESHOLD => {
417 Some(Self::Locomotion.fade(IDLE_LOCOMOTION_FADE))
418 }
419 (Self::Idle, i) if i.speed > WALK_THRESHOLD => {
420 Some(Self::Locomotion.fade(IDLE_LOCOMOTION_FADE))
421 }
422 (Self::Locomotion, i) if i.speed <= WALK_THRESHOLD => {
423 Some(Self::Idle.fade(IDLE_LOCOMOTION_FADE))
424 }
425 _ => None,
426 }
427 }
428}
429
430#[derive(Clone, Copy, Eq, PartialEq, Debug)]
432enum ScrubbedState {
433 SitDown,
434}
435
436#[derive(Default)]
438struct ScrubbedInput {
439 settled: f32,
442}
443
444impl AnimationStates for ScrubbedState {
445 type Clip = ElfClip;
446 type Input = ScrubbedInput;
447
448 fn entry() -> Self {
449 Self::SitDown
450 }
451
452 fn motion(&self, input: &ScrubbedInput) -> Motion<ElfClip> {
453 Motion::scrubbed(ElfClip::SitDown, input.settled)
454 }
455
456 fn next(&self, _input: &ScrubbedInput, _at: Progress) -> Option<Transition<Self>> {
457 None
458 }
459}
460
461#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
462struct Butterfly;
463
464#[derive(Clip, Clone, Debug, PartialEq, Eq, Hash)]
466enum ButterflyClip {
467 #[clip("fly")]
468 Fly,
469}
470
471impl Mesh<NoParts, ButterflyClip> for Butterfly {
472 fn build(&self, assets: &Assets) -> MeshData<NoParts, ButterflyClip> {
473 assets.model(BUTTERFLY_ROOT)
474 }
475}
476
477#[derive(Clone, Copy, Eq, PartialEq, Debug)]
479enum FlyingState {
480 Flying,
481}
482
483impl AnimationStates for FlyingState {
484 type Clip = ButterflyClip;
485 type Input = ();
486
487 fn entry() -> Self {
488 Self::Flying
489 }
490
491 fn motion(&self, _input: &()) -> Motion<ButterflyClip> {
492 Motion::looping(ButterflyClip::Fly)
493 }
494
495 fn next(&self, _input: &(), _at: Progress) -> Option<Transition<Self>> {
496 None
497 }
498}
499
500fn butterfly_pose(t: f32) -> (Vec3, f32) {
503 let angle = t * BUTTERFLY_ANGULAR_SPEED;
504 let position = BUTTERFLY_CENTER
505 + Vec3::new(
506 BUTTERFLY_RADIUS.x * angle.cos(),
507 BUTTERFLY_HEIGHT,
508 BUTTERFLY_RADIUS.y * angle.sin(),
509 );
510 let direction = Vec3::new(
511 -BUTTERFLY_RADIUS.x * angle.sin(),
512 0.0,
513 BUTTERFLY_RADIUS.y * angle.cos(),
514 );
515 (position, direction.x.atan2(direction.z))
516}
517
518fn settled_at(distance: f32) -> f32 {
521 1.0 - (distance - SCRUB_NEAR) / (SCRUB_FAR - SCRUB_NEAR)
522}
523
524fn orbit_camera(target: Vec3, yaw: f32, pitch: f32) -> Camera {
528 let look_at = target + Vec3::Y * CAMERA_LOOK_HEIGHT;
529 let base = Vec3::new(0.0, CAMERA_UP, CAMERA_BACK);
530 let offset = Quat::from_rotation_y(yaw) * (Quat::from_rotation_x(pitch) * base);
531 Camera::new(
532 View::look_at(look_at + offset, look_at),
533 Projection::perspective(CAMERA_FOV),
534 )
535}
536
537fn logical(pixel: Vec2, pixels_per_point: f32) -> egui::Pos2 {
539 let point = pixel / pixels_per_point;
540 egui::pos2(point.x, point.y)
541}
542
543#[derive(InputButtonAction, Clone, Copy)]
544enum Button {
545 Run,
546 Attack,
547 Jump,
548 Dance,
549 Interact,
550 Restart,
551 Hold,
552 Release,
553}
554
555impl InputButtonAction for Button {
556 fn bindings(&self) -> Vec<ButtonBinding> {
557 match self {
558 Button::Run => vec![Key::LeftShift.into()],
559 Button::Attack => vec![Key::F.into()],
560 Button::Jump => vec![Key::Space.into()],
561 Button::Dance => vec![Key::N.into()],
562 Button::Interact => vec![Key::E.into()],
563 Button::Restart => vec![Key::R.into()],
564 Button::Hold => vec![MouseButton::Left.into()],
565 Button::Release => vec![Key::Escape.into()],
566 }
567 }
568}
569
570#[derive(InputAxisAction, Clone, Copy)]
573enum Axis {
574 CameraYaw,
575 CameraPitch,
576}
577
578impl InputAxisAction for Axis {
579 fn bindings(&self) -> Vec<AxisBinding> {
580 match self {
581 Axis::CameraYaw => {
582 vec![AxisBinding::pointer_delta(PointerDelta::Sideways).scale(CAMERA_YAW_SCALE)]
583 }
584 Axis::CameraPitch => {
585 vec![AxisBinding::pointer_delta(PointerDelta::Up).scale(CAMERA_PITCH_SCALE)]
586 }
587 }
588 }
589}
590
591#[derive(InputAxis2Action, Clone, Copy)]
592enum Move {
593 Walk,
594}
595
596impl InputAxis2Action for Move {
597 fn bindings(&self) -> Vec<Axis2Binding> {
598 match self {
599 Move::Walk => vec![
600 Axis2Binding::from(ButtonAxis2 {
601 left: Key::A,
602 right: Key::D,
603 down: Key::S,
604 up: Key::W,
605 }),
606 Axis2Binding::from(ButtonAxis2 {
607 left: Key::Left,
608 right: Key::Right,
609 down: Key::Down,
610 up: Key::Up,
611 }),
612 ],
613 }
614 }
615}
616
617struct Controls;
618
619impl InputActions for Controls {
620 type Button = Button;
621 type Axis = Axis;
622 type Axis2 = Move;
623}
624
625fn panel_text(text: impl Into<String>) -> egui::RichText {
627 egui::RichText::new(text.into()).color(PANEL_TEXT_COLOR)
628}
629
630struct Scene {
631 elf_pos: Vec3,
632 elf_prev: Vec3,
633 elf_yaw: f32,
634 elf_height: f32,
637 elf_height_prev: f32,
638 jump_speed: f32,
641 elf_input: ElfInput,
642 elf_animator: Animator<Elf, ElfState>,
643 scrubbed_animator: Animator<Elf, ScrubbedState>,
644 butterfly_animator: Animator<Butterfly, FlyingState>,
645 hits: u32,
646 in_patch: bool,
649 holding: bool,
651 camera_yaw: f32,
653 camera_pitch: f32,
654 last_event: &'static str,
656}
657
658impl Scene {
659 fn init(_ctx: &mut InitContext<'_, Scene>) -> Result<Self, Error> {
660 Ok(Self {
661 elf_pos: ELF_START,
662 elf_prev: ELF_START,
663 elf_yaw: 0.0,
664 elf_height: 0.0,
665 elf_height_prev: 0.0,
666 jump_speed: 0.0,
667 elf_input: ElfInput::default(),
668 elf_animator: Animator::new(),
669 scrubbed_animator: Animator::new(),
670 butterfly_animator: Animator::new(),
671 hits: 0,
672 in_patch: false,
673 holding: false,
674 camera_yaw: 0.0,
675 camera_pitch: 0.0,
676 last_event: "none yet",
677 })
678 }
679
680 fn steer_camera(&mut self, ctx: &mut FrameContext<'_, Scene>) {
683 self.camera_yaw -= ctx.axis(Axis::CameraYaw);
684 self.camera_pitch = (self.camera_pitch + ctx.axis(Axis::CameraPitch))
685 .clamp(CAMERA_PITCH_RANGE.start, CAMERA_PITCH_RANGE.end);
686 }
687
688 fn advance(&mut self, ctx: &mut TickContext<'_, Scene>) -> f32 {
693 let control = ctx.axis2(Move::Walk).clamp_length_max(1.0);
694 let turn = Quat::from_rotation_y(self.camera_yaw);
695 let heading = turn * Vec3::X * control.x + turn * Vec3::NEG_Z * control.y;
696 let dt = ctx.dt().as_secs_f32();
697 if let Some(direction) = heading.try_normalize() {
698 let wanted = direction.x.atan2(direction.z);
699 let turn = (wanted - self.elf_yaw + core::f32::consts::PI).rem_euclid(TAU)
700 - core::f32::consts::PI;
701 self.elf_yaw += turn.clamp(-TURN_RATE * dt, TURN_RATE * dt);
702 }
703 let cap = if ctx.down(Button::Run) { 1.0 } else { WALK_CAP };
704 self.elf_pos += heading * cap * ELF_SPEED * dt;
705 heading.length() * cap
706 }
707
708 fn fall(&mut self, dt: f32) -> bool {
712 let off_ground = self.elf_height > 0.0;
713 self.jump_speed -= GRAVITY * dt;
714 self.elf_height = (self.elf_height + self.jump_speed * dt).max(0.0);
715 if self.elf_height == 0.0 {
716 self.jump_speed = 0.0;
717 }
718 off_ground && self.elf_height == 0.0
719 }
720
721 fn patch_underfoot(&self) -> Option<Vec3> {
723 HURT_PATCHES
724 .into_iter()
725 .find(|&patch| self.elf_pos.distance(patch) < HURT_RADIUS)
726 }
727
728 fn tick_elf(&mut self, ctx: &mut TickContext<'_, Scene>) {
731 let grounded = matches!(
732 self.elf_animator.state(),
733 ElfState::Idle | ElfState::Locomotion
734 );
735
736 self.elf_input.speed = self.advance(ctx);
737 self.elf_input.attack = ctx.pressed(Button::Attack);
738 self.elf_input.jump = ctx.pressed(Button::Jump);
739 self.elf_input.dance = ctx.pressed(Button::Dance);
740
741 self.elf_input.near_seat = self.elf_pos.distance(SEAT_POSITION) < SEAT_INTERACT_RADIUS;
742 self.elf_input.interact = ctx.pressed(Button::Interact);
743 if self.elf_input.interact && grounded && self.elf_input.near_seat {
744 self.elf_pos = SEAT_SPOT;
745 self.elf_yaw = SEAT_FACING;
746 }
747
748 let underfoot = self.patch_underfoot();
749 let entered_patch = underfoot.is_some() && !self.in_patch;
750 self.in_patch = underfoot.is_some();
751 self.hits += u32::from(entered_patch);
752 self.elf_input.hit = entered_patch && self.hits < FATAL_HITS;
753 self.elf_input.dying = entered_patch && self.hits >= FATAL_HITS;
754 if entered_patch {
755 self.last_event = match self.elf_input.dying {
756 true => "elf died",
757 false => "elf hit",
758 };
759 }
760 }
761
762 fn restart_elf(&mut self) {
765 self.elf_animator = Animator::new();
766 self.elf_pos = ELF_START;
767 self.elf_prev = ELF_START;
768 self.elf_yaw = 0.0;
769 self.elf_height = 0.0;
770 self.elf_height_prev = 0.0;
771 self.jump_speed = 0.0;
772 self.elf_input = ElfInput::default();
773 self.hits = 0;
774 self.in_patch = false;
775 self.last_event = "new elf started";
776 }
777
778 fn panel(&self, ctx: &mut FrameContext<'_, Scene>) {
779 let state = match self.elf_animator.state() {
780 ElfState::Idle => "idle",
781 ElfState::Locomotion if self.elf_input.speed > WALK_CAP => "running",
782 ElfState::Locomotion => "walking",
783 ElfState::Attack => "attacking",
784 ElfState::Hit => "hit",
785 ElfState::Death => "dead",
786 ElfState::SitDown => "sitting down",
787 ElfState::Sit => "sitting",
788 ElfState::StandUp => "standing up",
789 ElfState::Jump => "jumping",
790 ElfState::Dance => "dancing",
791 };
792 ctx.ui(|ui| {
793 egui::Frame::new()
794 .fill(egui::Color32::from_black_alpha(PANEL_BACKDROP))
795 .inner_margin(PANEL_PADDING)
796 .corner_radius(f32::from(PANEL_PADDING))
797 .show(ui, |ui| {
798 ui.heading(panel_text(format!("elf is {state}")));
799 ui.label(panel_text(format!(
800 "hits taken {} of the {} red patches hurt for, {}",
801 self.hits, FATAL_HITS, self.last_event
802 )));
803 ui.label(panel_text(match self.elf_animator.transitioning() {
804 true => "fading between clips",
805 false => "one clip playing",
806 }));
807 ui.add_space(f32::from(PANEL_PADDING));
808 egui::Grid::new("controls").show(ui, |ui| {
809 for (key, does) in CONTROLS {
810 ui.label(panel_text(key));
811 ui.label(panel_text(does));
812 ui.end_row();
813 }
814 });
815 });
816 });
817 }
818
819 fn draw_prompts(&self, ctx: &mut FrameContext<'_, Scene>, camera: Camera) {
824 let sit_key = ctx
825 .bindings(Button::Interact)
826 .into_iter()
827 .next()
828 .map_or_else(|| "interact".to_owned(), |binding| binding.to_string());
829 let sit = ctx.text_layout(
830 &format!("{sit_key} sits"),
831 egui::FontId::proportional(PROMPT_SIZE),
832 );
833 let hurts = ctx.text_layout("hurts", egui::FontId::proportional(PROMPT_SIZE));
834 let walk_closer = ctx.text_layout("walk closer", egui::FontId::proportional(PROMPT_SIZE));
835
836 let mut prompts = vec![(
837 SCRUBBED_ELF_POSITION + Vec3::Y * (ELF_HEIGHT + PROMPT_LIFT),
838 walk_closer,
839 )];
840 if !self.elf_animator.state().seated() {
841 prompts.push((
842 SEAT_POSITION + Vec3::Y * (SEAT_HEAD_HEIGHT + PROMPT_LIFT),
843 sit,
844 ));
845 }
846 prompts.extend(HURT_PATCHES.map(|patch| (patch + Vec3::Y * PROMPT_LIFT, hurts.clone())));
847
848 let window_size = ctx.window_size();
849 let pixels_per_point = ctx.pixels_per_point();
850 ctx.ui(|ui| {
851 let painter = ui.painter();
852 for (point, galley) in prompts {
853 let Some(pixel) = camera.pixel_of(point, window_size) else {
854 continue;
855 };
856 let at = logical(pixel, pixels_per_point);
857 let ink = galley.mesh_bounds;
858 let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
859 let backdrop = egui::Rect::from_center_size(
860 at,
861 ink.size() + egui::Vec2::splat(PROMPT_PADDING * 2.0),
862 );
863 painter.rect_filled(
864 backdrop,
865 PROMPT_PADDING,
866 egui::Color32::from_black_alpha(PANEL_BACKDROP),
867 );
868 painter.galley(pos, galley, PANEL_TEXT_COLOR);
869 }
870 });
871 }
872}
873
874impl Game for Scene {
875 type Meshes = Shape;
876 type Sounds = NoSounds;
877 type InputActions = Controls;
878 type Skyboxes = Sky;
879 type SurfaceStyles = NoSurfaceStyles;
880 type PostEffects = NoPostEffects;
881
882 fn tick(&mut self, ctx: &mut TickContext<'_, Scene>) {
883 self.elf_prev = self.elf_pos;
884 self.elf_height_prev = self.elf_height;
885
886 if ctx.pressed(Button::Restart) {
887 self.restart_elf();
888 }
889 if ctx.pressed(Button::Hold) {
890 self.holding = true;
891 }
892 if ctx.pressed(Button::Release) {
893 self.holding = false;
894 }
895
896 self.elf_input.landed = self.fall(ctx.dt().as_secs_f32());
897 self.tick_elf(ctx);
898 ctx.animate(Elf, &mut self.elf_animator, &self.elf_input);
899
900 if self.elf_animator.entered(ElfState::Jump) {
901 self.jump_speed = JUMP_LAUNCH_SPEED;
902 }
903 if self.elf_animator.left(ElfState::StandUp) {
904 self.last_event = "elf stood up";
905 }
906 if self.elf_animator.entered(ElfState::Sit) {
907 self.last_event = "elf sat down";
908 }
909 if self.elf_animator.entered(ElfState::Death) {
910 self.last_event = "elf died";
911 }
912
913 let scrubbed_input = ScrubbedInput {
914 settled: settled_at(self.elf_pos.distance(SCRUBBED_ELF_POSITION)),
915 };
916 ctx.animate(Elf, &mut self.scrubbed_animator, &scrubbed_input);
917 ctx.animate(Butterfly, &mut self.butterfly_animator, &());
918 }
919
920 fn frame(&mut self, ctx: &mut FrameContext<'_, Scene>) {
921 self.steer_camera(ctx);
922
923 let alpha = ctx.alpha();
924 let elf_pos = self.elf_prev.lerp(self.elf_pos, alpha);
925 let elf_height = self.elf_height_prev + (self.elf_height - self.elf_height_prev) * alpha;
926 let (butterfly_pos, butterfly_yaw) = butterfly_pose(ctx.elapsed().as_secs_f32());
927
928 let camera = orbit_camera(elf_pos, self.camera_yaw, self.camera_pitch);
929 ctx.set_camera(camera);
930 ctx.set_cursor(if self.holding {
931 Cursor::Held
932 } else {
933 Cursor::Arrow
934 });
935 ctx.set_skybox(Sky::Day);
936 ctx.set_exposure(3.0);
937 ctx.set_bloom(0.2);
938 ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
939 ctx.light(
940 Light::point(
941 LAMP_POST_POSITION + Vec3::Y * (LAMP_POST_HEIGHT + LAMP_HEAD_GAP * 0.5),
942 LAMP_LIGHT_COLOR,
943 LAMP_LIGHT_RANGE,
944 )
945 .shadow(),
946 );
947 ctx.light(
948 Light::spot(Spot {
949 position: SPOT_POSITION,
950 direction: SPOT_DIRECTION,
951 color: SPOT_COLOR,
952 range: SPOT_RANGE,
953 angle: SPOT_ANGLE,
954 })
955 .shadow(),
956 );
957 ctx.light(
958 Light::point(butterfly_pos, BUTTERFLY_LIGHT_COLOR, BUTTERFLY_LIGHT_RANGE).shadow(),
959 );
960
961 ctx.draw(
962 Plane
963 .at(Transform::from_scale(Vec3::new(
964 GROUND_SIZE,
965 1.0,
966 GROUND_SIZE,
967 )))
968 .material(Material::lit(GROUND_COLOR)),
969 );
970 for patch in HURT_PATCHES {
971 ctx.draw(
972 Plane
973 .at(Transform::from_scale_rotation_translation(
974 Vec3::splat(HURT_RADIUS * 2.0),
975 Quat::IDENTITY,
976 patch,
977 ))
978 .material(Material::lit(HURT_COLOR)),
979 );
980 }
981 ctx.draw(
982 Cube.at(Transform::from_scale_rotation_translation(
983 Vec3::new(SEAT_FOOTPRINT, SEAT_HEIGHT, SEAT_FOOTPRINT),
984 Quat::IDENTITY,
985 SEAT_POSITION + Vec3::Y * SEAT_HEIGHT * 0.5,
986 ))
987 .material(Material::lit(SEAT_COLOR)),
988 );
989 ctx.draw(
990 Cube.at(Transform::from_scale_rotation_translation(
991 Vec3::new(LAMP_POST_THICKNESS, LAMP_POST_HEIGHT, LAMP_POST_THICKNESS),
992 Quat::IDENTITY,
993 LAMP_POST_POSITION + Vec3::Y * LAMP_POST_HEIGHT * 0.5,
994 ))
995 .material(Material::lit(LAMP_POST_COLOR)),
996 );
997 ctx.draw(
998 Cube.at(Transform::from_scale_rotation_translation(
999 Vec3::splat(LAMP_HEAD_SIZE),
1000 Quat::IDENTITY,
1001 LAMP_POST_POSITION
1002 + Vec3::Y * (LAMP_POST_HEIGHT + LAMP_HEAD_GAP + LAMP_HEAD_SIZE * 0.5),
1003 ))
1004 .material(Material::color(Color::BLACK).emissive(LAMP_LIGHT_COLOR)),
1005 );
1006 ctx.draw(
1007 Cube.at(Transform::from_scale_rotation_translation(
1008 Vec3::splat(SPOT_FIXTURE_SIZE),
1009 Quat::IDENTITY,
1010 SPOT_POSITION + Vec3::Y * SPOT_FIXTURE_SIZE * 0.5,
1011 ))
1012 .material(Material::lit(SPOT_FIXTURE_COLOR)),
1013 );
1014
1015 ctx.draw(
1016 Elf.at(Transform::from_rotation_translation(
1017 Quat::from_rotation_y(self.elf_yaw),
1018 elf_pos + Vec3::Y * elf_height,
1019 ))
1020 .posed(&self.elf_animator),
1021 );
1022 ctx.draw(
1023 Elf.at(Transform::from_rotation_translation(
1024 Quat::from_rotation_y(core::f32::consts::PI),
1025 SCRUBBED_ELF_POSITION,
1026 ))
1027 .posed(&self.scrubbed_animator),
1028 );
1029 ctx.draw(
1030 Butterfly
1031 .at(Transform::from_rotation_translation(
1032 Quat::from_rotation_y(butterfly_yaw),
1033 butterfly_pos,
1034 ))
1035 .posed(&self.butterfly_animator)
1036 .material(Material::lit(Color::WHITE).emissive(BUTTERFLY_EMISSIVE)),
1037 );
1038
1039 self.draw_prompts(ctx, camera);
1040 self.panel(ctx);
1041 }
1042}
1043
1044fn main() {
1045 run(
1046 Config::new("Mirage: animation")
1047 .with_size(WINDOW_WIDTH, WINDOW_HEIGHT)
1048 .with_assets([ELF_SOURCE, BUTTERFLY_SOURCE]),
1049 Scene::init,
1050 );
1051}