Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config file for game settings #17

Merged
merged 7 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 225 additions & 26 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,25 @@ bevy_ninepatch = "0.9"
iyes_loopless = "0.9"
leafwing-input-manager = "0.7"

directories = "4.0"
itertools = "0.10"
noise = { git = "https://github.com/bsurmanski/noise-rs", rev = "5abdde1b819eccc47e74969c15e1b56ae5a055d6" }
rand = { version = "0.8", default_features = false, features = ["std", "small_rng"] }
serde = "1.0"
serde_derive = "1.0"
serde_ignored = "0.1.6"
toml = "0.5"

[dependencies.bevy]
version = "0.9"
default_features = false
features = [ "png", "x11" ]
features = ["png"]

[features]
default = ["x11"]
dev = ["bevy/dynamic"]

x11 = ["bevy/x11"]
wayland = ["bevy/wayland"]

[profile.dev.package."*"]
opt-level = 3
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ This game is part of the [Fish Folk](https://spicylobster.itch.io/jumpy/devlog/4
- [Folly](https://github.com/fishfolks/folly)
- [Ballsy](https://github.com/fishfolks/ballsy)

## Getting Started

The easiest way to build and run *Bomby* is by using Cargo. The easiest way to set cargo up is by using [rustup](https://rustup.rs/).

Then, you can run the game with

```console
$ cargo run --release
```

If you are using wayland, you can use

```console
$ cargo run --release --no-default-features --features wayland
```

## Contributing

Anyone involved in the Fish Folk community must follow our [code of conduct](https://github.com/fishfolks/jumpy/blob/main/CODE_OF_CONDUCT.md).
Expand All @@ -22,7 +38,7 @@ Anyone involved in the Fish Folk community must follow our [code of conduct](htt
Before committing and opening a PR, please run the following commands and follow their instructions:

```console
$ cargo clippy -D warnings
$ cargo clippy
$ cargo fmt
```

Expand Down
15 changes: 15 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{ pkgs ? import <nixpkgs> { } }:

with pkgs;

mkShell rec {
nativeBuildInputs = [
pkg-config
];
buildInputs = [
udev alsa-lib vulkan-loader
xorg.libX11 xorg.libXcursor xorg.libXi xorg.libXrandr # To use the x11 feature
libxkbcommon wayland # To use the wayland feature
];
LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;
}
23 changes: 19 additions & 4 deletions src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ use bevy_kira_audio::prelude::*;
use iyes_loopless::prelude::*;
use rand::prelude::*;

use crate::{GameRng, GameState};
use crate::{config::Config, GameRng, GameState};

pub struct AudioPlugin;

impl Plugin for AudioPlugin {
fn build(&self, app: &mut App) {
app.add_event::<PlaySfx>()
.add_audio_channel::<BgmChannel>()
.add_audio_channel::<SfxChannel>()
.add_startup_system(load_audio)
.add_startup_system(set_volume)
.add_enter_system(GameState::MainMenu, start_title_bgm)
.add_enter_system(GameState::InGame, start_fight_bgm)
.add_exit_system(GameState::MainMenu, stop_bgm)
Expand All @@ -25,11 +27,14 @@ impl Plugin for AudioPlugin {
}
}

/// Resource for the background music channel. Exists so in future a user may change BGM volume
/// independently of SFX.
/// Resource for the background music channel.
#[derive(Resource)]
struct BgmChannel;

/// Resource for the sound effects channel.
#[derive(Resource)]
struct SfxChannel;

/// Event for SFX.
#[derive(Debug)]
pub enum PlaySfx {
Expand All @@ -38,6 +43,16 @@ pub enum PlaySfx {
BombFuse,
}

/// Update the channel volumes based on values in the [`Config`] resource.
fn set_volume(
bgm_channel: Res<AudioChannel<BgmChannel>>,
sfx_channel: Res<AudioChannel<SfxChannel>>,
config: Res<Config>,
) {
bgm_channel.set_volume(config.bgm_volume);
sfx_channel.set_volume(config.sfx_volume);
}

fn start_title_bgm(audio: Res<AudioChannel<BgmChannel>>, bgm: Res<Bgm>) {
audio.play(bgm.title_screen.clone()).looped();
}
Expand All @@ -60,7 +75,7 @@ fn stop_bgm(audio: Res<AudioChannel<BgmChannel>>) {
}

fn play_sfx(
audio: Res<Audio>,
audio: Res<AudioChannel<SfxChannel>>,
sfx: Res<Sfx>,
mut rng: ResMut<GameRng>,
mut ev_sfx: EventReader<PlaySfx>,
Expand Down
15 changes: 8 additions & 7 deletions src/bomb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,13 @@ fn update_bombs(

// Destroy bombable tiles within 1 orthogonal tile
for tile in affected_tiles.iter().filter(|(_, parent, coords)| {
if let Ok(ldtk_layer) = ldtk_layer_meta_q.get(***parent) {
ldtk_layer.identifier == "Bombable"
} else {
warn!("LDtk tile not child of a layer with coords: {:?}", coords);
false
}
ldtk_layer_meta_q.get(***parent).map_or_else(
|_| {
warn!("LDtk tile not child of a layer with coords: {coords:?}");
false
},
|ldtk_layer| ldtk_layer.identifier == "Bombable",
)
}) {
commands.entity(tile.0).despawn_recursive();
}
Expand Down Expand Up @@ -165,7 +166,7 @@ fn animate_bombs(mut bombs: Query<(&Bomb, &mut Transform)>) {
+ (Vec2::ONE
* 0.1
* ((16.0 * std::f32::consts::PI / 6.0) * bomb.timer.elapsed_secs()).sin())
.extend(1.0)
.extend(1.0);
}
}

Expand Down
75 changes: 75 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//! Load the settings file for the game. This will be under the config folder by OS convention, for
//! example:
//!
//! Linux: `~/.config/bomby/config.toml`
//!
//! Currently, the config is loaded at startup and cannot be changed from inside the game. So, this
//! module does not export a bevy plugin (yet).

use bevy::prelude::*;

use directories::ProjectDirs;
use serde_derive::{Deserialize, Serialize};

use std::fs;

const DEFAULT_ASPECT_RATIO: f32 = 16.0 / 9.0;
const DEFAULT_WINDOW_HEIGHT: f32 = 900.0;
const DEFAULT_WINDOW_WIDTH: f32 = DEFAULT_WINDOW_HEIGHT * DEFAULT_ASPECT_RATIO;

/// Config resource containing runtime settings for the game.
#[derive(Resource, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub window_resizable: bool,
pub window_width: f32,
pub window_height: f32,
pub bgm_volume: f64,
pub sfx_volume: f64,
}

impl Default for Config {
fn default() -> Self {
Self {
window_resizable: true,
window_width: DEFAULT_WINDOW_WIDTH,
window_height: DEFAULT_WINDOW_HEIGHT,
bgm_volume: 1.0,
sfx_volume: 1.0,
}
}
}

/// Load the [`Config`] or generate a new one and insert it as a resource.
pub fn load_config() -> Config {
let dirs = ProjectDirs::from("com", "Spicy Lobster", "Bomby");
let mut config = dirs
.map(|dirs| {
let mut path = dirs.config_dir().to_path_buf();
path.push("config.toml");
let config_str = fs::read_to_string(&path).unwrap_or_else(|_| "".to_string());
let mut de = toml::de::Deserializer::new(&config_str);
let mut unused_keys = Vec::new();
let config =
serde_ignored::deserialize(&mut de, |path| unused_keys.push(path.to_string()))
.unwrap_or_else(|e| {
warn!("failed to parse config file {path:?}: {e}");
Config::default()
});

for key in unused_keys {
warn!("unrecognised config setting: {key}");
}
config
})
.unwrap_or_else(|| {
warn!("failed to get config path");
Config::default()
});

// Ensure sensible bounds.
config.bgm_volume = config.bgm_volume.clamp(0.0, 1.0);
config.sfx_volume = config.sfx_volume.clamp(0.0, 1.0);

config
}
2 changes: 1 addition & 1 deletion src/ldtk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ where
}
}

/// Detect if there is a `Spawned` event from bevy_ecs_ldtk, indicating that the level has spawned.
/// Detect if there is a `Spawned` event from [`bevy_ecs_ldtk`], indicating that the level has spawned.
/// This means we can rely on entities existing such as the player spawn points.
pub fn level_spawned(mut level_events: EventReader<LevelEvent>) -> bool {
level_events
Expand Down
17 changes: 10 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![warn(clippy::semicolon_if_nothing_returned, clippy::uninlined_format_args)]

use bevy::prelude::*;
use iyes_loopless::prelude::*;

Expand All @@ -6,16 +8,13 @@ use rand::{rngs::SmallRng, SeedableRng};
mod audio;
mod bomb;
mod camera;
mod config;
mod debug;
mod ldtk;
mod player;
mod ui;
mod z_sort;

const RESOLUTION: f32 = 16.0 / 9.0;
const WINDOW_HEIGHT: f32 = 900.0;
const WINDOW_WIDTH: f32 = WINDOW_HEIGHT * RESOLUTION;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GameState {
MainMenu,
Expand All @@ -27,22 +26,26 @@ pub enum GameState {
pub struct GameRng(SmallRng);

fn main() {
let config = config::load_config();
info!("Initialised config: {config:?}");

App::new()
.add_loopless_state(GameState::MainMenu)
.add_plugins(
DefaultPlugins
.set(ImagePlugin::default_nearest())
.set(WindowPlugin {
window: WindowDescriptor {
width: WINDOW_WIDTH,
height: WINDOW_HEIGHT,
width: config.window_width,
height: config.window_height,
title: "Bomby!".to_string(),
resizable: false,
resizable: config.window_resizable,
..default()
},
..default()
}),
)
.insert_resource(config)
.add_plugin(bevy_kira_audio::AudioPlugin)
.add_plugin(audio::AudioPlugin)
.add_plugin(debug::DebugPlugin)
Expand Down
19 changes: 10 additions & 9 deletions src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ fn spawn_players(
.filter(|(_, ldtk_entity)| ldtk_entity.identifier == player_name)
.map(|(transform, _)| transform.translation.truncate())
.next()
.unwrap_or_else(|| panic!("no spawn point found for player: {}", player_name))
.unwrap_or_else(|| panic!("no spawn point found for player: {player_name}"))
+ Vec2::Y * -8.0;

commands.spawn((
Expand All @@ -84,7 +84,7 @@ fn spawn_players(
texture_atlas: textures
.0
.get(i)
.unwrap_or_else(|| panic!("no sprite sheet for player: {}", player_name))
.unwrap_or_else(|| panic!("no sprite sheet for player: {player_name}"))
.clone(),
sprite: TextureAtlasSprite {
index: 0,
Expand Down Expand Up @@ -137,7 +137,7 @@ fn spawn_players(
.insert(GamepadButtonType::East, PlayerAction::Bomb)
.set_gamepad(Gamepad { id: 1 })
.build(),
_ => panic!("no input map for player: {}", player_name),
_ => panic!("no input map for player: {player_name}"),
},
..default()
},
Expand Down Expand Up @@ -216,12 +216,13 @@ fn player_collisions(
.iter()
.filter(|(parent, coords)| {
bomb_tiles.iter().any(|b| b == *coords)
|| if let Ok(ldtk_layer) = ldtk_layer_meta_q.get(***parent) {
matches!(ldtk_layer.identifier.as_str(), "Maze" | "Bombable")
} else {
warn!("LDtk tile not child of a layer with coords: {:?}", coords);
false
}
|| ldtk_layer_meta_q.get(***parent).map_or_else(
|_| {
warn!("LDtk tile not child of a layer with coords: {coords:?}");
false
},
|ldtk_layer| matches!(ldtk_layer.identifier.as_str(), "Maze" | "Bombable"),
)
})
.map(|(_, coords)| coords)
.collect::<Vec<_>>();
Expand Down
2 changes: 1 addition & 1 deletion src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ fn load_graphics(
commands.insert_resource(ButtonNinePatch {
texture: button_ninepatch_texture,
ninepatch: button_ninepatch_handle,
})
});
}

#[derive(Resource)]
Expand Down
Loading