1use core::time::Duration;
16use std::sync::Arc;
17
18use mirage_engine::prelude::*;
19
20const PROPORTIONAL_FONT: &str = "pixel-operator";
21const MONOSPACE_FONT: &str = "pixel-operator-mono";
22const DISPLAY_FONT: &str = "ferrum";
23const DISPLAY_FAMILY: &str = "display";
24const PIXEL_ONLY_FAMILY: &str = "pixel-only";
25const PROMPT_FONT: &str = "kenney-input-keyboard-mouse";
26const PROMPT_FAMILY: &str = "prompts";
27
28const BODY_SIZE: f32 = 16.0;
31const HEADING_SIZE: f32 = 32.0;
32const NUMBER_SIZE: f32 = 28.0;
33const PROMPT_SIZE: f32 = 32.0;
34const TEXT_COLOR: egui::Color32 = egui::Color32::from_gray(230);
35
36const PLATFORM_SIZE: f32 = 8.0;
37const PLATFORM_COLOR: Color = Color::rgb(0.05, 0.05, 0.06);
38
39const STATION_RADIUS: f32 = 3.1;
42const STATION_SIZE: Vec3 = Vec3::new(0.6, 0.9, 0.5);
43const STATION_FRONT_SIZE: Vec3 = Vec3::new(0.42, 0.5, 0.06);
45const STATION_FRONT_OUTWARD: f32 = 0.005;
49
50const RADAR_SWEEP_RATE: f32 = 40.0;
51const REACTOR_RATE: f32 = 0.5;
52const REACTOR_BASE: f32 = 55.0;
53const REACTOR_SWING: f32 = 35.0;
54
55const SUN_DIRECTION: Vec3 = Vec3::new(0.4, -1.0, -0.3);
56const SUN_COLOR: Color = Color::rgb(0.6, 0.62, 0.7);
57
58const CAMERA_FOV: f32 = 42.0;
59const CAMERA_TARGET: Vec3 = Vec3::new(0.0, STATION_SIZE.y * 0.5, 0.0);
60const START_YAW: f32 = 0.4;
61const START_PITCH: f32 = 0.35;
62const PITCH_LIMIT: f32 = 0.9;
64const START_DISTANCE: f32 = 8.0;
65const MIN_DISTANCE: f32 = 3.0;
66const MAX_DISTANCE: f32 = 14.0;
67const AUTO_TURN_RATE: f32 = 0.12;
69const TURN_SENSITIVITY: f32 = core::f32::consts::FRAC_PI_2 / 1280.0;
72const ZOOM_STEP: f32 = 1.12;
74
75const BRACKET_MARGIN: f32 = 10.0;
76const BRACKET_STROKE: f32 = 2.0;
77const BRACKET_CORNER: f32 = 7.0;
78const STACK_GAP: f32 = 4.0;
82
83const PROMPT_LIFT: f32 = 20.0;
86
87const DIALOGUE_PADDING_X: f32 = 28.0;
88const DIALOGUE_PADDING_Y: f32 = 20.0;
89const DIALOGUE_MARGIN: f32 = 24.0;
90
91const FALLBACK_SAMPLE: &str = "café λ";
94const FAMILY_SAMPLE: &str = "mill and wall";
96const GRID_SAMPLE: &str = "the quick fox";
97
98meshes! { enum Shape { Cube, Plane } }
99
100#[derive(Catalog, Clone, Copy, Debug, PartialEq, Eq, Hash)]
103enum Sky {
104 Dusk,
105}
106
107impl Skyboxes for Sky {
108 fn build(&self, _assets: &Assets) -> SkyboxData {
109 SkyboxData::gradient(
110 Color::rgb(0.05, 0.06, 0.12),
111 Color::rgb(0.18, 0.12, 0.16),
112 Color::rgb(0.01, 0.01, 0.02),
113 )
114 .lit_by(0.25)
115 }
116}
117
118struct StationLook {
120 name: &'static str,
121 color: Color,
122 glow: Color,
123 lines: [&'static str; 2],
124}
125
126#[derive(Clone, Copy, PartialEq, Eq)]
127enum StationKind {
128 Radar,
129 Reactor,
130 Clock,
131}
132
133impl StationKind {
134 const ALL: [Self; 3] = [Self::Radar, Self::Reactor, Self::Clock];
135
136 fn index(self) -> usize {
137 self as usize
138 }
139
140 fn look(self) -> StationLook {
141 match self {
142 Self::Radar => StationLook {
143 name: "radar",
144 color: Color::rgb(0.22, 0.26, 0.30),
145 glow: Color::rgb(0.3, 1.4, 1.1),
146 lines: [
147 "the radar sweeps the dark past the platform for anything that moves",
148 "nothing answers back tonight",
149 ],
150 },
151 Self::Reactor => StationLook {
152 name: "reactor",
153 color: Color::rgb(0.30, 0.22, 0.18),
154 glow: Color::rgb(1.6, 0.7, 0.2),
155 lines: [
156 "the reactor gauge holds steady at a comfortable idle",
157 "plenty of power left for the long watch ahead",
158 ],
159 },
160 Self::Clock => StationLook {
161 name: "clock",
162 color: Color::rgb(0.20, 0.24, 0.22),
163 glow: Color::rgb(0.8, 0.9, 1.6),
164 lines: [
165 "the clock keeps the same count it always has",
166 "the watch ends when it says so and not before",
167 ],
168 },
169 }
170 }
171
172 fn center(self) -> Vec3 {
174 let angle = self.index() as f32 / Self::ALL.len() as f32 * core::f32::consts::TAU;
175 Vec3::new(
176 STATION_RADIUS * angle.cos(),
177 STATION_SIZE.y * 0.5,
178 STATION_RADIUS * angle.sin(),
179 )
180 }
181
182 fn aabb(self) -> (Vec3, Vec3) {
183 let half = STATION_SIZE * 0.5;
184 (self.center() - half, self.center() + half)
185 }
186
187 fn reading(self, elapsed: f32) -> (String, String) {
191 match self {
192 Self::Radar => {
193 let bearing = (elapsed * RADAR_SWEEP_RATE).rem_euclid(360.0);
194 (
195 format!("{bearing:.0} degrees bearing"),
196 format!("{bearing:03.0}"),
197 )
198 }
199 Self::Reactor => {
200 let percent = REACTOR_BASE + REACTOR_SWING * (elapsed * REACTOR_RATE).sin();
201 (
202 format!("{percent:.0} percent output"),
203 format!("{percent:.0}%"),
204 )
205 }
206 Self::Clock => {
207 let seconds = elapsed.rem_euclid(60.0);
208 (
209 format!("{seconds:.1} seconds this minute"),
210 format!("{seconds:04.1}"),
211 )
212 }
213 }
214 }
215}
216
217fn hit_station(ray: Ray) -> Option<StationKind> {
219 StationKind::ALL
220 .into_iter()
221 .filter_map(|station| {
222 let (min, max) = station.aabb();
223 ray.hit_aabb(min, max).map(|distance| (distance, station))
224 })
225 .min_by(|(a, _), (b, _)| a.total_cmp(b))
226 .map(|(_, station)| station)
227}
228
229fn logical(pixel: Vec2, pixels_per_point: f32) -> egui::Pos2 {
231 let point = pixel / pixels_per_point;
232 egui::pos2(point.x, point.y)
233}
234
235fn styled(text: impl Into<String>, font: egui::FontId) -> egui::RichText {
237 egui::RichText::new(text.into())
238 .font(font)
239 .color(TEXT_COLOR)
240}
241
242fn ink_bottom_at(galley: &egui::Galley, center_x: f32, bottom: f32) -> egui::Pos2 {
245 let ink = galley.mesh_bounds;
246 egui::pos2(center_x - ink.center().x, bottom - ink.max.y)
247}
248
249enum Prompt {
252 Glyph(char),
253 Text(String),
254}
255
256impl Prompt {
257 fn text(&self) -> String {
258 match self {
259 Self::Glyph(glyph) => glyph.to_string(),
260 Self::Text(text) => text.clone(),
261 }
262 }
263
264 fn family(&self) -> egui::FontFamily {
265 match self {
266 Self::Glyph(_) => egui::FontFamily::Name(PROMPT_FAMILY.into()),
267 Self::Text(_) => egui::FontFamily::Proportional,
268 }
269 }
270}
271
272fn prompt(binding: &ButtonBinding) -> Prompt {
276 match binding {
277 ButtonBinding::Mouse(MouseButton::Left) => Prompt::Glyph('\u{E0EC}'),
278 ButtonBinding::Mouse(MouseButton::Right) => Prompt::Glyph('\u{E0F0}'),
279 other => Prompt::Text(other.to_string()),
280 }
281}
282
283fn bracket(
296 painter: &egui::Painter,
297 at: egui::Pos2,
298 name: Arc<egui::Galley>,
299 reading: Arc<egui::Galley>,
300 number: Arc<egui::Galley>,
301) {
302 let frame_bottom = at.y - name.rect.height();
303 let name_ink = name.mesh_bounds;
304 let frame_height = name_ink.height() + BRACKET_MARGIN * 2.0;
305 let frame = egui::Rect::from_min_size(
306 egui::pos2(
307 at.x - name_ink.width() * 0.5 - BRACKET_MARGIN,
308 frame_bottom - frame_height,
309 ),
310 egui::vec2(name_ink.width() + BRACKET_MARGIN * 2.0, frame_height),
311 );
312 let name_pos = ink_bottom_at(&name, at.x, frame_bottom - BRACKET_MARGIN);
313
314 let reading_bottom = frame.top() - STACK_GAP;
315 let reading_pos = ink_bottom_at(&reading, at.x, reading_bottom);
316
317 let number_bottom = reading_pos.y - reading.rect.height() - STACK_GAP;
318 let number_pos = ink_bottom_at(&number, at.x, number_bottom);
319
320 corners(painter, frame);
321 painter.galley(name_pos, name, TEXT_COLOR);
322 painter.galley(reading_pos, reading, TEXT_COLOR);
323 painter.galley(number_pos, number, TEXT_COLOR);
324}
325
326fn prompt_at(painter: &egui::Painter, at: egui::Pos2, glyph: Arc<egui::Galley>) {
329 let ink = glyph.mesh_bounds;
330 let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
331 painter.galley(pos, glyph, TEXT_COLOR);
332}
333
334fn corners(painter: &egui::Painter, rect: egui::Rect) {
336 let stroke = egui::Stroke::new(BRACKET_STROKE, TEXT_COLOR);
337 for (corner, inward) in [
338 (rect.left_top(), egui::vec2(1.0, 1.0)),
339 (rect.right_top(), egui::vec2(-1.0, 1.0)),
340 (rect.left_bottom(), egui::vec2(1.0, -1.0)),
341 (rect.right_bottom(), egui::vec2(-1.0, -1.0)),
342 ] {
343 painter.line_segment(
344 [corner, corner + egui::vec2(inward.x * BRACKET_CORNER, 0.0)],
345 stroke,
346 );
347 painter.line_segment(
348 [corner, corner + egui::vec2(0.0, inward.y * BRACKET_CORNER)],
349 stroke,
350 );
351 }
352}
353
354struct Dialogue {
358 name: String,
359 lines: [String; 2],
360 line: usize,
361 revealed: usize,
362}
363
364impl Dialogue {
365 fn start(name: impl Into<String>, lines: [String; 2]) -> Self {
366 Self {
367 name: name.into(),
368 lines,
369 line: 0,
370 revealed: 0,
371 }
372 }
373
374 fn current_line(&self) -> &str {
375 &self.lines[self.line]
376 }
377
378 fn tick(&mut self) {
380 let len = self.current_line().chars().count();
381 self.revealed = (self.revealed + 1).min(len);
382 }
383
384 fn advance(&mut self) -> bool {
387 if self.line + 1 < self.lines.len() {
388 self.line += 1;
389 self.revealed = 0;
390 true
391 } else {
392 false
393 }
394 }
395
396 fn draw(&self, ui: &mut egui::Ui, whole_line: egui::Vec2) {
402 let display = egui::FontFamily::Name(DISPLAY_FAMILY.into());
403 let box_size = egui::vec2(
404 whole_line.x + DIALOGUE_PADDING_X,
405 whole_line.y + HEADING_SIZE + DIALOGUE_PADDING_Y,
406 );
407 egui::Window::new("hail")
408 .title_bar(false)
409 .resizable(false)
410 .collapsible(false)
411 .anchor(
412 egui::Align2::CENTER_BOTTOM,
413 egui::vec2(0.0, -DIALOGUE_MARGIN),
414 )
415 .show(ui.ctx(), |ui| {
416 ui.set_min_size(box_size);
417 ui.label(styled(&self.name, egui::FontId::new(HEADING_SIZE, display)));
418 let shown: String = self.current_line().chars().take(self.revealed).collect();
419 ui.label(styled(shown, egui::FontId::proportional(BODY_SIZE)));
420 });
421 }
422}
423
424fn sheet(ui: &mut egui::Ui) {
431 egui::Window::new("font sheet")
432 .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
433 .collapsible(false)
434 .resizable(false)
435 .show(ui.ctx(), |ui| {
436 egui::Grid::new("font sheet grid").show(ui, |ui| {
437 for (label, family) in [
438 ("proportional", egui::FontFamily::Proportional),
439 ("monospace", egui::FontFamily::Monospace),
440 ("display", egui::FontFamily::Name(DISPLAY_FAMILY.into())),
441 ] {
442 ui.label(styled(label, egui::FontId::proportional(BODY_SIZE)));
443 ui.label(styled(
444 FAMILY_SAMPLE,
445 egui::FontId::new(HEADING_SIZE, family),
446 ));
447 ui.end_row();
448 }
449
450 let pixel_only = egui::FontFamily::Name(PIXEL_ONLY_FAMILY.into());
451 for (label, family) in [
452 ("egui's fonts behind", egui::FontFamily::Proportional),
453 ("pixel font alone", pixel_only),
454 ] {
455 ui.label(styled(label, egui::FontId::proportional(BODY_SIZE)));
456 ui.label(styled(
457 FALLBACK_SAMPLE,
458 egui::FontId::new(BODY_SIZE, family),
459 ));
460 ui.end_row();
461 }
462
463 for (label, size) in [("16 points", 16.0), ("17 points", 17.0)] {
464 ui.label(styled(label, egui::FontId::proportional(BODY_SIZE)));
465 ui.label(styled(GRID_SAMPLE, egui::FontId::proportional(size)));
466 ui.end_row();
467 }
468 });
469 });
470}
471
472#[derive(InputButtonAction, Clone, Copy)]
473enum Trigger {
474 Hail,
475 Sheet,
476 Close,
477}
478
479impl InputButtonAction for Trigger {
480 fn bindings(&self) -> Vec<ButtonBinding> {
481 match self {
482 Self::Hail => vec![MouseButton::Left.into()],
483 Self::Sheet => vec![Key::Tab.into()],
484 Self::Close => vec![Key::Escape.into()],
485 }
486 }
487}
488
489#[derive(InputAxis2Action, Clone, Copy)]
491enum Turn {
492 Look,
493}
494
495impl InputAxis2Action for Turn {
496 fn bindings(&self) -> Vec<Axis2Binding> {
497 match self {
498 Self::Look => vec![Axis2Binding::pointer().scale(TURN_SENSITIVITY)],
499 }
500 }
501}
502
503#[derive(InputAxisAction, Clone, Copy)]
504enum Zoom {
505 Wheel,
506}
507
508impl InputAxisAction for Zoom {
509 fn bindings(&self) -> Vec<AxisBinding> {
510 match self {
511 Self::Wheel => vec![AxisBinding::from(WheelDelta::Up).scale(4.0)],
512 }
513 }
514}
515
516struct Controls;
517
518impl InputActions for Controls {
519 type Button = Trigger;
520 type Axis = Zoom;
521 type Axis2 = Turn;
522}
523
524fn install_family(
527 fonts: &mut egui::FontDefinitions,
528 startup: &mut Startup,
529 family: egui::FontFamily,
530 names: &[&str],
531) -> Result<(), Error> {
532 for &name in names {
533 if !fonts.font_data.contains_key(name) {
534 fonts
535 .font_data
536 .insert(name.to_owned(), startup.font(name)?.into());
537 }
538 }
539 let list = fonts.families.entry(family).or_default();
540 for &name in names.iter().rev() {
541 list.insert(0, name.to_owned());
542 }
543 Ok(())
544}
545
546struct Orbit {
551 yaw: f32,
552 pitch: f32,
553 distance: f32,
554}
555
556impl Default for Orbit {
557 fn default() -> Self {
558 Self {
559 yaw: START_YAW,
560 pitch: START_PITCH,
561 distance: START_DISTANCE,
562 }
563 }
564}
565
566impl Orbit {
567 fn camera(&self) -> Camera {
568 let direction = Vec3::new(
569 self.pitch.cos() * self.yaw.sin(),
570 self.pitch.sin(),
571 self.pitch.cos() * self.yaw.cos(),
572 );
573 Camera::new(
574 View::look_at(CAMERA_TARGET + direction * self.distance, CAMERA_TARGET),
575 Projection::perspective(CAMERA_FOV),
576 )
577 }
578
579 fn turn(&mut self, by: Vec2) {
582 self.yaw -= by.x;
583 self.pitch = (self.pitch + by.y).clamp(-PITCH_LIMIT, PITCH_LIMIT);
584 }
585
586 fn zoom(&mut self, factor: f32) {
589 self.distance = (self.distance / factor).clamp(MIN_DISTANCE, MAX_DISTANCE);
590 }
591}
592
593struct WatchRoom {
594 elapsed: Duration,
595 orbit: Orbit,
596 hailed: Option<StationKind>,
597 dialogue: Option<Dialogue>,
598 sheet_open: bool,
599}
600
601impl WatchRoom {
602 fn init(ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
603 let startup = ctx.startup();
604 let mut fonts = egui::FontDefinitions::default();
605 for (family, names) in [
606 (egui::FontFamily::Proportional, &[PROPORTIONAL_FONT][..]),
607 (egui::FontFamily::Monospace, &[MONOSPACE_FONT][..]),
608 (
609 egui::FontFamily::Name(DISPLAY_FAMILY.into()),
610 &[DISPLAY_FONT][..],
611 ),
612 (
613 egui::FontFamily::Name(PIXEL_ONLY_FAMILY.into()),
614 &[PROPORTIONAL_FONT][..],
615 ),
616 (
617 egui::FontFamily::Name(PROMPT_FAMILY.into()),
618 &[PROMPT_FONT, PROPORTIONAL_FONT][..],
619 ),
620 ] {
621 install_family(&mut fonts, startup, family, names)?;
622 }
623 startup.set_fonts(fonts)?;
624
625 Ok(Self {
626 elapsed: Duration::ZERO,
627 orbit: Orbit::default(),
628 hailed: None,
629 dialogue: None,
630 sheet_open: false,
631 })
632 }
633
634 fn handle_hail(&mut self, ctx: &mut TickContext<'_, Self>) {
635 let ray = ctx
636 .last_camera()
637 .ray_through(ctx.pointer(), ctx.window_size());
638 let Some(station) = hit_station(ray) else {
639 return;
640 };
641
642 if self.hailed != Some(station) {
643 self.hailed = Some(station);
644 let look = station.look();
645 self.dialogue = Some(Dialogue::start(look.name, look.lines.map(str::to_owned)));
646 return;
647 }
648 let Some(dialogue) = &mut self.dialogue else {
649 return;
650 };
651 if !dialogue.advance() {
652 self.dialogue = None;
653 self.hailed = None;
654 }
655 }
656
657 fn draw_station(&self, ctx: &mut FrameContext<'_, Self>, station: StationKind) {
658 let look = station.look();
659 let center = station.center();
660 let front_offset =
661 STATION_SIZE.z * 0.5 - STATION_FRONT_SIZE.z * 0.5 + STATION_FRONT_OUTWARD;
662 let front = center - Vec3::new(0.0, 0.0, front_offset);
663 for (size, position, material) in [
664 (STATION_SIZE, center, Material::lit(look.color)),
665 (
666 STATION_FRONT_SIZE,
667 front,
668 Material::color(Color::BLACK).emissive(look.glow),
669 ),
670 ] {
671 ctx.draw(
672 Cube.at(Transform::from_scale_rotation_translation(
673 size,
674 Quat::IDENTITY,
675 position,
676 ))
677 .material(material),
678 );
679 }
680 }
681
682 fn draw_bracket(&self, ctx: &mut FrameContext<'_, Self>, camera: Camera, station: StationKind) {
683 let top = station.center() + Vec3::Y * (STATION_SIZE.y * 0.5);
684 let window_size = ctx.window_size();
685 let Some(pixel) = camera.pixel_of(top, window_size) else {
686 return;
687 };
688 let at = logical(pixel, ctx.pixels_per_point());
689
690 let name = ctx.text_layout(station.look().name, egui::FontId::proportional(BODY_SIZE));
691 let (reading_text, number_text) = station.reading(self.elapsed.as_secs_f32());
692 let reading = ctx.text_layout(&reading_text, egui::FontId::monospace(BODY_SIZE));
693 let number = ctx.text_layout(
694 &number_text,
695 egui::FontId::new(NUMBER_SIZE, egui::FontFamily::Name(DISPLAY_FAMILY.into())),
696 );
697
698 ctx.ui(|ui| bracket(ui.painter(), at, name, reading, number));
699 }
700
701 fn draw_prompts(
704 &self,
705 ctx: &mut FrameContext<'_, Self>,
706 camera: Camera,
707 hovered: Option<StationKind>,
708 ) {
709 let Some(binding) = ctx.bindings(Trigger::Hail).into_iter().next() else {
710 return;
711 };
712 let hint = prompt(&binding);
713 let glyph = ctx.text_layout(&hint.text(), egui::FontId::new(PROMPT_SIZE, hint.family()));
714 let window_size = ctx.window_size();
715 let pixels_per_point = ctx.pixels_per_point();
716
717 ctx.ui(|ui| {
718 let painter = ui.painter();
719 for station in StationKind::ALL {
720 if Some(station) == hovered {
721 continue;
722 }
723 let top = station.center() + Vec3::Y * (STATION_SIZE.y * 0.5);
724 let Some(pixel) = camera.pixel_of(top, window_size) else {
725 continue;
726 };
727 let at = logical(pixel, pixels_per_point);
728 let at = egui::pos2(at.x, at.y - PROMPT_LIFT);
729 prompt_at(painter, at, glyph.clone());
730 }
731 });
732 }
733
734 fn panel(&self, ctx: &mut FrameContext<'_, Self>) {
737 ctx.ui(|ui| {
738 ui.label(styled(
739 "a game's own fonts",
740 egui::FontId::proportional(HEADING_SIZE),
741 ));
742 ui.label(styled(
743 "drawn in Pixel Operator, the game's proportional font",
744 egui::FontId::proportional(BODY_SIZE),
745 ));
746 ui.label(styled(
747 "the readings above each station in Pixel Operator Mono",
748 egui::FontId::monospace(BODY_SIZE),
749 ));
750 });
751 }
752
753 fn draw_dialogue(&self, ctx: &mut FrameContext<'_, Self>) {
754 let Some(dialogue) = &self.dialogue else {
755 return;
756 };
757 let whole = ctx.text_layout(
758 dialogue.current_line(),
759 egui::FontId::proportional(BODY_SIZE),
760 );
761 let size = whole.size();
762 ctx.ui(|ui| dialogue.draw(ui, size));
763 }
764
765 fn hovered(ctx: &FrameContext<'_, Self>) -> Option<StationKind> {
767 if ctx.ui_wants_pointer() {
768 return None;
769 }
770 hit_station(
771 ctx.last_camera()
772 .ray_through(ctx.pointer(), ctx.window_size()),
773 )
774 }
775
776 fn steer(&mut self, ctx: &mut FrameContext<'_, Self>) {
779 if !ctx.ui_wants_pointer() && ctx.down(Trigger::Hail) {
780 self.orbit.turn(ctx.axis2(Turn::Look));
781 }
782 let wheel = ctx.axis(Zoom::Wheel);
783 if !ctx.ui_wants_pointer() && wheel != 0.0 {
784 self.orbit.zoom(ZOOM_STEP.powf(wheel));
785 }
786 }
787}
788
789impl Game for WatchRoom {
790 type Meshes = Shape;
791 type Sounds = NoSounds;
792 type InputActions = Controls;
793 type Skyboxes = Sky;
794 type SurfaceStyles = ();
795 type PostEffects = ();
796
797 fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
798 self.elapsed += ctx.dt();
799 self.orbit.yaw += AUTO_TURN_RATE * ctx.dt().as_secs_f32();
800
801 if let Some(dialogue) = &mut self.dialogue {
802 dialogue.tick();
803 }
804 if ctx.pressed(Trigger::Close) {
805 self.dialogue = None;
806 self.hailed = None;
807 }
808 if ctx.pressed(Trigger::Sheet) {
809 self.sheet_open = !self.sheet_open;
810 }
811 if ctx.pressed(Trigger::Hail) && !ctx.ui_wants_pointer() {
812 self.handle_hail(ctx);
813 }
814 }
815
816 fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
817 self.steer(ctx);
818
819 let camera = self.orbit.camera();
820 ctx.set_camera(camera);
821 ctx.set_skybox(Sky::Dusk);
822 ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
823
824 ctx.draw(
825 Plane
826 .at(Transform::from_scale(Vec3::new(
827 PLATFORM_SIZE,
828 1.0,
829 PLATFORM_SIZE,
830 )))
831 .material(Material::lit(PLATFORM_COLOR)),
832 );
833 for station in StationKind::ALL {
834 self.draw_station(ctx, station);
835 }
836
837 let hovered = Self::hovered(ctx);
838 if !self.sheet_open {
839 if let Some(station) = hovered {
840 ctx.set_cursor(Cursor::Pointer);
841 self.draw_bracket(ctx, camera, station);
842 }
843 self.draw_prompts(ctx, camera, hovered);
844 }
845 if self.dialogue.is_some() {
846 self.draw_dialogue(ctx);
847 }
848 if self.sheet_open {
849 ctx.ui(sheet);
850 }
851 self.panel(ctx);
852 }
853}
854
855fn main() {
856 run(
857 Config::new("Mirage: a game's own fonts")
858 .with_size(1280, 720)
859 .with_assets([
860 "examples/assets/pixel-operator.ttf",
861 "examples/assets/pixel-operator-mono.ttf",
862 "examples/assets/ferrum.otf",
863 "examples/assets/kenney-input-keyboard-mouse.ttf",
864 ]),
865 WatchRoom::init,
866 );
867}