merge main

This commit is contained in:
LorrensP-2158466
2025-04-06 22:55:53 +02:00
11 changed files with 418 additions and 23 deletions

Binary file not shown.

BIN
assets/audio/untitled.mp3 Normal file

Binary file not shown.

BIN
assets/audio/untitled.ogg Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,5 +1,9 @@
({ ({
"lebron": File (path: "images/KingLebron.png"), "lebron": File (path: "images/KingLebron.png"),
"flash_hold_4": File (path: "images/pixelart/Flashlight_hold_4.png"),
"flash_hold_4_pressed": File (path: "images/pixelart/Flashlight_click_4.png"),
"monster_footsteps": File (path: "audio/monster-footsteps.ogg"),
"theme": File (path: "audio/untitled.ogg"),
"house": File (path: "meshes/House.glb"), "house": File (path: "meshes/House.glb"),
"library": Folder ( "library": Folder (
path: "meshes/library", path: "meshes/library",

View File

@@ -30,6 +30,10 @@ pub(super) fn plugin(app: &mut App) {
pub(crate) struct AudioAssets { pub(crate) struct AudioAssets {
#[asset(key = "flashlight_click")] #[asset(key = "flashlight_click")]
pub(crate) flash_click: Handle<AudioSource>, pub(crate) flash_click: Handle<AudioSource>,
#[asset(key = "monster_footsteps")]
pub(crate) monster_footsteps: Handle<AudioSource>,
#[asset(key = "theme")]
pub(crate) theme_song: Handle<AudioSource>,
} }
#[derive(AssetCollection, Resource, Clone)] #[derive(AssetCollection, Resource, Clone)]

View File

@@ -537,10 +537,8 @@ impl GameLevel {
let end_node = self.nodes.get_mut(&self.end_node).unwrap(); let end_node = self.nodes.get_mut(&self.end_node).unwrap();
if self.end_node.1 >= self.end_node.0 { if self.end_node.1 >= self.end_node.0 {
end_node.north = Side::Connection; end_node.north = Side::Connection;
end_node.south = Side::Connection;
} else { } else {
end_node.east = Side::Connection; end_node.east = Side::Connection;
end_node.west = Side::Connection;
} }
} }

View File

@@ -1,5 +1,6 @@
use asset_loading::ImageAssets; use asset_loading::{AudioAssets, ImageAssets};
use bevy::prelude::*; use bevy::{prelude::*};
use bevy_kira_audio::{Audio, AudioControl};
use bevy_rapier3d::prelude::*; use bevy_rapier3d::prelude::*;
mod asset_loading; mod asset_loading;
@@ -10,6 +11,7 @@ mod level_instantiation;
mod main_menu; mod main_menu;
mod player; mod player;
mod util; mod util;
mod monster;
fn main() { fn main() {
App::new() App::new()
@@ -22,15 +24,18 @@ fn main() {
RapierPhysicsPlugin::<NoUserData>::default(), RapierPhysicsPlugin::<NoUserData>::default(),
// RapierDebugRenderPlugin::default(), // RapierDebugRenderPlugin::default(),
player::plugin, player::plugin,
monster::plugin,
// debugging::plugin // debugging::plugin
)) ))
.init_state::<GameState>() .init_state::<GameState>()
.insert_resource(AmbientLight { .insert_resource(AmbientLight {
color: Color::srgba(0.8, 0.8, 1.0, 1.0), color: Color::srgba(0.8, 0.8, 1.0, 1.0),
brightness: 10.0, brightness: 11.0,
// brightness: 80.0, // brightness: 80.0,
}) })
.add_systems(OnEnter(GameState::Playing), setup) .add_systems(OnEnter(GameState::Playing), setup)
.add_systems(OnEnter(GameState::Menu), play_song)
.add_systems(OnExit(GameState::Menu), stop_song)
.run(); .run();
} }
@@ -75,3 +80,18 @@ fn setup(
// Transform::from_xyz(4.0, 8.0, 4.0), // Transform::from_xyz(4.0, 8.0, 4.0),
// )); // ));
} }
fn play_song(
audio_assets: Res<AudioAssets>,
audio: Res<Audio>,
) {
audio.play(audio_assets.theme_song.clone())
.looped()
.with_volume(0.20);
}
fn stop_song(
audio: Res<Audio>,
) {
audio.stop();
}

355
src/monster.rs Normal file
View File

@@ -0,0 +1,355 @@
use std::time::Duration;
use bevy::{
prelude::*,
time::Stopwatch,
};
use bevy_kira_audio::{prelude::Volume, Audio, AudioControl, AudioTween};
use bevy_rapier3d::prelude::*;
use rand::prelude::*;
use crate::{
asset_loading::AudioAssets,
player::Player,
GameState
};
// Monster states and behavior configuration
#[derive(Debug, Component)]
pub struct Monster {
pub speed: f32,
pub detection_range: f32,
pub state_timer: Timer,
pub footstep_timer: Timer,
pub state: MonsterState,
}
#[derive(Debug, Clone, PartialEq)]
pub enum MonsterState {
Lurking,
Hunting,
Wandering,
Dormant,
}
impl Default for Monster {
fn default() -> Self {
Self {
speed: 2.0,
detection_range: 15.0,
state_timer: Timer::from_seconds(1.0, TimerMode::Once),
footstep_timer: Timer::from_seconds(3.0, TimerMode::Repeating),
state: MonsterState::Dormant,
}
}
}
#[derive(Debug, Component, Default)]
pub struct MonsterPathfinding {
pub current_target: Option<Vec3>,
pub wander_target_timer: Timer,
}
#[derive(Debug, Component, Default)]
pub struct DangerIndicator {
pub last_sound_timer: Stopwatch,
}
pub fn plugin(app: &mut App) {
app
.add_systems(OnEnter(GameState::Playing), spawn_monster)
.add_systems(
Update,
(
update_monster_state,
move_monster,
play_monster_sounds,
).run_if(in_state(GameState::Playing)),
);
}
fn spawn_monster(
mut commands: Commands,
// mut meshes: ResMut<Assets<Mesh>>,
// mut materials: ResMut<Assets<StandardMaterial>>,
) {
let spawn_position = Vec3::new(20.0, 0.5, 20.0);
commands.spawn((
Monster::default(),
MonsterPathfinding {
current_target: None,
wander_target_timer: Timer::from_seconds(5.0, TimerMode::Repeating),
},
DangerIndicator::default(),
RigidBody::Dynamic,
Collider::capsule(Vec3::new(0.0, -0.5, 0.0), Vec3::new(0.0, 1.0, 0.0), 0.6),
Velocity::zero(),
LockedAxes::ROTATION_LOCKED,
Damping {
linear_damping: 1.0,
angular_damping: 1.0,
},
Transform::from_translation(spawn_position),
));
}
fn update_monster_state(
time: Res<Time>,
mut monster_query: Query<(&mut Monster, &Transform, &mut MonsterPathfinding, &mut DangerIndicator), Without<Player>>,
player_query: Query<&Transform, With<Player>>,
) {
let mut rand = rand::rng();
if let Ok((mut monster, monster_transform, mut pathfinding, mut danger)) = monster_query.get_single_mut() {
let player_pos = if let Ok(player_transform) = player_query.get_single() {
Some(player_transform.translation)
} else {
None
};
monster.state_timer.tick(time.delta());
// when timer expires, potentially change state
if monster.state_timer.just_finished() {
let next_state = match monster.state {
MonsterState::Dormant => {
// 70% chance to start lurking
if rand.random_bool(0.85) {
MonsterState::Lurking
} else {
MonsterState::Dormant
}
},
MonsterState::Lurking => {
// 30% chance to start hunting, 20% to go dormant, 50% to start wandering
let roll = rand.random_range(0.0..1.0);
if roll < 0.3 {
danger.last_sound_timer.reset();
MonsterState::Hunting
} else if roll < 0.5 {
MonsterState::Dormant
} else {
MonsterState::Wandering
}
},
MonsterState::Wandering => {
// 20% chance to start hunting, 30% to go lurking, 50% to keep wandering
let roll = rand.random_range(0.0..1.0);
if roll < 0.2 {
danger.last_sound_timer.reset();
MonsterState::Hunting
} else if roll < 0.5 {
MonsterState::Lurking
} else {
MonsterState::Wandering
}
},
MonsterState::Hunting => {
// after hunting, always go dormant for a while
MonsterState::Dormant
}
};
// set state
monster.state = next_state.clone();
monster.state_timer = match next_state {
MonsterState::Dormant => Timer::from_seconds(15.0, TimerMode::Once),
MonsterState::Lurking => Timer::from_seconds(20.0, TimerMode::Once),
MonsterState::Wandering => Timer::from_seconds(30.0, TimerMode::Once),
MonsterState::Hunting => Timer::from_seconds(15.0, TimerMode::Once),
};
// reset pathfinding when state changes
pathfinding.current_target = None;
println!("new state={:?}", monster.state);
}
// override state in some cases
if let Some(player_pos) = player_pos {
let distance = monster_transform.translation.distance(player_pos);
// if player is very close and monster isn't hunting, switch to hunting
if distance < monster.detection_range * 0.5 && monster.state != MonsterState::Hunting {
if rand.random_bool(0.75) { // 75% chance to notice player
monster.state = MonsterState::Hunting;
monster.state_timer = Timer::from_seconds(20.0, TimerMode::Once);
danger.last_sound_timer.reset();
pathfinding.current_target = None;
}
}
}
}
}
fn move_monster(
time: Res<Time>,
mut monster_query: Query<(&Monster, &mut Transform, &mut Velocity, &mut MonsterPathfinding), Without<Player>>,
player_query: Query<&Transform, With<Player>>,
) {
let mut rand = rand::rng();
if let Ok((monster, mut transform, mut velocity, mut pathfinding)) = monster_query.get_single_mut() {
pathfinding.wander_target_timer.tick(time.delta());
// move based on state
match monster.state {
MonsterState::Dormant => {
velocity.linvel = Vec3::ZERO;
},
MonsterState::Lurking => {
// get a random wander target if we don't have one or timer expired
if pathfinding.current_target.is_none() || pathfinding.wander_target_timer.just_finished() {
let range = 30.0;
let random_offset = Vec3::new(
rand.random_range(-range..range),
0.0,
rand.random_range(-range..range)
);
let base_pos = if let Ok(player_transform) = player_query.get_single() {
player_transform.translation
} else {
transform.translation
};
pathfinding.current_target = Some(base_pos + random_offset);
pathfinding.wander_target_timer = Timer::from_seconds(10.0, TimerMode::Once);
}
// move toward target at slow pace
if let Some(target) = pathfinding.current_target {
move_towards_target(&mut transform, &mut velocity, target, monster.speed * 0.65);
}
},
MonsterState::Wandering => {
if pathfinding.current_target.is_none() || pathfinding.wander_target_timer.just_finished() {
if let Ok(player_transform) = player_query.get_single() {
// choose a random position somewhat close to player
let range = 15.0;
let random_offset = Vec3::new(
rand.random_range(-range..range),
0.0,
rand.random_range(-range..range)
);
pathfinding.current_target = Some(player_transform.translation + random_offset);
pathfinding.wander_target_timer = Timer::from_seconds(7.0, TimerMode::Once);
}
}
// move toward target at medium pace
if let Some(target) = pathfinding.current_target {
move_towards_target(&mut transform, &mut velocity, target, monster.speed * 0.85);
}
},
MonsterState::Hunting => {
// chase the motherfucker
if let Ok(player_transform) = player_query.get_single() {
move_towards_target(
&mut transform,
&mut velocity,
player_transform.translation,
monster.speed * 1.1
);
}
}
}
}
}
fn move_towards_target(
transform: &mut Transform,
velocity: &mut Velocity,
target: Vec3,
speed: f32
) {
let direction = (target - transform.translation).normalize_or_zero();
let direction = Vec3::new(direction.x, 0.0, direction.z).normalize_or_zero();
velocity.linvel = direction * speed;
if direction.length_squared() > 0.01 {
let look_direction = Vec3::new(direction.x, 0.0, direction.z).normalize_or_zero();
if look_direction != Vec3::ZERO {
let target_rotation = Quat::from_rotation_arc(Vec3::Z, look_direction);
transform.rotation = transform.rotation.slerp(target_rotation, 0.1);
}
}
}
fn play_monster_sounds(
time: Res<Time>,
audio: Res<Audio>,
audio_assets: Res<AudioAssets>,
mut monster_query: Query<(&mut Monster, &Transform), Without<Player>>,
player_query: Query<&Transform, With<Player>>,
) {
let mut rand = rand::rng();
if let (Ok((mut monster, monster_transform)), Ok(player_transform)) =
(monster_query.get_single_mut(), player_query.get_single()) {
monster.footstep_timer.tick(time.delta());
let distance = monster_transform.translation.distance(player_transform.translation);
// play footstep sound if the timer finished
if monster.footstep_timer.just_finished() {
match monster.state {
MonsterState::Hunting => {
monster.footstep_timer = Timer::from_seconds(1.5, TimerMode::Once);
},
MonsterState::Lurking | MonsterState::Wandering => {
monster.footstep_timer = Timer::from_seconds(
rand.random_range(3.0..7.0),
TimerMode::Once
);
},
MonsterState::Dormant => {
monster.footstep_timer = Timer::from_seconds(
rand.random_range(10.0..20.0),
TimerMode::Once
);
}
}
play_footstep_segment(audio, &audio_assets, distance, &monster.state, monster.footstep_timer.duration().as_secs_f32());
if monster.state == MonsterState::Hunting && rand.random_bool(0.3) {
println!("Monster growl!");
}
}
}
}
fn play_footstep_segment(
audio: Res<Audio>,
audio_assets: &Res<AudioAssets>,
distance: f32,
state: &MonsterState,
dur: f32
) {
let base_volume: f32 = match state {
MonsterState::Dormant => 0.6,
MonsterState::Lurking => 0.65,
MonsterState::Wandering => 0.8,
MonsterState::Hunting => 1.0,
};
// adjust volume based on distance (closer = louder)
let distance_factor: f32 = 1.0 - (distance / 100.0).clamp(0.0, 1.0);
let volume = base_volume * distance_factor;
// play only a short segment of the footstep sound
// by only playing the last part
let mut start_time = 30.0 - dur;
// let mut rand = rand::rng();
// start_time += rand.random_range(0.0..5.0);
audio.play(audio_assets.monster_footsteps.clone())
.with_volume(volume as f64)
.start_from((start_time as f64).min(27.0))
.fade_in(AudioTween::linear(Duration::from_millis(100)))
.handle();
println!("Monster footstep: State={:?}, Distance={:.2}, Volume={:.2}, Dur={:1}, Start={:1}",
state, distance, volume, dur, (start_time as f64).min(27.0));
}

View File

@@ -93,7 +93,7 @@ pub fn plugin(app: &mut App) {
handle_input, handle_input,
apply_head_bob, apply_head_bob,
on_resize_system, on_resize_system,
handle_flashlight, (handle_flashlight, handle_spotlight).chain(),
( (
update_flashlight_button_animation, update_flashlight_button_animation,
update_flashlight_charge, update_flashlight_charge,
@@ -416,7 +416,7 @@ pub fn apply_head_bob(
} }
pub fn handle_flashlight( pub fn handle_flashlight(
player_query: Query<&PlayerAction, With<Player>>, player_query: Query<&PlayerAction, With<Player>>,
mut flashlight_query: Query<&mut SpotLight, With<SpotlightFlashlight>>, mut flashlight_query: Query<&mut Flashlight>,
mut flashlight_sprite_query: Query<&mut FlashlightButtonAnimation>, mut flashlight_sprite_query: Query<&mut FlashlightButtonAnimation>,
audio_assets: Res<AudioAssets>, audio_assets: Res<AudioAssets>,
audio: Res<Audio>, audio: Res<Audio>,
@@ -427,18 +427,30 @@ pub fn handle_flashlight(
if *action != PlayerAction::ToggleFlashlight { if *action != PlayerAction::ToggleFlashlight {
return; return;
} }
if let Ok(flashlight) = flashlight_query.get_single() {
if flashlight.charge < 1.0 {
return;
}
}
if let Ok(mut animation) = flashlight_sprite_query.get_single_mut() { if let Ok(mut animation) = flashlight_sprite_query.get_single_mut() {
animation.is_pressed = true; animation.is_pressed = true;
animation.timer.reset(); animation.timer.reset();
} }
if let Ok(mut spotlight) = flashlight_query.get_single_mut() { if let Ok(mut flashlight) = flashlight_query.get_single_mut() {
audio.play(audio_assets.flash_click.clone()); audio.play(audio_assets.flash_click.clone());
flashlight.is_on = !flashlight.is_on;
}
}
spotlight.intensity = if spotlight.intensity > 0.0 { pub fn handle_spotlight(
0.0 mut spotlight_query: Query<&mut SpotLight, With<SpotlightFlashlight>>,
} else { mut flashlight_query: Query<&mut Flashlight>,
320_000.0 ) {
}; if let (Ok(mut spotlight), Ok(flashlight)) = (
spotlight_query.get_single_mut(),
flashlight_query.get_single_mut(),
) {
spotlight.intensity = if !flashlight.is_on { 0.0 } else { 320_000.0 };
} }
} }
@@ -476,7 +488,7 @@ pub fn update_flashlight_sprite(
flashlights: Res<FlashlightAssets>, flashlights: Res<FlashlightAssets>,
) { ) {
for (animation, mut image, flashlight) in query.iter_mut() { for (animation, mut image, flashlight) in query.iter_mut() {
let charge = flashlight.charge.ceil() as i32; let charge = flashlight.charge.round() as i32;
let sprite_image = match (charge, animation.is_pressed) { let sprite_image = match (charge, animation.is_pressed) {
(4, true) => flashlights.flash_hold_4_pressed.clone(), (4, true) => flashlights.flash_hold_4_pressed.clone(),
(4, false) => flashlights.flash_hold_4.clone(), (4, false) => flashlights.flash_hold_4.clone(),

View File

@@ -1,5 +1,5 @@
use bevy::prelude::*; use bevy::prelude::*;
use bevy_egui::{EguiContexts, egui}; use bevy_egui::{egui::{self, Shadow, Stroke}, EguiContexts};
use std::default::Default; use std::default::Default;
use crate::{GameState, interaction::Interact, util::single}; use crate::{GameState, interaction::Interact, util::single};
@@ -58,12 +58,14 @@ fn bottom_panel(
}); });
egui::TopBottomPanel::bottom("inventory_toolbar") egui::TopBottomPanel::bottom("inventory_toolbar")
.show_separator_line(false)
.frame(egui::Frame { .frame(egui::Frame {
fill: egui::Color32::from_rgba_premultiplied(0, 0, 0, 0), fill: egui::Color32::from_rgba_premultiplied(0, 0, 0, 0),
// Removed the stroke/border by setting it to none // Removed the stroke/border by setting it to none
stroke: egui::Stroke::NONE, stroke: egui::Stroke::NONE,
outer_margin: egui::epaint::Margin::same(0), outer_margin: egui::epaint::Margin::same(0),
inner_margin: egui::epaint::Margin::same(0), inner_margin: egui::epaint::Margin::same(0),
shadow: Shadow::NONE,
..Default::default() ..Default::default()
}) })
.show(egui_ctx.ctx_mut(), |ui| { .show(egui_ctx.ctx_mut(), |ui| {
@@ -75,8 +77,8 @@ fn bottom_panel(
// Create a frame for the slot // Create a frame for the slot
let slot_frame = egui::Frame { let slot_frame = egui::Frame {
fill: egui::Color32::from_rgba_premultiplied(75, 75, 75, 100), fill: egui::Color32::from_rgba_premultiplied(10, 10, 10, 100),
stroke: egui::Stroke::new(2.0, egui::Color32::WHITE), stroke: Stroke::NONE,
..Default::default() ..Default::default()
}; };
@@ -92,15 +94,15 @@ fn bottom_panel(
)); ));
} }
None => { None => {
ui.label(egui::RichText::new("Empty").size(12.0)); // ui.label(egui::RichText::new("Empty").size(12.0));
return; return;
} }
} }
ui.label( // ui.label(
egui::RichText::new(name) // egui::RichText::new(name)
.color(egui::Color32::WHITE) // .color(egui::Color32::WHITE)
.size(12.0), // .size(12.0),
); // );
}); });
}); });
ui.allocate_response(ui.available_size(), egui::Sense::click()) ui.allocate_response(ui.available_size(), egui::Sense::click())