Changed hitboxed from an 'is a hitbox' to a 'has a hitbox'
[bug-basher.git] / src / main.rs
1 #![warn(clippy)]
2
3 #[macro_use]
4 extern crate gate;
5
6 gate_header!();
7
8 use gate::{App, AppContext, AppInfo, KeyCode};
9 use gate::renderer::{Renderer, Affine};
10
11 extern crate rand;
12
13 use rand::distributions::IndependentSample;
14 use rand::Isaac64Rng;
15 use std::f64::consts::PI;
16 use std::f64::consts::FRAC_PI_2;
17
18 mod asset_id { include!(concat!(env!("OUT_DIR"), "/asset_id.rs")); }
19 use asset_id::*;
20
21 mod geometry;
22 use geometry::*;
23     
24 mod hitbox;
25 use hitbox::*;
26
27 mod entities;
28 use entities::bug::Bug;
29 use entities::home::Home;
30
31 struct BugBasherGame {
32     rng: Isaac64Rng,
33     bugs: Vec<Bug>,
34     home: Home,
35     points: i64,
36     lives: i64,
37     game_over: bool,
38     time_to_next_bug: f64,
39     total_time: f64,
40     camera: Affine,
41     cursor: SpriteId
42 }
43
44 impl App<AssetId> for BugBasherGame {
45     fn start(&mut self, _ctx: &mut AppContext<AssetId>) {
46     }
47
48     fn advance(&mut self, seconds: f64, _ctx: &mut AppContext<AssetId>) {
49         if !self.game_over {
50             self.bugs.retain(|b| b.alive);
51             for bug in &mut self.bugs {
52                 bug.advance(seconds);
53
54                 if Hitbox::intersect(&self.home.hitbox(), &bug.hitbox()) {
55                     bug.alive = false;
56                     self.lives -= 1;
57                 }
58             }
59             self.home.advance(seconds);
60             if self.lives <= 0 {
61                 self.game_over = true;
62             }
63         
64             self.time_to_next_bug -= seconds;
65             self.total_time += seconds;
66             
67             if self.time_to_next_bug <= 0. {
68                 let mean = if self.total_time < 30. {
69                     f64::max(4. - (self.total_time as f64 * 0.2), 1.)
70                 } else if self.total_time < 60. {
71                     f64::max(1. - ((self.total_time as f64 - 30.) * 0.05), 0.5)
72                 } else if self.total_time < 90. {
73                     f64::max(0.5 - ((self.total_time as f64 - 60.) * 0.05), 0.3)
74                 } else {
75                     f64::max(0.3 - ((self.total_time as f64 - 90.) * 0.005), 0.2)
76                 };
77                 
78                 let sd = mean / 3.;
79                 let time_dist = rand::distributions::Normal::new(mean, sd);
80                 self.time_to_next_bug = time_dist.ind_sample(&mut self.rng);
81
82                 let angle_dist = rand::distributions::Range::new(0., 2.*PI);
83                 let angle = angle_dist.ind_sample(&mut self.rng);
84                 self.bugs.push(Bug::new(
85                     angle.cos()*1000.,
86                     angle.sin()*1000.
87                 ));
88                 
89             }
90         }
91     }
92
93     fn key_down(&mut self, key: KeyCode, ctx: &mut AppContext<AssetId>) {
94         let (x, y) = self.camera.invert_translate().apply_f64(ctx.cursor());
95         let cursor_hitbox = Hitbox::Circle(CircleHitbox {
96             pos: Vec2d::new(x, y),
97             radius: 10.
98         });
99         match key {
100             KeyCode::MouseLeft => {
101                 let mut hit = false;
102                 for bug in self.bugs.iter_mut().filter(|bug| Hitbox::intersect(&cursor_hitbox, &bug.hitbox())) {
103                     if !self.game_over && bug.alive {
104                         self.points += 1;
105                     }
106                     bug.alive = false;
107                     hit = true;
108                 }
109                 if hit {
110                     self.rotate_cursor();
111                 }
112             },
113             KeyCode::Return => {
114                 self.reset();
115             },
116             _ => {}
117         }
118     }
119
120     fn render(&mut self, renderer: &mut Renderer<AssetId>, ctx: &AppContext<AssetId>) {
121         let (app_width, app_height) = ctx.dims();
122         let (cursor_x, cursor_y) = ctx.cursor();
123         self.camera = Affine::translate(app_width/2., app_height/2.);
124         
125         renderer.clear((255,255,255));
126         {
127             let points_str = format!("{}", self.points);
128             let lives_str = format!("{}", self.lives);
129             BugBasherGame::print_string(renderer, &points_str, &Alignment::Left, 50., app_height - 50.);
130             BugBasherGame::print_string(renderer, &lives_str, &Alignment::Right, app_width - 50., app_height - 50.);
131         }
132         {
133             let mut renderer = renderer.sprite_mode();
134             renderer.draw(
135                 &self.camera.post_translate(self.home.pos.x, self.home.pos.y),
136                 self.home.sprite
137             );
138             for bug in &self.bugs {
139                 let affine = if bug.rotation <= FRAC_PI_2 && bug.rotation >= -FRAC_PI_2 {
140                     self.camera.post_translate(bug.pos.x, bug.pos.y).pre_rotate(bug.rotation)
141                 } else {
142                     self.camera.post_translate(bug.pos.x, bug.pos.y).pre_rotate(bug.rotation).pre_scale_axes(1., -1.)
143                 };
144                 
145                 renderer.draw(
146                     &affine,
147                     SpriteId::Pom
148                 );
149             }
150         }
151         
152         if self.game_over {
153             {
154                 let mut renderer = renderer.sprite_mode();
155                 renderer.draw(
156                     &self.camera,
157                     SpriteId::Gameover
158                 );
159             }
160             {
161                 let points_str = format!("{}", self.points);
162                 BugBasherGame::print_string(renderer, &points_str, &Alignment::Center, app_width/2., app_height/2.-25.);
163             }
164         }
165
166         {
167             let mut renderer = renderer.sprite_mode();
168             renderer.draw(
169                 &Affine::translate(cursor_x, cursor_y),
170                 self.cursor
171             );
172         }
173     }
174 }
175
176 enum Alignment {
177     Left,
178     Right,
179     Center
180 }
181
182 impl BugBasherGame {
183     fn new() -> BugBasherGame {
184         let mut game = BugBasherGame {
185             rng: Isaac64Rng::new_unseeded(),
186             home: Home::new(0., 0.),
187             bugs: Vec::with_capacity(1000),
188             points: 0,
189             lives: 0,
190             game_over: true,
191             time_to_next_bug: 0.,
192             total_time: 0.,
193             camera: Affine::id(),
194             cursor: SpriteId::Cursor1
195         };
196         game.reset();
197         game
198     }
199
200     fn reset(&mut self) {
201         self.bugs = Vec::with_capacity(1000);
202         self.points = 0;
203         self.lives = 3;
204         self.game_over = false;
205         self.time_to_next_bug = 0.;
206         self.total_time = 0.;
207     }
208
209     fn print_string(renderer: &mut Renderer<AssetId>, string: &str, alignment: &Alignment, x: f64, y: f64) {
210         let letter_spacing = 45.;
211         let left = match alignment {
212             Alignment::Left => x,
213             Alignment::Right => x - string.len() as f64 * letter_spacing,
214             Alignment::Center => x - string.len() as f64 * letter_spacing / 2.
215         };
216         
217         let mut renderer = renderer.sprite_mode();
218         for (i, c) in string.chars().enumerate() {
219             let affine = Affine::translate(left + i as f64 * letter_spacing, y);
220             let tile = match c {
221                 '-' => SpriteId::NumberFontR0C0,
222                 '0' => SpriteId::NumberFontR0C1,
223                 '1' => SpriteId::NumberFontR0C2,
224                 '2' => SpriteId::NumberFontR0C3,
225                 '3' => SpriteId::NumberFontR0C4,
226                 '4' => SpriteId::NumberFontR0C5,
227                 '5' => SpriteId::NumberFontR0C6,
228                 '6' => SpriteId::NumberFontR0C7,
229                 '7' => SpriteId::NumberFontR0C8,
230                 '8' => SpriteId::NumberFontR0C9,
231                 '9' => SpriteId::NumberFontR0C10,
232                 _ => SpriteId::NumberFontR0C0,
233             };
234             renderer.draw(&affine, tile);
235         };
236     }
237
238     fn rotate_cursor(&mut self) {
239         self.cursor = match self.cursor {
240             SpriteId::Cursor1 => SpriteId::Cursor2,
241             SpriteId::Cursor2 => SpriteId::Cursor3,
242             _ => SpriteId::Cursor1
243         }
244     }
245 }
246
247 fn main() {
248     let info = AppInfo::with_max_dims(2000., 1000.)
249         .tile_width(16)
250         .title("Nap Attack");
251     gate::run(info, BugBasherGame::new());
252 }