Compare commits
17 Commits
b118a2580b
...
3a3182d52d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a3182d52d | ||
|
|
9fb16f6cdd | ||
|
|
79f69c4383 | ||
|
|
e5fc52e3b9 | ||
|
|
a91e2bb8c2 | ||
|
|
a3f4177d57 | ||
|
|
3ca0db21b4 | ||
|
|
902ed398e4 | ||
|
|
8c5081b2dc | ||
|
|
3b2cda716b | ||
|
|
5d7df7b650 | ||
|
|
2aa48b191e | ||
|
|
cb758681ae | ||
|
|
78358a830e | ||
|
|
d69024b984 | ||
|
|
c47fa8b282 | ||
|
|
c88ff20217 |
BIN
assets/audio/monster-footsteps.ogg
Normal file
BIN
assets/audio/monster-footsteps.ogg
Normal file
Binary file not shown.
BIN
assets/audio/monster_growl.ogg
Normal file
BIN
assets/audio/monster_growl.ogg
Normal file
Binary file not shown.
BIN
assets/audio/untitled.mp3
Normal file
BIN
assets/audio/untitled.mp3
Normal file
Binary file not shown.
BIN
assets/audio/untitled.ogg
Normal file
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 |
@@ -1,5 +1,10 @@
|
|||||||
({
|
({
|
||||||
"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"),
|
||||||
|
"monster_growl": File (path: "audio/monster_growl.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",
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ 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 = "monster_growl")]
|
||||||
|
pub(crate) monster_growl: Handle<AudioSource>,
|
||||||
|
#[asset(key = "theme")]
|
||||||
|
pub(crate) theme_song: Handle<AudioSource>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(AssetCollection, Resource, Clone)]
|
#[derive(AssetCollection, Resource, Clone)]
|
||||||
|
|||||||
@@ -116,12 +116,11 @@ fn spawn_level(
|
|||||||
Transform::from_xyz(-500.0, 0.0, -500.0),
|
Transform::from_xyz(-500.0, 0.0, -500.0),
|
||||||
));
|
));
|
||||||
// huge roof
|
// huge roof
|
||||||
// commands.spawn((
|
commands.spawn((
|
||||||
// RigidBody::Fixed,
|
RigidBody::Fixed,
|
||||||
// Collider::cuboid(1000.0, 0.1, 1000.0),
|
Collider::cuboid(1000.0, 0.1, 1000.0),
|
||||||
// Transform::from_xyz(-500.0, 3.0, -500.0),
|
Transform::from_xyz(-500.0, 3.0, -500.0),
|
||||||
// ));
|
));
|
||||||
// let map = GameMap::test();
|
|
||||||
|
|
||||||
let levels = create_levels(3);
|
let levels = create_levels(3);
|
||||||
|
|
||||||
@@ -165,7 +164,7 @@ fn spawn_level(
|
|||||||
pos.with_rotation(Quat::from_rotation_y(90.0_f32.to_radians())),
|
pos.with_rotation(Quat::from_rotation_y(90.0_f32.to_radians())),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
round_door.clone(),
|
door.clone(),
|
||||||
pos.with_rotation(Quat::from_rotation_y(180.0_f32.to_radians())),
|
pos.with_rotation(Quat::from_rotation_y(180.0_f32.to_radians())),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -181,7 +180,7 @@ fn spawn_level(
|
|||||||
pos.with_rotation(Quat::from_rotation_y(90.0_f32.to_radians())),
|
pos.with_rotation(Quat::from_rotation_y(90.0_f32.to_radians())),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
round_door.clone(),
|
door.clone(),
|
||||||
pos.with_rotation(Quat::from_rotation_y(0.0_f32.to_radians())),
|
pos.with_rotation(Quat::from_rotation_y(0.0_f32.to_radians())),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -194,7 +193,7 @@ fn spawn_level(
|
|||||||
vec![
|
vec![
|
||||||
(wall.clone(), pos),
|
(wall.clone(), pos),
|
||||||
(
|
(
|
||||||
round_door.clone(),
|
door.clone(),
|
||||||
pos.with_rotation(Quat::from_rotation_y(90.0_f32.to_radians())),
|
pos.with_rotation(Quat::from_rotation_y(90.0_f32.to_radians())),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -207,7 +206,7 @@ fn spawn_level(
|
|||||||
vec![
|
vec![
|
||||||
(wall.clone(), pos),
|
(wall.clone(), pos),
|
||||||
(
|
(
|
||||||
round_door.clone(),
|
door.clone(),
|
||||||
pos.with_rotation(Quat::from_rotation_y(270.0_f32.to_radians())),
|
pos.with_rotation(Quat::from_rotation_y(270.0_f32.to_radians())),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -343,12 +342,6 @@ fn spawn_level(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let (x, z) = level.end_node;
|
|
||||||
commands.spawn((
|
|
||||||
Mesh3d(meshes.add(Cuboid::new(1.0, 20.0, 1.0))),
|
|
||||||
MeshMaterial3d(materials.add(Color::srgb_u8(255, 0, 0))),
|
|
||||||
Transform::from_xyz(2.0 * x as f32, 0.5, -2.0 * z as f32),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
commands.insert_resource(GameLevels { levels });
|
commands.insert_resource(GameLevels { levels });
|
||||||
}
|
}
|
||||||
@@ -377,18 +370,9 @@ fn create_levels(n: i32) -> Vec<GameLevel> {
|
|||||||
next_node.west = Side::Connection;
|
next_node.west = Side::Connection;
|
||||||
next_node.north = Side::Connection;
|
next_node.north = Side::Connection;
|
||||||
|
|
||||||
if map.end_node.0 >= map.end_node.1 {
|
let node = maps.last().unwrap().nodes.get(&maps.last().unwrap().end_node).unwrap();
|
||||||
next_node.south = Side::Closed;
|
|
||||||
next_node.north = Side::Closed;
|
|
||||||
pos.0 += 1;
|
|
||||||
} else {
|
|
||||||
next_node.east = Side::Closed;
|
|
||||||
next_node.west = Side::Closed;
|
|
||||||
pos.1 += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
initial_node = next_node;
|
let map = GameLevel::new(pos, node.clone(), 5);
|
||||||
let map = GameLevel::new(pos, initial_node.clone(), 5);
|
|
||||||
maps.push(map);
|
maps.push(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,10 +521,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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
src/main.rs
20
src/main.rs
@@ -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;
|
||||||
@@ -8,6 +9,7 @@ mod debugging;
|
|||||||
mod interaction;
|
mod interaction;
|
||||||
mod level_instantiation;
|
mod level_instantiation;
|
||||||
mod main_menu;
|
mod main_menu;
|
||||||
|
mod monster;
|
||||||
mod player;
|
mod player;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
@@ -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,14 @@ 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();
|
||||||
|
}
|
||||||
|
|||||||
371
src/monster.rs
Normal file
371
src/monster.rs
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
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(20.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(10.0, TimerMode::Once);
|
||||||
|
monster.speed = 1.1
|
||||||
|
},
|
||||||
|
MonsterState::Lurking | MonsterState::Wandering => {
|
||||||
|
monster.footstep_timer = Timer::from_seconds(
|
||||||
|
10.0,
|
||||||
|
TimerMode::Once
|
||||||
|
);
|
||||||
|
monster.speed = 1.0
|
||||||
|
},
|
||||||
|
MonsterState::Dormant => {
|
||||||
|
monster.footstep_timer = Timer::from_seconds(
|
||||||
|
10.0,
|
||||||
|
TimerMode::Once
|
||||||
|
);
|
||||||
|
monster.speed = 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
play_footstep_segment(
|
||||||
|
&audio,
|
||||||
|
&audio_assets,
|
||||||
|
distance,
|
||||||
|
&monster.state,
|
||||||
|
monster.footstep_timer.duration().as_secs_f32(),
|
||||||
|
monster.speed
|
||||||
|
);
|
||||||
|
|
||||||
|
if monster.state == MonsterState::Hunting && rand.random_bool(0.3) {
|
||||||
|
audio.play(audio_assets.monster_growl.clone())
|
||||||
|
.with_volume(0.8 as f64)
|
||||||
|
.fade_in(AudioTween::linear(Duration::from_millis(100)))
|
||||||
|
.handle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play_footstep_segment(
|
||||||
|
audio: &Res<Audio>,
|
||||||
|
audio_assets: &Res<AudioAssets>,
|
||||||
|
distance: f32,
|
||||||
|
state: &MonsterState,
|
||||||
|
dur: f32,
|
||||||
|
speed: 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 / 30.0).powf(2.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.stop();
|
||||||
|
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)))
|
||||||
|
.with_playback_rate(speed as f64)
|
||||||
|
.handle();
|
||||||
|
|
||||||
|
println!("Monster footstep: State={:?}, Distance={:.2}, Volume={:.2}, Dur={:1}, Start={:1}",
|
||||||
|
state, distance, volume, dur, (start_time as f64).min(27.0));
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
@@ -213,7 +213,7 @@ fn on_resize_system(
|
|||||||
fn flashlight_base_transform(window_width: f32, window_height: f32) -> BaseTransform {
|
fn flashlight_base_transform(window_width: f32, window_height: f32) -> BaseTransform {
|
||||||
let window_size = Vec2::new(window_width, window_height);
|
let window_size = Vec2::new(window_width, window_height);
|
||||||
let sprite_size = Vec2::new(404.0, 404.0);
|
let sprite_size = Vec2::new(404.0, 404.0);
|
||||||
let scale = window_width / 2400.0;
|
let scale = window_width / 2200.0;
|
||||||
let world_size = sprite_size * scale;
|
let world_size = sprite_size * scale;
|
||||||
let xoffset = window_size.x / 4.0 - 40.0;
|
let xoffset = window_size.x / 4.0 - 40.0;
|
||||||
let yoffset = 15.0;
|
let yoffset = 15.0;
|
||||||
@@ -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,27 +427,39 @@ 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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_flashlight_charge(time: Res<Time>, mut flashlight_query: Query<&mut Flashlight>) {
|
pub fn update_flashlight_charge(time: Res<Time>, mut flashlight_query: Query<&mut Flashlight>) {
|
||||||
for mut flashlight in flashlight_query.iter_mut() {
|
for mut flashlight in flashlight_query.iter_mut() {
|
||||||
if flashlight.is_on {
|
if flashlight.is_on {
|
||||||
flashlight.charge = flashlight.charge - time.delta_secs() * 0.1;
|
flashlight.charge = flashlight.charge - time.delta_secs() * 0.2;
|
||||||
} else {
|
} else {
|
||||||
flashlight.charge = flashlight.charge + time.delta_secs() * 0.1;
|
flashlight.charge = flashlight.charge + time.delta_secs() * 0.2;
|
||||||
}
|
}
|
||||||
flashlight.charge = flashlight.charge.clamp(0.0, 4.0);
|
flashlight.charge = flashlight.charge.clamp(0.0, 4.0);
|
||||||
if flashlight.charge <= 0.0 {
|
if flashlight.charge <= 0.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(),
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
Reference in New Issue
Block a user