1mod sim;
3use ferravex_core::FixedClock;
4use ferravex_render::{
5 demo::Demo,
6 mesh::Mesh3d,
7 prelude::*,
8 shaders::{LitMaterial, LitSettings},
9 visibility::{LodChain, LodState},
10};
11use sim::{CHECKPOINTS, Controls, FINISH, GEMS, OBSTACLES, PLATFORMS, RADIUS, Run};
12
13const SKY: Color = Color::from_rgba(172, 194, 189, 255);
14const CREAM: Color = Color::from_rgba(255, 241, 205, 255);
15const INK: Color = Color::from_rgba(32, 57, 60, 255);
16const TEAL: Color = Color::from_rgba(62, 143, 133, 255);
17const GOLD: Color = Color::from_rgba(236, 175, 65, 255);
18fn conf() -> Conf {
19 ferravex_render::window_conf("Marble Run / Ferravex")
20}
21fn rgb(r: u8, g: u8, b: u8) -> Color {
22 Color::from_rgba(r, g, b, 255)
23}
24struct Prop {
25 mesh: Mesh3d,
26 position: Vec3,
27 rotation: Quat,
28 lod: Option<LodChain>,
29 lod_state: std::cell::Cell<LodState>,
30}
31impl Prop {
32 fn new(mesh: Mesh3d, position: Vec3) -> Self {
33 Self {
34 mesh,
35 position,
36 rotation: Quat::IDENTITY,
37 lod: None,
38 lod_state: Default::default(),
39 }
40 }
41 fn with_lod(mut self, low: Mesh3d) -> Self {
42 self.lod = Some(
43 LodChain::new(
44 vec![self.mesh.resident().clone(), low.resident().clone()],
45 vec![50.],
46 0.12,
47 )
48 .unwrap(),
49 );
50 self
51 }
52 fn draw(&self) {
53 if let Some(chain) = &self.lod {
54 let mut state = self.lod_state.get();
55 ferravex_render::draw_lod(
56 chain,
57 Mat4::from_rotation_translation(self.rotation, self.position),
58 &mut state,
59 );
60 self.lod_state.set(state);
61 } else {
62 self.mesh.draw(self.position, self.rotation);
63 }
64 }
65}
66struct Scene {
67 platforms: Vec<Prop>,
68 decoration: Vec<Prop>,
69 marble: Mesh3d,
70 gem: Mesh3d,
71 gem_base: Mesh3d,
72 pillar: Mesh3d,
73 cap: Mesh3d,
74 checkpoint: Mesh3d,
75 checkpoint_lit: Mesh3d,
76 shadow: Mesh3d,
77}
78impl Scene {
79 fn new() -> Self {
80 let platforms = PLATFORMS
81 .iter()
82 .enumerate()
83 .map(|(i, p)| {
84 Prop::new(
85 Mesh3d::cuboid(
86 p.size,
87 if i % 2 == 0 {
88 rgb(190, 149, 113)
89 } else {
90 rgb(210, 172, 126)
91 },
92 ),
93 p.center,
94 )
95 })
96 .collect();
97 let mut decoration = Vec::new();
98 for (i, p) in PLATFORMS.iter().enumerate() {
100 decoration.push(Prop::new(
101 Mesh3d::cuboid(
102 vec3(p.size.x + 0.08, 0.12, p.size.z + 0.08),
103 rgb(129, 102, 85),
104 ),
105 p.center - vec3(0., 0.26, 0.),
106 ));
107 if i % 2 == 0 {
108 decoration.push(Prop::new(
109 Mesh3d::frustum(0.65, 2.0, 6.0, rgb(101, 121, 112), 5),
110 p.center - vec3(0., 3.3, 0.),
111 ));
112 decoration.push(Prop::new(
113 Mesh3d::cuboid(vec3(1.15, 1.6, 1.15), rgb(161, 132, 104)),
114 p.center - vec3(0., 1.1, 0.),
115 ));
116 }
117 for side in [-1., 1.] {
119 let (size, offset) = if p.size.z > p.size.x {
120 (
121 vec3(0.11, 0.018, p.size.z),
122 vec3(side * (p.size.x * 0.5 - 0.15), 0.311, 0.),
123 )
124 } else {
125 (
126 vec3(p.size.x, 0.018, 0.11),
127 vec3(0., 0.311, side * (p.size.z * 0.5 - 0.15)),
128 )
129 };
130 decoration.push(Prop::new(Mesh3d::cuboid(size, TEAL), p.center + offset));
131 }
132 }
133 for p in PLATFORMS.iter().filter(|p| p.size.x.max(p.size.z) > 7.) {
135 let length = p.size.x.max(p.size.z);
136 for i in 1..length as usize {
137 let d = i as f32 - length * 0.5;
138 let (size, offset) = if p.size.z > p.size.x {
139 (vec3(p.size.x - 0.4, 0.013, 0.025), vec3(0., 0.308, d))
140 } else {
141 (vec3(0.025, 0.013, p.size.z - 0.4), vec3(d, 0.308, 0.))
142 };
143 decoration.push(Prop::new(
144 Mesh3d::cuboid(size, rgb(160, 128, 99)),
145 p.center + offset,
146 ));
147 }
148 }
149 for p in CHECKPOINTS {
150 decoration.push(Prop::new(
151 Mesh3d::cylinder(0.07, 2.8, INK, 6),
152 p + vec3(-2., 0.6, -1.8),
153 ));
154 decoration.push(Prop::new(
155 Mesh3d::cuboid(vec3(0.95, 0.5, 0.04), TEAL),
156 p + vec3(-1.6, 1.65, -1.8),
157 ));
158 }
159 for x in [-2.1, 2.1] {
160 decoration.push(Prop::new(
161 Mesh3d::cuboid(vec3(0.3, 3.2, 0.3), TEAL),
162 FINISH + vec3(x, 1., -0.8),
163 ));
164 }
165 decoration.push(Prop::new(
166 Mesh3d::cuboid(vec3(4.6, 0.5, 0.4), TEAL),
167 FINISH + vec3(0., 2.75, -0.8),
168 ));
169 for x in -4..=4 {
170 decoration.push(Prop::new(
171 Mesh3d::cuboid(vec3(0.45, 0.02, 1.4), if x % 2 == 0 { CREAM } else { INK }),
172 FINISH + vec3(x as f32 * 0.45, -0.58, 0.),
173 ));
174 }
175 for i in 0..25 {
177 let angle = i as f32 * 2.39996;
178 let distance = 40. + (i % 5) as f32 * 9.;
179 let p = vec3(angle.cos() * distance, -15., -20. + angle.sin() * distance);
180 let radius = 8. + (i % 4) as f32 * 2.;
181 let height = 15. + (i % 3) as f32 * 7.;
182 let color = if i % 2 == 0 {
183 rgb(110, 144, 130)
184 } else {
185 rgb(131, 155, 139)
186 };
187 decoration.push(
188 Prop::new(Mesh3d::cone(radius, height, color, 5), p)
189 .with_lod(Mesh3d::cone(radius, height, color, 3)),
190 );
191 }
192 let mut marble = Mesh3d::sphere(RADIUS, CREAM, 12, 8);
193 for v in &mut marble.mesh.vertices {
194 if v.position.y.abs() < 0.18 {
195 v.color = GOLD.to_rgba();
196 } else if v.position.x < -0.2 {
197 v.color = TEAL.to_rgba();
198 }
199 }
200 Self {
201 platforms,
202 decoration,
203 marble,
204 gem: Mesh3d::sphere(0.36, GOLD, 4, 2),
205 gem_base: Mesh3d::cylinder(0.48, 0.022, rgb(185, 134, 61), 12),
206 pillar: Mesh3d::cylinder(0.38, 1.3, rgb(146, 80, 61), 8),
207 cap: Mesh3d::cylinder(0.39, 0.13, CREAM, 8),
208 checkpoint: Mesh3d::cylinder(1.25, 0.02, rgb(105, 142, 128), 16),
209 checkpoint_lit: Mesh3d::cylinder(1.25, 0.025, TEAL, 16),
210 shadow: Mesh3d::cylinder(0.62, 0.015, rgb(131, 113, 90), 12),
211 }
212 }
213 fn draw(&self, game: &Run, position: Vec3, rotation: Quat, time: f32) {
214 for prop in &self.platforms {
215 prop.draw();
216 }
217 for prop in &self.decoration {
218 prop.draw();
219 }
220 for p in OBSTACLES {
221 self.pillar.draw(p, Quat::IDENTITY);
222 self.cap.draw(p + vec3(0., 0.55, 0.), Quat::IDENTITY);
223 }
224 for (i, p) in CHECKPOINTS.iter().enumerate() {
225 (if game.checkpoint > i {
226 &self.checkpoint_lit
227 } else {
228 &self.checkpoint
229 })
230 .draw(vec3(p.x, 0.323, p.z), Quat::IDENTITY);
231 }
232 for (i, p) in GEMS.iter().enumerate() {
233 if !game.collected[i] {
234 self.gem_base.draw(vec3(p.x, 0.325, p.z), Quat::IDENTITY);
235 self.gem.draw(
236 *p + vec3(0., 0.15 + (time * 2. + i as f32).sin() * 0.1, 0.),
237 Quat::from_rotation_y(time * 1.3),
238 );
239 }
240 }
241 if position.y > 0.3
242 && PLATFORMS.iter().any(|p| {
243 (position.x - p.center.x).abs() < p.size.x * 0.5
244 && (position.z - p.center.z).abs() < p.size.z * 0.5
245 })
246 {
247 self.shadow.draw(
248 vec3(position.x + 0.12, 0.322, position.z + 0.18),
249 Quat::IDENTITY,
250 );
251 }
252 self.marble.draw(position, rotation);
253 }
254}
255fn text(s: &str, x: f32, y: f32, size: f32, color: Color) {
256 draw_text(s, x + 1., y + 1., size, Color::new(0.1, 0.2, 0.2, 0.25));
257 draw_text(s, x, y, size, color);
258}
259fn centered(s: &str, y: f32, size: u16, color: Color) {
260 let m = measure_text(s, None, size, 1.);
261 text(s, (screen_width() - m.width) * 0.5, y, size as f32, color);
262}
263fn touch_input() -> (Vec2, bool, bool) {
264 let mut direction = Vec2::ZERO;
265 let mut brake = false;
266 let mut active = false;
267 for touch in ferravex_render::input::logical_touches()
268 .iter()
269 .filter(|t| !matches!(t.phase, TouchPhase::Ended | TouchPhase::Cancelled))
270 {
271 active = true;
272 let p = touch.position;
273 let base = vec2(100., screen_height() - 105.);
274 if p.distance(base) < 100. {
275 direction = (p - base).clamp_length_max(48.) / 48.;
276 }
277 if p.distance(vec2(screen_width() - 75., screen_height() - 90.)) < 65. {
278 brake = true;
279 }
280 }
281 (direction, brake, active)
282}
283ferravex_render::game!(conf, game);
284async fn game() {
285 let mut game = Run::new();
286 let scene = Scene::new();
287 let material = LitMaterial::new().expect("Ferravex's portable lit material must compile");
288 let mut clock = FixedClock::new(120., 12);
289 let mut demo = Demo::new();
290 let mut touch_mode = false;
291 let mut camera_target = game.position();
292 let forward = vec3(-8., 0., -19.).normalize();
293 let right = vec3(-forward.z, 0., forward.x);
294 loop {
295 ferravex_render::ui::warm_up_font_sizes(&[12, 17, 18, 20, 23, 26, 30]);
296 let (touch, touch_brake, touched) = touch_input();
297 touch_mode |= touched;
298 let restart = is_key_pressed(KeyCode::R)
299 || game.finished && is_mouse_button_pressed(MouseButton::Left);
300 if restart {
301 game = Run::new();
302 clock.reset();
303 camera_target = game.position();
304 }
305 let held = |a, b| f32::from(is_key_down(a) || is_key_down(b));
306 let horizontal =
307 held(KeyCode::D, KeyCode::Right) - held(KeyCode::A, KeyCode::Left) + touch.x;
308 let vertical = held(KeyCode::W, KeyCode::Up) - held(KeyCode::S, KeyCode::Down) - touch.y;
309 let direction = right * horizontal + forward * vertical;
310 let input = Controls {
311 roll: vec2(direction.x, direction.z).clamp_length_max(1.),
312 brake: is_key_down(KeyCode::Space) || touch_brake,
313 };
314 let frame = clock.advance(demo.dt() as f64);
315 for _ in 0..frame.steps {
316 let controls = if demo.autoplay {
317 game.autopilot()
318 } else {
319 input
320 };
321 game.step(controls, frame.dt);
322 }
323 let position = game.previous_position.lerp(game.position(), frame.alpha);
324 let rotation = game.previous_rotation.slerp(game.rotation(), frame.alpha);
325 camera_target = camera_target.lerp(position, 1. - (-demo.dt() * 6.).exp());
326 clear_background(SKY);
327 let overview = is_key_down(KeyCode::Tab);
328 let camera_position = if overview {
329 vec3(31., 40., 20.)
330 } else {
331 camera_target + vec3(8., 12., 14.)
332 };
333 let target = if overview {
334 vec3(3., 0., -18.)
335 } else {
336 camera_target + vec3(0., -0.2, -5.)
337 };
338 set_camera(&Camera3D {
339 position: camera_position,
340 target,
341 up: Vec3::Y,
342 fovy: 48.0_f32.to_radians(),
343 z_near: 0.1,
344 z_far: 220.,
345 ..Default::default()
346 });
347 material.begin(LitSettings {
348 camera_position,
349 fog_color: SKY,
350 fog_start: 38.,
351 fog_end: 115.,
352 light_direction: vec3(-0.55, 0.85, 0.35),
353 ambient: 0.48,
354 });
355 scene.draw(&game, position, rotation, demo.time());
356 material.end();
357 set_default_camera();
358 text("MARBLE RUN", 27., 39., 26., INK);
359 if screen_width() > 620. {
360 text("THE HANGING AQUEDUCT", 28., 60., 12., INK);
361 }
362 let status = format!(
363 "{} / 6 {:02}:{:04.1}",
364 game.gems(),
365 (game.elapsed / 60.) as u32,
366 game.elapsed % 60.
367 );
368 let status_width = measure_text(&status, None, 23, 1.).width;
369 text(&status, screen_width() - status_width - 27., 39., 23., INK);
370 draw_poly(screen_width() - status_width - 43., 31., 4, 8., 0., GOLD);
371 if game.notice > 0. && !game.finished {
372 centered(
373 if game.recovered {
374 "BACK ON TRACK"
375 } else if game.checkpoint > 0 {
376 "CHECKPOINT SAVED"
377 } else {
378 "BACK ON TRACK"
379 },
380 89.,
381 17,
382 INK,
383 );
384 }
385 if touch_mode && !game.finished {
386 let center = vec2(100., screen_height() - 105.);
387 draw_circle(center.x, center.y, 66., Color::new(0.12, 0.25, 0.24, 0.15));
388 draw_circle_lines(center.x, center.y, 66., 2., CREAM);
389 draw_circle(
390 center.x + touch.x * 35.,
391 center.y + touch.y * 35.,
392 24.,
393 Color::new(0.98, 0.94, 0.81, 0.7),
394 );
395 draw_circle(
396 screen_width() - 75.,
397 screen_height() - 90.,
398 45.,
399 Color::new(0.12, 0.25, 0.24, 0.35),
400 );
401 text(
402 "BRAKE",
403 screen_width() - 102.,
404 screen_height() - 85.,
405 18.,
406 CREAM,
407 );
408 } else if game.elapsed < 10. {
409 centered(
410 if screen_width() > 700. {
411 "WASD / ARROWS roll SPACE brake TAB course R restart"
412 } else {
413 "Arrows roll / Space brake / R restart"
414 },
415 screen_height() - 27.,
416 17,
417 INK,
418 );
419 }
420 if game.finished {
421 let w = 330_f32.min(screen_width() - 30.);
422 let y = screen_height() * 0.43;
423 draw_rectangle(
424 (screen_width() - w) * 0.5,
425 y - 62.,
426 w,
427 182.,
428 Color::new(0.12, 0.23, 0.23, 0.94),
429 );
430 centered("COURSE CLEAR", y - 20., 30, CREAM);
431 centered(
432 &format!("6 crystals / {:.1} seconds", game.elapsed),
433 y + 17.,
434 20,
435 CREAM,
436 );
437 centered(&format!("{} falls", game.falls), y + 48., 18, CREAM);
438 centered("R or tap to roll again", y + 90., 17, CREAM);
439 }
440 if demo.finish_frame() {
441 break;
442 }
443 next_frame().await;
444 }
445}