1use std::f32::consts::PI;
4
5use bevy::prelude::*;
6
7use chacha20::ChaCha8Rng;
8use rand::{RngExt, SeedableRng};
9
10#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
11enum GameState {
12 #[default]
13 Playing,
14 GameOver,
15}
16
17#[derive(Resource)]
18struct BonusSpawnTimer(Timer);
19
20fn main() {
21 App::new()
22 .add_plugins(DefaultPlugins)
23 .init_resource::<Game>()
24 .insert_resource(BonusSpawnTimer(Timer::from_seconds(
25 5.0,
26 TimerMode::Repeating,
27 )))
28 .init_state::<GameState>()
29 .add_systems(Startup, setup_cameras)
30 .add_systems(OnEnter(GameState::Playing), setup)
31 .add_systems(
32 Update,
33 (
34 move_player,
35 focus_camera,
36 rotate_bonus,
37 scoreboard_system,
38 spawn_bonus,
39 )
40 .run_if(in_state(GameState::Playing)),
41 )
42 .add_systems(OnEnter(GameState::GameOver), display_score)
43 .add_systems(
44 Update,
45 game_over_keyboard.run_if(in_state(GameState::GameOver)),
46 )
47 .run();
48}
49
50struct Cell {
51 height: f32,
52}
53
54#[derive(Default)]
55struct Player {
56 entity: Option<Entity>,
57 i: usize,
58 j: usize,
59 move_cooldown: Timer,
60}
61
62#[derive(Default)]
63struct Bonus {
64 entity: Option<Entity>,
65 i: usize,
66 j: usize,
67 handle: Handle<WorldAsset>,
68}
69
70#[derive(Resource, Default)]
71struct Game {
72 board: Vec<Vec<Cell>>,
73 player: Player,
74 bonus: Bonus,
75 score: i32,
76 cake_eaten: u32,
77 camera_should_focus: Vec3,
78 camera_is_focus: Vec3,
79}
80
81#[derive(Resource, Deref, DerefMut)]
82struct Random(ChaCha8Rng);
83
84const BOARD_SIZE_I: usize = 14;
85const BOARD_SIZE_J: usize = 21;
86
87const RESET_FOCUS: [f32; 3] = [
88 BOARD_SIZE_I as f32 / 2.0,
89 0.0,
90 BOARD_SIZE_J as f32 / 2.0 - 0.5,
91];
92
93fn setup_cameras(mut commands: Commands, mut game: ResMut<Game>) {
94 game.camera_should_focus = Vec3::from(RESET_FOCUS);
95 game.camera_is_focus = game.camera_should_focus;
96 commands.spawn((
97 Camera3d::default(),
98 Transform::from_xyz(
99 -(BOARD_SIZE_I as f32 / 2.0),
100 2.0 * BOARD_SIZE_J as f32 / 3.0,
101 BOARD_SIZE_J as f32 / 2.0 - 0.5,
102 )
103 .looking_at(game.camera_is_focus, Vec3::Y),
104 ));
105}
106
107fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut game: ResMut<Game>) {
108 let mut rng = if std::env::var("GITHUB_ACTIONS") == Ok("true".to_string()) {
109 ChaCha8Rng::seed_from_u64(19878367467713)
112 } else {
113 rand::make_rng()
114 };
115
116 game.cake_eaten = 0;
118 game.score = 0;
119 game.player.i = BOARD_SIZE_I / 2;
120 game.player.j = BOARD_SIZE_J / 2;
121 game.player.move_cooldown = Timer::from_seconds(0.3, TimerMode::Once);
122
123 commands.spawn((
124 DespawnOnExit(GameState::Playing),
125 PointLight {
126 intensity: 2_000_000.0,
127 shadow_maps_enabled: true,
128 range: 30.0,
129 ..default()
130 },
131 Transform::from_xyz(4.0, 10.0, 4.0),
132 ));
133
134 let cell_scene =
136 asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/tile.glb"));
137 game.board = (0..BOARD_SIZE_J)
138 .map(|j| {
139 (0..BOARD_SIZE_I)
140 .map(|i| {
141 let height = rng.random_range(-0.1..0.1);
142 commands.spawn((
143 DespawnOnExit(GameState::Playing),
144 Transform::from_xyz(i as f32, height - 0.2, j as f32),
145 WorldAssetRoot(cell_scene.clone()),
146 ));
147 Cell { height }
148 })
149 .collect()
150 })
151 .collect();
152
153 game.player.entity = Some(
155 commands
156 .spawn((
157 DespawnOnExit(GameState::Playing),
158 Transform {
159 translation: Vec3::new(
160 game.player.i as f32,
161 game.board[game.player.j][game.player.i].height,
162 game.player.j as f32,
163 ),
164 rotation: Quat::from_rotation_y(-PI / 2.),
165 ..default()
166 },
167 WorldAssetRoot(
168 asset_server
169 .load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/alien.glb")),
170 ),
171 ))
172 .id(),
173 );
174
175 game.bonus.handle =
177 asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/cakeBirthday.glb"));
178
179 commands.spawn((
181 DespawnOnExit(GameState::Playing),
182 Text::new("Score:"),
183 TextFont {
184 font_size: FontSize::Px(33.0),
185 ..default()
186 },
187 TextColor(Color::srgb(0.5, 0.5, 1.0)),
188 Node {
189 position_type: PositionType::Absolute,
190 top: px(5),
191 left: px(5),
192 ..default()
193 },
194 ));
195
196 commands.insert_resource(Random(rng));
197}
198
199fn move_player(
201 mut commands: Commands,
202 keyboard_input: Res<ButtonInput<KeyCode>>,
203 mut game: ResMut<Game>,
204 mut transforms: Query<&mut Transform>,
205 time: Res<Time>,
206) {
207 if game.player.move_cooldown.tick(time.delta()).is_finished() {
208 let mut moved = false;
209 let mut rotation = 0.0;
210
211 if keyboard_input.pressed(KeyCode::ArrowUp) {
212 if game.player.i < BOARD_SIZE_I - 1 {
213 game.player.i += 1;
214 }
215 rotation = -PI / 2.;
216 moved = true;
217 }
218 if keyboard_input.pressed(KeyCode::ArrowDown) {
219 if game.player.i > 0 {
220 game.player.i -= 1;
221 }
222 rotation = PI / 2.;
223 moved = true;
224 }
225 if keyboard_input.pressed(KeyCode::ArrowRight) {
226 if game.player.j < BOARD_SIZE_J - 1 {
227 game.player.j += 1;
228 }
229 rotation = PI;
230 moved = true;
231 }
232 if keyboard_input.pressed(KeyCode::ArrowLeft) {
233 if game.player.j > 0 {
234 game.player.j -= 1;
235 }
236 rotation = 0.0;
237 moved = true;
238 }
239
240 if moved {
242 game.player.move_cooldown.reset();
243 *transforms.get_mut(game.player.entity.unwrap()).unwrap() = Transform {
244 translation: Vec3::new(
245 game.player.i as f32,
246 game.board[game.player.j][game.player.i].height,
247 game.player.j as f32,
248 ),
249 rotation: Quat::from_rotation_y(rotation),
250 ..default()
251 };
252 }
253 }
254
255 if let Some(entity) = game.bonus.entity
257 && game.player.i == game.bonus.i
258 && game.player.j == game.bonus.j
259 {
260 game.score += 2;
261 game.cake_eaten += 1;
262 commands.entity(entity).despawn();
263 game.bonus.entity = None;
264 }
265}
266
267fn focus_camera(
269 time: Res<Time>,
270 mut game: ResMut<Game>,
271 mut transforms: ParamSet<(Query<&mut Transform, With<Camera3d>>, Query<&Transform>)>,
272) {
273 const SPEED: f32 = 2.0;
274 if let (Some(player_entity), Some(bonus_entity)) = (game.player.entity, game.bonus.entity) {
276 let transform_query = transforms.p1();
277 if let (Ok(player_transform), Ok(bonus_transform)) = (
278 transform_query.get(player_entity),
279 transform_query.get(bonus_entity),
280 ) {
281 game.camera_should_focus = player_transform
282 .translation
283 .lerp(bonus_transform.translation, 0.5);
284 }
285 } else if let Some(player_entity) = game.player.entity {
287 if let Ok(player_transform) = transforms.p1().get(player_entity) {
288 game.camera_should_focus = player_transform.translation;
289 }
290 } else {
292 game.camera_should_focus = Vec3::from(RESET_FOCUS);
293 }
294 let mut camera_motion = game.camera_should_focus - game.camera_is_focus;
298 if camera_motion.length() > 0.2 {
299 camera_motion *= SPEED * time.delta_secs();
300 game.camera_is_focus += camera_motion;
302 }
303 for mut transform in transforms.p0().iter_mut() {
305 *transform = transform.looking_at(game.camera_is_focus, Vec3::Y);
306 }
307}
308
309fn spawn_bonus(
311 time: Res<Time>,
312 mut timer: ResMut<BonusSpawnTimer>,
313 mut next_state: ResMut<NextState<GameState>>,
314 mut commands: Commands,
315 mut game: ResMut<Game>,
316 mut rng: ResMut<Random>,
317) {
318 if !timer.0.tick(time.delta()).is_finished() {
320 return;
321 }
322
323 if let Some(entity) = game.bonus.entity {
324 game.score -= 3;
325 commands.entity(entity).despawn();
326 game.bonus.entity = None;
327 if game.score <= -5 {
328 next_state.set(GameState::GameOver);
329 return;
330 }
331 }
332
333 loop {
335 game.bonus.i = rng.random_range(0..BOARD_SIZE_I);
336 game.bonus.j = rng.random_range(0..BOARD_SIZE_J);
337 if game.bonus.i != game.player.i || game.bonus.j != game.player.j {
338 break;
339 }
340 }
341 game.bonus.entity = Some(
342 commands
343 .spawn((
344 DespawnOnExit(GameState::Playing),
345 Transform::from_xyz(
346 game.bonus.i as f32,
347 game.board[game.bonus.j][game.bonus.i].height + 0.2,
348 game.bonus.j as f32,
349 ),
350 WorldAssetRoot(game.bonus.handle.clone()),
351 children![(
352 PointLight {
353 color: Color::srgb(1.0, 1.0, 0.0),
354 intensity: 500_000.0,
355 range: 10.0,
356 ..default()
357 },
358 Transform::from_xyz(0.0, 2.0, 0.0),
359 )],
360 ))
361 .id(),
362 );
363}
364
365fn rotate_bonus(game: Res<Game>, time: Res<Time>, mut transforms: Query<&mut Transform>) {
367 if let Some(entity) = game.bonus.entity
368 && let Ok(mut cake_transform) = transforms.get_mut(entity)
369 {
370 cake_transform.rotate_y(time.delta_secs());
371 cake_transform.scale =
372 Vec3::splat(1.0 + (game.score as f32 / 10.0 * ops::sin(time.elapsed_secs())).abs());
373 }
374}
375
376fn scoreboard_system(game: Res<Game>, mut display: Single<&mut Text>) {
378 display.0 = format!("Sugar Rush: {}", game.score);
379}
380
381fn game_over_keyboard(
383 mut next_state: ResMut<NextState<GameState>>,
384 keyboard_input: Res<ButtonInput<KeyCode>>,
385) {
386 if keyboard_input.just_pressed(KeyCode::Space) {
387 next_state.set(GameState::Playing);
388 }
389}
390
391fn display_score(mut commands: Commands, game: Res<Game>) {
393 commands.spawn((
394 DespawnOnExit(GameState::GameOver),
395 Node {
396 width: percent(100),
397 align_items: AlignItems::Center,
398 justify_content: JustifyContent::Center,
399 ..default()
400 },
401 children![(
402 Text::new(format!("Cake eaten: {}", game.cake_eaten)),
403 TextFont {
404 font_size: FontSize::Px(67.0),
405 ..default()
406 },
407 TextColor(Color::srgb(0.5, 0.5, 1.0)),
408 )],
409 ));
410}