1use core::time::Duration;
11
12use mirage_engine::prelude::*;
13use mirage_engine::ray;
14
15const BOARD_TILES: i32 = 6;
17
18const TILE_SIZE: f32 = 1.0;
20
21const TILE_GAP: f32 = 0.05;
23
24const TILE_THICKNESS: f32 = 0.16;
25
26const BOARD_HALF: f32 = BOARD_TILES as f32 * TILE_SIZE * 0.5;
28
29const UNIT_SPEED: f32 = 2.5;
31
32const SPRITE_HEIGHT: f32 = 0.9;
35const SPRITE_WIDTH: f32 = SPRITE_HEIGHT * 16.0 / 27.0;
36
37const BLOCK_SIZE: f32 = 0.6;
38
39const SPRITE_HALF_EXTENTS: Vec3 =
41 Vec3::new(SPRITE_WIDTH * 0.5, SPRITE_HEIGHT * 0.5, SPRITE_WIDTH * 0.5);
42
43const BLOCK_HALF_EXTENTS: Vec3 = Vec3::splat(BLOCK_SIZE * 0.5);
45
46const SPRITE_TEXTURE: &str = "walker";
48const SPRITE_SOURCE: &str = "examples/assets/walker.png";
49const CLICK_SOURCE: &str = "examples/assets/click.ogg";
50
51const SPRITE_COLUMNS: u32 = 4;
56const SPRITE_ROWS: u32 = 4;
57const SPRITE_ROW: u32 = 1;
58const SPRITE_COLUMN: u32 = 0;
59
60const ROCKS: [(f32, f32, u32, f32); 5] = [
63 (-BOARD_HALF - 1.2, -BOARD_HALF - 0.6, 11, 1.0),
64 (-BOARD_HALF - 0.8, BOARD_HALF + 1.0, 37, 0.8),
65 (BOARD_HALF + 1.4, -BOARD_HALF - 0.2, 58, 1.3),
66 (BOARD_HALF + 1.0, BOARD_HALF + 1.2, 71, 0.9),
67 (0.3, BOARD_HALF + 1.6, 94, 1.1),
68];
69
70const ROCK_JITTER: f32 = 0.16;
73
74const GROUND_HALF: f32 = BOARD_HALF + 3.0;
77
78const GROUND_Y: f32 = -0.01;
81
82const LIGHT_TILE: Color = Color::rgb(0.80, 0.76, 0.64);
83const DARK_TILE: Color = Color::rgb(0.55, 0.50, 0.40);
84const REACHABLE_MARK: Color = Color::rgb(0.20, 0.85, 0.35);
87const REACHABLE_MARK_SCALE: f32 = 0.45;
90const REACHABLE_MARK_LIFT: f32 = 0.01;
93const CURRENT_MARK: Color = Color::rgb(1.0, 0.2, 0.75);
96const CURRENT_MARK_SCALE: f32 = 0.85;
97const TURN_MARK: Color = Color::rgb(0.85, 0.75, 0.15);
100const TURN_MARK_SCALE: f32 = 0.5;
101const HOVER_REACHABLE_TILE: Color = Color::rgb(0.35, 0.75, 0.68);
104const HOVER_BLOCKED_TILE: Color = Color::rgb(0.62, 0.28, 0.26);
105const BLOCK_IDLE: Color = Color::rgb(0.32, 0.42, 0.58);
106const BLOCK_TURN: Color = Color::rgb(0.42, 0.54, 0.72);
108const GROUND_COLOR: Color = Color::rgb(0.15, 0.16, 0.13);
109const ROCK_COLOR: Color = Color::rgb(0.42, 0.40, 0.38);
110const SUN_COLOR: Color = Color::rgb(0.95, 0.92, 0.85);
111
112const HOVER_TINT: Color = Color::rgb(0.9, 1.15, 1.15);
117const HOVER_GLOW: Color = Color::rgb(0.02, 0.15, 0.2);
118
119const SELECTED_TINT: Color = Color::rgb(1.15, 0.95, 0.85);
124const SELECTED_GLOW: Color = Color::rgb(0.22, 0.11, 0.0);
125
126const TURN_TINT: Color = Color::rgb(1.0, 1.0, 0.82);
129const TURN_GLOW: Color = Color::rgb(0.08, 0.08, 0.02);
130
131const BLOOM: f32 = 0.35;
135
136const PROMPT_SIZE: f32 = 15.0;
139const PROMPT_TILE_LIFT: f32 = 0.55;
142const PROMPT_UNIT_LIFT: f32 = 0.25;
144const PROMPT_PADDING: f32 = 4.0;
146const PROMPT_BACKDROP: u8 = 190;
148const PROMPT_TEXT_COLOR: egui::Color32 = egui::Color32::from_gray(230);
149
150const TICK_INTERVAL: Duration = Duration::from_nanos(33_333_333);
153
154const ROCK_FACES: [(Vec3, Vec3, Vec3); 6] = [
158 (Vec3::X, Vec3::NEG_Z, Vec3::Y),
159 (Vec3::NEG_X, Vec3::Z, Vec3::Y),
160 (Vec3::Y, Vec3::X, Vec3::NEG_Z),
161 (Vec3::NEG_Y, Vec3::X, Vec3::Z),
162 (Vec3::Z, Vec3::X, Vec3::Y),
163 (Vec3::NEG_Z, Vec3::NEG_X, Vec3::Y),
164];
165
166const ROCK_TRIANGLES: [u32; 6] = [0, 1, 2, 0, 2, 3];
167
168fn main() {
169 run(
170 Config::new("Mirage: isometric board")
171 .with_size(1280, 720)
172 .with_assets([SPRITE_SOURCE, CLICK_SOURCE])
173 .with_tick_interval(TICK_INTERVAL),
174 Board::init,
175 );
176}
177
178#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
180#[catalog(Self { seed: 0 })]
181struct Rock {
182 seed: u32,
183}
184
185impl Mesh for Rock {
186 fn build(&self, _: &Assets) -> MeshData {
187 build_rock(self.seed)
188 }
189}
190
191#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
193struct Sprite;
194
195impl Mesh for Sprite {
196 fn build(&self, assets: &Assets) -> MeshData {
197 Quad.build(assets)
198 .with_texture(assets.texture(SPRITE_TEXTURE).pixelated())
199 }
200}
201
202meshes! { enum Shape { Plane, Cube, Rock, Sprite } }
206
207#[derive(Catalog, Clone, Copy, Debug, PartialEq, Eq, Hash)]
210enum Sky {
211 Day,
212}
213
214impl Skyboxes for Sky {
215 fn build(&self, _assets: &Assets) -> SkyboxData {
216 SkyboxData::gradient(
217 Color::rgb(0.55, 0.75, 0.95),
218 Color::rgb(0.85, 0.90, 0.95),
219 Color::rgb(0.35, 0.33, 0.30),
220 )
221 .lit_by(0.3)
222 }
223}
224
225#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
227enum Sound {
228 Click,
229}
230
231impl Sounds for Sound {
232 fn build(&self, assets: &Assets) -> SoundData {
233 match self {
234 Sound::Click => assets.sound("click"),
235 }
236 }
237}
238
239#[derive(InputButtonAction, Clone, Copy)]
242enum Button {
243 Select,
244}
245
246impl InputButtonAction for Button {
247 fn bindings(&self) -> Vec<ButtonBinding> {
248 match self {
249 Button::Select => vec![MouseButton::Left.into()],
250 }
251 }
252}
253
254struct Controls;
255
256impl InputActions for Controls {
257 type Button = Button;
258 type Axis = NoInputAxes;
259 type Axis2 = NoInputAxes2;
260}
261
262#[derive(Clone, Copy, PartialEq, Eq)]
265enum Turn {
266 Sprite,
267 Block,
268}
269
270impl Turn {
271 fn other(self) -> Self {
272 match self {
273 Turn::Sprite => Turn::Block,
274 Turn::Block => Turn::Sprite,
275 }
276 }
277}
278
279struct Unit {
281 tile: (i32, i32),
282 position: Vec3,
283 previous: Vec3,
284 target: Option<Vec3>,
285 facing_right: bool,
288}
289
290impl Unit {
291 fn resting(tile: (i32, i32), lift: f32) -> Self {
292 let position = tile_center(tile) + Vec3::Y * lift;
293 Self {
294 tile,
295 position,
296 previous: position,
297 target: None,
298 facing_right: true,
299 }
300 }
301
302 fn advance(&mut self, dt: Duration) -> bool {
305 let Some(target) = self.target else {
306 return false;
307 };
308 let to_target = target - self.position;
309 let distance = to_target.length();
310 let step = UNIT_SPEED * dt.as_secs_f32();
311 if distance <= step {
312 self.position = target;
313 self.target = None;
314 true
315 } else {
316 self.position += to_target * (step / distance);
317 false
318 }
319 }
320}
321
322fn unit_geometry(turn: Turn) -> (f32, Vec3) {
325 match turn {
326 Turn::Sprite => (SPRITE_HEIGHT * 0.5, SPRITE_HALF_EXTENTS),
327 Turn::Block => (BLOCK_SIZE * 0.5, BLOCK_HALF_EXTENTS),
328 }
329}
330
331fn tile_center((col, row): (i32, i32)) -> Vec3 {
334 let half = (BOARD_TILES - 1) as f32 * 0.5;
335 Vec3::new(
336 (col as f32 - half) * TILE_SIZE,
337 0.0,
338 (row as f32 - half) * TILE_SIZE,
339 )
340}
341
342fn tile_at(point: Vec3) -> Option<(i32, i32)> {
344 let half = (BOARD_TILES - 1) as f32 * 0.5;
345 let col = (point.x / TILE_SIZE + half + 0.5).floor() as i32;
346 let row = (point.z / TILE_SIZE + half + 0.5).floor() as i32;
347 ((0..BOARD_TILES).contains(&col) && (0..BOARD_TILES).contains(&row)).then_some((col, row))
348}
349
350#[derive(Clone, Copy, PartialEq)]
353enum Hover {
354 None,
355 CurrentUnit,
356 Tile((i32, i32)),
357}
358
359struct Board {
360 sprite: Unit,
361 block: Unit,
362 turn: Turn,
363 selected: bool,
364}
365
366impl Board {
367 fn init(ctx: &mut InitContext<'_, Board>) -> Result<Self, Error> {
368 for &(_, _, seed, _) in &ROCKS {
369 ctx.prepare(Rock { seed });
370 }
371
372 let (sprite_lift, _) = unit_geometry(Turn::Sprite);
373 let (block_lift, _) = unit_geometry(Turn::Block);
374 Ok(Self {
375 sprite: Unit::resting((1, 1), sprite_lift),
376 block: Unit::resting((BOARD_TILES - 2, BOARD_TILES - 2), block_lift),
377 turn: Turn::Sprite,
378 selected: false,
379 })
380 }
381
382 fn camera() -> Camera {
383 let eye = Vec3::new(9.0, 9.0, 9.0);
384 Camera::new(
385 View::look_at(eye, Vec3::ZERO),
386 Projection::orthographic(11.0),
387 )
388 }
389
390 fn current(&self) -> &Unit {
391 match self.turn {
392 Turn::Sprite => &self.sprite,
393 Turn::Block => &self.block,
394 }
395 }
396
397 fn current_mut(&mut self) -> &mut Unit {
398 match self.turn {
399 Turn::Sprite => &mut self.sprite,
400 Turn::Block => &mut self.block,
401 }
402 }
403
404 fn other(&self) -> &Unit {
405 match self.turn {
406 Turn::Sprite => &self.block,
407 Turn::Block => &self.sprite,
408 }
409 }
410
411 fn handle_click(&mut self, ctx: &mut TickContext<'_, Board>) {
415 if ctx.ui_wants_pointer() || !ctx.pressed(Button::Select) {
416 return;
417 }
418 let ray = ctx
419 .last_camera()
420 .ray_through(ctx.pointer(), ctx.window_size());
421 let (lift, half) = unit_geometry(self.turn);
422
423 if self.current().target.is_none() {
424 let center = self.current().position;
425 if ray.hit_aabb(center - half, center + half).is_some() {
426 self.selected = !self.selected;
427 return;
428 }
429 }
430 if !self.selected {
431 return;
432 }
433
434 let Some(distance) = ray.hit_plane(ray::Plane {
435 point: Vec3::ZERO,
436 normal: Vec3::Y,
437 }) else {
438 return;
439 };
440 let Some(tile) = tile_at(ray.at(distance)) else {
441 return;
442 };
443 if tile == self.current().tile || tile == self.other().tile {
444 return;
445 }
446
447 let destination = tile_center(tile) + Vec3::Y * lift;
448 let heading = destination.x - self.current().position.x;
449 let current = self.current_mut();
450 if heading.abs() > f32::EPSILON {
451 current.facing_right = heading > 0.0;
452 }
453 current.target = Some(destination);
454 self.selected = false;
455 ctx.play(Sound::Click);
456 }
457
458 fn hovered(&self, ctx: &FrameContext<'_, Board>) -> Hover {
460 if ctx.ui_wants_pointer() {
461 return Hover::None;
462 }
463 let ray = ctx
464 .last_camera()
465 .ray_through(ctx.pointer(), ctx.window_size());
466
467 if self.current().target.is_none() {
468 let (_, half) = unit_geometry(self.turn);
469 let center = self.current().position;
470 if ray.hit_aabb(center - half, center + half).is_some() {
471 return Hover::CurrentUnit;
472 }
473 }
474 let Some(distance) = ray.hit_plane(ray::Plane {
475 point: Vec3::ZERO,
476 normal: Vec3::Y,
477 }) else {
478 return Hover::None;
479 };
480 match tile_at(ray.at(distance)) {
481 Some(tile) => Hover::Tile(tile),
482 None => Hover::None,
483 }
484 }
485
486 fn reachable(&self, tile: (i32, i32)) -> bool {
489 tile != self.current().tile && tile != self.other().tile
490 }
491
492 fn draw_board(&self, ctx: &mut FrameContext<'_, Board>, hover: Hover) {
493 let scale = Vec3::new(TILE_SIZE - TILE_GAP, TILE_THICKNESS, TILE_SIZE - TILE_GAP);
494 for col in 0..BOARD_TILES {
495 for row in 0..BOARD_TILES {
496 let tile = (col, row);
497 let center = tile_center(tile) - Vec3::Y * (TILE_THICKNESS * 0.5);
498 let reachable = self.selected && self.reachable(tile);
499 let hovered = self.selected && hover == Hover::Tile(tile);
500 let color = if hovered {
501 if reachable {
502 HOVER_REACHABLE_TILE
503 } else {
504 HOVER_BLOCKED_TILE
505 }
506 } else if (col + row) % 2 == 0 {
507 LIGHT_TILE
508 } else {
509 DARK_TILE
510 };
511 ctx.draw(
512 Cube.at(Transform::from_scale_rotation_translation(
513 scale,
514 Quat::IDENTITY,
515 center,
516 ))
517 .material(Material::lit(color)),
518 );
519 if reachable && !hovered {
520 self.draw_reachable_mark(ctx, tile);
521 }
522 }
523 }
524 }
525
526 fn draw_reachable_mark(&self, ctx: &mut FrameContext<'_, Board>, tile: (i32, i32)) {
530 let center = tile_center(tile) + Vec3::Y * REACHABLE_MARK_LIFT;
531 ctx.draw(
532 Plane
533 .at(Transform::from_scale_rotation_translation(
534 Vec3::new(
535 TILE_SIZE * REACHABLE_MARK_SCALE,
536 1.0,
537 TILE_SIZE * REACHABLE_MARK_SCALE,
538 ),
539 Quat::IDENTITY,
540 center,
541 ))
542 .material(Material::color(REACHABLE_MARK)),
543 );
544 }
545
546 fn draw_current_mark(&self, ctx: &mut FrameContext<'_, Board>) {
551 let (color, scale) = if self.selected {
552 (CURRENT_MARK, CURRENT_MARK_SCALE)
553 } else {
554 (TURN_MARK, TURN_MARK_SCALE)
555 };
556 let center = tile_center(self.current().tile) + Vec3::Y * REACHABLE_MARK_LIFT;
557 ctx.draw(
558 Plane
559 .at(Transform::from_scale_rotation_translation(
560 Vec3::new(TILE_SIZE * scale, 1.0, TILE_SIZE * scale),
561 Quat::IDENTITY,
562 center,
563 ))
564 .material(Material::color(color)),
565 );
566 }
567
568 fn draw_rocks(&self, ctx: &mut FrameContext<'_, Board>) {
569 for &(x, z, seed, scale) in &ROCKS {
570 let angle = hash_signed(seed, 99) * core::f32::consts::PI;
571 ctx.draw(
572 Rock { seed }
573 .at(Transform::from_scale_rotation_translation(
574 Vec3::splat(scale),
575 Quat::from_rotation_y(angle),
576 Vec3::new(x, 0.5 * scale, z),
577 ))
578 .material(Material::lit(ROCK_COLOR)),
579 );
580 }
581 }
582
583 fn draw_sprite(&self, ctx: &mut FrameContext<'_, Board>, hover: Hover) {
584 let position = self.sprite.previous.lerp(self.sprite.position, ctx.alpha());
585 let current = self.turn == Turn::Sprite;
586 let (tint, glow) = if current && self.selected {
587 (SELECTED_TINT, SELECTED_GLOW)
588 } else if current && hover == Hover::CurrentUnit {
589 (HOVER_TINT, HOVER_GLOW)
590 } else if current {
591 (TURN_TINT, TURN_GLOW)
592 } else {
593 (Color::WHITE, Color::BLACK)
594 };
595 ctx.draw(
596 Sprite
597 .at(Transform::from_scale_rotation_translation(
598 Vec3::new(SPRITE_WIDTH, SPRITE_HEIGHT, 1.0),
599 Quat::IDENTITY,
600 position,
601 ))
602 .upright()
603 .frame(sprite_frame(self.sprite.facing_right))
604 .material(Material::lit(tint).cutout().emissive(glow)),
605 );
606 }
607
608 fn draw_block(&self, ctx: &mut FrameContext<'_, Board>, hover: Hover) {
609 let position = self.block.previous.lerp(self.block.position, ctx.alpha());
610 let current = self.turn == Turn::Block;
611 let (color, glow) = if current && self.selected {
612 (SELECTED_TINT, SELECTED_GLOW)
613 } else if current && hover == Hover::CurrentUnit {
614 (HOVER_TINT, HOVER_GLOW)
615 } else if current {
616 (BLOCK_TURN, TURN_GLOW)
617 } else {
618 (BLOCK_IDLE, Color::BLACK)
619 };
620 ctx.draw(
621 Cube.at(Transform::from_scale_rotation_translation(
622 Vec3::splat(BLOCK_SIZE),
623 Quat::IDENTITY,
624 position,
625 ))
626 .material(Material::lit(color).emissive(glow)),
627 );
628 }
629
630 fn draw_ground(&self, ctx: &mut FrameContext<'_, Board>) {
631 ctx.draw(
632 Plane
633 .at(Transform::from_scale_rotation_translation(
634 Vec3::new(GROUND_HALF * 2.0, 1.0, GROUND_HALF * 2.0),
635 Quat::IDENTITY,
636 Vec3::new(0.0, GROUND_Y, 0.0),
637 ))
638 .material(Material::lit(GROUND_COLOR)),
639 );
640 }
641
642 fn overlay(&self, ctx: &mut FrameContext<'_, Board>) {
644 ctx.ui(|ui| {
645 ui.label(match self.turn {
646 Turn::Sprite => "the sprite unit's turn",
647 Turn::Block => "the block unit's turn",
648 });
649 ui.label(if self.selected {
650 "click a marked tile to order the move"
651 } else {
652 "click the glowing unit to select it"
653 });
654 });
655 }
656
657 fn click_effect(&self, hover: Hover) -> Option<&'static str> {
660 match hover {
661 Hover::CurrentUnit if self.selected => Some("deselect"),
662 Hover::CurrentUnit => Some("select"),
663 Hover::Tile(tile) if self.selected && self.reachable(tile) => Some("move here"),
664 Hover::Tile(_) if self.selected => Some("occupied"),
665 _ => None,
666 }
667 }
668
669 fn draw_prompt(&self, ctx: &mut FrameContext<'_, Board>, camera: Camera, hover: Hover) {
672 let Some(text) = self.click_effect(hover) else {
673 return;
674 };
675 let point = match hover {
676 Hover::CurrentUnit => {
677 let (lift, _) = unit_geometry(self.turn);
678 self.current().position + Vec3::Y * (lift * 2.0 + PROMPT_UNIT_LIFT)
679 }
680 Hover::Tile(tile) => tile_center(tile) + Vec3::Y * PROMPT_TILE_LIFT,
681 Hover::None => return,
682 };
683 let galley = ctx.text_layout(text, egui::FontId::proportional(PROMPT_SIZE));
684 let window_size = ctx.window_size();
685 let pixels_per_point = ctx.pixels_per_point();
686 let Some(pixel) = camera.pixel_of(point, window_size) else {
687 return;
688 };
689 let at = logical(pixel, pixels_per_point);
690 ctx.ui(|ui| {
691 let painter = ui.painter();
692 let ink = galley.mesh_bounds;
693 let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
694 let backdrop = egui::Rect::from_center_size(
695 at,
696 ink.size() + egui::Vec2::splat(PROMPT_PADDING * 2.0),
697 );
698 painter.rect_filled(
699 backdrop,
700 PROMPT_PADDING,
701 egui::Color32::from_black_alpha(PROMPT_BACKDROP),
702 );
703 painter.galley(pos, galley, PROMPT_TEXT_COLOR);
704 });
705 }
706}
707
708fn logical(pixel: Vec2, pixels_per_point: f32) -> egui::Pos2 {
710 let point = pixel / pixels_per_point;
711 egui::pos2(point.x, point.y)
712}
713
714fn sprite_frame(facing_right: bool) -> Frame {
717 let cell = Sheet::new(UVec2::new(SPRITE_COLUMNS, SPRITE_ROWS))
718 .cell_at(UVec2::new(SPRITE_COLUMN, SPRITE_ROW));
719 if facing_right { cell } else { cell.mirrored() }
720}
721
722fn build_rock(seed: u32) -> MeshData {
725 let corners: [Vec3; 8] = core::array::from_fn(|index| {
726 let sign = Vec3::new(
727 if index & 1 == 0 { -0.5 } else { 0.5 },
728 if index & 2 == 0 { -0.5 } else { 0.5 },
729 if index & 4 == 0 { -0.5 } else { 0.5 },
730 );
731 sign + corner_offset(seed, index as u32)
732 });
733 let corner_at = |sign: Vec3| corners[corner_index(sign)];
734
735 let mut vertices = Vec::with_capacity(ROCK_FACES.len() * 4);
736 let mut indices = Vec::with_capacity(ROCK_FACES.len() * 6);
737 for (face, &(normal, right, up)) in ROCK_FACES.iter().enumerate() {
738 let quad = [
739 corner_at(normal - right - up),
740 corner_at(normal + right - up),
741 corner_at(normal + right + up),
742 corner_at(normal - right + up),
743 ];
744 let normal = (quad[1] - quad[0]).cross(quad[3] - quad[0]).normalize();
745 let uvs = [
746 Vec2::new(0.0, 1.0),
747 Vec2::new(1.0, 1.0),
748 Vec2::new(1.0, 0.0),
749 Vec2::new(0.0, 0.0),
750 ];
751 vertices.extend(
752 quad.into_iter()
753 .zip(uvs)
754 .map(|(corner, uv)| Vertex::new(corner, normal, uv)),
755 );
756 let base = face as u32 * 4;
757 indices.extend(ROCK_TRIANGLES.map(|index| base + index));
758 }
759 MeshData::new(vertices, indices)
760}
761
762fn corner_index(sign: Vec3) -> usize {
765 let bit = |component: f32| usize::from(component > 0.0);
766 bit(sign.x) | bit(sign.y) << 1 | bit(sign.z) << 2
767}
768
769fn corner_offset(seed: u32, index: u32) -> Vec3 {
773 Vec3::new(
774 hash_signed(seed, index * 3),
775 hash_signed(seed, index * 3 + 1),
776 hash_signed(seed, index * 3 + 2),
777 ) * ROCK_JITTER
778}
779
780fn hash(seed: u32, salt: u32) -> u32 {
782 let mut x = seed ^ salt.wrapping_mul(0x9E37_79B9);
783 x ^= x >> 16;
784 x = x.wrapping_mul(0x7FEB_352D);
785 x ^= x >> 15;
786 x = x.wrapping_mul(0x846C_A68B);
787 x ^= x >> 16;
788 x
789}
790
791fn hash_signed(seed: u32, salt: u32) -> f32 {
793 hash(seed, salt) as f32 / u32::MAX as f32 * 2.0 - 1.0
794}
795
796impl Game for Board {
797 type Meshes = Shape;
798 type Sounds = Sound;
799 type InputActions = Controls;
800 type Skyboxes = Sky;
801 type SurfaceStyles = NoSurfaceStyles;
802 type PostEffects = NoPostEffects;
803
804 fn tick(&mut self, ctx: &mut TickContext<'_, Board>) {
805 self.sprite.previous = self.sprite.position;
806 self.block.previous = self.block.position;
807
808 self.handle_click(ctx);
809
810 if self.current_mut().advance(ctx.dt()) {
811 if let Some(tile) = tile_at(self.current().position) {
812 self.current_mut().tile = tile;
813 }
814 self.turn = self.turn.other();
815 self.selected = false;
816 }
817 }
818
819 fn frame(&mut self, ctx: &mut FrameContext<'_, Board>) {
820 let camera = Self::camera();
821 ctx.set_camera(camera);
822 ctx.set_skybox(Sky::Day);
823 ctx.light(Light::directional(Vec3::new(-0.35, -1.0, -0.5), SUN_COLOR).shadow());
824 ctx.set_bloom(BLOOM);
825
826 let hover = self.hovered(ctx);
827 self.draw_ground(ctx);
828 self.draw_board(ctx, hover);
829 self.draw_current_mark(ctx);
830 self.draw_rocks(ctx);
831 self.draw_sprite(ctx, hover);
832 self.draw_block(ctx, hover);
833 self.draw_prompt(ctx, camera, hover);
834
835 self.overlay(ctx);
836 }
837}