1mod sim;
3use ferravex_core::FixedClock;
4use ferravex_render::{demo::Demo, prelude::*};
5use sim::{Controls, LAPS, Pose, Race, Track};
6use std::collections::VecDeque;
7
8const CREAM: Color = Color::new(0.96, 0.92, 0.81, 1.);
9const INK: Color = Color::new(0.12, 0.19, 0.17, 1.);
10fn conf() -> Conf {
11 ferravex_render::window_conf("Pocket Rally / Ferravex")
12}
13fn rgba(r: u8, g: u8, b: u8) -> Color {
14 Color::from_rgba(r, g, b, 255)
15}
16fn line(a: Vec2, b: Vec2, width: f32, color: Color) {
17 draw_line(a.x, a.y, b.x, b.y, width, color);
18}
19fn quad(points: [Vec2; 4], color: Color) {
20 draw_triangle(points[0], points[1], points[2], color);
21 draw_triangle(points[0], points[2], points[3], color);
22}
23fn local_quad(pose: Pose, center: Vec2, half: Vec2, color: Color) {
24 let rotation = Vec2::from_angle(pose.angle);
25 quad(
26 [
27 vec2(-half.x, -half.y),
28 vec2(half.x, -half.y),
29 vec2(half.x, half.y),
30 vec2(-half.x, half.y),
31 ]
32 .map(|p| pose.position + rotation.rotate(p + center)),
33 color,
34 );
35}
36fn car(pose: Pose, ghost: bool) {
37 if ghost {
38 local_quad(
39 pose,
40 Vec2::ZERO,
41 vec2(12., 6.),
42 Color::new(0.95, 0.89, 0.60, 0.28),
43 );
44 return;
45 }
46 local_quad(
47 Pose {
48 position: pose.position + vec2(3., 4.),
49 ..pose
50 },
51 Vec2::ZERO,
52 vec2(13., 7.),
53 Color::new(0.08, 0.12, 0.10, 0.32),
54 );
55 for x in [-7., 7.] {
56 for y in [-6., 6.] {
57 local_quad(pose, vec2(x, y), vec2(3., 1.6), rgba(25, 30, 27));
58 }
59 }
60 local_quad(pose, Vec2::ZERO, vec2(12., 5.8), rgba(184, 62, 39));
61 local_quad(pose, vec2(1., 0.), vec2(10., 2.), CREAM);
62 local_quad(pose, vec2(-1., 0.), vec2(4., 5.), rgba(224, 101, 58));
63 local_quad(pose, vec2(3., 0.), vec2(2., 4.7), rgba(42, 66, 65));
64 local_quad(pose, vec2(-5., 0.), vec2(1.5, 4.7), rgba(42, 66, 65));
65 for y in [-3.7, 3.7] {
66 local_quad(pose, vec2(11., y), vec2(1., 1.3), CREAM);
67 }
68 local_quad(pose, vec2(-11., 0.), vec2(1.2, 6.8), rgba(90, 46, 33));
69}
70fn tree(p: Vec2, size: f32, variant: u32) {
71 let shadow = Color::new(0.12, 0.20, 0.13, 0.18);
72 draw_poly(p.x + 5., p.y + 6., 6, size * 0.9, 15., shadow);
73 let light = if variant.is_multiple_of(2) {
74 rgba(80, 108, 61)
75 } else {
76 rgba(64, 95, 59)
77 };
78 draw_poly(p.x, p.y, 5, size, 18., light);
79 draw_triangle(
80 p + vec2(-size * 0.8, -size * 0.45),
81 p + vec2(0., -size),
82 p + vec2(0., size * 0.65),
83 rgba(111, 128, 70),
84 );
85 draw_triangle(
86 p + vec2(0., -size),
87 p + vec2(size * 0.9, size * 0.3),
88 p + vec2(0., size * 0.65),
89 rgba(48, 78, 50),
90 );
91}
92fn barn(p: Vec2) {
93 draw_rectangle(
94 p.x + 5.,
95 p.y + 6.,
96 62.,
97 33.,
98 Color::new(0.1, 0.15, 0.1, 0.25),
99 );
100 draw_rectangle(p.x, p.y, 58., 32., rgba(223, 204, 159));
101 quad(
102 [
103 p + vec2(-3., -3.),
104 p + vec2(61., -3.),
105 p + vec2(61., 14.),
106 p + vec2(-3., 14.),
107 ],
108 rgba(154, 82, 52),
109 );
110 quad(
111 [
112 p + vec2(-3., 14.),
113 p + vec2(61., 14.),
114 p + vec2(61., 31.),
115 p + vec2(-3., 31.),
116 ],
117 rgba(114, 66, 46),
118 );
119 line(
120 p + vec2(-3., 14.),
121 p + vec2(61., 14.),
122 2.,
123 rgba(190, 119, 72),
124 );
125}
126fn draw_land(track: &Track) {
127 draw_rectangle(-100., -100., 1300., 850., rgba(158, 161, 111));
128 let fields = [
129 rgba(164, 164, 111),
130 rgba(172, 166, 112),
131 rgba(147, 155, 103),
132 rgba(180, 169, 120),
133 rgba(145, 154, 102),
134 ];
135 for y in 0..5 {
136 for x in 0..8 {
137 let p = vec2(x as f32 * 160. - 60., y as f32 * 160. - 60.);
138 let k = ((x * 13 + y * 7) % fields.len() as i32) as usize;
139 draw_triangle(p, p + vec2(160., 0.), p + vec2(160., 160.), fields[k]);
140 draw_triangle(
141 p,
142 p + vec2(160., 160.),
143 p + vec2(0., 160.),
144 fields[(k + 1) % fields.len()],
145 );
146 }
147 }
148 for y in 0..13 {
149 line(
150 vec2(370., 235. + y as f32 * 8.),
151 vec2(470., 210. + y as f32 * 8.),
152 2.,
153 rgba(133, 145, 88),
154 );
155 }
156 for y in 0..14 {
157 line(
158 vec2(710., 240. + y as f32 * 8.),
159 vec2(810., 230. + y as f32 * 8.),
160 2.,
161 rgba(151, 144, 91),
162 );
163 }
164 let pond = [
165 vec2(930., 560.),
166 vec2(985., 534.),
167 vec2(1060., 555.),
168 vec2(1080., 612.),
169 vec2(980., 625.),
170 ];
171 for i in 1..pond.len() - 1 {
172 draw_triangle(pond[0], pond[i], pond[i + 1], rgba(115, 145, 132));
173 }
174 draw_triangle(pond[1], pond[2], vec2(975., 603.), rgba(128, 157, 140));
175 barn(vec2(620., 260.));
176 barn(vec2(275., 280.));
177 for i in 0..150_u32 {
178 let x = ((i * 197 + 43) % 1090) as f32 + 5.;
179 let y = ((i * 157 + i * i * 3 + 31) % 640) as f32;
180 let p = vec2(x, y);
181 if track.nearest(p).1 > 67.
182 && !Rect::new(270., 245., 430., 90.).contains(p)
183 && !(x > 920. && y > 525.)
184 {
185 tree(p, 8. + (i % 8) as f32, i);
186 }
187 }
188 for (width, color) in [
189 (84., rgba(185, 169, 128)),
190 (73., rgba(206, 189, 146)),
191 (66., rgba(80, 86, 77)),
192 ] {
193 for i in 0..track.points.len() {
194 let a = track.points[i];
195 let b = track.points[(i + 1) % track.points.len()];
196 line(a, b, width, color);
197 draw_circle(a.x, a.y, width * 0.5, color);
198 }
199 }
200 for i in 0..track.points.len() {
201 if i % 4 == 0 {
202 line(
203 track.points[i],
204 track.points[(i + 1) % track.points.len()],
205 1.4,
206 rgba(180, 178, 148),
207 );
208 }
209 if i % 2 == 0 {
210 let normal = vec2(-track.tangent(i).y, track.tangent(i).x);
211 let a = track.points[i] + normal * 35.;
212 let b = track.points[(i + 1) % track.points.len()] + normal * 35.;
213 if i % 48 < 15 {
214 line(a, b, 4., if i % 4 == 0 { rgba(180, 93, 69) } else { CREAM });
215 }
216 }
217 }
218 let (start, tangent) = track.gate(0);
219 let normal = vec2(-tangent.y, tangent.x);
220 for x in 0..2 {
221 for y in 0..10 {
222 let center = start + tangent * (x as f32 * 5. - 2.5) + normal * (y as f32 * 6. - 27.);
223 local_quad(
224 Pose {
225 position: center,
226 angle: tangent.y.atan2(tangent.x),
227 },
228 Vec2::ZERO,
229 vec2(2.5, 3.),
230 if (x + y) % 2 == 0 { CREAM } else { INK },
231 );
232 }
233 }
234 for side in [-1., 1.] {
235 let post = start + normal * 46. * side;
236 draw_circle(
237 post.x + 2.,
238 post.y + 3.,
239 5.,
240 Color::new(0.1, 0.15, 0.1, 0.2),
241 );
242 draw_circle(post.x, post.y, 3., CREAM);
243 line(post, post - tangent * 16., 3., CREAM);
244 }
245}
246fn text(s: &str, x: f32, y: f32, size: f32, color: Color) {
247 draw_text(s, x + 1., y + 1., size, Color::new(0.05, 0.1, 0.08, 0.35));
248 draw_text(s, x, y, size, color);
249}
250fn time(t: f32) -> String {
251 format!("{:02}:{:05.2}", (t / 60.) as u32, t % 60.)
252}
253fn touch_controls() -> (Controls, bool) {
254 let mut c = Controls::default();
255 let active = ferravex_render::input::logical_touches()
256 .iter()
257 .any(|t| t.phase != TouchPhase::Ended && t.phase != TouchPhase::Cancelled);
258 for touch in ferravex_render::input::logical_touches()
259 .into_iter()
260 .filter(|t| !matches!(t.phase, TouchPhase::Ended | TouchPhase::Cancelled))
261 {
262 if touch.position.y > screen_height() - 130. {
263 let x = touch.position.x;
264 if x < 100. {
265 c.steer = -1.;
266 } else if x < 200. {
267 c.steer = 1.;
268 } else if x > screen_width() - 100. {
269 c.throttle = 1.;
270 } else if x > screen_width() - 200. {
271 c.handbrake = true;
272 c.throttle = -0.3;
273 }
274 }
275 }
276 (c, active)
277}
278
279ferravex_render::game!(conf, game);
280async fn game() {
281 let track = Track::new();
282 let mut race = Race::new(&track);
283 let mut clock = FixedClock::new(120., 8);
284 let mut demo = Demo::new();
285 let mut marks: VecDeque<(Vec2, Vec2)> = VecDeque::new();
286 let mut touch_mode = false;
287 loop {
288 ferravex_render::ui::warm_up_font_sizes(&[12, 13, 16, 17, 18, 20, 21, 23, 25, 26, 32]);
289 if is_key_pressed(KeyCode::R)
290 || race.finished() && is_mouse_button_pressed(MouseButton::Left)
291 {
292 race.reset(&track);
293 clock.reset();
294 marks.clear();
295 }
296 let (touch, touched) = touch_controls();
297 touch_mode |= touched;
298 let held = |a, b| f32::from(is_key_down(a) || is_key_down(b));
299 let controls = Controls {
300 throttle: (held(KeyCode::W, KeyCode::Up) - held(KeyCode::S, KeyCode::Down)
301 + touch.throttle)
302 .clamp(-1., 1.),
303 steer: (held(KeyCode::D, KeyCode::Right) - held(KeyCode::A, KeyCode::Left)
304 + touch.steer)
305 .clamp(-1., 1.),
306 handbrake: is_key_down(KeyCode::Space) || touch.handbrake,
307 };
308 let frame = clock.advance(demo.dt() as f64);
309 for _ in 0..frame.steps {
310 let previous = race.car.pose.position;
311 let input = if demo.autoplay {
312 race.autopilot(&track)
313 } else {
314 controls
315 };
316 race.step(&track, input, frame.dt);
317 if !race.finished()
318 && (race.car.sliding || race.car.offroad)
319 && race.ticks.is_multiple_of(3)
320 {
321 marks.push_back((previous, race.car.pose.position));
322 if marks.len() > 500 {
323 marks.pop_front();
324 }
325 }
326 }
327 clear_background(rgba(137, 148, 103));
328 let scale = (screen_width() / 1140.).min((screen_height() - 135.).max(150.) / 650.);
329 let mut camera = Camera2D::from_display_rect(Rect::new(
332 550. - screen_width() / (2. * scale),
333 325. - screen_height() / (2. * scale),
334 screen_width() / scale,
335 screen_height() / scale,
336 ));
337 camera.zoom.y = camera.zoom.y.abs();
338 set_camera(&camera);
339 draw_land(&track);
340 for (a, b) in &marks {
341 line(*a, *b, 3., Color::new(0.20, 0.22, 0.17, 0.34));
342 }
343 if !race.finished() {
344 let (gate, tangent) = track.gate(race.next_gate);
345 let normal = vec2(-tangent.y, tangent.x);
346 for side in [-1., 1.] {
347 let p = gate + normal * 44. * side;
348 draw_triangle(
349 p - tangent * 6. - normal * 3.,
350 p + tangent * 5.,
351 p - tangent * 6. + normal * 3.,
352 CREAM,
353 );
354 }
355 if let Some(ghost) = race.ghost() {
356 car(ghost, true);
357 }
358 }
359 car(race.car.pose, false);
360 set_default_camera();
361 let width = screen_width();
362 text("POCKET RALLY", 26., 36., 25., CREAM);
363 if width > 660. {
364 text(
365 "VALLE VERDE",
366 27.,
367 57.,
368 13.,
369 Color::new(0.96, 0.92, 0.81, 0.68),
370 );
371 }
372 let lap_x = if width > 660. {
373 width * 0.45
374 } else {
375 width * 0.51
376 };
377 text(
378 &format!("LAP {} / {LAPS}", (race.laps + 1).min(LAPS)),
379 lap_x,
380 36.,
381 20.,
382 CREAM,
383 );
384 if width > 440. {
385 text(
386 &time(if race.finished() {
387 race.last_lap.unwrap_or(0.)
388 } else {
389 race.lap_time
390 }),
391 width - 220.,
392 36.,
393 23.,
394 CREAM,
395 );
396 }
397 text(
398 &format!(
399 "{:03}",
400 if race.finished() {
401 0
402 } else {
403 (race.car.velocity.length() * 0.48) as u32
404 }
405 ),
406 width - 86.,
407 36.,
408 25.,
409 CREAM,
410 );
411 text("KM/H", width - 85., 54., 12., CREAM);
412 if let Some(best) = race.best_time {
413 text(
414 &format!("BEST {}", time(best)),
415 26.,
416 screen_height() - 24.,
417 17.,
418 CREAM,
419 );
420 }
421 if touch_mode {
422 for (x, label) in [
423 (46., "<"),
424 (144., ">"),
425 (width - 150., "BRAKE"),
426 (width - 52., "GAS"),
427 ] {
428 draw_circle(
429 x,
430 screen_height() - 65.,
431 36.,
432 Color::new(0.1, 0.2, 0.14, 0.35),
433 );
434 let measure = measure_text(label, None, 16, 1.);
435 text(
436 label,
437 x - measure.width / 2.,
438 screen_height() - 59.,
439 16.,
440 CREAM,
441 );
442 }
443 } else if demo.time() < 9. || race.ticks == 0 {
444 let hint = if width > 700. {
445 "WASD / ARROWS drive SPACE handbrake R restart"
446 } else {
447 "Arrows drive / Space brake / R restart"
448 };
449 let measure = measure_text(hint, None, 17, 1.);
450 text(
451 hint,
452 (width - measure.width) / 2.,
453 screen_height() - 25.,
454 17.,
455 CREAM,
456 );
457 }
458 if race.finished() {
459 let cx = width / 2.;
460 let cy = screen_height() / 2.;
461 draw_rectangle(
462 cx - 190.,
463 cy - 105.,
464 380.,
465 210.,
466 Color::new(0.12, 0.19, 0.17, 0.94),
467 );
468 text("FINISH", cx - 54., cy - 54., 32., CREAM);
469 text(
470 &format!("3 laps {}", time(race.total_time)),
471 cx - 107.,
472 cy - 12.,
473 26.,
474 CREAM,
475 );
476 text(
477 &format!("Best lap {}", time(race.best_time.unwrap_or(0.))),
478 cx - 104.,
479 cy + 22.,
480 21.,
481 CREAM,
482 );
483 text(
484 "R or tap to race your ghost",
485 cx - 109.,
486 cy + 70.,
487 18.,
488 CREAM,
489 );
490 }
491 if demo.finish_frame() {
492 break;
493 }
494 next_frame().await;
495 }
496}