create scaffolding

This commit is contained in:
Jim 2025-08-24 09:42:18 -04:00
commit 82a0fbdb50
Signed by: jim
GPG key ID: 3236D2F059A7C0AC
25 changed files with 6993 additions and 0 deletions

3
.cargo/config.toml Normal file
View file

@ -0,0 +1,3 @@
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

6148
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

43
Cargo.toml Normal file
View file

@ -0,0 +1,43 @@
[package]
name = "coreheart"
version = "0.1.0"
edition = "2024"
[workspace]
members = ["crates/*"]
[workspace.lints]
[profile.dev]
opt-level = 0
# increases incremental compilation speed by about 10% while keeping debug info
split-debuginfo = "unpacked"
[profile.dev.package."*"]
opt-level = 3
[workspace.dependencies]
serde = { version = "1.0.219", features = ["derive"] }
bevy = { version = "0.16.1", features = ["serialize"] }
rand = "0.8.5"
rand_distr = "0.4.3"
itertools = "0.13.0"
avian3d = "0.3.1"
bevy-inspector-egui = "0.33.1"
ron = "0.10.1"
[dependencies]
bevy = { workspace = true }
bevy-inspector-egui = { workspace = true }
#internal crates
state = { path = "crates/state" }
physics = { path = "crates/physics" }
ui_state = { path = "crates/ui_state" }
ui_debug = { path = "crates/ui_debug" }
input = { path = "crates/input" }
ui = { path = "crates/ui" }
setup = { path = "crates/setup" }
ui_pause = { path = "crates/ui_pause" }
#new internal crates go here

View file

@ -0,0 +1,6 @@
(
map: {
KeyCode(F1): ToggleDebugUI,
KeyCode(KeyA): TogglePause,
},
)

13
crates/input/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "input"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = { workspace = true }
serde = { workspace = true }
ron = { workspace = true }
state = { path = "../state" }
[lints]
workspace = true

View file

@ -0,0 +1,27 @@
use bevy::{platform::collections::HashMap, prelude::*};
use serde::{Deserialize, Serialize};
use crate::{InputMap, InputMethod};
#[derive(Hash, Clone, Copy, Debug, Reflect, PartialEq, Eq, Serialize, Deserialize)]
pub enum InputAction {
ToggleDebugUI,
TogglePause,
}
impl Default for InputMap {
fn default() -> Self {
Self {
map: HashMap::from_iter(vec![
(
InputMethod::KeyCode(KeyCode::F1),
InputAction::ToggleDebugUI,
),
(
InputMethod::KeyCode(KeyCode::Escape),
InputAction::TogglePause,
),
]),
}
}
}

View file

@ -0,0 +1,27 @@
use bevy::prelude::*;
use crate::{Inputs, actions::InputAction};
pub fn action_pressed<T: Send + Sync + 'static>(
action: InputAction,
) -> impl FnMut(Res<Inputs<T>>) -> bool + Clone {
move |inputs: Res<Inputs<T>>| inputs.pressed(action)
}
pub fn action_just_pressed<T: Send + Sync + 'static>(
action: InputAction,
) -> impl FnMut(Res<Inputs<T>>) -> bool + Clone {
move |inputs: Res<Inputs<T>>| inputs.just_pressed(action)
}
pub fn action_released<T: Send + Sync + 'static>(
action: InputAction,
) -> impl FnMut(Res<Inputs<T>>) -> bool + Clone {
move |inputs: Res<Inputs<T>>| inputs.released(action)
}
pub fn action_just_released<T: Send + Sync + 'static>(
action: InputAction,
) -> impl FnMut(Res<Inputs<T>>) -> bool + Clone {
move |inputs: Res<Inputs<T>>| inputs.just_released(action)
}

212
crates/input/src/lib.rs Normal file
View file

@ -0,0 +1,212 @@
//bevy system signatures often violate these rules
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
pub mod actions;
pub mod conditions;
use std::{fs, marker::PhantomData};
use bevy::{input::mouse::AccumulatedMouseMotion, platform::collections::HashMap, prelude::*};
use ron::ser::PrettyConfig;
use serde::{Deserialize, Serialize};
use state::GameSystemSet;
use crate::actions::InputAction;
pub struct InputPlugin;
impl Plugin for InputPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<Inputs<FixedUpdate>>()
.init_resource::<Inputs<Update>>()
.configure_sets(Update, GameSystemSet::UpdateInputs)
.add_systems(Startup, setup_input_map)
.add_systems(
Update,
(update_inputs_frame, save_input_map).in_set(GameSystemSet::UpdateInputs),
)
.add_systems(FixedPostUpdate, clear_changed_fixed);
}
}
#[derive(Hash, Clone, Copy, Debug, Reflect, PartialEq, Eq, Serialize, Deserialize)]
pub enum InputMethod {
MouseButton(MouseButton),
KeyCode(KeyCode),
MouseMovementX,
MouseMovementY,
}
#[derive(Resource, Serialize, Deserialize)]
pub struct InputMap {
pub map: HashMap<InputMethod, InputAction>,
}
impl InputMap {
fn save_to_file(&self, path: &str) -> Result<()> {
let json = ron::ser::to_string_pretty(self, PrettyConfig::default())?;
fs::write(path, json)?;
Ok(())
}
fn load_from_file(path: &str) -> Result<Self> {
let content = fs::read_to_string(path)?;
let input_map = ron::from_str(&content)?;
Ok(input_map)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct InputState {
changed: bool,
value: f32,
}
impl InputState {
pub fn pressed(&self) -> bool {
self.value > 0.
}
pub fn just_pressed(&self) -> bool {
self.pressed() && self.changed
}
pub fn released(&self) -> bool {
!self.pressed()
}
pub fn just_released(&self) -> bool {
self.released() && self.changed
}
pub fn get(&self) -> f32 {
self.value
}
}
#[derive(Resource, Default)]
/// updates before pretick
pub struct Inputs<T> {
schedule: PhantomData<T>,
map: HashMap<InputAction, InputState>,
}
impl<T> Inputs<T> {
pub fn pressed(&self, action: InputAction) -> bool {
self.map.get(&action).map_or(false, |state| state.pressed())
}
pub fn just_pressed(&self, action: InputAction) -> bool {
self.map
.get(&action)
.map_or(false, |state| state.just_pressed())
}
pub fn released(&self, action: InputAction) -> bool {
self.map
.get(&action)
.map_or(false, |state| state.released())
}
pub fn just_released(&self, action: InputAction) -> bool {
self.map
.get(&action)
.map_or(false, |state| state.just_released())
}
pub fn get(&self, action: InputAction) -> f32 {
self.map.get(&action).map_or(0.0, |state| state.get())
}
}
fn update_inputs_frame(
mut fixed_inputs: ResMut<Inputs<FixedUpdate>>,
mut frame_inputs: ResMut<Inputs<Update>>,
input_map: Res<InputMap>,
mouse_movement: Res<AccumulatedMouseMotion>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
keyboard: Res<ButtonInput<KeyCode>>,
) {
fn set_value(action: &InputAction, value: f32, map: &mut HashMap<InputAction, InputState>) {
let mut current_state = map.get(action).copied().unwrap_or_default();
if current_state.value != value {
current_state.changed = true;
}
current_state.value = value;
map.insert(*action, current_state);
}
fn add_value(action: &InputAction, value: f32, map: &mut HashMap<InputAction, InputState>) {
let mut current_state = map.get(action).copied().unwrap_or_default();
if value != 0. {
current_state.changed = true;
}
current_state.value += value;
map.insert(*action, current_state);
}
for input in frame_inputs.map.values_mut() {
input.changed = false;
}
for (method, action) in input_map.map.iter() {
match method {
InputMethod::MouseButton(mouse_button) => {
let value = if mouse_buttons.pressed(*mouse_button) {
1.0
} else {
-1.0
};
set_value(action, value, &mut frame_inputs.map);
set_value(action, value, &mut fixed_inputs.map);
}
InputMethod::KeyCode(key_code) => {
let value = if keyboard.pressed(*key_code) {
1.0
} else {
-1.0
};
set_value(action, value, &mut frame_inputs.map);
set_value(action, value, &mut fixed_inputs.map);
}
InputMethod::MouseMovementX => {
let value = mouse_movement.delta.x;
set_value(action, value, &mut frame_inputs.map);
// want to get accumulated mouse movement in here if needed
add_value(action, value, &mut fixed_inputs.map);
}
InputMethod::MouseMovementY => {
let value = mouse_movement.delta.y;
set_value(action, value, &mut frame_inputs.map);
// want to get accumulated mouse movement in here if needed
add_value(action, value, &mut fixed_inputs.map);
}
}
}
}
fn clear_changed_fixed(mut fixed_inputs: ResMut<Inputs<FixedUpdate>>) {
for input in fixed_inputs.map.values_mut() {
input.changed = false;
}
}
const INPUT_MAP_PATH: &'static str = "assets/settings/input_map.ron";
fn save_input_map(input_map: Res<InputMap>) {
if input_map.is_changed() {
if let Err(e) = input_map.save_to_file(INPUT_MAP_PATH) {
error!("Failed to save input map: {}", e);
} else {
info!("Input map saved successfully");
}
}
}
fn setup_input_map(mut commands: Commands) {
let input_map = match InputMap::load_from_file(INPUT_MAP_PATH) {
Ok(map) => {
info!("Loaded input map");
map
}
Err(e) => {
error!("Failed to load input map: {}", e);
default()
}
};
commands.insert_resource(input_map);
}

13
crates/physics/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "physics"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = { workspace = true }
avian3d = { workspace = true }
state = { path = "../state" }
[lints]
workspace = true

55
crates/physics/src/lib.rs Normal file
View file

@ -0,0 +1,55 @@
//bevy system signatures often violate these rules
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
use avian3d::prelude::*;
use bevy::prelude::*;
use state::AppState;
pub struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(PhysicsPlugins::default())
.add_systems(OnEnter(AppState::Game), setup);
}
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Static physics object with a collision shape
commands.spawn((
RigidBody::Static,
Collider::cylinder(4.0, 0.1),
Mesh3d(meshes.add(Cylinder::new(4.0, 0.1))),
MeshMaterial3d(materials.add(Color::WHITE)),
));
// Dynamic physics object with a collision shape and initial angular velocity
commands.spawn((
RigidBody::Dynamic,
Collider::cuboid(1.0, 1.0, 1.0),
AngularVelocity(Vec3::new(2.5, 3.5, 1.5)),
Mesh3d(meshes.add(Cuboid::from_length(1.0))),
MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
Transform::from_xyz(0.0, 4.0, 0.0),
));
// Light
commands.spawn((
PointLight {
shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));
// Camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Dir3::Y),
));
}

12
crates/setup/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "setup"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = { workspace = true }
state = { path = "../state" }
[lints]
workspace = true

49
crates/setup/src/lib.rs Normal file
View file

@ -0,0 +1,49 @@
//bevy system signatures often violate these rules
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
use bevy::prelude::*;
use state::AppState;
pub struct SetupPlugin;
impl Plugin for SetupPlugin {
fn build(&self, app: &mut App) {
app.add_sub_state::<LoadState>()
.add_sub_state::<ExampleLoadSubState>()
.add_systems(OnEnter(LoadState::NotStarted), start_loading)
.add_systems(
Update,
finish_loading.run_if(
in_state(LoadState::Loading).and(in_state(ExampleLoadSubState::Loaded)),
),
);
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, SubStates, Default)]
#[source(AppState = AppState::Setup)]
enum LoadState {
#[default]
NotStarted,
Loading,
}
fn start_loading(mut next_state: ResMut<NextState<LoadState>>) {
info!("started loading...");
next_state.set(LoadState::Loading);
}
fn finish_loading(mut next_state: ResMut<NextState<AppState>>) {
info!("finished loading");
next_state.set(AppState::Game);
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, SubStates, Default)]
#[source(LoadState = LoadState::Loading)]
#[allow(dead_code)]
enum ExampleLoadSubState {
Loading,
#[default]
Loaded,
}

11
crates/state/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "state"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = { workspace = true }
avian3d = { workspace = true }
[lints]
workspace = true

88
crates/state/src/lib.rs Normal file
View file

@ -0,0 +1,88 @@
//bevy system signatures often violate these rules
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
use avian3d::prelude::*;
use bevy::prelude::*;
pub struct StatePlugin;
impl Plugin for StatePlugin {
fn build(&self, app: &mut App) {
app.init_state::<AppState>()
.enable_state_scoped_entities::<AppState>()
.add_sub_state::<GameState>()
.enable_state_scoped_entities::<GameState>()
.configure_sets(
FixedUpdate,
GameSystemSet::LoadingTick.run_if(in_state(GameState::Loading)),
)
.configure_sets(
FixedUpdate,
(
// these all run before physics, which is in FixedPostUpdate by default
GameSystemSet::PreTick,
GameSystemSet::Tick,
GameSystemSet::PostTick,
)
.run_if(in_state(GameState::Running))
.chain(),
)
.configure_sets(
Update,
(
GameSystemSet::UpdateInputs,
(
GameSystemSet::PreUpdate,
GameSystemSet::Update,
GameSystemSet::PostUpdate,
)
.run_if(in_state(GameState::Running).or(in_state(GameState::Paused)))
.chain(),
)
.chain(),
)
.add_systems(OnEnter(GameState::Running), start_physics)
.add_systems(OnExit(GameState::Running), stop_physics);
}
}
#[derive(States, Default, Debug, Hash, PartialEq, Eq, Clone)]
pub enum AppState {
#[default]
Setup,
Menu,
Game,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, SubStates, Default)]
#[source(AppState = AppState::Game)]
pub enum GameState {
Loading,
#[default]
Running,
Paused,
}
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum GameSystemSet {
// runs even if game is not loaded/active
UpdateInputs,
// update - cosmetic only. runs while game is paused
PreUpdate,
Update,
PostUpdate,
// fixed update - contains all gameplay logic. does not run when game is paused
LoadingTick,
PreTick,
Tick,
PostTick,
}
fn start_physics(mut time: ResMut<Time<Physics>>) {
time.unpause();
}
fn stop_physics(mut time: ResMut<Time<Physics>>) {
time.pause();
}

11
crates/ui/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "ui"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = { workspace = true }
bevy-inspector-egui = { workspace = true }
[lints]
workspace = true

14
crates/ui/src/lib.rs Normal file
View file

@ -0,0 +1,14 @@
//bevy system signatures often violate these rules
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
use bevy::prelude::*;
use bevy_inspector_egui::bevy_egui::EguiPlugin;
pub struct UiPlugin;
impl Plugin for UiPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(EguiPlugin::default());
}
}

View file

@ -0,0 +1,14 @@
[package]
name = "ui_debug"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = { workspace = true }
bevy-inspector-egui = { workspace = true }
input = { path = "../input" }
state = { path = "../state" }
[lints]
workspace = true

View file

@ -0,0 +1,40 @@
//bevy system signatures often violate these rules
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
use bevy::prelude::*;
use bevy_inspector_egui::quick::WorldInspectorPlugin;
use input::{actions::InputAction, conditions::action_just_pressed};
use state::GameSystemSet;
pub struct UiDebugPlugin;
impl Plugin for UiDebugPlugin {
fn build(&self, app: &mut App) {
app.init_state::<DebugUIState>()
.add_plugins(WorldInspectorPlugin::new().run_if(in_state(DebugUIState::Enabled)))
.add_systems(
Update,
toggle_debug_ui
.run_if(action_just_pressed::<Update>(InputAction::ToggleDebugUI))
.after(GameSystemSet::UpdateInputs),
);
}
}
#[derive(States, Default, Debug, Hash, PartialEq, Eq, Clone)]
pub enum DebugUIState {
#[default]
Disabled,
Enabled,
}
fn toggle_debug_ui(
mut next_state: ResMut<NextState<DebugUIState>>,
state: Res<State<DebugUIState>>,
) {
match state.get() {
DebugUIState::Disabled => next_state.set(DebugUIState::Enabled),
DebugUIState::Enabled => next_state.set(DebugUIState::Disabled),
}
}

View file

@ -0,0 +1,14 @@
[package]
name = "ui_pause"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = { workspace = true }
state = { path = "../state" }
input = { path = "../input" }
ui_state = { path = "../ui_state" }
[lints]
workspace = true

View file

@ -0,0 +1,30 @@
//bevy system signatures often violate these rules
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
use bevy::prelude::*;
use input::{actions::InputAction, conditions::action_just_pressed};
use state::{GameState, GameSystemSet};
pub struct UiPausePlugin;
impl Plugin for UiPausePlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
toggle_pause
.in_set(GameSystemSet::PreUpdate)
.run_if(action_just_pressed::<Update>(InputAction::TogglePause)),
);
}
}
fn toggle_pause(state: Res<State<GameState>>, mut next_state: ResMut<NextState<GameState>>) {
let next = match state.get() {
GameState::Loading => GameState::Loading,
GameState::Running => GameState::Paused,
GameState::Paused => GameState::Running,
};
info!("toggle pause, changing to {:?}", next);
next_state.set(next);
}

View file

@ -0,0 +1,12 @@
[package]
name = "ui_state"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = { workspace = true }
state = { path = "../state" }
[lints]
workspace = true

View file

@ -0,0 +1,32 @@
//bevy system signatures often violate these rules
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
use bevy::prelude::*;
use state::{AppState, GameState};
pub struct UiStatePlugin;
impl Plugin for UiStatePlugin {
fn build(&self, app: &mut App) {
app.add_sub_state::<GameUIState>()
.add_systems(OnEnter(GameState::Paused), on_pause)
.add_systems(OnExit(GameState::Paused), on_unpause);
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, SubStates, Default)]
#[source(AppState = AppState::Game)]
pub enum GameUIState {
#[default]
Default,
Paused,
}
fn on_pause(mut next: ResMut<NextState<GameUIState>>) {
next.set(GameUIState::Paused)
}
fn on_unpause(mut next: ResMut<NextState<GameUIState>>) {
next.set(GameUIState::Default)
}

92
mkcrate Executable file
View file

@ -0,0 +1,92 @@
#!/bin/bash
# Exit on any error
set -e
# Check if a name was provided
if [ $# -ne 1 ]; then
echo "Usage: $0 <crate-name>"
exit 1
fi
CRATE_NAME=$1
# Create the PascalCase version of the name for the plugin
PLUGIN_NAME="$(echo $CRATE_NAME | sed -r 's/(^|_)(.)/\U\2/g')"
CRATE_PATH="crates/$CRATE_NAME"
# Step 2: Create a new library crate
echo "Creating new library crate: $CRATE_NAME"
cargo new --lib "$CRATE_PATH"
# Step 3 & 4: Modify the Cargo.toml file to add bevy dependency and lints
echo "Updating $CRATE_PATH/Cargo.toml"
# Create a temporary file
TMP_FILE=$(mktemp)
# Process the Cargo.toml file
cat "$CRATE_PATH/Cargo.toml" | awk -v name="$CRATE_NAME" '
BEGIN {print_mode=1}
/\[dependencies\]/ {
print $0;
print "bevy = { workspace = true }";
print_mode=0;
next;
}
{
if (print_mode) print $0;
}
END {
if (!print_mode) print "";
print "[lints]";
print "workspace = true";
}' > "$TMP_FILE"
# Replace the original file
mv "$TMP_FILE" "$CRATE_PATH/Cargo.toml"
# Step 5: Replace the default lib.rs file
echo "Updating $CRATE_PATH/src/lib.rs"
cat > "$CRATE_PATH/src/lib.rs" << EOF
//bevy system signatures often violate these rules
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
use bevy::prelude::*;
pub struct ${PLUGIN_NAME}Plugin;
impl Plugin for ${PLUGIN_NAME}Plugin {
fn build(&self, app: &mut App) {}
}
EOF
# Step 6: Add as a dependency to the main Cargo.toml
echo "Adding dependency to main Cargo.toml"
TMP_FILE=$(mktemp)
awk -v name="$CRATE_NAME" '
/^#new internal crates go here/ {
print name " = { path = \"crates/" name "\" }";
print $0;
next;
}
{ print $0; }
' Cargo.toml > "$TMP_FILE"
mv "$TMP_FILE" Cargo.toml
# Step 7: Add plugin to main.rs
echo "Adding plugin to main.rs"
TMP_FILE=$(mktemp)
awk -v name="$CRATE_NAME" -v plugin_name="$PLUGIN_NAME" '
/[[:space:]]*\/\/ new internal crates go here/ {
# Preserve the same indentation as the comment line
match($0, /^[[:space:]]*/);
indent = substr($0, RSTART, RLENGTH);
print indent name "::" plugin_name "Plugin,";
print $0;
next;
}
{ print $0; }
' src/main.rs > "$TMP_FILE"
mv "$TMP_FILE" src/main.rs
echo "Successfully created and integrated the $CRATE_NAME crate!"

26
src/main.rs Normal file
View file

@ -0,0 +1,26 @@
use bevy::prelude::*;
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Coreheart".to_string(),
..default()
}),
..default()
}))
.add_plugins((
state::StatePlugin,
physics::PhysicsPlugin,
// ui must come before the sub crates
ui::UiPlugin,
ui_state::UiStatePlugin,
ui_debug::UiDebugPlugin,
input::InputPlugin,
setup::SetupPlugin,
ui_pause::UiPausePlugin,
// new internal crates go here
));
app.run();
}