use core::f32::consts::TAU;
use core::ops::Range;
use core::time::Duration;
use mirage_engine::prelude::*;
const ELF_SOURCE: &str = "examples/assets/elf.glb";
const ELF_ROOT: &str = "Elf";
const ELF_HEIGHT: f32 = 1.6;
const WINDOW_WIDTH: u32 = 1280;
const WINDOW_HEIGHT: u32 = 720;
const ELF_SPEED: f32 = 4.0;
const WALK_CAP: f32 = 0.5;
const WALK_THRESHOLD: f32 = 0.1;
const TURN_RATE: f32 = TAU * 2.0;
const JUMP_LAUNCH_SPEED: f32 = 4.5;
const GRAVITY: f32 = 9.8;
const IDLE_LOCOMOTION_FADE: Duration = Duration::from_millis(200);
const ATTACK_ENTER_FADE: Duration = Duration::from_millis(80);
const ATTACK_CHAIN_FADE: Duration = Duration::from_millis(60);
const ATTACK_EXIT_FADE: Duration = Duration::from_millis(200);
const ATTACK_CHAIN_ENTRY: f32 = 0.15;
const ATTACK_RELEASE: f32 = 0.8;
const HIT_ENTER_FADE: Duration = Duration::from_millis(50);
const HIT_EXIT_FADE: Duration = Duration::from_millis(150);
const DEATH_FADE: Duration = Duration::from_millis(150);
const JUMP_ENTER_FADE: Duration = Duration::from_millis(100);
const JUMP_EXIT_FADE: Duration = Duration::from_millis(150);
const SIT_DOWN_FADE: Duration = Duration::from_millis(200);
const STAND_UP_FADE: Duration = Duration::from_millis(150);
const STAND_EXIT_FADE: Duration = Duration::from_millis(150);
const DANCE_FADE: Duration = Duration::from_millis(200);
const ELF_START: Vec3 = Vec3::new(-3.0, 0.0, 4.0);
const HURT_PATCHES: [Vec3; 3] = [
Vec3::new(1.5, 0.0, -1.0),
Vec3::new(-1.5, 0.0, -3.5),
Vec3::new(3.0, 0.0, 2.0),
];
const HURT_RADIUS: f32 = 0.9;
const FATAL_HITS: u32 = 3;
const SEAT_POSITION: Vec3 = Vec3::new(-3.5, 0.0, -3.0);
const SEAT_FOOTPRINT: f32 = 1.0;
const SEAT_HEIGHT: f32 = 0.45;
const SEATED_HEIGHT: f32 = 0.75;
const SEAT_HEAD_HEIGHT: f32 = SEAT_HEIGHT + SEATED_HEIGHT;
const SEAT_STAND_CLEARANCE: f32 = 0.05;
const SEAT_SPOT: Vec3 = Vec3::new(
SEAT_POSITION.x,
0.0,
SEAT_POSITION.z + SEAT_FOOTPRINT * 0.5 + SEAT_STAND_CLEARANCE,
);
const SEAT_FACING: f32 = 0.0;
const SEAT_INTERACT_RADIUS: f32 = 1.6;
const SCRUBBED_ELF_POSITION: Vec3 = Vec3::new(3.5, 0.0, 4.0);
const SCRUB_NEAR: f32 = 1.5;
const SCRUB_FAR: f32 = 5.0;
const LAMP_POST_POSITION: Vec3 = Vec3::new(-4.9, 0.0, -2.0);
const LAMP_POST_HEIGHT: f32 = 2.2;
const LAMP_POST_THICKNESS: f32 = 0.16;
const LAMP_POST_COLOR: Color = Color::rgb(0.16, 0.14, 0.12);
const LAMP_HEAD_SIZE: f32 = 0.34;
const LAMP_HEAD_GAP: f32 = 0.06;
const LAMP_LIGHT_COLOR: Color = Color::rgb(5.5, 4.2, 2.2);
const LAMP_LIGHT_RANGE: f32 = 6.0;
const SPOT_POSITION: Vec3 = Vec3::new(1.0, 6.0, -0.83);
const SPOT_DIRECTION: Vec3 = Vec3::NEG_Y;
const SPOT_COLOR: Color = Color::rgb(11.0, 9.8, 8.2);
const SPOT_RANGE: f32 = 9.0;
const SPOT_ANGLE: f32 = 0.85;
const SPOT_FIXTURE_SIZE: f32 = 0.22;
const SPOT_FIXTURE_COLOR: Color = Color::rgb(0.2, 0.2, 0.22);
const BUTTERFLY_SOURCE: &str = "examples/assets/butterfly.glb";
const BUTTERFLY_ROOT: &str = "Butterfly";
const BUTTERFLY_CENTER: Vec3 = Vec3::new(-4.2, 0.0, -2.5);
const BUTTERFLY_RADIUS: Vec2 = Vec2::new(1.8, 1.4);
const BUTTERFLY_HEIGHT: f32 = LAMP_POST_HEIGHT;
const BUTTERFLY_ANGULAR_SPEED: f32 = TAU / 14.0;
const BUTTERFLY_LIGHT_COLOR: Color = Color::rgb(1.8, 5.5, 5.0);
const BUTTERFLY_LIGHT_RANGE: f32 = 3.0;
const BUTTERFLY_EMISSIVE: Color = Color::rgb(0.6, 1.8, 1.6);
const GROUND_SIZE: f32 = 400.0;
const GROUND_COLOR: Color = Color::rgb(0.24, 0.30, 0.22);
const HURT_COLOR: Color = Color::rgb(0.75, 0.12, 0.10);
const SEAT_COLOR: Color = Color::rgb(0.5, 0.42, 0.3);
const SUN_DIRECTION: Vec3 = Vec3::new(-0.85, -0.18, -0.5);
const SUN_COLOR: Color = Color::rgb(0.55, 0.32, 0.22);
const SKY_ZENITH: Color = Color::rgb(0.06, 0.07, 0.2);
const SKY_HORIZON: Color = Color::rgb(0.55, 0.35, 0.28);
const SKY_NADIR: Color = Color::rgb(0.05, 0.05, 0.07);
const SKY_LIGHT: f32 = 0.15;
const CAMERA_BACK: f32 = 3.4;
const CAMERA_UP: f32 = 1.7;
const CAMERA_LOOK_HEIGHT: f32 = 0.8;
const CAMERA_FOV: f32 = 50.0;
const CAMERA_YAW_PER_DRAG: f32 = core::f32::consts::PI;
const CAMERA_PITCH_PER_DRAG: f32 = core::f32::consts::FRAC_PI_3;
const CAMERA_YAW_SCALE: f32 = CAMERA_YAW_PER_DRAG / WINDOW_WIDTH as f32;
const CAMERA_PITCH_SCALE: f32 = CAMERA_PITCH_PER_DRAG / WINDOW_HEIGHT as f32;
const CAMERA_PITCH_RANGE: Range<f32> = -0.4..0.9;
const CONTROLS: [(&str, &str); 10] = [
("mouse", "turns the camera"),
("click", "locks the pointer"),
("escape", "frees the pointer"),
("wasd or arrows", "walk"),
("left shift", "runs"),
("f", "attacks, chains on a second press"),
("space", "jumps"),
("n", "dances while idle"),
("e", "sits on the seat and stands back up"),
("r", "starts a new elf"),
];
const PANEL_TEXT_COLOR: egui::Color32 = egui::Color32::from_gray(230);
const PANEL_BACKDROP: u8 = 190;
const PANEL_PADDING: i8 = 8;
const PROMPT_SIZE: f32 = 15.0;
const PROMPT_LIFT: f32 = 0.35;
const PROMPT_PADDING: f32 = 4.0;
meshes! { enum Shape { Plane, Cube, Elf, Butterfly } }
#[derive(Catalog, Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Sky {
Day,
}
impl Skyboxes for Sky {
fn build(&self, _assets: &Assets) -> SkyboxData {
match self {
Self::Day => SkyboxData::gradient(SKY_ZENITH, SKY_HORIZON, SKY_NADIR).lit_by(SKY_LIGHT),
}
}
}
#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
struct Elf;
#[derive(Clip, Clone, Debug, PartialEq, Eq, Hash)]
enum ElfClip {
#[clip("idle")]
Idle,
#[clip("walk")]
Walk,
#[clip("jog")]
Jog,
#[clip("attack")]
Attack,
#[clip("hit")]
Hit,
#[clip("death")]
Death,
#[clip("sit_down")]
SitDown,
#[clip("sit")]
Sit,
#[clip("stand_up")]
StandUp,
#[clip("jump")]
Jump,
#[clip("dance")]
Dance,
}
impl Mesh<NoParts, ElfClip> for Elf {
fn build(&self, assets: &Assets) -> MeshData<NoParts, ElfClip> {
assets.model(ELF_ROOT)
}
}
#[derive(Default)]
struct ElfInput {
speed: f32,
attack: bool,
hit: bool,
dying: bool,
jump: bool,
landed: bool,
dance: bool,
interact: bool,
near_seat: bool,
}
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
enum ElfState {
Idle,
Locomotion,
Attack,
Hit,
Death,
SitDown,
Sit,
StandUp,
Jump,
Dance,
}
impl ElfState {
fn seated(self) -> bool {
matches!(self, Self::SitDown | Self::Sit | Self::StandUp)
}
fn grounded(input: &ElfInput) -> Self {
match input.speed > WALK_THRESHOLD {
true => Self::Locomotion,
false => Self::Idle,
}
}
}
impl AnimationStates for ElfState {
type Clip = ElfClip;
type Input = ElfInput;
fn entry() -> Self {
Self::Idle
}
fn motion(&self, input: &ElfInput) -> Motion<ElfClip> {
match self {
Self::Idle => Motion::looping(ElfClip::Idle),
Self::Locomotion => {
Motion::blend(ElfClip::Walk, ElfClip::Jog, input.speed).paced(input.speed)
}
Self::Attack => Motion::once(ElfClip::Attack),
Self::Hit => Motion::once(ElfClip::Hit),
Self::Death => Motion::once(ElfClip::Death),
Self::SitDown => Motion::once(ElfClip::SitDown),
Self::Sit => Motion::looping(ElfClip::Sit),
Self::StandUp => Motion::once(ElfClip::StandUp),
Self::Jump => Motion::once(ElfClip::Jump),
Self::Dance => Motion::looping(ElfClip::Dance),
}
}
fn next(&self, input: &ElfInput, at: Progress) -> Option<Transition<Self>> {
match (self, input) {
(Self::Death, _) => None,
(_, ElfInput { dying: true, .. }) => Some(Self::Death.fade(DEATH_FADE)),
(_, ElfInput { hit: true, .. }) if *self != Self::Hit => {
Some(Self::Hit.fade(HIT_ENTER_FADE))
}
(Self::Hit, _) if at.ended() => Some(ElfState::grounded(input).fade(HIT_EXIT_FADE)),
(Self::Attack, ElfInput { attack: true, .. }) if at.past(ATTACK_RELEASE) => Some(
Self::Attack
.restarted()
.entering_at(ATTACK_CHAIN_ENTRY)
.fade(ATTACK_CHAIN_FADE),
),
(Self::Attack, _) if at.past(ATTACK_RELEASE) => {
Some(ElfState::grounded(input).fade(ATTACK_EXIT_FADE))
}
(Self::SitDown | Self::Sit | Self::StandUp, i) if i.speed > WALK_THRESHOLD => {
Some(Self::Locomotion.fade(STAND_EXIT_FADE))
}
(Self::SitDown | Self::Sit | Self::StandUp, ElfInput { attack: true, .. }) => {
Some(Self::Attack.fade(ATTACK_ENTER_FADE))
}
(Self::SitDown | Self::Sit | Self::StandUp, ElfInput { jump: true, .. }) => {
Some(Self::Jump.fade(JUMP_ENTER_FADE))
}
(Self::SitDown, _) if at.ended() => Some(Self::Sit.at_once()),
(Self::Sit, ElfInput { interact: true, .. }) => Some(Self::StandUp.fade(STAND_UP_FADE)),
(Self::StandUp, _) if at.ended() => Some(Self::Idle.fade(STAND_EXIT_FADE)),
(Self::Jump, ElfInput { landed: true, .. }) => {
Some(ElfState::grounded(input).fade(JUMP_EXIT_FADE))
}
(Self::Jump, _) if at.ended() => Some(ElfState::grounded(input).fade(JUMP_EXIT_FADE)),
(
Self::Idle | Self::Locomotion,
ElfInput {
interact: true,
near_seat: true,
..
},
) => Some(Self::SitDown.fade(SIT_DOWN_FADE)),
(Self::Idle | Self::Locomotion, ElfInput { attack: true, .. }) => {
Some(Self::Attack.fade(ATTACK_ENTER_FADE))
}
(Self::Idle | Self::Locomotion, ElfInput { jump: true, .. }) => {
Some(Self::Jump.fade(JUMP_ENTER_FADE))
}
(Self::Idle, ElfInput { dance: true, .. }) => Some(Self::Dance.fade(DANCE_FADE)),
(Self::Dance, i) if i.speed > WALK_THRESHOLD => {
Some(Self::Locomotion.fade(IDLE_LOCOMOTION_FADE))
}
(Self::Idle, i) if i.speed > WALK_THRESHOLD => {
Some(Self::Locomotion.fade(IDLE_LOCOMOTION_FADE))
}
(Self::Locomotion, i) if i.speed <= WALK_THRESHOLD => {
Some(Self::Idle.fade(IDLE_LOCOMOTION_FADE))
}
_ => None,
}
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
enum ScrubbedState {
SitDown,
}
#[derive(Default)]
struct ScrubbedInput {
settled: f32,
}
impl AnimationStates for ScrubbedState {
type Clip = ElfClip;
type Input = ScrubbedInput;
fn entry() -> Self {
Self::SitDown
}
fn motion(&self, input: &ScrubbedInput) -> Motion<ElfClip> {
Motion::scrubbed(ElfClip::SitDown, input.settled)
}
fn next(&self, _input: &ScrubbedInput, _at: Progress) -> Option<Transition<Self>> {
None
}
}
#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
struct Butterfly;
#[derive(Clip, Clone, Debug, PartialEq, Eq, Hash)]
enum ButterflyClip {
#[clip("fly")]
Fly,
}
impl Mesh<NoParts, ButterflyClip> for Butterfly {
fn build(&self, assets: &Assets) -> MeshData<NoParts, ButterflyClip> {
assets.model(BUTTERFLY_ROOT)
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
enum FlyingState {
Flying,
}
impl AnimationStates for FlyingState {
type Clip = ButterflyClip;
type Input = ();
fn entry() -> Self {
Self::Flying
}
fn motion(&self, _input: &()) -> Motion<ButterflyClip> {
Motion::looping(ButterflyClip::Fly)
}
fn next(&self, _input: &(), _at: Progress) -> Option<Transition<Self>> {
None
}
}
fn butterfly_pose(t: f32) -> (Vec3, f32) {
let angle = t * BUTTERFLY_ANGULAR_SPEED;
let position = BUTTERFLY_CENTER
+ Vec3::new(
BUTTERFLY_RADIUS.x * angle.cos(),
BUTTERFLY_HEIGHT,
BUTTERFLY_RADIUS.y * angle.sin(),
);
let direction = Vec3::new(
-BUTTERFLY_RADIUS.x * angle.sin(),
0.0,
BUTTERFLY_RADIUS.y * angle.cos(),
);
(position, direction.x.atan2(direction.z))
}
fn settled_at(distance: f32) -> f32 {
1.0 - (distance - SCRUB_NEAR) / (SCRUB_FAR - SCRUB_NEAR)
}
fn orbit_camera(target: Vec3, yaw: f32, pitch: f32) -> Camera {
let look_at = target + Vec3::Y * CAMERA_LOOK_HEIGHT;
let base = Vec3::new(0.0, CAMERA_UP, CAMERA_BACK);
let offset = Quat::from_rotation_y(yaw) * (Quat::from_rotation_x(pitch) * base);
Camera::new(
View::look_at(look_at + offset, look_at),
Projection::perspective(CAMERA_FOV),
)
}
fn logical(pixel: Vec2, pixels_per_point: f32) -> egui::Pos2 {
let point = pixel / pixels_per_point;
egui::pos2(point.x, point.y)
}
#[derive(InputButtonAction, Clone, Copy)]
enum Button {
Run,
Attack,
Jump,
Dance,
Interact,
Restart,
Hold,
Release,
}
impl InputButtonAction for Button {
fn bindings(&self) -> Vec<ButtonBinding> {
match self {
Button::Run => vec![Key::LeftShift.into()],
Button::Attack => vec![Key::F.into()],
Button::Jump => vec![Key::Space.into()],
Button::Dance => vec![Key::N.into()],
Button::Interact => vec![Key::E.into()],
Button::Restart => vec![Key::R.into()],
Button::Hold => vec![MouseButton::Left.into()],
Button::Release => vec![Key::Escape.into()],
}
}
}
#[derive(InputAxisAction, Clone, Copy)]
enum Axis {
CameraYaw,
CameraPitch,
}
impl InputAxisAction for Axis {
fn bindings(&self) -> Vec<AxisBinding> {
match self {
Axis::CameraYaw => {
vec![AxisBinding::pointer_delta(PointerDelta::Sideways).scale(CAMERA_YAW_SCALE)]
}
Axis::CameraPitch => {
vec![AxisBinding::pointer_delta(PointerDelta::Up).scale(CAMERA_PITCH_SCALE)]
}
}
}
}
#[derive(InputAxis2Action, Clone, Copy)]
enum Move {
Walk,
}
impl InputAxis2Action for Move {
fn bindings(&self) -> Vec<Axis2Binding> {
match self {
Move::Walk => vec![
Axis2Binding::from(ButtonAxis2 {
left: Key::A,
right: Key::D,
down: Key::S,
up: Key::W,
}),
Axis2Binding::from(ButtonAxis2 {
left: Key::Left,
right: Key::Right,
down: Key::Down,
up: Key::Up,
}),
],
}
}
}
struct Controls;
impl InputActions for Controls {
type Button = Button;
type Axis = Axis;
type Axis2 = Move;
}
fn panel_text(text: impl Into<String>) -> egui::RichText {
egui::RichText::new(text.into()).color(PANEL_TEXT_COLOR)
}
struct Scene {
elf_pos: Vec3,
elf_prev: Vec3,
elf_yaw: f32,
elf_height: f32,
elf_height_prev: f32,
jump_speed: f32,
elf_input: ElfInput,
elf_animator: Animator<Elf, ElfState>,
scrubbed_animator: Animator<Elf, ScrubbedState>,
butterfly_animator: Animator<Butterfly, FlyingState>,
hits: u32,
in_patch: bool,
holding: bool,
camera_yaw: f32,
camera_pitch: f32,
last_event: &'static str,
}
impl Scene {
fn init(_ctx: &mut InitContext<'_, Scene>) -> Result<Self, Error> {
Ok(Self {
elf_pos: ELF_START,
elf_prev: ELF_START,
elf_yaw: 0.0,
elf_height: 0.0,
elf_height_prev: 0.0,
jump_speed: 0.0,
elf_input: ElfInput::default(),
elf_animator: Animator::new(),
scrubbed_animator: Animator::new(),
butterfly_animator: Animator::new(),
hits: 0,
in_patch: false,
holding: false,
camera_yaw: 0.0,
camera_pitch: 0.0,
last_event: "none yet",
})
}
fn steer_camera(&mut self, ctx: &mut FrameContext<'_, Scene>) {
self.camera_yaw -= ctx.axis(Axis::CameraYaw);
self.camera_pitch = (self.camera_pitch + ctx.axis(Axis::CameraPitch))
.clamp(CAMERA_PITCH_RANGE.start, CAMERA_PITCH_RANGE.end);
}
fn advance(&mut self, ctx: &mut TickContext<'_, Scene>) -> f32 {
let control = ctx.axis2(Move::Walk).clamp_length_max(1.0);
let turn = Quat::from_rotation_y(self.camera_yaw);
let heading = turn * Vec3::X * control.x + turn * Vec3::NEG_Z * control.y;
let dt = ctx.dt().as_secs_f32();
if let Some(direction) = heading.try_normalize() {
let wanted = direction.x.atan2(direction.z);
let turn = (wanted - self.elf_yaw + core::f32::consts::PI).rem_euclid(TAU)
- core::f32::consts::PI;
self.elf_yaw += turn.clamp(-TURN_RATE * dt, TURN_RATE * dt);
}
let cap = if ctx.down(Button::Run) { 1.0 } else { WALK_CAP };
self.elf_pos += heading * cap * ELF_SPEED * dt;
heading.length() * cap
}
fn fall(&mut self, dt: f32) -> bool {
let off_ground = self.elf_height > 0.0;
self.jump_speed -= GRAVITY * dt;
self.elf_height = (self.elf_height + self.jump_speed * dt).max(0.0);
if self.elf_height == 0.0 {
self.jump_speed = 0.0;
}
off_ground && self.elf_height == 0.0
}
fn patch_underfoot(&self) -> Option<Vec3> {
HURT_PATCHES
.into_iter()
.find(|&patch| self.elf_pos.distance(patch) < HURT_RADIUS)
}
fn tick_elf(&mut self, ctx: &mut TickContext<'_, Scene>) {
let grounded = matches!(
self.elf_animator.state(),
ElfState::Idle | ElfState::Locomotion
);
self.elf_input.speed = self.advance(ctx);
self.elf_input.attack = ctx.pressed(Button::Attack);
self.elf_input.jump = ctx.pressed(Button::Jump);
self.elf_input.dance = ctx.pressed(Button::Dance);
self.elf_input.near_seat = self.elf_pos.distance(SEAT_POSITION) < SEAT_INTERACT_RADIUS;
self.elf_input.interact = ctx.pressed(Button::Interact);
if self.elf_input.interact && grounded && self.elf_input.near_seat {
self.elf_pos = SEAT_SPOT;
self.elf_yaw = SEAT_FACING;
}
let underfoot = self.patch_underfoot();
let entered_patch = underfoot.is_some() && !self.in_patch;
self.in_patch = underfoot.is_some();
self.hits += u32::from(entered_patch);
self.elf_input.hit = entered_patch && self.hits < FATAL_HITS;
self.elf_input.dying = entered_patch && self.hits >= FATAL_HITS;
if entered_patch {
self.last_event = match self.elf_input.dying {
true => "elf died",
false => "elf hit",
};
}
}
fn restart_elf(&mut self) {
self.elf_animator = Animator::new();
self.elf_pos = ELF_START;
self.elf_prev = ELF_START;
self.elf_yaw = 0.0;
self.elf_height = 0.0;
self.elf_height_prev = 0.0;
self.jump_speed = 0.0;
self.elf_input = ElfInput::default();
self.hits = 0;
self.in_patch = false;
self.last_event = "new elf started";
}
fn panel(&self, ctx: &mut FrameContext<'_, Scene>) {
let state = match self.elf_animator.state() {
ElfState::Idle => "idle",
ElfState::Locomotion if self.elf_input.speed > WALK_CAP => "running",
ElfState::Locomotion => "walking",
ElfState::Attack => "attacking",
ElfState::Hit => "hit",
ElfState::Death => "dead",
ElfState::SitDown => "sitting down",
ElfState::Sit => "sitting",
ElfState::StandUp => "standing up",
ElfState::Jump => "jumping",
ElfState::Dance => "dancing",
};
ctx.ui(|ui| {
egui::Frame::new()
.fill(egui::Color32::from_black_alpha(PANEL_BACKDROP))
.inner_margin(PANEL_PADDING)
.corner_radius(f32::from(PANEL_PADDING))
.show(ui, |ui| {
ui.heading(panel_text(format!("elf is {state}")));
ui.label(panel_text(format!(
"hits taken {} of the {} red patches hurt for, {}",
self.hits, FATAL_HITS, self.last_event
)));
ui.label(panel_text(match self.elf_animator.transitioning() {
true => "fading between clips",
false => "one clip playing",
}));
ui.add_space(f32::from(PANEL_PADDING));
egui::Grid::new("controls").show(ui, |ui| {
for (key, does) in CONTROLS {
ui.label(panel_text(key));
ui.label(panel_text(does));
ui.end_row();
}
});
});
});
}
fn draw_prompts(&self, ctx: &mut FrameContext<'_, Scene>, camera: Camera) {
let sit_key = ctx
.bindings(Button::Interact)
.into_iter()
.next()
.map_or_else(|| "interact".to_owned(), |binding| binding.to_string());
let sit = ctx.text_layout(
&format!("{sit_key} sits"),
egui::FontId::proportional(PROMPT_SIZE),
);
let hurts = ctx.text_layout("hurts", egui::FontId::proportional(PROMPT_SIZE));
let walk_closer = ctx.text_layout("walk closer", egui::FontId::proportional(PROMPT_SIZE));
let mut prompts = vec![(
SCRUBBED_ELF_POSITION + Vec3::Y * (ELF_HEIGHT + PROMPT_LIFT),
walk_closer,
)];
if !self.elf_animator.state().seated() {
prompts.push((
SEAT_POSITION + Vec3::Y * (SEAT_HEAD_HEIGHT + PROMPT_LIFT),
sit,
));
}
prompts.extend(HURT_PATCHES.map(|patch| (patch + Vec3::Y * PROMPT_LIFT, hurts.clone())));
let window_size = ctx.window_size();
let pixels_per_point = ctx.pixels_per_point();
ctx.ui(|ui| {
let painter = ui.painter();
for (point, galley) in prompts {
let Some(pixel) = camera.pixel_of(point, window_size) else {
continue;
};
let at = logical(pixel, pixels_per_point);
let ink = galley.mesh_bounds;
let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
let backdrop = egui::Rect::from_center_size(
at,
ink.size() + egui::Vec2::splat(PROMPT_PADDING * 2.0),
);
painter.rect_filled(
backdrop,
PROMPT_PADDING,
egui::Color32::from_black_alpha(PANEL_BACKDROP),
);
painter.galley(pos, galley, PANEL_TEXT_COLOR);
}
});
}
}
impl Game for Scene {
type Meshes = Shape;
type Sounds = NoSounds;
type InputActions = Controls;
type Skyboxes = Sky;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, ctx: &mut TickContext<'_, Scene>) {
self.elf_prev = self.elf_pos;
self.elf_height_prev = self.elf_height;
if ctx.pressed(Button::Restart) {
self.restart_elf();
}
if ctx.pressed(Button::Hold) {
self.holding = true;
}
if ctx.pressed(Button::Release) {
self.holding = false;
}
self.elf_input.landed = self.fall(ctx.dt().as_secs_f32());
self.tick_elf(ctx);
ctx.animate(Elf, &mut self.elf_animator, &self.elf_input);
if self.elf_animator.entered(ElfState::Jump) {
self.jump_speed = JUMP_LAUNCH_SPEED;
}
if self.elf_animator.left(ElfState::StandUp) {
self.last_event = "elf stood up";
}
if self.elf_animator.entered(ElfState::Sit) {
self.last_event = "elf sat down";
}
if self.elf_animator.entered(ElfState::Death) {
self.last_event = "elf died";
}
let scrubbed_input = ScrubbedInput {
settled: settled_at(self.elf_pos.distance(SCRUBBED_ELF_POSITION)),
};
ctx.animate(Elf, &mut self.scrubbed_animator, &scrubbed_input);
ctx.animate(Butterfly, &mut self.butterfly_animator, &());
}
fn frame(&mut self, ctx: &mut FrameContext<'_, Scene>) {
self.steer_camera(ctx);
let alpha = ctx.alpha();
let elf_pos = self.elf_prev.lerp(self.elf_pos, alpha);
let elf_height = self.elf_height_prev + (self.elf_height - self.elf_height_prev) * alpha;
let (butterfly_pos, butterfly_yaw) = butterfly_pose(ctx.elapsed().as_secs_f32());
let camera = orbit_camera(elf_pos, self.camera_yaw, self.camera_pitch);
ctx.set_camera(camera);
ctx.set_cursor(if self.holding {
Cursor::Held
} else {
Cursor::Arrow
});
ctx.set_skybox(Sky::Day);
ctx.set_exposure(3.0);
ctx.set_bloom(0.2);
ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
ctx.light(
Light::point(
LAMP_POST_POSITION + Vec3::Y * (LAMP_POST_HEIGHT + LAMP_HEAD_GAP * 0.5),
LAMP_LIGHT_COLOR,
LAMP_LIGHT_RANGE,
)
.shadow(),
);
ctx.light(
Light::spot(Spot {
position: SPOT_POSITION,
direction: SPOT_DIRECTION,
color: SPOT_COLOR,
range: SPOT_RANGE,
angle: SPOT_ANGLE,
})
.shadow(),
);
ctx.light(
Light::point(butterfly_pos, BUTTERFLY_LIGHT_COLOR, BUTTERFLY_LIGHT_RANGE).shadow(),
);
ctx.draw(
Plane
.at(Transform::from_scale(Vec3::new(
GROUND_SIZE,
1.0,
GROUND_SIZE,
)))
.material(Material::lit(GROUND_COLOR)),
);
for patch in HURT_PATCHES {
ctx.draw(
Plane
.at(Transform::from_scale_rotation_translation(
Vec3::splat(HURT_RADIUS * 2.0),
Quat::IDENTITY,
patch,
))
.material(Material::lit(HURT_COLOR)),
);
}
ctx.draw(
Cube.at(Transform::from_scale_rotation_translation(
Vec3::new(SEAT_FOOTPRINT, SEAT_HEIGHT, SEAT_FOOTPRINT),
Quat::IDENTITY,
SEAT_POSITION + Vec3::Y * SEAT_HEIGHT * 0.5,
))
.material(Material::lit(SEAT_COLOR)),
);
ctx.draw(
Cube.at(Transform::from_scale_rotation_translation(
Vec3::new(LAMP_POST_THICKNESS, LAMP_POST_HEIGHT, LAMP_POST_THICKNESS),
Quat::IDENTITY,
LAMP_POST_POSITION + Vec3::Y * LAMP_POST_HEIGHT * 0.5,
))
.material(Material::lit(LAMP_POST_COLOR)),
);
ctx.draw(
Cube.at(Transform::from_scale_rotation_translation(
Vec3::splat(LAMP_HEAD_SIZE),
Quat::IDENTITY,
LAMP_POST_POSITION
+ Vec3::Y * (LAMP_POST_HEIGHT + LAMP_HEAD_GAP + LAMP_HEAD_SIZE * 0.5),
))
.material(Material::color(Color::BLACK).emissive(LAMP_LIGHT_COLOR)),
);
ctx.draw(
Cube.at(Transform::from_scale_rotation_translation(
Vec3::splat(SPOT_FIXTURE_SIZE),
Quat::IDENTITY,
SPOT_POSITION + Vec3::Y * SPOT_FIXTURE_SIZE * 0.5,
))
.material(Material::lit(SPOT_FIXTURE_COLOR)),
);
ctx.draw(
Elf.at(Transform::from_rotation_translation(
Quat::from_rotation_y(self.elf_yaw),
elf_pos + Vec3::Y * elf_height,
))
.posed(&self.elf_animator),
);
ctx.draw(
Elf.at(Transform::from_rotation_translation(
Quat::from_rotation_y(core::f32::consts::PI),
SCRUBBED_ELF_POSITION,
))
.posed(&self.scrubbed_animator),
);
ctx.draw(
Butterfly
.at(Transform::from_rotation_translation(
Quat::from_rotation_y(butterfly_yaw),
butterfly_pos,
))
.posed(&self.butterfly_animator)
.material(Material::lit(Color::WHITE).emissive(BUTTERFLY_EMISSIVE)),
);
self.draw_prompts(ctx, camera);
self.panel(ctx);
}
}
fn main() {
run(
Config::new("Mirage: animation")
.with_size(WINDOW_WIDTH, WINDOW_HEIGHT)
.with_assets([ELF_SOURCE, BUTTERFLY_SOURCE]),
Scene::init,
);
}