| 1 |
|
| 2 |
|
| 3 |
use std::f32::consts::PI; |
| 4 |
|
| 5 |
use bevy::prelude::*; |
| 6 |
|
| 7 |
use chacha20::ChaCha8Rng; |
| 8 |
use rand::{RngExt, SeedableRng}; |
| 9 |
|
| 10 |
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] |
| 11 |
enum GameState { |
| 12 |
#[default] |
| 13 |
Playing, |
| 14 |
GameOver, |
| 15 |
} |
| 16 |
|
| 17 |
#[derive(Resource)] |
| 18 |
struct BonusSpawnTimer(Timer); |
| 19 |
|
| 20 |
fn 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 |
|
| 50 |
struct Cell { |
| 51 |
height: f32, |
| 52 |
} |
| 53 |
|
| 54 |
#[derive(Default)] |
| 55 |
struct Player { |
| 56 |
entity: Option<Entity>, |
| 57 |
i: usize, |
| 58 |
j: usize, |
| 59 |
move_cooldown: Timer, |
| 60 |
} |
| 61 |
|
| 62 |
#[derive(Default)] |
| 63 |
struct Bonus { |
| 64 |
entity: Option<Entity>, |
| 65 |
i: usize, |
| 66 |
j: usize, |
| 67 |
handle: Handle<WorldAsset>, |
| 68 |
} |
| 69 |
|
| 70 |
#[derive(Resource, Default)] |
| 71 |
struct 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)] |
| 82 |
struct Random(ChaCha8Rng); |
| 83 |
|
| 84 |
const BOARD_SIZE_I: usize = 14; |
| 85 |
const BOARD_SIZE_J: usize = 21; |
| 86 |
|
| 87 |
const 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 |
|
| 93 |
fn 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 |
|
| 107 |
fn 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 |
|
| 110 |
|
| 111 |
ChaCha8Rng::seed_from_u64(19878367467713) |
| 112 |
} else { |
| 113 |
rand::make_rng() |
| 114 |
}; |
| 115 |
|
| 116 |
|
| 117 |
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 |
|
| 135 |
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 |
|
| 154 |
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 |
|
| 176 |
game.bonus.handle = |
| 177 |
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/cakeBirthday.glb")); |
| 178 |
|
| 179 |
|
| 180 |
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 |
|
| 199 |
|
| 200 |
fn 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 |
|
| 241 |
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 |
|
| 256 |
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 |
|
| 267 |
|
| 268 |
fn 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 |
|
| 275 |
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 |
|
| 286 |
} 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 |
|
| 291 |
} else { |
| 292 |
game.camera_should_focus = Vec3::from(RESET_FOCUS); |
| 293 |
} |
| 294 |
|
| 295 |
|
| 296 |
|
| 297 |
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 |
|
| 301 |
game.camera_is_focus += camera_motion; |
| 302 |
} |
| 303 |
|
| 304 |
for mut transform in transforms.p0().iter_mut() { |
| 305 |
*transform = transform.looking_at(game.camera_is_focus, Vec3::Y); |
| 306 |
} |
| 307 |
} |
| 308 |
|
| 309 |
|
| 310 |
fn 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 |
|
| 319 |
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 |
|
| 334 |
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 |
|
| 365 |
|
| 366 |
fn 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 |
|
| 376 |
|
| 377 |
fn scoreboard_system(game: Res<Game>, mut display: Single<&mut Text>) { |
| 378 |
display.0 = format!("Sugar Rush: {}", game.score); |
| 379 |
} |
| 380 |
|
| 381 |
|
| 382 |
fn 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 |
|
| 391 |
|
| 392 |
fn 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 |
} |