orchard_guard/
lib.rs

1mod game;
2use ferravex_core::FixedClock;
3use ferravex_render::{canvas::Canvas2D, demo::Demo, prelude::*};
4use game::*;
5
6const SIZE: Vec2 = vec2(1200., 760.);
7const INK: Color = Color::new(0.19, 0.25, 0.19, 1.);
8const CREAM: Color = Color::new(0.98, 0.95, 0.84, 1.);
9const GREEN: Color = Color::new(0.23, 0.39, 0.24, 1.);
10const GOLD: Color = Color::new(0.88, 0.62, 0.22, 1.);
11fn conf() -> Conf {
12    let mut c = ferravex_render::window_conf("Orchard Guard · Ferravex");
13    c.window_width = 1200;
14    c.window_height = 760;
15    c
16}
17fn label(s: &str, x: f32, y: f32, size: f32, color: Color) {
18    draw_text(s, x, y, size, color);
19}
20fn center(s: &str, x: f32, y: f32, size: f32, color: Color) {
21    label(
22        s,
23        x - measure_text(s, None, size as u16, 1.).width * 0.5,
24        y,
25        size,
26        color,
27    );
28}
29fn button(rect: Rect, title: &str, subtitle: &str, enabled: bool, pointer: Option<Vec2>) -> bool {
30    let hover = pointer.is_some_and(|p| rect.contains(p));
31    draw_rectangle(
32        rect.x,
33        rect.y,
34        rect.w,
35        rect.h,
36        if enabled {
37            if hover {
38                Color::from_hex(0x4d7044)
39            } else {
40                GREEN
41            }
42        } else {
43            Color::from_hex(0xb1b49c)
44        },
45    );
46    label(title, rect.x + 15., rect.y + 24., 19., CREAM);
47    label(
48        subtitle,
49        rect.x + 15.,
50        rect.y + 43.,
51        14.,
52        if enabled {
53            Color::from_hex(0xd9dfb8)
54        } else {
55            CREAM
56        },
57    );
58    enabled && hover
59}
60fn tree(p: Vec2, variant: usize) {
61    draw_ellipse(
62        p.x + 7.,
63        p.y + 18.,
64        25.,
65        9.,
66        0.,
67        Color::new(0.22, 0.32, 0.19, 0.13),
68    );
69    draw_rectangle(p.x - 3., p.y, 6., 25., Color::from_hex(0x85704c));
70    draw_poly(p.x, p.y - 8., 7, 25., 15., Color::from_hex(0x69894f));
71    draw_poly(p.x - 8., p.y - 14., 7, 20., 0., Color::from_hex(0x78975b));
72    for i in 0..3 {
73        let a = (i as f32 * 2.1 + variant as f32) * 1.4;
74        draw_circle(
75            p.x + a.cos() * 14.,
76            p.y - 10. + a.sin() * 13.,
77            3.4,
78            Color::from_hex(0xc67545),
79        );
80    }
81}
82fn field(g: &Game) {
83    draw_rectangle(0., 70., 1200., 590., Color::from_hex(0xa7b87a));
84    for i in 0..130 {
85        let x = ((i * 97 + 31) % 1200) as f32;
86        let y = 85. + ((i * 67 + 21) % 560) as f32;
87        draw_line(x, y, x + 3., y - 5., 1., Color::new(0.35, 0.46, 0.25, 0.20));
88    }
89    for pair in PATH.windows(2) {
90        draw_line(
91            pair[0].x + 3.,
92            pair[0].y + 6.,
93            pair[1].x + 3.,
94            pair[1].y + 6.,
95            51.,
96            Color::new(0.38, 0.43, 0.24, 0.18),
97        );
98    }
99    for pair in PATH.windows(2) {
100        draw_line(
101            pair[0].x,
102            pair[0].y,
103            pair[1].x,
104            pair[1].y,
105            45.,
106            Color::from_hex(0xd5bf8a),
107        );
108    }
109    for p in PATH {
110        draw_circle(p.x, p.y, 22.5, Color::from_hex(0xd5bf8a));
111    }
112    for pair in PATH.windows(2) {
113        let len = pair[0].distance(pair[1]);
114        let d = (pair[1] - pair[0]) / len;
115        for i in 0..(len / 23.) as usize {
116            let p = pair[0] + d * (i as f32 * 23.);
117            draw_circle(p.x, p.y, 1., Color::new(0.52, 0.42, 0.26, 0.25));
118        }
119    }
120    for (i, p) in [
121        vec2(65., 365.),
122        vec2(100., 455.),
123        vec2(180., 495.),
124        vec2(80., 565.),
125        vec2(180., 595.),
126        vec2(335., 590.),
127        vec2(540., 570.),
128        vec2(620., 570.),
129        vec2(870., 590.),
130        vec2(1090., 590.),
131        vec2(1150., 540.),
132        vec2(1140., 180.),
133        vec2(1035., 180.),
134        vec2(930., 175.),
135        vec2(670., 125.),
136        vec2(570., 125.),
137        vec2(375., 130.),
138        vec2(80., 115.),
139    ]
140    .into_iter()
141    .enumerate()
142    {
143        tree(p, i);
144    }
145    // Orchard gate and farmhouse establish the path's destination.
146    draw_rectangle(1120., 385., 69., 43., Color::from_hex(0xf0e5bc));
147    draw_triangle(
148        vec2(1111., 386.),
149        vec2(1154., 351.),
150        vec2(1197., 386.),
151        Color::from_hex(0xac6848),
152    );
153    draw_rectangle(1147., 404., 15., 24., Color::from_hex(0x7d6848));
154    for y in [302., 353.] {
155        for x in [1135., 1156., 1177.] {
156            draw_rectangle(x, y, 5., 23., CREAM);
157        }
158        draw_rectangle(1135., y + 7., 47., 4., CREAM);
159    }
160    label("ORCHARD", 1106., 452., 15., GREEN);
161    for (i, p) in PLOTS.iter().enumerate() {
162        if g.selected == i && g.outcome == Outcome::Playing {
163            let range = g.towers[i].map_or(158., Tower::range);
164            draw_circle(p.x, p.y, range, Color::new(0.96, 0.95, 0.72, 0.10));
165            draw_circle_lines(p.x, p.y, range, 1., Color::new(0.96, 0.95, 0.72, 0.4));
166        }
167        draw_ellipse(
168            p.x + 4.,
169            p.y + 15.,
170            25.,
171            11.,
172            0.,
173            Color::new(0.24, 0.31, 0.18, 0.22),
174        );
175        draw_circle(p.x, p.y, 25., Color::from_hex(0xc9bd8a));
176        draw_circle_lines(
177            p.x,
178            p.y,
179            25.,
180            2.,
181            if g.selected == i {
182                CREAM
183            } else {
184                Color::from_hex(0x8c9866)
185            },
186        );
187        if let Some(tower) = g.towers[i] {
188            match tower.kind {
189                Kind::Seed => {
190                    draw_rectangle(p.x - 12., p.y - 8., 24., 19., Color::from_hex(0x84623e));
191                    draw_rectangle(p.x - 17., p.y - 16., 34., 8., Color::from_hex(0xbaa477));
192                    let muzzle = *p + tower.aim * 25.;
193                    draw_line(p.x, p.y - 5., muzzle.x, muzzle.y - 5., 10., GREEN);
194                    draw_circle(p.x, p.y - 5., 12., Color::from_hex(0x496c3b));
195                }
196                Kind::Frost => {
197                    draw_rectangle(p.x - 14., p.y - 12., 28., 22., Color::from_hex(0x679b99));
198                    for y in [-12., -4., 4.] {
199                        draw_line(
200                            p.x - 15.,
201                            p.y + y,
202                            p.x + 15.,
203                            p.y + y,
204                            3.,
205                            Color::from_hex(0x9ec6b5),
206                        );
207                    }
208                    draw_triangle(
209                        vec2(p.x - 20., p.y - 14.),
210                        vec2(p.x, p.y - 28.),
211                        vec2(p.x + 20., p.y - 14.),
212                        Color::from_hex(0x447d80),
213                    );
214                    draw_circle(p.x, p.y, 5., INK);
215                }
216            }
217            for n in 0..=tower.level {
218                draw_circle(p.x - 6. + n as f32 * 6., p.y + 20., 2., CREAM);
219            }
220        } else {
221            draw_line(p.x - 7., p.y, p.x + 7., p.y, 2., Color::from_hex(0x7d8a5f));
222            draw_line(p.x, p.y - 7., p.x, p.y + 7., 2., Color::from_hex(0x7d8a5f));
223        }
224    }
225    for pest in &g.pests {
226        let p = pest.pos;
227        draw_ellipse(
228            p.x + 3.,
229            p.y + 7.,
230            13.,
231            6.,
232            0.,
233            Color::new(0.2, 0.2, 0.15, 0.2),
234        );
235        let dir = (PATH[(pest.segment + 1).min(PATH.len() - 1)] - p).normalize_or_zero();
236        let side = vec2(-dir.y, dir.x);
237        let wiggle = (g.time * 24. + pest.id as f32).sin() * 3.;
238        for n in [-1., 1.] {
239            for t in [-6., 0., 6.] {
240                let leg = p + side * n * 14. + dir * (t + wiggle * n);
241                let root = p + dir * t;
242                draw_line(root.x, root.y, leg.x, leg.y, 2., INK);
243            }
244        }
245        let color = if pest.slow > 0. {
246            Color::from_hex(0x75bfca)
247        } else if pest.fast {
248            Color::from_hex(0x9a7451)
249        } else {
250            Color::from_hex(0xa65e4d)
251        };
252        draw_circle(p.x, p.y, if pest.fast { 8. } else { 11. }, color);
253        draw_line(
254            p.x - dir.x * 8.,
255            p.y - dir.y * 8.,
256            p.x + dir.x * 8.,
257            p.y + dir.y * 8.,
258            1.5,
259            INK,
260        );
261        let head = p + dir * 10.;
262        draw_circle(head.x, head.y, 5., INK);
263        if pest.hp < pest.max_hp {
264            draw_rectangle(
265                p.x - 12.,
266                p.y - 20.,
267                24.,
268                3.,
269                Color::new(0.2, 0.25, 0.18, 0.4),
270            );
271            draw_rectangle(
272                p.x - 12.,
273                p.y - 20.,
274                24. * (pest.hp / pest.max_hp).max(0.),
275                3.,
276                CREAM,
277            );
278        }
279    }
280    for s in &g.shots {
281        draw_circle(
282            s.pos.x,
283            s.pos.y,
284            4.,
285            if s.kind == Kind::Seed {
286                Color::from_hex(0xf3d080)
287            } else {
288                Color::from_hex(0xd1f1e8)
289            },
290        );
291    }
292}
293fn hud(g: &Game, pointer: Option<Vec2>, click: bool) -> Option<Action> {
294    draw_rectangle(0., 0., 1200., 70., CREAM);
295    label("ORCHARD GUARD", 28., 33., 28., INK);
296    label(
297        "Keep the harvest safe",
298        29.,
299        53.,
300        15.,
301        Color::from_hex(0x727b59),
302    );
303    center(
304        &format!("WAVE  {} / {}", g.wave, WAVES),
305        600.,
306        33.,
307        20.,
308        INK,
309    );
310    center(
311        if g.active {
312            "Pests on the path"
313        } else {
314            "Build, then send the next wave"
315        },
316        600.,
317        53.,
318        14.,
319        Color::from_hex(0x727b59),
320    );
321    label(&format!("{}  HEARTS", g.lives), 880., 39., 19., INK);
322    draw_circle(1060., 32., 9., GOLD);
323    label(&format!("{}", g.money), 1078., 39., 24., INK);
324    draw_rectangle(0., 660., 1200., 100., CREAM);
325    let mut action = None;
326    if let Some(tower) = g.towers[g.selected] {
327        label(tower.kind.name(), 28., 692., 20., INK);
328        label(
329            &format!("Level {} / 3", tower.level + 1),
330            28.,
331            716.,
332            17.,
333            GREEN,
334        );
335        label(
336            if tower.kind == Kind::Seed {
337                "Fast, focused damage"
338            } else {
339                "Chills pests to 45% speed"
340            },
341            28.,
342            740.,
343            14.,
344            Color::from_hex(0x727b59),
345        );
346        let cost = tower.upgrade_cost();
347        let title = if tower.level == 2 {
348            "FULLY GROWN".into()
349        } else {
350            format!("UPGRADE · {cost}")
351        };
352        if button(
353            Rect::new(260., 683., 190., 55.),
354            &title,
355            "[U]  More power + range",
356            tower.level < 2 && g.money >= cost,
357            pointer,
358        ) && click
359        {
360            action = Some(Action::Upgrade);
361        }
362    } else {
363        label("EMPTY PLOT", 28., 696., 20., INK);
364        label(
365            "Choose what to grow",
366            28.,
367            721.,
368            15.,
369            Color::from_hex(0x727b59),
370        );
371        if button(
372            Rect::new(240., 683., 190., 55.),
373            "SEED SLING · 60",
374            "[1]  Fast damage",
375            g.money >= 60,
376            pointer,
377        ) && click
378        {
379            action = Some(Action::Build(Kind::Seed));
380        }
381        if button(
382            Rect::new(443., 683., 190., 55.),
383            "FROST HIVE · 80",
384            "[2]  Slow the swarm",
385            g.money >= 80,
386            pointer,
387        ) && click
388        {
389            action = Some(Action::Build(Kind::Frost));
390        }
391    }
392    label("Click a plot to build or upgrade", 670., 701., 16., GREEN);
393    label("R restart", 670., 727., 14., Color::from_hex(0x727b59));
394    if button(
395        Rect::new(957., 683., 215., 55.),
396        if g.active {
397            "WAVE IN PROGRESS"
398        } else {
399            "SEND NEXT WAVE"
400        },
401        "[SPACE]  Protect 4 harvests",
402        !g.active,
403        pointer,
404    ) && click
405    {
406        action = Some(Action::Wave);
407    }
408    action
409}
410enum Action {
411    Build(Kind),
412    Upgrade,
413    Wave,
414}
415ferravex_render::game!(conf, game);
416async fn game() {
417    let mut g = Game::new();
418    let mut clock = FixedClock::new(60., 8);
419    let mut demo = Demo::new();
420    loop {
421        ferravex_render::ui::warm_up_font_sizes(&[14, 15, 16, 17, 18, 19, 20, 24, 28, 30]);
422        let canvas = Canvas2D::fit(SIZE, vec2(screen_width().max(1.), screen_height().max(1.)));
423        let touch = ferravex_render::input::logical_touches()
424            .into_iter()
425            .find(|t| t.phase == TouchPhase::Started);
426        let pointer = canvas.pointer(touch.as_ref().map_or_else(
427            || {
428                let (x, y) = mouse_position();
429                vec2(x, y)
430            },
431            |t| t.position,
432        ));
433        let click = is_mouse_button_pressed(MouseButton::Left) || touch.is_some();
434        if is_key_pressed(KeyCode::R) {
435            g = Game::new();
436            clock.reset();
437        }
438        if g.outcome == Outcome::Playing {
439            if click && let Some(p) = pointer {
440                for (i, plot) in PLOTS.iter().enumerate() {
441                    if p.distance(*plot) < 30. {
442                        g.selected = i;
443                    }
444                }
445            }
446            if is_key_pressed(KeyCode::Key1) {
447                g.build(g.selected, Kind::Seed);
448            }
449            if is_key_pressed(KeyCode::Key2) {
450                g.build(g.selected, Kind::Frost);
451            }
452            if is_key_pressed(KeyCode::U) {
453                g.upgrade(g.selected);
454            }
455            if is_key_pressed(KeyCode::Space) {
456                g.launch();
457            }
458        }
459        let step = clock.advance(demo.dt() as f64);
460        for _ in 0..step.steps {
461            if demo.autoplay {
462                g.autoplay();
463            }
464            g.step(step.dt);
465        }
466        clear_background(Color::from_hex(0x27372a));
467        set_camera(&canvas.camera());
468        field(&g);
469        let action = hud(&g, pointer, click);
470        if g.outcome == Outcome::Playing {
471            match action {
472                Some(Action::Build(kind)) => {
473                    g.build(g.selected, kind);
474                }
475                Some(Action::Upgrade) => {
476                    g.upgrade(g.selected);
477                }
478                Some(Action::Wave) => {
479                    g.launch();
480                }
481                None => {}
482            }
483        } else {
484            draw_rectangle(0., 70., 1200., 590., Color::new(0.14, 0.20, 0.13, 0.45));
485            draw_rectangle(375., 235., 450., 250., CREAM);
486            center(
487                if g.outcome == Outcome::Won {
488                    "HARVEST SAVED"
489                } else {
490                    "THE PESTS GOT THROUGH"
491                },
492                600.,
493                294.,
494                30.,
495                INK,
496            );
497            center(
498                &format!("{} pests stopped · {} hearts remaining", g.kills, g.lives),
499                600.,
500                334.,
501                18.,
502                GREEN,
503            );
504            center("Four waves. One little orchard.", 600., 366., 17., GREEN);
505            if button(
506                Rect::new(490., 398., 220., 55.),
507                "PLANT AGAIN",
508                "[R]  A fresh start",
509                true,
510                pointer,
511            ) && click
512            {
513                g = Game::new();
514                clock.reset();
515            }
516        }
517        set_default_camera();
518        if demo.finish_frame() {
519            break;
520        }
521        next_frame().await;
522    }
523}