use crate::config;
use crate::core::basic_components::Direction;
use crate::core::character_asset::{AnimationConfigAsset, CharacterAsset};
use crate::core::input::Action;
use anyhow::{Context, Result, anyhow};
use bevy::math::Vec2;
use bevy::prelude::Resource;
use serde::Deserialize;
use std::fs;
#[derive(Resource, Debug, Clone)]
pub struct PlayerBehavior {
pub spawn_position: Vec2,
pub initial_facing: Direction,
pub sprite_source: String,
pub initial_state: String,
pub initial_clip: String,
pub collider_size: Vec2,
pub collider_offset: Vec2,
pub base_speed: f32,
pub animation_config_path: String,
pub run_action: Option<Action>,
pub run_speed_multiplier: f32,
pub invincibility: InvincibilityConfig,
}
impl PlayerBehavior {
pub fn load() -> Result<Self> {
let global_config = config::load_config();
let behavior_path = &global_config.game.player_behavior_path;
if behavior_path.is_empty() {
return Err(anyhow!("Player behavior path is empty"));
}
let config_data = PlayerBehaviorFile::read(behavior_path)?;
let character_asset: CharacterAsset = load_ron_asset(&config_data.character_asset)
.with_context(|| {
format!(
"Failed to read character asset {} referenced by player behavior",
config_data.character_asset
)
})?;
let animation_config: AnimationConfigAsset =
load_ron_asset(&character_asset.animation_config).with_context(|| {
format!(
"Failed to read animation config {} referenced by character asset",
character_asset.animation_config
)
})?;
let sprite_source = animation_config.sprite_source.clone();
let initial_state = config_data.initial_state.clone();
let clip_name = resolve_initial_clip(
&initial_state,
&config_data.initial_facing,
&animation_config,
)?;
let (run_action, run_speed_multiplier) = if let Some(run) = config_data.run.clone() {
(Some(run.action), run.speed_multiplier)
} else {
(None, default_run_speed_multiplier())
};
let invincibility = config_data
.invincibility
.map(InvincibilityConfig::from)
.unwrap_or_default();
Ok(Self {
spawn_position: config_data.spawn_position.into_vec2(),
initial_facing: config_data.initial_facing,
sprite_source,
initial_state,
initial_clip: clip_name.to_string(),
collider_size: character_asset.collider_size,
collider_offset: character_asset.collider_offset,
base_speed: character_asset.base_speed,
animation_config_path: character_asset.animation_config,
run_action,
run_speed_multiplier,
invincibility,
})
}
}
fn resolve_initial_clip<'a>(
state_name: &str,
facing: &Direction,
animation_config: &'a AnimationConfigAsset,
) -> Result<&'a str> {
let mapping = animation_config
.states
.get(state_name)
.or_else(|| animation_config.states.values().next())
.ok_or_else(|| anyhow!("Animation config does not contain any states"))?;
Ok(mapping.get_clip_name(facing))
}
fn load_ron_asset<T>(relative_path: &str) -> Result<T>
where
T: for<'de> Deserialize<'de>,
{
let contents = read_prioritized_file(relative_path)?;
ron::de::from_str(&contents)
.with_context(|| format!("Failed to parse RON file: {}", relative_path))
}
fn read_prioritized_file(relative_path: &str) -> Result<String> {
if let Some(path) = config::resolve_path(relative_path) {
return fs::read_to_string(&path)
.with_context(|| format!("Failed to read file {}", path.display()));
}
Err(anyhow!(
"Unable to locate asset {} in project or fallback directories",
relative_path
))
}
#[derive(Debug, Clone, Deserialize)]
struct PlayerBehaviorFile {
character_asset: String,
#[serde(default = "default_spawn_position")]
spawn_position: Vec2Config,
#[serde(default)]
initial_facing: Direction,
#[serde(default = "default_initial_state")]
initial_state: String,
#[serde(default)]
run: Option<RunConfig>,
#[serde(default)]
invincibility: Option<InvincibilityFileConfig>,
}
impl PlayerBehaviorFile {
fn read(path: &str) -> Result<Self> {
let contents = read_prioritized_file(path)
.with_context(|| format!("Failed to read player behavior config at {}", path))?;
ron::de::from_str(&contents)
.with_context(|| format!("Failed to parse player behavior config at {}", path))
}
}
#[derive(Debug, Clone, Deserialize)]
struct RunConfig {
action: Action,
#[serde(default = "default_run_speed_multiplier")]
speed_multiplier: f32,
}
fn default_initial_state() -> String {
"Idle".to_string()
}
#[derive(Debug, Clone, Deserialize)]
struct Vec2Config {
x: f32,
y: f32,
}
impl Vec2Config {
fn into_vec2(self) -> Vec2 {
Vec2::new(self.x, self.y)
}
}
fn default_spawn_position() -> Vec2Config {
Vec2Config { x: 0.0, y: 0.0 }
}
fn default_run_speed_multiplier() -> f32 {
2.0
}
#[derive(Debug, Clone, Deserialize)]
struct InvincibilityFileConfig {
#[serde(default = "default_invincibility_duration")]
duration: f32,
#[serde(default = "default_flash_interval")]
flash_interval: f32,
#[serde(default = "default_normal_color")]
normal_color: String,
#[serde(default = "default_flash_color")]
flash_color: String,
}
impl Default for InvincibilityFileConfig {
fn default() -> Self {
Self {
duration: default_invincibility_duration(),
flash_interval: default_flash_interval(),
normal_color: default_normal_color(),
flash_color: default_flash_color(),
}
}
}
fn default_invincibility_duration() -> f32 {
1.0
}
fn default_flash_interval() -> f32 {
0.1
}
fn default_normal_color() -> String {
"#FF0000".to_string()
}
fn default_flash_color() -> String {
"#800000".to_string()
}
#[derive(Debug, Clone)]
pub struct InvincibilityConfig {
pub duration: f32,
pub flash_interval: f32,
pub normal_color: String,
pub flash_color: String,
}
impl Default for InvincibilityConfig {
fn default() -> Self {
Self {
duration: default_invincibility_duration(),
flash_interval: default_flash_interval(),
normal_color: default_normal_color(),
flash_color: default_flash_color(),
}
}
}
impl From<InvincibilityFileConfig> for InvincibilityConfig {
fn from(file_config: InvincibilityFileConfig) -> Self {
Self {
duration: file_config.duration,
flash_interval: file_config.flash_interval,
normal_color: file_config.normal_color,
flash_color: file_config.flash_color,
}
}
}