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 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}