1use core::ops::Range;
13use core::time::Duration;
14
15use mirage_engine::prelude::*;
16
17const WALKER_SOURCE: &str = "examples/assets/walker.png";
27const WALKER_SHEET: &str = "walker";
28const WALKER_RELIEF_SOURCE: &str = "examples/assets/walker-relief.png";
35const WALKER_RELIEF: &str = "walker-relief";
36const WALKER_COLUMNS: u32 = 4;
37const WALKER_ROWS: u32 = 4;
38const WALKER_WIDTH: f32 = 1.0;
39const WALKER_HEIGHT: f32 = 1.6875;
43const WALK_SPEED: f32 = 4.0;
44const TICKS_PER_WALK_FRAME: u32 = 8;
45
46const GROUND_SOURCE: &str = "examples/assets/keep-ground.png";
50const GROUND_SHEET: &str = "keep-ground";
51const GROUND_COLUMNS: u32 = 4;
52const GROUND_ROWS: u32 = 2;
53const GRASS_ROW: u32 = 0;
54const PATH_ROW: u32 = 1;
55const PATH_WEST_VERGE: u32 = 0;
56const PATH_DIRT: u32 = 1;
58const PATH_EAST_VERGE: u32 = 3;
59
60const BUSH_SOURCE: &str = "examples/assets/keep-bush.png";
63const BUSH_SPRITE: &str = "keep-bush";
64const BUSH_RELIEF_SOURCE: &str = "examples/assets/keep-bush-relief.png";
67const BUSH_RELIEF: &str = "keep-bush-relief";
68const ROCK_SOURCE: &str = "examples/assets/keep-rock.png";
71const ROCK_SPRITE: &str = "keep-rock";
72const ROCK_RELIEF_SOURCE: &str = "examples/assets/keep-rock-relief.png";
75const ROCK_RELIEF: &str = "keep-rock-relief";
76
77const CRATE_SOURCE: &str = "examples/assets/keep-crate.png";
78const CRATE_TEXTURE: &str = "keep-crate";
79
80const WELL_SOURCE: &str = "examples/assets/keep-well.png";
83const WELL_SHEET: &str = "keep-well";
84const WELL_CELLS: u32 = 2;
85const WELL_RIM_CELL: u32 = 0;
86const WELL_MOUTH_CELL: u32 = 1;
87
88const STONE_SOURCE: &str = "examples/assets/keep-stone.png";
91const STONE_SHEET: &str = "keep-stone";
92const STONE_ROWS: u32 = 2;
93
94const POND_SOURCE: &str = "examples/assets/keep-pond.png";
97const POND_SHEET: &str = "keep-pond";
98const POND_CELLS: u32 = 2;
99const POND_SHORE_CELL: u32 = 1;
100
101const TORCH_SOURCE: &str = "examples/assets/keep-torch.png";
106const TORCH_SPRITE: &str = "keep-torch";
107const TORCH_RELIEF_SOURCE: &str = "examples/assets/keep-torch-relief.png";
110const TORCH_RELIEF: &str = "keep-torch-relief";
111const FLAME_SOURCE: &str = "examples/assets/keep-flame.png";
114const FLAME_SHEET: &str = "keep-flame";
115const FLAME_CELLS: u32 = 7;
116
117const CAVE_SOURCE: &str = "examples/assets/keep-cave.png";
120const CAVE_SHEET: &str = "keep-cave";
121const CAVE_COLUMNS: u32 = 4;
122const CAVE_ROWS: u32 = 4;
123const CAVE_FLOOR_ROW: u32 = 0;
124
125const MODEL: &str = "examples/assets/keep.glb";
127const DOOR_MESH: &str = "door";
128const GEM_MESH: &str = "gem";
129
130const INTERACT_SOUND: &str = "examples/assets/click.ogg";
131const GEM_SOUND: &str = "examples/assets/win.ogg";
132
133const TILE_SIZE: f32 = 1.0;
138const GROUND_DRAW_HALF: i32 = 31;
142const SIDES: [f32; 2] = [-1.0, 1.0];
145const PLAYER_RADIUS: f32 = 0.3;
148const PATH_COLUMN: i32 = 0;
151
152const PLAYER_SPAWN: Vec3 = Vec3::new(0.0, 0.0, 5.0);
153const CAVE_MOUTH: Vec3 = Vec3::new(0.0, 0.0, -6.0);
154const RETURN_SPAWN: Vec3 = Vec3::new(
157 CAVE_MOUTH.x,
158 0.0,
159 CAVE_MOUTH.z + MOUTH_CROSSING_INSET + PORTAL_CLEARANCE,
160);
161
162const POND_CENTER: Vec3 = Vec3::new(4.2, 0.01, -1.5);
169const POND_HALF: f32 = 1.5;
170const POND_WATER_HALF: f32 = POND_HALF * 0.6;
174const WATER_COLOR: Color = Color::rgba(0.06, 0.20, 0.27, 0.92);
175const WATER_LITNESS: f32 = 0.45;
176
177const CRATE_POSITIONS: [(f32, f32, f32); 2] = [(-2.7, 3.5, 0.5), (-1.2, 2.6, -0.3)];
180const CRATE_SIZE: f32 = 1.0;
181const WELL_POSITION: Vec3 = Vec3::new(2.6, 0.0, 3.2);
182const WELL_SIZE: Vec3 = Vec3::new(2.0, 0.6, 2.0);
185const WELL_MOUTH_LIFT: f32 = 0.01;
187
188const FLORA: [(f32, f32, bool); 6] = [
190 (-4.2, 1.4, false),
191 (-4.8, -1.8, true),
192 (5.8, 2.6, false),
193 (6.6, -1.0, false),
194 (3.2, -4.8, true),
195 (-2.2, -5.0, true),
196];
197const BUSH_WIDTH: f32 = 2.0;
201const BUSH_HEIGHT: f32 = 1.9375;
202const ROCK_WIDTH: f32 = 2.0;
203const ROCK_HEIGHT: f32 = 1.875;
204const BUSH_FOOTPRINT: f32 = 0.6;
207const ROCK_FOOTPRINT: f32 = 0.8;
208
209const HEDGE_INNER_HALF: f32 = 10.4;
213const HEDGE_OUTER_HALF: f32 = 11.8;
214const HEDGE_STEP: f32 = 1.4;
215const HEDGE_GATE_HALF: f32 = 2.2;
216
217const CLEARING_HALF: f32 = HEDGE_INNER_HALF - BUSH_WIDTH * 0.5;
220
221const MOUTH_PILLAR_OFFSET: f32 = 1.1;
224const MOUTH_PILLAR_SIZE: Vec3 = Vec3::new(1.0, STONE_ROWS as f32 * TILE_SIZE, 1.0);
228const MOUTH_LINTEL_TILES: f32 = 4.0;
232const MOUTH_LINTEL_SIZE: Vec3 = Vec3::new(
235 MOUTH_LINTEL_TILES * TILE_SIZE,
236 TILE_SIZE,
237 MOUTH_PILLAR_SIZE.z + 0.1,
238);
239const MOUTH_DARK_HEIGHT: f32 = MOUTH_PILLAR_SIZE.y + MOUTH_LINTEL_SIZE.y * 0.5;
242const MOUTH_OPENING_HALF: f32 = MOUTH_PILLAR_OFFSET - MOUTH_PILLAR_SIZE.x * 0.5;
244const MOUTH_CROSSING_INSET: f32 = 0.1;
247const PORTAL_CLEARANCE: f32 = 0.8;
249
250const ENTRANCE: Mouth = Mouth {
253 at: CAVE_MOUTH,
254 room_side: 1.0,
255};
256const EXIT: Mouth = Mouth {
257 at: CAVE_EXIT,
258 room_side: -1.0,
259};
260
261const SUN_COLOR: Color = Color::rgb(0.92, 0.87, 0.72);
262const SUN_DIRECTION: Vec3 = Vec3::new(0.6, -0.75, 0.4);
266
267const CAVE_HALF_WIDTH: f32 = 4.0;
272const CAVE_NEAR_Z: f32 = 10.0;
275const CAVE_FAR_Z: f32 = -6.0;
276const CAVE_WALK_FAR_Z: f32 = CAVE_FAR_Z + TILE_SIZE * 0.5 + WALKER_WIDTH * 0.5;
280const CAVE_WALK_NEAR_Z: f32 = CAVE_EXIT.z + PLAYER_RADIUS;
284const WALL_HEIGHT: f32 = 3.0;
287const WALL_TOP: f32 = CAVE_CAMERA_OFFSET.y + TILE_SIZE * 0.5;
290const WALL_COURSES: i32 = (WALL_TOP / WALL_HEIGHT) as i32 + 1;
292const CAVE_LIP_Z: f32 = 5.0;
295const CAVE_LIP_HEIGHT: f32 = TILE_SIZE;
298const CAVE_EXIT: Vec3 = Vec3::new(0.0, 0.0, CAVE_LIP_Z + MOUTH_PILLAR_SIZE.z * 0.5);
301const CAVE_SPAWN: Vec3 = Vec3::new(
304 CAVE_EXIT.x,
305 0.0,
306 CAVE_EXIT.z - MOUTH_CROSSING_INSET - PORTAL_CLEARANCE,
307);
308
309const DOOR_Z: f32 = 0.0;
310const DOOR_WALL_NEAR_Z: f32 = DOOR_Z + TILE_SIZE * 0.5;
313const GHOST_ALPHA: f32 = 0.6;
317const SOLID: f32 = 1.0;
319const GHOST_RAMP_TICKS: u32 = 6;
322const GHOST_CORRIDOR_HALF: f32 = 1.5;
326const DOOR_HINGE: Vec3 = Vec3::new(-DOOR_WIDTH * 0.5, 0.0, DOOR_Z);
329const DOOR_WIDTH: f32 = 1.0;
331const DOOR_THICKNESS: f32 = 0.12;
333const DOOR_HEIGHT: f32 = 1.9;
336const DOORWAY_HALF: f32 = TILE_SIZE * 0.5;
338const DOOR_WALL_END: f32 = CAVE_HALF_WIDTH + TILE_SIZE * 0.5;
341const INTERACT_POINT: Vec3 = Vec3::new(0.0, 0.0, DOOR_Z);
342const INTERACT_RADIUS: f32 = 1.8;
343
344const DOOR_COLOR: Color = Color::rgb(0.58, 0.16, 0.09);
347const DOOR_LITNESS: f32 = 0.85;
348const DOOR_FRAME_THICKNESS: f32 = 0.14;
351const DOOR_FRAME_STANDOFF: f32 = 0.03;
352const DOOR_FRAME_COLOR: Color = Color::rgb(0.85, 0.55, 0.2);
355const DOOR_FRAME_LITNESS: f32 = 0.9;
356const DOOR_FRAME_GLOW: Color = Color::rgb(1.6, 0.9, 0.35);
359
360const GEM_POSITION: Vec3 = Vec3::new(0.0, 0.5, -4.6);
361const GEM_BOB_HEIGHT: f32 = 0.12;
362const GEM_SPIN_SPEED: f32 = 1.4;
363const PICKUP_RADIUS: f32 = 1.0;
364const GEM_COLOR: Color = Color::rgb(0.35, 0.95, 0.85);
365const GEM_LIGHT_COLOR: Color = Color::rgb(0.3, 0.85, 0.78);
368const GEM_LIGHT_RANGE: f32 = 4.0;
371const GEM_LIGHT_LIFT: f32 = 0.7;
374
375const TORCH_POSITIONS: [(f32, f32); 2] = [(-3.0, 0.6), (3.0, 0.6)];
378const TORCH_STAND_HEIGHT: f32 = 2.0;
379const TORCH_STAND_WIDTH: f32 = 0.25;
382const TORCH_SPRITE_WIDTH: f32 = TORCH_STAND_WIDTH * 2.0;
385const TORCH_LIGHT_RANGE: f32 = 10.0;
386const TORCH_LIGHT_LIFT: f32 = 0.7;
390const TORCH_LIGHT_STANDOFF: f32 = 0.8;
391const TORCH_LIGHT_COLOR: Color = Color::rgb(1.0, 0.6, 0.28);
392const FLAME_SIZE: f32 = TILE_SIZE;
395const FLAME_LIFT: f32 = FLAME_SIZE * 0.5 - 0.1;
398const FLAME_TINT: Color = Color::rgb(2.2, 1.5, 0.7);
402const FLAME_RATE: f32 = 12.0;
404const FLAME_FLICKER_SPEED: f32 = 9.0;
406const FLAME_BOB: f32 = 0.03;
407const FLAME_ROLL: f32 = 0.12;
408
409const HUD_BACKDROP: u8 = 200;
413const HUD_PADDING: i8 = 8;
414
415const DOOR_SWING_TICKS: u32 = 24;
418const DOOR_PROMPT_SIZE: f32 = 15.0;
421const DOOR_PROMPT_LIFT: f32 = 0.3;
422const DOOR_PROMPT_PADDING: f32 = 4.0;
425const DOOR_PROMPT_BACKDROP: u8 = 190;
426const DOOR_PROMPT_COLOR: egui::Color32 = egui::Color32::from_gray(230);
427
428const CAMERA_FOV: f32 = 45.0;
429const OVERWORLD_CAMERA_OFFSET: Vec3 = Vec3::new(0.0, 9.0, 11.0);
432const CAVE_CAMERA_OFFSET: Vec3 = Vec3::new(0.0, 6.5, 7.5);
433
434fn turned_span(turn: f32) -> f32 {
441 turn.cos().abs() + turn.sin().abs()
442}
443
444#[derive(Clone, Copy)]
450struct Obstacle {
451 center: Vec2,
452 half: Vec2,
453}
454
455impl Obstacle {
456 fn footprint(at: Vec2, size: Vec2) -> Self {
459 Self {
460 center: at,
461 half: size * 0.5,
462 }
463 }
464
465 fn over(corners: [Vec2; 4]) -> Self {
468 let [first, rest @ ..] = corners;
469 let low = rest.iter().fold(first, |low, &corner| low.min(corner));
470 let high = rest.iter().fold(first, |high, &corner| high.max(corner));
471
472 Self {
473 center: (low + high) * 0.5,
474 half: (high - low) * 0.5,
475 }
476 }
477
478 fn push_out(self, point: Vec2, radius: f32) -> Vec2 {
481 let delta = point - self.center;
482 let escape = self.half + Vec2::splat(radius) - delta.abs();
483
484 if escape.min_element() <= 0.0 {
485 point
486 } else if escape.x < escape.y {
487 let x = self.center.x + delta.x.signum() * (self.half.x + radius);
488 Vec2::new(x, point.y)
489 } else {
490 let z = self.center.y + delta.y.signum() * (self.half.y + radius);
491 Vec2::new(point.x, z)
492 }
493 }
494}
495
496#[derive(Clone, Copy)]
499struct Mouth {
500 at: Vec3,
501 room_side: f32,
502}
503
504impl Mouth {
505 fn holds(&self, position: Vec3) -> bool {
509 let plane = self.at.z + self.room_side * MOUTH_CROSSING_INSET;
510
511 (position.x - self.at.x).abs() < MOUTH_OPENING_HALF
512 && (plane - position.z) * self.room_side > 0.0
513 }
514
515 fn pillars(&self) -> impl Iterator<Item = Vec3> + Clone {
517 let at = self.at;
518
519 SIDES
520 .into_iter()
521 .map(move |side| at + Vec3::X * (side * MOUTH_PILLAR_OFFSET))
522 }
523
524 fn looked_into(&self) -> bool {
530 self.room_side > 0.0
531 }
532}
533
534#[derive(Default, ShaderValues)]
541struct Water {
542 time: f32,
543}
544
545impl SurfaceStyle for Water {
546 const PASS: DrawPass = DrawPass::Translucent;
547 const SURFACE: Option<&'static str> = Some(include_str!("sprite_adventure_water.wgsl"));
548}
549
550surface_styles! { enum Looks { Water } }
551
552#[derive(Clone, Copy, PartialEq, Eq)]
558enum Area {
559 Overworld,
560 Cave,
561}
562
563#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
566struct Ground;
567
568impl Mesh for Ground {
569 fn build(&self, assets: &Assets) -> MeshData {
570 Plane
571 .build(assets)
572 .with_texture(assets.texture(GROUND_SHEET).pixelated())
573 }
574}
575
576#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
579struct Shore;
580
581impl Mesh for Shore {
582 fn build(&self, assets: &Assets) -> MeshData {
583 Plane
584 .build(assets)
585 .with_texture(assets.texture(POND_SHEET).pixelated())
586 .with_material(Material::lit(Color::WHITE).cutout())
587 }
588}
589
590#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
592struct Crate;
593
594impl Mesh for Crate {
595 fn build(&self, assets: &Assets) -> MeshData {
596 Cube.build(assets)
597 .with_texture(assets.texture(CRATE_TEXTURE).pixelated())
598 }
599}
600
601#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
603struct Well;
604
605impl Mesh for Well {
606 fn build(&self, assets: &Assets) -> MeshData {
607 Cube.build(assets)
608 .with_texture(assets.texture(WELL_SHEET).pixelated())
609 }
610}
611
612#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
614struct WellMouth;
615
616impl Mesh for WellMouth {
617 fn build(&self, assets: &Assets) -> MeshData {
618 Plane
619 .build(assets)
620 .with_texture(assets.texture(WELL_SHEET).pixelated())
621 }
622}
623
624#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
626struct Stone;
627
628impl Mesh for Stone {
629 fn build(&self, assets: &Assets) -> MeshData {
630 Cube.build(assets)
631 .with_texture(assets.texture(STONE_SHEET).pixelated())
632 }
633}
634
635#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
637struct Bush;
638
639impl Mesh for Bush {
640 fn build(&self, assets: &Assets) -> MeshData {
641 Quad.build(assets)
642 .with_texture(assets.texture(BUSH_SPRITE).pixelated())
643 .with_relief(assets.relief(BUSH_RELIEF))
644 .with_material(Material::lit(Color::WHITE).cutout())
645 }
646}
647
648#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
650struct Rock;
651
652impl Mesh for Rock {
653 fn build(&self, assets: &Assets) -> MeshData {
654 Quad.build(assets)
655 .with_texture(assets.texture(ROCK_SPRITE).pixelated())
656 .with_relief(assets.relief(ROCK_RELIEF))
657 .with_material(Material::lit(Color::WHITE).cutout())
658 }
659}
660
661#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
663struct Torch;
664
665impl Mesh for Torch {
666 fn build(&self, assets: &Assets) -> MeshData {
667 Quad.build(assets)
668 .with_texture(assets.texture(TORCH_SPRITE).pixelated())
669 .with_relief(assets.relief(TORCH_RELIEF))
670 .with_material(Material::lit(Color::WHITE).cutout())
671 }
672}
673
674#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
676struct Flame;
677
678impl Mesh for Flame {
679 fn build(&self, assets: &Assets) -> MeshData {
680 Quad.build(assets)
681 .with_texture(assets.texture(FLAME_SHEET).pixelated())
682 .with_material(Material::color(FLAME_TINT).additive())
683 }
684}
685
686#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
689struct Walker;
690
691impl Mesh for Walker {
692 fn build(&self, assets: &Assets) -> MeshData {
693 Quad.build(assets)
694 .with_texture(assets.texture(WALKER_SHEET).pixelated())
695 .with_relief(assets.relief(WALKER_RELIEF))
696 .with_material(Material::lit(Color::WHITE).cutout())
697 }
698}
699
700#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
702struct CaveFloor;
703
704impl Mesh for CaveFloor {
705 fn build(&self, assets: &Assets) -> MeshData {
706 Plane
707 .build(assets)
708 .with_texture(assets.texture(CAVE_SHEET).pixelated())
709 }
710}
711
712#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
714struct CaveWall;
715
716impl Mesh for CaveWall {
717 fn build(&self, assets: &Assets) -> MeshData {
718 Cube.build(assets)
719 .with_texture(assets.texture(CAVE_SHEET).pixelated())
720 }
721}
722
723#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
725struct Door;
726
727impl Mesh for Door {
728 fn build(&self, assets: &Assets) -> MeshData {
729 assets.mesh(DOOR_MESH)
730 }
731}
732
733#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
735struct Gem;
736
737impl Mesh for Gem {
738 fn build(&self, assets: &Assets) -> MeshData {
739 assets.mesh(GEM_MESH)
740 }
741}
742
743meshes! {
747 enum Shape {
748 Ground, Shore, Crate, Well, WellMouth, Stone, Bush, Rock, Torch,
749 Flame, Walker, CaveFloor, CaveWall, Door, Gem, Plane, Quad, Cube,
750 }
751}
752
753#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
755enum Sound {
756 Interact,
757 Gem,
758}
759
760impl Sounds for Sound {
761 fn build(&self, assets: &Assets) -> SoundData {
762 match self {
763 Sound::Interact => assets.sound("click"),
764 Sound::Gem => assets.sound("win"),
765 }
766 }
767}
768
769#[derive(InputAxis2Action, Clone, Copy, PartialEq)]
776enum Move {
777 Walk,
778}
779
780impl InputAxis2Action for Move {
781 fn bindings(&self) -> Vec<Axis2Binding> {
782 match self {
783 Move::Walk => vec![
784 Axis2Binding::from(ButtonAxis2 {
785 left: Key::A,
786 right: Key::D,
787 down: Key::S,
788 up: Key::W,
789 }),
790 Axis2Binding::from(ButtonAxis2 {
791 left: Key::Left,
792 right: Key::Right,
793 down: Key::Down,
794 up: Key::Up,
795 }),
796 Axis2Binding::stick(Stick::Left),
797 ],
798 }
799 }
800}
801
802#[derive(InputButtonAction, Clone, Copy, PartialEq)]
805enum Button {
806 Interact,
807 Reset,
808}
809
810impl InputButtonAction for Button {
811 fn bindings(&self) -> Vec<ButtonBinding> {
812 match self {
813 Button::Interact => vec![Key::E.into(), Pad::West.into()],
814 Button::Reset => vec![Key::R.into()],
815 }
816 }
817}
818
819struct Controls;
820
821impl InputActions for Controls {
822 type Button = Button;
823 type Axis = NoInputAxes;
824 type Axis2 = Move;
825}
826
827#[derive(Saves, Clone, Copy)]
834enum Position {
835 X,
836 Z,
837}
838
839impl SaveKey for Position {
840 type Value = f64;
841
842 fn fallback(&self) -> f64 {
843 match self {
844 Position::X => PLAYER_SPAWN.x as f64,
845 Position::Z => PLAYER_SPAWN.z as f64,
846 }
847 }
848}
849
850#[derive(Saves, Clone, Copy)]
852enum Flag {
853 InCave,
854 GemTaken,
855}
856
857impl SaveKey for Flag {
858 type Value = bool;
859
860 fn fallback(&self) -> bool {
861 false
862 }
863}
864
865#[derive(Clone, Copy, PartialEq)]
871enum Facing {
872 Toward = 0,
873 Right = 1,
874 Away = 2,
875 Left = 3,
876}
877
878impl Facing {
879 fn from_heading(heading: Vec2) -> Option<Self> {
882 if heading == Vec2::ZERO {
883 return None;
884 }
885 Some(if heading.x.abs() > heading.y.abs() {
886 if heading.x > 0.0 {
887 Self::Right
888 } else {
889 Self::Left
890 }
891 } else if heading.y > 0.0 {
892 Self::Away
893 } else {
894 Self::Toward
895 })
896 }
897}
898
899fn ground_cell(col: i32, row: i32) -> Frame {
906 let (column, sheet_row) = match col - PATH_COLUMN {
907 0 => (PATH_DIRT + row.rem_euclid(2) as u32, PATH_ROW),
908 -1 => (PATH_WEST_VERGE, PATH_ROW),
909 1 => (PATH_EAST_VERGE, PATH_ROW),
910 _ => (
911 (col * 31 + row * 17).rem_euclid(GROUND_COLUMNS as i32) as u32,
912 GRASS_ROW,
913 ),
914 };
915
916 Sheet::new(UVec2::new(GROUND_COLUMNS, GROUND_ROWS)).cell_at(UVec2::new(column, sheet_row))
917}
918
919fn masonry(tiles: f32) -> Frame {
922 let course = 1.0 / STONE_ROWS as f32;
923
924 Frame::rect(Vec2::new(0.0, 1.0 - course), Vec2::new(tiles, 1.0))
925}
926
927fn ghost_alpha(fraction: f32) -> f32 {
930 SOLID + (GHOST_ALPHA - SOLID) * fraction
931}
932
933fn cave_wall_face(variant: u32, standing: Range<f32>) -> Frame {
938 let cell = Vec2::new(1.0 / CAVE_COLUMNS as f32, 1.0 / CAVE_ROWS as f32);
939 let left = (variant % CAVE_COLUMNS) as f32 * cell.x;
940 let face = (CAVE_FLOOR_ROW + 1) as f32 * cell.y;
941 let up_from_base = |height: f32| 1.0 - (1.0 - face) * (height / WALL_HEIGHT);
942
943 Frame::rect(
944 Vec2::new(left, up_from_base(standing.end)),
945 Vec2::new(left + cell.x, up_from_base(standing.start)),
946 )
947}
948
949fn logical(pixel: Vec2, pixels_per_point: f32) -> egui::Pos2 {
951 let point = pixel / pixels_per_point;
952 egui::pos2(point.x, point.y)
953}
954
955fn main() {
956 run(
957 Config::new("Mirage: sprite adventure")
958 .with_size(1280, 720)
959 .with_assets([
960 MODEL,
961 WALKER_SOURCE,
962 WALKER_RELIEF_SOURCE,
963 GROUND_SOURCE,
964 BUSH_SOURCE,
965 BUSH_RELIEF_SOURCE,
966 ROCK_SOURCE,
967 ROCK_RELIEF_SOURCE,
968 TORCH_RELIEF_SOURCE,
969 CRATE_SOURCE,
970 WELL_SOURCE,
971 STONE_SOURCE,
972 CAVE_SOURCE,
973 POND_SOURCE,
974 TORCH_SOURCE,
975 FLAME_SOURCE,
976 INTERACT_SOUND,
977 GEM_SOUND,
978 ]),
979 Keep::init,
980 );
981}
982
983struct Keep {
984 area: Area,
985 position: Vec3,
986 previous: Vec3,
987 facing: Facing,
988 walk_ticks: u32,
989 simulated: Duration,
990 door_opening: bool,
991 swing_ticks: u32,
995 gem_taken: bool,
996 ghost: f32,
999 reset_requested: bool,
1002}
1003
1004impl Keep {
1005 fn init(ctx: &mut InitContext<'_, Keep>) -> Result<Self, Error> {
1008 let startup = ctx.startup();
1009 let gem_taken = startup.saved(Flag::GemTaken);
1010 let area = if startup.saved(Flag::InCave) {
1011 Area::Cave
1012 } else {
1013 Area::Overworld
1014 };
1015 let position = Vec3::new(
1016 startup.saved(Position::X) as f32,
1017 0.0,
1018 startup.saved(Position::Z) as f32,
1019 );
1020
1021 Ok(Self {
1022 area,
1023 position,
1024 previous: position,
1025 facing: Facing::Toward,
1026 walk_ticks: 0,
1027 simulated: Duration::ZERO,
1028 door_opening: gem_taken,
1029 swing_ticks: if gem_taken { DOOR_SWING_TICKS } else { 0 },
1030 gem_taken,
1031 ghost: 0.0,
1032 reset_requested: false,
1033 })
1034 }
1035
1036 fn camera(position: Vec3, offset: Vec3) -> Camera {
1037 Camera::new(
1038 View::look_at(position + offset, position),
1039 Projection::perspective(CAMERA_FOV),
1040 )
1041 }
1042
1043 fn overworld_obstacles() -> impl Iterator<Item = Obstacle> + Clone {
1047 CRATE_POSITIONS
1048 .into_iter()
1049 .map(|(x, z, turn)| {
1050 Obstacle::footprint(Vec2::new(x, z), Vec2::splat(CRATE_SIZE * turned_span(turn)))
1051 })
1052 .chain([
1053 Obstacle::footprint(WELL_POSITION.xz(), WELL_SIZE.xz()),
1054 Obstacle::footprint(POND_CENTER.xz(), Vec2::splat(POND_WATER_HALF * 2.0)),
1055 ])
1056 .chain(
1057 ENTRANCE
1058 .pillars()
1059 .map(|at| Obstacle::footprint(at.xz(), MOUTH_PILLAR_SIZE.xz())),
1060 )
1061 .chain(FLORA.into_iter().map(|(x, z, rock)| {
1062 let base = if rock { ROCK_FOOTPRINT } else { BUSH_FOOTPRINT };
1063 Obstacle::footprint(Vec2::new(x, z), Vec2::splat(base))
1064 }))
1065 }
1066
1067 fn cave_obstacles(door: Obstacle) -> impl Iterator<Item = Obstacle> + Clone {
1071 TORCH_POSITIONS
1072 .into_iter()
1073 .map(|(x, z)| Obstacle::footprint(Vec2::new(x, z), Vec2::splat(TORCH_STAND_WIDTH)))
1074 .chain(
1075 EXIT.pillars()
1076 .map(|at| Obstacle::footprint(at.xz(), MOUTH_PILLAR_SIZE.xz())),
1077 )
1078 .chain(SIDES.into_iter().flat_map(|side| {
1079 [
1080 Self::wall_run(side, DOOR_Z),
1081 Self::wall_run(side, CAVE_LIP_Z),
1082 ]
1083 }))
1084 .chain([door])
1085 }
1086
1087 fn wall_run(side: f32, z: f32) -> Obstacle {
1090 Obstacle::footprint(
1091 Vec2::new(side * (DOORWAY_HALF + DOOR_WALL_END) * 0.5, z),
1092 Vec2::new(DOOR_WALL_END - DOORWAY_HALF, TILE_SIZE),
1093 )
1094 }
1095
1096 fn door_obstacle(&self) -> Obstacle {
1099 let hinge = DOOR_HINGE.xz();
1100 let across = DOOR_THICKNESS * 0.5;
1101 let corner = |along: f32, aside: f32| {
1102 let (x, z) = if self.door_opening {
1103 (aside, -along)
1104 } else {
1105 (along, aside)
1106 };
1107 hinge + Vec3::new(x, 0.0, z).xz()
1108 };
1109
1110 Obstacle::over([
1111 corner(0.0, -across),
1112 corner(0.0, across),
1113 corner(DOOR_WIDTH, -across),
1114 corner(DOOR_WIDTH, across),
1115 ])
1116 }
1117
1118 fn push_out_of(&mut self, obstacles: impl Iterator<Item = Obstacle> + Clone) {
1122 const PASSES: u32 = 4;
1125
1126 let mut standing = self.position.xz();
1127 for _ in 0..PASSES {
1128 let settled = obstacles.clone().fold(standing, |point, obstacle| {
1129 obstacle.push_out(point, PLAYER_RADIUS)
1130 });
1131 if settled == standing {
1132 break;
1133 }
1134 standing = settled;
1135 }
1136
1137 self.position.x = standing.x;
1138 self.position.z = standing.y;
1139 }
1140
1141 fn tick_overworld(&mut self, ctx: &mut TickContext<'_, Keep>) {
1142 self.push_out_of(Self::overworld_obstacles());
1143 self.position.x = self.position.x.clamp(-CLEARING_HALF, CLEARING_HALF);
1144 self.position.z = self.position.z.clamp(-CLEARING_HALF, CLEARING_HALF);
1145
1146 if ENTRANCE.holds(self.position) {
1147 if ENTRANCE.holds(self.previous) {
1148 self.position.z = self.previous.z;
1149 } else {
1150 self.enter_cave(ctx);
1151 }
1152 }
1153 }
1154
1155 fn tick_cave(&mut self, ctx: &mut TickContext<'_, Keep>) {
1156 self.push_out_of(Self::cave_obstacles(self.door_obstacle()));
1157 self.position.x = self.position.x.clamp(-CAVE_HALF_WIDTH, CAVE_HALF_WIDTH);
1158 self.position.z = self.position.z.clamp(CAVE_WALK_FAR_Z, CAVE_WALK_NEAR_Z);
1159
1160 let target = if self.position.z < DOOR_WALL_NEAR_Z {
1161 1.0
1162 } else {
1163 0.0
1164 };
1165 let step = 1.0 / GHOST_RAMP_TICKS as f32;
1166 self.ghost += (target - self.ghost).clamp(-step, step);
1167
1168 if !self.door_opening
1169 && ctx.pressed(Button::Interact)
1170 && self.position.distance(INTERACT_POINT) < INTERACT_RADIUS
1171 {
1172 self.door_opening = true;
1173 self.swing_ticks = 0;
1174 ctx.play(Sound::Interact);
1175 }
1176 if self.door_opening && self.swing_ticks < DOOR_SWING_TICKS {
1177 self.swing_ticks += 1;
1178 }
1179
1180 if !self.gem_taken && self.position.distance(GEM_POSITION) < PICKUP_RADIUS {
1181 self.gem_taken = true;
1182 ctx.play(Sound::Gem);
1183 ctx.save(Flag::GemTaken, true);
1184 ctx.save(Position::X, self.position.x as f64);
1185 ctx.save(Position::Z, self.position.z as f64);
1186 }
1187
1188 if EXIT.holds(self.position) {
1189 if EXIT.holds(self.previous) {
1190 self.position.z = self.previous.z;
1191 } else {
1192 self.exit_cave(ctx);
1193 }
1194 }
1195 }
1196
1197 fn reset(&mut self, ctx: &mut TickContext<'_, Keep>) {
1201 ctx.save(Position::X, Position::X.fallback());
1202 ctx.save(Position::Z, Position::Z.fallback());
1203 ctx.save(Flag::InCave, Flag::InCave.fallback());
1204 ctx.save(Flag::GemTaken, Flag::GemTaken.fallback());
1205
1206 self.area = Area::Overworld;
1207 self.position = PLAYER_SPAWN;
1208 self.previous = PLAYER_SPAWN;
1209 self.gem_taken = false;
1210 self.door_opening = false;
1211 self.swing_ticks = 0;
1212 self.ghost = 0.0;
1213 }
1214
1215 fn enter_cave(&mut self, ctx: &mut TickContext<'_, Keep>) {
1217 self.area = Area::Cave;
1218 self.position = CAVE_SPAWN;
1219 self.previous = CAVE_SPAWN;
1220 ctx.save(Flag::InCave, true);
1221 ctx.save(Position::X, CAVE_SPAWN.x as f64);
1222 ctx.save(Position::Z, CAVE_SPAWN.z as f64);
1223 }
1224
1225 fn exit_cave(&mut self, ctx: &mut TickContext<'_, Keep>) {
1228 self.area = Area::Overworld;
1229 self.position = RETURN_SPAWN;
1230 self.previous = RETURN_SPAWN;
1231 ctx.save(Flag::InCave, false);
1232 ctx.save(Position::X, RETURN_SPAWN.x as f64);
1233 ctx.save(Position::Z, RETURN_SPAWN.z as f64);
1234 }
1235
1236 fn draw_ground(&self, ctx: &mut FrameContext<'_, Keep>) {
1237 for col in -GROUND_DRAW_HALF..=GROUND_DRAW_HALF {
1238 for row in -GROUND_DRAW_HALF..=GROUND_DRAW_HALF {
1239 ctx.draw(
1240 Ground
1241 .at(Vec3::new(
1242 col as f32 * TILE_SIZE,
1243 0.0,
1244 row as f32 * TILE_SIZE,
1245 ))
1246 .frame(ground_cell(col, row)),
1247 );
1248 }
1249 }
1250 }
1251
1252 fn draw_hedgerow(&self, ctx: &mut FrameContext<'_, Keep>) {
1257 for (row, half) in [HEDGE_INNER_HALF, HEDGE_OUTER_HALF].into_iter().enumerate() {
1258 let row = row as i32;
1259 let spans = ((2.0 * half / HEDGE_STEP).round() as i32).max(1);
1262 let span = 2.0 * half / spans as f32;
1263 let steps = spans - row;
1264 for step in 0..=steps {
1265 let along = -half + (step as f32 + 0.5 * row as f32) * span;
1266 let scale = if (step + row) % 2 == 0 { 1.0 } else { 0.8 };
1267 let (width, height) = (BUSH_WIDTH * scale, BUSH_HEIGHT * scale);
1268 let gated = along.abs() < HEDGE_GATE_HALF;
1271 let corner = step == 0 || step == steps;
1272 let places = [
1273 (along, -half, gated),
1274 (along, half, gated),
1275 (-half, along, corner),
1276 (half, along, corner),
1277 ];
1278 for (x, z, skip) in places {
1279 if skip {
1280 continue;
1281 }
1282 ctx.draw(
1283 Bush.at(Transform::from_scale_rotation_translation(
1284 Vec3::new(width, height, width),
1285 Quat::IDENTITY,
1286 Vec3::new(x, height * 0.5, z),
1287 ))
1288 .upright(),
1289 );
1290 }
1291 }
1292 }
1293 }
1294
1295 fn draw_pond(&self, ctx: &mut FrameContext<'_, Keep>) {
1298 ctx.draw(
1299 Plane
1300 .at(Transform::from_scale_rotation_translation(
1301 Vec3::splat(POND_WATER_HALF * 2.0),
1302 Quat::IDENTITY,
1303 POND_CENTER,
1304 ))
1305 .material(Material::shaded(WATER_COLOR, WATER_LITNESS))
1306 .surface_style::<Water>(),
1307 );
1308 ctx.draw(
1309 Shore
1310 .at(Transform::from_scale_rotation_translation(
1311 Vec3::splat(POND_HALF * 2.0),
1312 Quat::IDENTITY,
1313 Vec3::new(POND_CENTER.x, 0.0, POND_CENTER.z),
1314 ))
1315 .frame(Sheet::new(UVec2::new(POND_CELLS, 1)).cell(POND_SHORE_CELL)),
1316 );
1317 }
1318
1319 fn draw_crates(&self, ctx: &mut FrameContext<'_, Keep>) {
1320 for &(x, z, turn) in &CRATE_POSITIONS {
1321 ctx.draw(Crate.at(Transform::from_scale_rotation_translation(
1322 Vec3::splat(CRATE_SIZE),
1323 Quat::from_rotation_y(turn),
1324 Vec3::new(x, CRATE_SIZE * 0.5, z),
1325 )));
1326 }
1327 }
1328
1329 fn draw_well(&self, ctx: &mut FrameContext<'_, Keep>) {
1332 let cells = Sheet::new(UVec2::new(WELL_CELLS, 1));
1333 ctx.draw(
1334 Well.at(Transform::from_scale_rotation_translation(
1335 WELL_SIZE,
1336 Quat::IDENTITY,
1337 WELL_POSITION + Vec3::Y * (WELL_SIZE.y * 0.5),
1338 ))
1339 .frame(cells.cell(WELL_RIM_CELL)),
1340 );
1341 ctx.draw(
1342 WellMouth
1343 .at(Transform::from_scale_rotation_translation(
1344 Vec3::new(WELL_SIZE.x, 1.0, WELL_SIZE.z),
1345 Quat::IDENTITY,
1346 WELL_POSITION + Vec3::Y * (WELL_SIZE.y + WELL_MOUTH_LIFT),
1347 ))
1348 .frame(cells.cell(WELL_MOUTH_CELL)),
1349 );
1350 }
1351
1352 fn draw_flora(&self, ctx: &mut FrameContext<'_, Keep>) {
1353 for &(x, z, rock) in &FLORA {
1354 let (width, height) = if rock {
1355 (ROCK_WIDTH, ROCK_HEIGHT)
1356 } else {
1357 (BUSH_WIDTH, BUSH_HEIGHT)
1358 };
1359 let standing = Transform::from_scale_rotation_translation(
1360 Vec3::new(width, height, width),
1361 Quat::IDENTITY,
1362 Vec3::new(x, height * 0.5, z),
1363 );
1364 let flora: Instance<Shape, _> = if rock {
1365 Rock.at(standing).into_set()
1366 } else {
1367 Bush.at(standing).into_set()
1368 };
1369 ctx.draw(flora.upright());
1370 }
1371 }
1372
1373 fn draw_stone(ctx: &mut FrameContext<'_, Keep>, at: Vec3, size: Vec3, frame: Frame) {
1376 ctx.draw(
1377 Stone
1378 .at(Transform::from_scale_rotation_translation(
1379 size,
1380 Quat::IDENTITY,
1381 at + Vec3::Y * (size.y * 0.5),
1382 ))
1383 .frame(frame),
1384 );
1385 }
1386
1387 fn draw_mouth(ctx: &mut FrameContext<'_, Keep>, mouth: Mouth) {
1392 for at in mouth.pillars() {
1393 Self::draw_stone(ctx, at, MOUTH_PILLAR_SIZE, Frame::default());
1394 }
1395 if !mouth.looked_into() {
1396 return;
1397 }
1398
1399 Self::draw_stone(
1400 ctx,
1401 mouth.at + Vec3::Y * MOUTH_PILLAR_SIZE.y,
1402 MOUTH_LINTEL_SIZE,
1403 masonry(MOUTH_LINTEL_TILES),
1404 );
1405 ctx.draw(
1406 Quad.at(Transform::from_scale_rotation_translation(
1407 Vec3::new(MOUTH_PILLAR_OFFSET * 2.0, MOUTH_DARK_HEIGHT, 1.0),
1408 Quat::IDENTITY,
1409 mouth.at + Vec3::Y * (MOUTH_DARK_HEIGHT * 0.5),
1410 ))
1411 .material(Material::color(Color::BLACK)),
1412 );
1413 }
1414
1415 fn draw_cave_floor(&self, ctx: &mut FrameContext<'_, Keep>) {
1416 let half = CAVE_HALF_WIDTH as i32;
1417 let near = CAVE_NEAR_Z as i32;
1418 let far = CAVE_FAR_Z as i32;
1419 for col in -half..=half {
1420 for row in far..=near {
1421 let variant = (col * 13 + row * 7).rem_euclid(CAVE_COLUMNS as i32) as u32;
1422 ctx.draw(
1423 CaveFloor
1424 .at(Vec3::new(
1425 col as f32 * TILE_SIZE,
1426 0.0,
1427 row as f32 * TILE_SIZE,
1428 ))
1429 .frame(
1430 Sheet::new(UVec2::new(CAVE_COLUMNS, CAVE_ROWS))
1431 .cell_at(UVec2::new(variant, CAVE_FLOOR_ROW)),
1432 ),
1433 );
1434 }
1435 }
1436 }
1437
1438 fn draw_wall(
1443 ctx: &mut FrameContext<'_, Keep>,
1444 at: Vec2,
1445 standing: Range<f32>,
1446 seed: i32,
1447 fade: f32,
1448 ) {
1449 for course in 0..WALL_COURSES {
1450 let base = course as f32 * WALL_HEIGHT;
1451 let low = (standing.start - base).max(0.0);
1452 let high = (standing.end - base).min(WALL_HEIGHT);
1453 if high <= low {
1454 continue;
1455 }
1456
1457 let variant = (seed + course).rem_euclid(CAVE_COLUMNS as i32) as u32;
1458 ctx.draw(
1459 CaveWall
1460 .at(Transform::from_scale_rotation_translation(
1461 Vec3::new(TILE_SIZE, high - low, TILE_SIZE),
1462 Quat::IDENTITY,
1463 Vec3::new(at.x, base + (low + high) * 0.5, at.y),
1464 ))
1465 .frame(cave_wall_face(variant, low..high))
1466 .faded(fade),
1467 );
1468 }
1469 }
1470
1471 fn draw_cave_walls(&self, ctx: &mut FrameContext<'_, Keep>) {
1476 let half = CAVE_HALF_WIDTH as i32 + 1;
1477 let near = CAVE_NEAR_Z as i32;
1478 let far = CAVE_FAR_Z as i32;
1479
1480 for row in far..=near {
1481 let z = row as f32 * TILE_SIZE;
1482 let west = Vec2::new(-half as f32 * TILE_SIZE, z);
1483 let east = Vec2::new(half as f32 * TILE_SIZE, z);
1484 Self::draw_wall(ctx, west, 0.0..WALL_TOP, row * 5, SOLID);
1485 Self::draw_wall(ctx, east, 0.0..WALL_TOP, row * 5 + 1, SOLID);
1486 }
1487 for col in (-half + 1)..half {
1488 let x = col as f32 * TILE_SIZE;
1489 let back = Vec2::new(x, far as f32 * TILE_SIZE);
1490 Self::draw_wall(ctx, back, 0.0..WALL_TOP, col * 5 + 2, SOLID);
1491 if col != 0 {
1492 let lip = Vec2::new(x, CAVE_LIP_Z);
1493 Self::draw_wall(ctx, lip, 0.0..CAVE_LIP_HEIGHT, col * 5 + 4, SOLID);
1494 }
1495 }
1496 }
1497
1498 fn draw_door_wall(ctx: &mut FrameContext<'_, Keep>, seen_through: Option<f32>, ghost: f32) {
1504 let stone = |x: f32| match seen_through {
1505 Some(at) if (x - at).abs() < GHOST_CORRIDOR_HALF => ghost_alpha(ghost),
1506 _ => SOLID,
1507 };
1508 let half = CAVE_HALF_WIDTH as i32;
1509
1510 for col in (-half..=half).filter(|&col| col != 0) {
1511 let x = col as f32 * TILE_SIZE;
1512 Self::draw_wall(
1513 ctx,
1514 Vec2::new(x, DOOR_Z),
1515 0.0..WALL_TOP,
1516 col * 5 + 3,
1517 stone(x),
1518 );
1519 }
1520 Self::draw_wall(
1521 ctx,
1522 Vec2::new(0.0, DOOR_Z),
1523 DOOR_HEIGHT..WALL_TOP,
1524 3,
1525 stone(0.0),
1526 );
1527 }
1528
1529 fn draw_torches(&self, ctx: &mut FrameContext<'_, Keep>) {
1532 let elapsed = self.simulated.as_secs_f32();
1533 let loop_cells = Sheet::new(UVec2::new(FLAME_CELLS, 1));
1534
1535 for (index, &(x, z)) in TORCH_POSITIONS.iter().enumerate() {
1536 let base = Vec3::new(x, 0.0, z);
1537 ctx.draw(
1538 Torch
1539 .at(Transform::from_scale_rotation_translation(
1540 Vec3::new(TORCH_SPRITE_WIDTH, TORCH_STAND_HEIGHT, 1.0),
1541 Quat::IDENTITY,
1542 base + Vec3::Y * (TORCH_STAND_HEIGHT * 0.5),
1543 ))
1544 .upright(),
1545 );
1546
1547 let phase = index as f32 * 2.1;
1548 let flicker = (elapsed * FLAME_FLICKER_SPEED + phase).sin();
1549 let flame_pos =
1550 base + Vec3::Y * (TORCH_STAND_HEIGHT + FLAME_LIFT + flicker * FLAME_BOB);
1551
1552 let light_pos = flame_pos + Vec3::new(0.0, TORCH_LIGHT_LIFT, TORCH_LIGHT_STANDOFF);
1553 ctx.light(Light::point(light_pos, TORCH_LIGHT_COLOR, TORCH_LIGHT_RANGE).shadow());
1554 let offset = index as u32 * FLAME_CELLS / TORCH_POSITIONS.len() as u32;
1556 ctx.draw(
1557 Flame
1558 .at(Transform::from_scale_rotation_translation(
1559 Vec3::splat(FLAME_SIZE),
1560 Quat::IDENTITY,
1561 flame_pos,
1562 ))
1563 .billboard()
1564 .roll(flicker * FLAME_ROLL)
1565 .frame(loop_cells.cell((elapsed * FLAME_RATE) as u32 + offset)),
1566 );
1567 }
1568 }
1569
1570 fn draw_door(&self, ctx: &mut FrameContext<'_, Keep>, ghost: f32) {
1573 let fade = ghost_alpha(ghost);
1574 let swung = if self.door_opening {
1575 Quat::from_rotation_y(core::f32::consts::FRAC_PI_2)
1576 } else {
1577 Quat::IDENTITY
1578 };
1579
1580 ctx.draw(
1581 Door.at(Transform::from_rotation_translation(swung, DOOR_HINGE))
1582 .material(Material::shaded(DOOR_COLOR, DOOR_LITNESS))
1583 .faded(fade),
1584 );
1585 }
1586
1587 fn draw_door_frame(&self, ctx: &mut FrameContext<'_, Keep>, ghost: f32) {
1592 let reachable =
1593 !self.door_opening && self.position.distance(INTERACT_POINT) < INTERACT_RADIUS;
1594 let material =
1595 Material::shaded(DOOR_FRAME_COLOR, DOOR_FRAME_LITNESS).emissive(if reachable {
1596 DOOR_FRAME_GLOW
1597 } else {
1598 Color::BLACK
1599 });
1600 let fade = ghost_alpha(ghost);
1601 let z = DOOR_WALL_NEAR_Z + DOOR_FRAME_STANDOFF;
1602 let jamb_height = DOOR_HEIGHT + DOOR_FRAME_THICKNESS;
1603
1604 for side in SIDES {
1605 ctx.draw(
1606 Cube.at(Transform::from_scale_rotation_translation(
1607 Vec3::new(DOOR_FRAME_THICKNESS, jamb_height, DOOR_FRAME_THICKNESS),
1608 Quat::IDENTITY,
1609 Vec3::new(
1610 side * (DOORWAY_HALF + DOOR_FRAME_THICKNESS * 0.5),
1611 jamb_height * 0.5,
1612 z,
1613 ),
1614 ))
1615 .material(material)
1616 .faded(fade),
1617 );
1618 }
1619 ctx.draw(
1620 Cube.at(Transform::from_scale_rotation_translation(
1621 Vec3::new(
1622 DOOR_WIDTH + DOOR_FRAME_THICKNESS * 2.0,
1623 DOOR_FRAME_THICKNESS,
1624 DOOR_FRAME_THICKNESS,
1625 ),
1626 Quat::IDENTITY,
1627 Vec3::new(0.0, DOOR_HEIGHT + DOOR_FRAME_THICKNESS * 0.5, z),
1628 ))
1629 .material(material)
1630 .faded(fade),
1631 );
1632 }
1633
1634 fn draw_door_prompt(&self, ctx: &mut FrameContext<'_, Keep>, camera: Camera) {
1639 let near = self.position.distance(INTERACT_POINT) < INTERACT_RADIUS;
1640 let swinging = self.door_opening && self.swing_ticks < DOOR_SWING_TICKS;
1641 let text = if swinging {
1642 "opening"
1643 } else if near && !self.door_opening {
1644 "e opens the door"
1645 } else {
1646 return;
1647 };
1648
1649 let galley = ctx.text_layout(text, egui::FontId::proportional(DOOR_PROMPT_SIZE));
1650 let point = INTERACT_POINT + Vec3::Y * (DOOR_HEIGHT + DOOR_PROMPT_LIFT);
1651 let window_size = ctx.window_size();
1652 let pixels_per_point = ctx.pixels_per_point();
1653 let Some(pixel) = camera.pixel_of(point, window_size) else {
1654 return;
1655 };
1656
1657 ctx.ui(|ui| {
1658 let painter = ui.painter();
1659 let at = logical(pixel, pixels_per_point);
1660 let ink = galley.mesh_bounds;
1661 let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
1662 let backdrop = egui::Rect::from_center_size(
1663 at,
1664 ink.size() + egui::Vec2::splat(DOOR_PROMPT_PADDING * 2.0),
1665 );
1666 painter.rect_filled(
1667 backdrop,
1668 DOOR_PROMPT_PADDING,
1669 egui::Color32::from_black_alpha(DOOR_PROMPT_BACKDROP),
1670 );
1671 painter.galley(pos, galley, DOOR_PROMPT_COLOR);
1672 });
1673 }
1674
1675 fn draw_gem(&self, ctx: &mut FrameContext<'_, Keep>) {
1678 let t = self.simulated.as_secs_f32();
1679 let bob = (t * 2.0).sin() * GEM_BOB_HEIGHT;
1680 ctx.light(
1681 Light::point(
1682 GEM_POSITION + Vec3::Y * (bob + GEM_LIGHT_LIFT),
1683 GEM_LIGHT_COLOR,
1684 GEM_LIGHT_RANGE,
1685 )
1686 .shadow(),
1687 );
1688 ctx.draw(
1689 Gem.at(Transform::from_scale_rotation_translation(
1690 Vec3::ONE,
1691 Quat::from_rotation_y(t * GEM_SPIN_SPEED),
1692 GEM_POSITION + Vec3::Y * bob,
1693 ))
1694 .material(Material::shaded(GEM_COLOR, 0.7).emissive(GEM_COLOR.dimmed(1.6))),
1695 );
1696 }
1697
1698 fn draw_walker(&self, ctx: &mut FrameContext<'_, Keep>, ground: Vec3) {
1701 let step = if self.walk_ticks > 0 {
1702 (self.walk_ticks / TICKS_PER_WALK_FRAME) % WALKER_COLUMNS
1703 } else {
1704 0
1705 };
1706 let cell = Sheet::new(UVec2::new(WALKER_COLUMNS, WALKER_ROWS))
1707 .cell_at(UVec2::new(step, self.facing as u32));
1708 let size = Vec2::new(WALKER_WIDTH, WALKER_HEIGHT);
1709
1710 ctx.draw(
1711 Walker
1712 .at(Transform::from_scale_rotation_translation(
1713 size.extend(1.0),
1714 Quat::IDENTITY,
1715 ground + Vec3::Y * (WALKER_HEIGHT * 0.5),
1716 ))
1717 .upright()
1718 .frame(cell),
1719 );
1720 }
1721
1722 fn frame_overworld(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1723 let drawn_at = self.previous.lerp(self.position, ctx.alpha());
1724 ctx.set_camera(Self::camera(drawn_at, OVERWORLD_CAMERA_OFFSET));
1725 ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
1726
1727 self.draw_ground(ctx);
1728 self.draw_hedgerow(ctx);
1729 self.draw_pond(ctx);
1730 self.draw_crates(ctx);
1731 self.draw_well(ctx);
1732 self.draw_flora(ctx);
1733 Self::draw_mouth(ctx, ENTRANCE);
1734 self.draw_walker(ctx, drawn_at);
1735 }
1736
1737 fn frame_cave(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1738 let drawn_at = self.previous.lerp(self.position, ctx.alpha());
1739 let camera = Self::camera(drawn_at, CAVE_CAMERA_OFFSET);
1740 ctx.set_camera(camera);
1741
1742 self.draw_cave_floor(ctx);
1743 self.draw_cave_walls(ctx);
1744 Self::draw_door_wall(ctx, (self.ghost > 0.0).then_some(drawn_at.x), self.ghost);
1745 Self::draw_mouth(ctx, EXIT);
1746 self.draw_torches(ctx);
1747 self.draw_door(ctx, self.ghost);
1748 self.draw_door_frame(ctx, self.ghost);
1749 if !self.gem_taken {
1750 self.draw_gem(ctx);
1751 }
1752 self.draw_walker(ctx, drawn_at);
1753 self.draw_door_prompt(ctx, camera);
1754 }
1755
1756 fn overlay(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1759 let near_door = self.area == Area::Cave
1760 && !self.door_opening
1761 && self.position.distance(INTERACT_POINT) < INTERACT_RADIUS;
1762 let gem_taken = self.area == Area::Cave && self.gem_taken;
1763 let mut reset_clicked = false;
1764
1765 ctx.ui(|ui| {
1766 egui::Frame::new()
1767 .fill(egui::Color32::from_black_alpha(HUD_BACKDROP))
1768 .inner_margin(HUD_PADDING)
1769 .corner_radius(f32::from(HUD_PADDING))
1770 .show(ui, |ui| {
1771 ui.visuals_mut().override_text_color = Some(egui::Color32::WHITE);
1772 ui.label("wasd / arrows / stick to walk");
1773 if near_door {
1774 ui.label("e / west button to open the door");
1775 }
1776 if gem_taken {
1777 ui.label("gem recovered");
1778 }
1779 ui.label("kept between runs: position, gem, cave");
1780 ui.label("r to reset world");
1781 if ui.button("reset world").clicked() {
1782 reset_clicked = true;
1783 }
1784 });
1785 });
1786
1787 if reset_clicked {
1788 self.reset_requested = true;
1789 }
1790 }
1791}
1792
1793impl Game for Keep {
1794 type Meshes = Shape;
1795 type Sounds = Sound;
1796 type InputActions = Controls;
1797 type Skyboxes = NoSkyboxes;
1798 type SurfaceStyles = Looks;
1799 type PostEffects = NoPostEffects;
1800
1801 fn tick(&mut self, ctx: &mut TickContext<'_, Keep>) {
1802 self.previous = self.position;
1803
1804 if self.reset_requested || ctx.pressed(Button::Reset) {
1805 self.reset_requested = false;
1806 self.reset(ctx);
1807 return;
1808 }
1809
1810 let heading = if ctx.ui_wants_keyboard() {
1811 Vec2::ZERO
1812 } else {
1813 ctx.axis2(Move::Walk)
1814 };
1815 match Facing::from_heading(heading) {
1816 Some(facing) => {
1817 self.facing = facing;
1818 self.walk_ticks += 1;
1819 }
1820 None => self.walk_ticks = 0,
1821 }
1822 let stride = Vec3::new(heading.x, 0.0, -heading.y) * WALK_SPEED * ctx.dt().as_secs_f32();
1823 self.position += stride;
1824 self.simulated += ctx.dt();
1825
1826 match self.area {
1827 Area::Overworld => self.tick_overworld(ctx),
1828 Area::Cave => self.tick_cave(ctx),
1829 }
1830 }
1831
1832 fn frame(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1833 ctx.set_surface_style(Water {
1834 time: self.simulated.as_secs_f32(),
1835 });
1836
1837 match self.area {
1838 Area::Overworld => self.frame_overworld(ctx),
1839 Area::Cave => self.frame_cave(ctx),
1840 }
1841
1842 self.overlay(ctx);
1843 }
1844}