head database for aim and ai

This commit is contained in:
2025-04-11 18:35:59 +02:00
parent 806d10e1bd
commit 9df3c00abb
12 changed files with 163 additions and 55 deletions

View File

@@ -5,6 +5,7 @@ use crate::{
GameState,
aim::AimTarget,
global_observer,
head_asset::HeadsDatabase,
heads::ActiveHeads,
hitpoints::Hit,
physics_layers::GameLayer,
@@ -14,6 +15,7 @@ use crate::{
};
use avian3d::prelude::*;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Component)]
pub struct Projectile {
@@ -29,8 +31,9 @@ pub enum TriggerState {
#[derive(Event, Reflect)]
pub struct TriggerCashHeal;
#[derive(Debug, Copy, Clone, PartialEq, Reflect)]
#[derive(Debug, Copy, Clone, PartialEq, Reflect, Default, Serialize, Deserialize)]
pub enum HeadAbility {
#[default]
None,
Arrow,
Thrown,
@@ -112,6 +115,7 @@ fn on_trigger_state(
player_rot: Query<&Transform, With<PlayerBodyMesh>>,
player_query: Query<(&Transform, &AimTarget), With<Player>>,
mut active_heads: ResMut<ActiveHeads>,
heads_db: Res<HeadsDatabase>,
time: Res<Time>,
) {
if matches!(trigger.event(), TriggerState::Active) {
@@ -143,7 +147,8 @@ fn on_trigger_state(
active_heads.use_ammo(time.elapsed_secs());
match state.ability {
let ability = heads_db.head_stats(state.head).ability;
match ability {
HeadAbility::Thrown => commands.trigger(TriggerThrow(trigger_state)),
HeadAbility::Gun => commands.trigger(TriggerGun(trigger_state)),
_ => (),

View File

@@ -2,8 +2,9 @@ use bevy::prelude::*;
use crate::{
GameState,
abilities::{TriggerData, TriggerThrow},
abilities::{HeadAbility, TriggerData, TriggerThrow},
aim::AimTarget,
head_asset::HeadsDatabase,
npc::Npc,
};
@@ -19,12 +20,20 @@ fn update(
mut commands: Commands,
mut query: Query<(&mut Npc, &AimTarget, &Transform), With<Ai>>,
time: Res<Time>,
heads_db: Res<HeadsDatabase>,
) {
for (mut npc, target, t) in query.iter_mut() {
if target.0.is_none() {
continue;
}
let ability = heads_db.head_stats(npc.head).ability;
//TODO: support other abilities
if ability != HeadAbility::Thrown {
continue;
}
let can_shoot_again = npc.last_use + 1. < time.elapsed_secs();
if can_shoot_again && npc.has_ammo() {

View File

@@ -4,6 +4,7 @@ mod target_ui;
use crate::{
GameState,
head::ActiveHead,
head_asset::HeadsDatabase,
physics_layers::GameLayer,
player::{Player, PlayerBodyMesh},
tb_entities::EnemySpawn,
@@ -53,15 +54,15 @@ fn add_aim(mut commands: Commands, query: Query<Entity, Added<ActiveHead>>) {
}
}
fn head_change(mut query: Query<(&ActiveHead, &mut AimState), Changed<ActiveHead>>) {
fn head_change(
mut query: Query<(&ActiveHead, &mut AimState), Changed<ActiveHead>>,
heads_db: Res<HeadsDatabase>,
) {
for (head, mut state) in query.iter_mut() {
// info!("head changed: {}", head.0);
// state.max_angle = if head.0 == 0 { PI / 8. } else { PI / 2. }
state.range = match head.0 {
0 => 80.,
3 => 60.,
_ => 40.,
};
let stats = heads_db.head_stats(head.0);
state.range = stats.range;
}
}

54
src/head_asset.rs Normal file
View File

@@ -0,0 +1,54 @@
use crate::abilities::HeadAbility;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Reflect, Serialize, Deserialize)]
pub struct HeadStats {
pub key: String,
#[serde(default)]
pub ability: HeadAbility,
#[serde(default)]
pub range: f32,
}
#[derive(Debug, Asset, Reflect, Serialize, Deserialize)]
pub struct HeadDatabaseAsset(pub Vec<HeadStats>);
#[derive(Debug, Resource, Reflect)]
#[reflect(Resource)]
pub struct HeadsDatabase {
pub heads: Vec<HeadStats>,
}
impl HeadsDatabase {
pub fn head_key(&self, id: usize) -> &str {
&self.heads[id].key
}
pub fn head_stats(&self, id: usize) -> &HeadStats {
&self.heads[id]
}
}
// #[cfg(test)]
// mod test {
// use super::*;
// #[test]
// fn test_serialize() {
// let asset = HeadDatabaseAsset(vec![
// HeadStats {
// key: String::from("foo"),
// range: 90.,
// ..Default::default()
// },
// HeadStats {
// key: String::from("bar"),
// ability: HeadAbility::Gun,
// range: 0.,
// },
// ]);
// std::fs::write("assets/test.headsb.ron", ron::to_string(&asset).unwrap()).unwrap();
// }
// }

View File

@@ -2,11 +2,11 @@ mod heads_ui;
use crate::{
GameState,
abilities::HeadAbility,
backpack::{BackbackSwapEvent, Backpack},
global_observer,
head_asset::HeadsDatabase,
hitpoints::Hitpoints,
player::{Player, head_id_to_str},
player::Player,
sounds::PlaySound,
};
use bevy::prelude::*;
@@ -22,7 +22,6 @@ pub struct HeadsImages {
#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
pub struct HeadState {
pub head: usize,
pub ability: HeadAbility,
pub health: u32,
pub health_max: u32,
pub ammo: u32,
@@ -39,17 +38,11 @@ impl HeadState {
health_max: 100,
ammo,
ammo_max: ammo,
ability: HeadAbility::None,
reload_duration: 5.,
last_use: 0.,
}
}
pub fn with_ability(mut self, ability: HeadAbility) -> Self {
self.ability = ability;
self
}
pub fn has_ammo(&self) -> bool {
self.ammo > 0
}
@@ -119,11 +112,11 @@ pub fn plugin(app: &mut App) {
app.insert_resource(ActiveHeads {
heads: [
Some(HeadState::new(0, 10).with_ability(HeadAbility::Thrown)),
Some(HeadState::new(3, 10).with_ability(HeadAbility::Gun)),
Some(HeadState::new(6, 10).with_ability(HeadAbility::Arrow)),
Some(HeadState::new(8, 10).with_ability(HeadAbility::Thrown)),
Some(HeadState::new(9, 10).with_ability(HeadAbility::Gun)),
Some(HeadState::new(0, 10)),
Some(HeadState::new(3, 10)),
Some(HeadState::new(6, 10)),
Some(HeadState::new(8, 10)),
Some(HeadState::new(9, 10)),
],
current_slot: 0,
});
@@ -138,10 +131,10 @@ pub fn plugin(app: &mut App) {
global_observer!(app, on_swap_backpack);
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, heads: Res<HeadsDatabase>) {
// TODO: load via asset loader
let heads = (0usize..HEAD_COUNT)
.map(|i| asset_server.load(format!("ui/heads/{}.png", head_id_to_str(i))))
.map(|i| asset_server.load(format!("ui/heads/{}.png", heads.head_key(i))))
.collect();
commands.insert_resource(HeadsImages { heads });

View File

@@ -1,4 +1,7 @@
use crate::GameState;
use crate::{
GameState,
head_asset::{HeadDatabaseAsset, HeadsDatabase},
};
use bevy::{prelude::*, utils::HashMap};
use bevy_asset_loader::prelude::*;
@@ -43,6 +46,12 @@ pub struct AudioAssets {
pub head: HashMap<AssetFileName, Handle<AudioSource>>,
}
#[derive(AssetCollection, Resource)]
struct HeadsAssets {
#[asset(path = "all.headsdb.ron")]
heads: Handle<HeadDatabaseAsset>,
}
#[derive(AssetCollection, Resource)]
pub struct UIAssets {
#[asset(path = "font.ttf")]
@@ -98,12 +107,26 @@ pub struct GameAssets {
pub struct LoadingPlugin;
impl Plugin for LoadingPlugin {
fn build(&self, app: &mut App) {
app.add_systems(OnExit(GameState::AssetLoading), on_exit);
app.add_loading_state(
LoadingState::new(GameState::AssetLoading)
.continue_to_state(GameState::MapLoading)
.load_collection::<AudioAssets>()
.load_collection::<GameAssets>()
.load_collection::<HeadsAssets>()
.load_collection::<UIAssets>(),
);
}
}
fn on_exit(
mut cmds: Commands,
res: Res<HeadsAssets>,
mut assets: ResMut<Assets<HeadDatabaseAsset>>,
) {
let asset = assets
.remove(res.heads.id())
.expect("headsdb failed to load");
cmds.insert_resource(HeadsDatabase { heads: asset.0 });
}

View File

@@ -11,6 +11,7 @@ mod cutscene;
mod debug;
mod gates;
mod head;
mod head_asset;
mod heads;
mod hitpoints;
mod keys;
@@ -33,6 +34,7 @@ use bevy::{
prelude::*,
render::view::ColorGrading,
};
use bevy_common_assets::ron::RonAssetPlugin;
use bevy_polyline::PolylinePlugin;
use bevy_sprite3d::Sprite3dPlugin;
use bevy_steamworks::{FriendFlags, SteamworksClient, SteamworksPlugin};
@@ -40,6 +42,7 @@ use bevy_trenchbroom::prelude::*;
use bevy_ui_gradients::UiGradientsPlugin;
use camera::MainCamera;
use control::controller::CharacterControllerPlugin;
use head_asset::HeadDatabaseAsset;
use loading_assets::AudioAssets;
use std::io::{Read, Write};
use utils::{billboards, sprite_3d_animation, squish_animation};
@@ -108,6 +111,7 @@ fn main() {
app.add_plugins(Sprite3dPlugin);
app.add_plugins(TrenchBroomPlugin(TrenchBroomConfig::new("hedz")));
app.add_plugins(UiGradientsPlugin);
app.add_plugins(RonAssetPlugin::<HeadDatabaseAsset>::new(&["headsdb.ron"]));
#[cfg(feature = "dbg")]
{

View File

@@ -2,10 +2,10 @@ use crate::{
GameState,
ai::Ai,
head::ActiveHead,
head_asset::HeadsDatabase,
heads::{HEAD_COUNT, HeadState},
hitpoints::{Hitpoints, Kill},
keys::KeySpawn,
player::head_id_to_str,
tb_entities::EnemySpawn,
};
use bevy::prelude::*;
@@ -19,18 +19,20 @@ pub fn plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::Playing), init);
}
fn init(mut commands: Commands, query: Query<(Entity, &EnemySpawn)>) {
fn init(mut commands: Commands, query: Query<(Entity, &EnemySpawn)>, heads_db: Res<HeadsDatabase>) {
//TODO: move into HeadsDatabase
let mut names: HashMap<String, usize> = HashMap::default();
for i in 0..HEAD_COUNT {
names.insert(head_id_to_str(i).to_string(), i);
names.insert(heads_db.head_key(i).to_string(), i);
}
for (e, spawn) in query.iter() {
let id = names[&spawn.head];
commands
.entity(e)
.insert((
Hitpoints::new(100),
Npc(HeadState::new(id, 10).with_ability(crate::abilities::HeadAbility::Thrown)),
Npc(HeadState::new(id, 10)),
ActiveHead(id),
Ai,
))

View File

@@ -9,6 +9,7 @@ use crate::{
},
global_observer,
head::ActiveHead,
head_asset::HeadsDatabase,
heads::HeadChanged,
hitpoints::Hitpoints,
loading_assets::GameAssets,
@@ -251,6 +252,7 @@ fn on_update_head(
asset_server: Res<AssetServer>,
head: Query<Entity, With<PlayerHeadMesh>>,
mut player_head: Query<&mut ActiveHead, With<Player>>,
head_db: Res<HeadsDatabase>,
) {
let Ok(head) = head.get_single() else {
return;
@@ -262,7 +264,7 @@ fn on_update_head(
player.0 = trigger.0;
let head_str = head_id_to_str(trigger.0);
let head_str = head_db.head_key(trigger.0);
commands.trigger(PlaySound::Head(head_str.to_string()));
@@ -271,27 +273,3 @@ fn on_update_head(
commands.entity(head).insert(SceneRoot(mesh));
}
pub fn head_id_to_str(head: usize) -> &'static str {
match head {
0 => "angry demonstrator",
1 => "carnival knife thrower",
2 => "chicago gangster",
3 => "commando",
4 => "field medic",
5 => "geisha",
6 => "goblin",
7 => "green grocer",
8 => "highland hammer thrower",
9 => "legionnaire",
10 => "mig pilot",
11 => "nanny",
12 => "panic attack",
13 => "salty sea dog",
14 => "snow plough operator",
15 => "soldier ant",
16 => "super market shopper",
17 => "troll",
_ => unimplemented!(),
}
}