pocket_rally/
lib.rs

1//! A complete small rally time trial; all art is procedural and all motion is simulated.
2mod 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        // A full-window projection keeps input in logical display pixels, avoiding raw
330        // framebuffer viewport coordinates on Retina and fractional-scale displays.
331        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}