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

16
Cargo.lock generated
View File

@@ -679,6 +679,19 @@ dependencies = [
"wgpu-types", "wgpu-types",
] ]
[[package]]
name = "bevy_common_assets"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3521990269672c442f2bf0fbed0fce9db719e3dd136dd4012a97809464a4389d"
dependencies = [
"anyhow",
"bevy",
"ron",
"serde",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "bevy_core" name = "bevy_core"
version = "0.15.3" version = "0.15.3"
@@ -2971,12 +2984,15 @@ dependencies = [
"bevy-ui-gradients", "bevy-ui-gradients",
"bevy_asset_loader", "bevy_asset_loader",
"bevy_ballistic", "bevy_ballistic",
"bevy_common_assets",
"bevy_debug_log", "bevy_debug_log",
"bevy_polyline", "bevy_polyline",
"bevy_sprite3d", "bevy_sprite3d",
"bevy_trenchbroom", "bevy_trenchbroom",
"nil", "nil",
"rand", "rand",
"ron",
"serde",
] ]
[[package]] [[package]]

View File

@@ -36,6 +36,9 @@ bevy-steamworks = "0.13.0"
bevy_ballistic = "0.1.0" bevy_ballistic = "0.1.0"
bevy-ui-gradients = "0.2.0" bevy-ui-gradients = "0.2.0"
bevy_debug_log = "0.5.0" bevy_debug_log = "0.5.0"
bevy_common_assets = { version = "0.12.0", features = ["ron"] }
serde = { version = "1.0.219", features = ["derive"] }
ron = "0.8"
[lints.clippy] [lints.clippy]
too_many_arguments = "allow" too_many_arguments = "allow"

20
assets/all.headsdb.ron Normal file
View File

@@ -0,0 +1,20 @@
([
(key:"angry demonstrator", ability:Thrown, range:80),
(key:"carnival knife thrower", range:60),
(key:"chicago gangster", ability:Gun, range:60),
(key:"commando", ability:Gun, range:60),
(key:"field medic"),
(key:"geisha"),
(key:"goblin", ability:Arrow, range:60),
(key:"green grocer", range:60),
(key:"highland hammer thrower", ability:Thrown, range:80),
(key:"legionnaire", ability:Gun, range:60),
(key:"mig pilot", ability:Gun, range:60),
(key:"nanny", ability:Thrown, range:60),
(key:"panic attack"),
(key:"salty sea dog"),
(key:"snow plough operator"),
(key:"soldier ant"),
(key:"super market shopper", ability:Thrown, range:80),
(key:"troll", ability:Thrown, range:80),
])

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ mod target_ui;
use crate::{ use crate::{
GameState, GameState,
head::ActiveHead, head::ActiveHead,
head_asset::HeadsDatabase,
physics_layers::GameLayer, physics_layers::GameLayer,
player::{Player, PlayerBodyMesh}, player::{Player, PlayerBodyMesh},
tb_entities::EnemySpawn, 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() { for (head, mut state) in query.iter_mut() {
// info!("head changed: {}", head.0); // info!("head changed: {}", head.0);
// state.max_angle = if head.0 == 0 { PI / 8. } else { PI / 2. } // state.max_angle = if head.0 == 0 { PI / 8. } else { PI / 2. }
state.range = match head.0 { let stats = heads_db.head_stats(head.0);
0 => 80., state.range = stats.range;
3 => 60.,
_ => 40.,
};
} }
} }

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::{ use crate::{
GameState, GameState,
abilities::HeadAbility,
backpack::{BackbackSwapEvent, Backpack}, backpack::{BackbackSwapEvent, Backpack},
global_observer, global_observer,
head_asset::HeadsDatabase,
hitpoints::Hitpoints, hitpoints::Hitpoints,
player::{Player, head_id_to_str}, player::Player,
sounds::PlaySound, sounds::PlaySound,
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -22,7 +22,6 @@ pub struct HeadsImages {
#[derive(Clone, Copy, Debug, PartialEq, Reflect)] #[derive(Clone, Copy, Debug, PartialEq, Reflect)]
pub struct HeadState { pub struct HeadState {
pub head: usize, pub head: usize,
pub ability: HeadAbility,
pub health: u32, pub health: u32,
pub health_max: u32, pub health_max: u32,
pub ammo: u32, pub ammo: u32,
@@ -39,17 +38,11 @@ impl HeadState {
health_max: 100, health_max: 100,
ammo, ammo,
ammo_max: ammo, ammo_max: ammo,
ability: HeadAbility::None,
reload_duration: 5., reload_duration: 5.,
last_use: 0., last_use: 0.,
} }
} }
pub fn with_ability(mut self, ability: HeadAbility) -> Self {
self.ability = ability;
self
}
pub fn has_ammo(&self) -> bool { pub fn has_ammo(&self) -> bool {
self.ammo > 0 self.ammo > 0
} }
@@ -119,11 +112,11 @@ pub fn plugin(app: &mut App) {
app.insert_resource(ActiveHeads { app.insert_resource(ActiveHeads {
heads: [ heads: [
Some(HeadState::new(0, 10).with_ability(HeadAbility::Thrown)), Some(HeadState::new(0, 10)),
Some(HeadState::new(3, 10).with_ability(HeadAbility::Gun)), Some(HeadState::new(3, 10)),
Some(HeadState::new(6, 10).with_ability(HeadAbility::Arrow)), Some(HeadState::new(6, 10)),
Some(HeadState::new(8, 10).with_ability(HeadAbility::Thrown)), Some(HeadState::new(8, 10)),
Some(HeadState::new(9, 10).with_ability(HeadAbility::Gun)), Some(HeadState::new(9, 10)),
], ],
current_slot: 0, current_slot: 0,
}); });
@@ -138,10 +131,10 @@ pub fn plugin(app: &mut App) {
global_observer!(app, on_swap_backpack); 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 // TODO: load via asset loader
let heads = (0usize..HEAD_COUNT) 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(); .collect();
commands.insert_resource(HeadsImages { heads }); 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::{prelude::*, utils::HashMap};
use bevy_asset_loader::prelude::*; use bevy_asset_loader::prelude::*;
@@ -43,6 +46,12 @@ pub struct AudioAssets {
pub head: HashMap<AssetFileName, Handle<AudioSource>>, pub head: HashMap<AssetFileName, Handle<AudioSource>>,
} }
#[derive(AssetCollection, Resource)]
struct HeadsAssets {
#[asset(path = "all.headsdb.ron")]
heads: Handle<HeadDatabaseAsset>,
}
#[derive(AssetCollection, Resource)] #[derive(AssetCollection, Resource)]
pub struct UIAssets { pub struct UIAssets {
#[asset(path = "font.ttf")] #[asset(path = "font.ttf")]
@@ -98,12 +107,26 @@ pub struct GameAssets {
pub struct LoadingPlugin; pub struct LoadingPlugin;
impl Plugin for LoadingPlugin { impl Plugin for LoadingPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(OnExit(GameState::AssetLoading), on_exit);
app.add_loading_state( app.add_loading_state(
LoadingState::new(GameState::AssetLoading) LoadingState::new(GameState::AssetLoading)
.continue_to_state(GameState::MapLoading) .continue_to_state(GameState::MapLoading)
.load_collection::<AudioAssets>() .load_collection::<AudioAssets>()
.load_collection::<GameAssets>() .load_collection::<GameAssets>()
.load_collection::<HeadsAssets>()
.load_collection::<UIAssets>(), .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 debug;
mod gates; mod gates;
mod head; mod head;
mod head_asset;
mod heads; mod heads;
mod hitpoints; mod hitpoints;
mod keys; mod keys;
@@ -33,6 +34,7 @@ use bevy::{
prelude::*, prelude::*,
render::view::ColorGrading, render::view::ColorGrading,
}; };
use bevy_common_assets::ron::RonAssetPlugin;
use bevy_polyline::PolylinePlugin; use bevy_polyline::PolylinePlugin;
use bevy_sprite3d::Sprite3dPlugin; use bevy_sprite3d::Sprite3dPlugin;
use bevy_steamworks::{FriendFlags, SteamworksClient, SteamworksPlugin}; use bevy_steamworks::{FriendFlags, SteamworksClient, SteamworksPlugin};
@@ -40,6 +42,7 @@ use bevy_trenchbroom::prelude::*;
use bevy_ui_gradients::UiGradientsPlugin; use bevy_ui_gradients::UiGradientsPlugin;
use camera::MainCamera; use camera::MainCamera;
use control::controller::CharacterControllerPlugin; use control::controller::CharacterControllerPlugin;
use head_asset::HeadDatabaseAsset;
use loading_assets::AudioAssets; use loading_assets::AudioAssets;
use std::io::{Read, Write}; use std::io::{Read, Write};
use utils::{billboards, sprite_3d_animation, squish_animation}; use utils::{billboards, sprite_3d_animation, squish_animation};
@@ -108,6 +111,7 @@ fn main() {
app.add_plugins(Sprite3dPlugin); app.add_plugins(Sprite3dPlugin);
app.add_plugins(TrenchBroomPlugin(TrenchBroomConfig::new("hedz"))); app.add_plugins(TrenchBroomPlugin(TrenchBroomConfig::new("hedz")));
app.add_plugins(UiGradientsPlugin); app.add_plugins(UiGradientsPlugin);
app.add_plugins(RonAssetPlugin::<HeadDatabaseAsset>::new(&["headsdb.ron"]));
#[cfg(feature = "dbg")] #[cfg(feature = "dbg")]
{ {

View File

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

View File

@@ -9,6 +9,7 @@ use crate::{
}, },
global_observer, global_observer,
head::ActiveHead, head::ActiveHead,
head_asset::HeadsDatabase,
heads::HeadChanged, heads::HeadChanged,
hitpoints::Hitpoints, hitpoints::Hitpoints,
loading_assets::GameAssets, loading_assets::GameAssets,
@@ -251,6 +252,7 @@ fn on_update_head(
asset_server: Res<AssetServer>, asset_server: Res<AssetServer>,
head: Query<Entity, With<PlayerHeadMesh>>, head: Query<Entity, With<PlayerHeadMesh>>,
mut player_head: Query<&mut ActiveHead, With<Player>>, mut player_head: Query<&mut ActiveHead, With<Player>>,
head_db: Res<HeadsDatabase>,
) { ) {
let Ok(head) = head.get_single() else { let Ok(head) = head.get_single() else {
return; return;
@@ -262,7 +264,7 @@ fn on_update_head(
player.0 = trigger.0; 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())); commands.trigger(PlaySound::Head(head_str.to_string()));
@@ -271,27 +273,3 @@ fn on_update_head(
commands.entity(head).insert(SceneRoot(mesh)); 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!(),
}
}