diff --git a/Cargo.toml b/Cargo.toml index 1c6328b..537e519 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ authors = [""] [features] default = ["development", "debug_features"] -development = ["bevy/dynamic_linking", "bevy/file_watcher"] +development = ["bevy/dynamic_linking", "bevy/file_watcher", "bevy/embedded_watcher"] debug_features = ["bevy/free_camera"] [dependencies] diff --git a/src/debug_tools/save_state.rs b/src/debug_tools/save_state.rs index 27b1d1d..4845211 100644 --- a/src/debug_tools/save_state.rs +++ b/src/debug_tools/save_state.rs @@ -2,7 +2,7 @@ use bevy::prelude::*; use crate::{ camera::MatchCamera, fighter::{ - FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterPendingRespawn, FighterPreviousFacingDirection, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity, animation::FighterAnimationFrame, attack::{ + FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterInvulnerabilityState, FighterPendingRespawn, FighterPreviousFacingDirection, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity, animation::FighterAnimationFrame, attack::{ FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, FighterPendingHits, FighterSolvedHurtboxes, FighterStaleMoveQueue, }, baked_animation::FighterBoneMatrices, state::FighterState, @@ -47,6 +47,7 @@ struct FighterCheckpoint { bone_matrices: FighterBoneMatrices, surface: FighterSurfaceState, pending_respawn: FighterPendingRespawn, + invulnerability_state: FighterInvulnerabilityState, } pub(super) fn save_or_restore(world: &mut World) { @@ -149,6 +150,7 @@ pub(super) fn save_or_restore(world: &mut World) { entity.insert(saved.bone_matrices.clone()); entity.insert(saved.surface.clone()); entity.insert(saved.pending_respawn.clone()); + entity.insert(saved.invulnerability_state.clone()); } world.insert_resource(checkpoint.pending_hits.clone()); world.insert_resource(checkpoint.stage_collision); @@ -185,6 +187,7 @@ fn capture_fighter(world: &World, entity: Entity) -> Option { bone_matrices: fighter.get::()?.clone(), surface: fighter.get::()?.clone(), pending_respawn: fighter.get::()?.clone(), + invulnerability_state: fighter.get::()?.clone(), }) } diff --git a/src/fighter/attributes.rs b/src/fighter/attributes.rs index 1c6c8a3..229992a 100644 --- a/src/fighter/attributes.rs +++ b/src/fighter/attributes.rs @@ -44,6 +44,9 @@ pub struct FighterAttributes { pub air_dodge_duration: u32, pub air_dodge_velocity: FGi32, pub air_dodge_decay: FGi32, + + pub air_dodge_intangible_duration: u32, + pub air_dodge_intangible_start: u32, } impl FighterAttributes { diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index 5289d39..4cdb3e6 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -41,8 +41,8 @@ use crate::{ AttackKind, FighterAttackPlugin, FighterAttackScriptAssets, FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, FighterSolvedHurtboxes, FighterStaleMoveQueue, - }, baked_animation::{FighterBoneIndexMap, FighterBoneMatrices}, ecb::FighterECBBoneIndices, manifest::FighterManifest, state::{FighterState, air::AirborneStateCommon, fall::FallState}, visual::FighterAnimations, - }, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript, stage::line::StageCollision, + }, baked_animation::{FighterBoneIndexMap, FighterBoneMatrices}, ecb::FighterECBBoneIndices, manifest::FighterManifest, state::{FighterState, air::AirborneStateCommon, fall::FallState}, visual::{FighterAnimations, FighterInvulnerabilityVisualState}, + }, game_settings::GameSettings, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript, stage::line::StageCollision, }; use state::state_interrupt_system; @@ -85,6 +85,14 @@ pub struct FighterHitstop { pub remaining_frames: u32, } +#[derive(Component, Default, Copy, Clone, Hash, PartialEq, Eq)] +pub enum FighterInvulnerabilityState { + #[default] + Vulnerable, + Invulnerable, + Intangible +} + impl FighterFacingDirection { fn reverse(&self) -> Self { match self { @@ -140,7 +148,9 @@ pub struct FighterPendingRespawn(bool); FighterPendingRespawn, FighterHitstop, FighterKnockbackVelocity, - FighterTranslation + FighterTranslation, + FighterInvulnerabilityState, + FighterInvulnerabilityVisualState, )] pub struct Fighter { pub manifest: Handle, @@ -229,6 +239,8 @@ impl Plugin for FighterPlugin { .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() + .rollback_component_with_copy::() + .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_resource_with_reflect::() @@ -256,6 +268,9 @@ impl Plugin for FighterPlugin { .run_if(crate::debug_tools::attack_hitboxes_enabled) .run_if(in_state(AppState::InMatch)), ) + .add_systems(FixedPostUpdate, + visual::update_status_colors.run_if(resource_exists::) + ) .add_systems( FixedPostUpdate, debug::hurtbox_debug.run_if(crate::debug_tools::hurtboxes_enabled), diff --git a/src/fighter/state/air_dodge.rs b/src/fighter/state/air_dodge.rs index 42165df..5dcc2f7 100644 --- a/src/fighter/state/air_dodge.rs +++ b/src/fighter/state/air_dodge.rs @@ -1,16 +1,13 @@ use crate::{ fighter::{ - animation::AnimKind, - state::{ + FighterInvulnerabilityState, animation::AnimKind, state::{ CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, fall::FallState, land::LandingType, }, - }, - input, - math::vec::FGVec2, + }, input, math::vec::FGVec2, }; #[derive(Debug, Clone, Hash)] @@ -32,6 +29,12 @@ impl FighterStateImpl for AirDodgeState { } fn update(&mut self, state_context: &mut UpdateContext) { + let airdodge_intangible_end = state_context.fighter_attributes.air_dodge_intangible_start + state_context.fighter_attributes.air_dodge_intangible_duration; + if state_context.animation_frame == state_context.fighter_attributes.air_dodge_intangible_start { + *state_context.vulnerability = FighterInvulnerabilityState::Intangible; + } else if state_context.animation_frame > airdodge_intangible_end { + *state_context.vulnerability = FighterInvulnerabilityState::Vulnerable; + } state_context.velocity.0 *= state_context.fighter_attributes.air_dodge_decay; air::apply_air_motion( @@ -62,6 +65,14 @@ impl FighterStateImpl for AirDodgeState { fn surface_policy(&self) -> super::SurfacePolicy { super::SurfacePolicy::Airborne(crate::fighter::surface::EcbLockPolicy::StartOnDetach) } + + fn on_exit(&mut self, state_context: &mut super::ExitContext) { + *state_context.vulnerability = FighterInvulnerabilityState::Vulnerable; + } + + fn name(&self) -> String { + Self::NAME.to_owned() + } } pub fn check_input(state_context: &InterruptContext) -> Option { diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index 08bc6bf..adc9bda 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -18,10 +18,7 @@ use crate::fighter::state::hitstun::HitstunState; use crate::fighter::surface::EcbLockPolicy; use crate::fighter::visual::FighterAnimations; use crate::fighter::{ - Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, - FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, FighterPendingRespawn, - FighterPresentationSnap, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, - FighterVelocity, FighterDamage, + Fighter, FighterDamage, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, FighterInvulnerabilityState, FighterPendingRespawn, FighterPresentationSnap, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity, }; use crate::game_settings::GameSettings; use crate::input::FighterInput; @@ -173,6 +170,7 @@ impl EnterContext<'_> { pub struct ExitContext<'a> { pub hitboxes: &'a mut FighterHitboxes, pub surface: &'a mut FighterSurfaceState, + pub vulnerability: &'a mut FighterInvulnerabilityState, } pub struct UpdateContext<'a> { @@ -183,6 +181,7 @@ pub struct UpdateContext<'a> { pub velocity: &'a mut FighterVelocity, pub surface: &'a mut FighterSurfaceState, pub knockback_velocity: &'a mut FighterKnockbackVelocity, + pub vulnerability: &'a mut FighterInvulnerabilityState, pub game_settings: &'a GameSettings, pub animation_frame: u32, pub fighter_attributes: &'a FighterAttributes, @@ -264,6 +263,7 @@ pub struct FighterFrameQuery { pub prev_ecb: &'static FighterPreviousECB, pub facing_direction: &'static mut FighterFacingDirection, pub animation_frame: &'static mut FighterAnimationFrame, + pub vulnerability: &'static mut FighterInvulnerabilityState, pub fighter: &'static Fighter, } @@ -329,6 +329,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { ExitContext { hitboxes: &mut self.hitboxes, surface: &mut self.surface, + vulnerability: &mut self.vulnerability, } } @@ -349,6 +350,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { knockback_velocity: &mut self.knockback_velocity, game_settings, animation_frame: self.animation_frame.frame, + vulnerability: &mut self.vulnerability, fighter_attributes: attributes, attack_scripts, attack_script_assets, diff --git a/src/fighter/visual.rs b/src/fighter/visual.rs index 96f36fb..f2be2a4 100644 --- a/src/fighter/visual.rs +++ b/src/fighter/visual.rs @@ -1,4 +1,8 @@ -use crate::fighter::baked_animation::BakedFighterAnimations; +use crate::{ + fighter::{FighterInvulnerabilityState, baked_animation::BakedFighterAnimations}, + game_settings::GameSettings, + materials::fighter_material::{FighterMaterial, FighterMesh}, +}; use bevy::prelude::*; #[derive(Component, Clone)] @@ -6,3 +10,62 @@ pub struct FighterAnimations { /// The single animation source for gameplay and the visual skeleton. pub baked: Handle, } + +#[derive(Component, Clone, Default)] +pub struct FighterInvulnerabilityVisualState { + pub last_known_state: FighterInvulnerabilityState, + pub frames_in_state: u32, +} + +pub fn update_status_colors( + mut fighters: Query<( + &FighterInvulnerabilityState, + &mut FighterInvulnerabilityVisualState, + )>, + meshes: Query<(&FighterMesh, &MeshMaterial3d)>, + mut materials: ResMut>, + game_settings: Res, +) { + for (invuln_state, mut invuln_state_visual) in &mut fighters { + if *invuln_state != invuln_state_visual.last_known_state { + invuln_state_visual.last_known_state = *invuln_state; + invuln_state_visual.frames_in_state = 0; + } + } + for (mesh, mat) in meshes { + let (_, invuln_state_visual) = fighters + .get(mesh.fighter) + .expect("Material should have relevant fighter available"); + + let status_color = match invuln_state_visual.last_known_state { + FighterInvulnerabilityState::Vulnerable => bevy::color::palettes::css::WHITE.into(), + FighterInvulnerabilityState::Invulnerable => { + game_settings.fighter_common.intangible_color + } + FighterInvulnerabilityState::Intangible => { + game_settings.fighter_common.intangible_color + } + }; + + let intensity = match invuln_state_visual.last_known_state { + FighterInvulnerabilityState::Vulnerable => 0.0, + FighterInvulnerabilityState::Invulnerable | FighterInvulnerabilityState::Intangible => { + let cycle_frames = invuln_state_visual.frames_in_state + % game_settings.fighter_common.intangible_pulse_duration; + 1.0 - cycle_frames as f32 + / ((game_settings.fighter_common.intangible_pulse_duration) as f32) + } + }; + + let mut material = materials + .get_mut(mat.id()) + .expect("Material should exist here..."); + material + .extension + .update_status_color(status_color.into(), intensity); + } + + for (_, mut invuln_state_visual) in fighters { + invuln_state_visual.frames_in_state = invuln_state_visual.frames_in_state.saturating_add(1); + } +} diff --git a/src/game_settings.rs b/src/game_settings.rs index cc3254d..be52183 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -42,6 +42,9 @@ pub struct FighterSettingsCommon { pub leave_ground_ecb_frame_lock_duration: u32, pub platform_pass_platform_ignore_duration_frames: u32, pub platform_pass_initial_downwards_velocity: FGi32, + + pub intangible_color: Color, + pub intangible_pulse_duration: u32, } #[derive(Reflect, Serialize, Deserialize, Clone)] #[reflect(opaque)] diff --git a/src/lib.rs b/src/lib.rs index 4233226..eebc28d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,8 +35,9 @@ use ron_asset_manager::RonAssetPlugin; use crate::{ camera::MatchCameraPlugin, fighter::manifest::FighterManifest, game_settings::GameSettings, - match_runtime::MatchRuntimePlugin, menus::style::FGUiStyle, schedule::GameplaySchedulePlugin, - stage::manifest::StageManifest, ui::InGameUIPlugin, + match_runtime::MatchRuntimePlugin, materials::ShinesparkMaterialPlugin, + menus::style::FGUiStyle, schedule::GameplaySchedulePlugin, stage::manifest::StageManifest, + ui::InGameUIPlugin, }; mod args; @@ -49,6 +50,7 @@ pub mod game_settings; pub mod input; pub mod match_loading; pub mod match_runtime; +pub mod materials; mod math; pub mod menus; pub mod netcode; @@ -109,22 +111,25 @@ impl Plugin for GamePlugin { // game-level content and configuration. app.init_state::() .add_plugins(( - SettingsPlugin::new("moe.eirteam.shinespark.debug_settings"), - // Brings up GGRS (and with it `GgrsSchedule`), so it goes first. - netcode::FighterNetcodePlugin::default(), - GameplaySchedulePlugin, - MatchRuntimePlugin, - input::FighterInputPlugin, - replay::ReplayPlugin, - debug_tools::DebugToolsPlugin, - filled_gizmos::FilledGizmosPlugin, - fighter::FighterPlugin, - fighter::baked_animation::BakedAnimationPlugin, - scripting::importer::FighterScriptImportPlugin, - stage::StagePlugin, - MatchCameraPlugin, - GameAssetPlugin, - InGameUIPlugin, + ( + SettingsPlugin::new("moe.eirteam.shinespark.debug_settings"), + // Brings up GGRS (and with it `GgrsSchedule`), so it goes first. + netcode::FighterNetcodePlugin::default(), + GameplaySchedulePlugin, + MatchRuntimePlugin, + input::FighterInputPlugin, + replay::ReplayPlugin, + debug_tools::DebugToolsPlugin, + filled_gizmos::FilledGizmosPlugin, + fighter::FighterPlugin, + fighter::baked_animation::BakedAnimationPlugin, + scripting::importer::FighterScriptImportPlugin, + stage::StagePlugin, + MatchCameraPlugin, + GameAssetPlugin, + InGameUIPlugin, + ), + ShinesparkMaterialPlugin, )) .add_plugins(menus::MenuPlugin) .add_loading_state( diff --git a/src/match_loading.rs b/src/match_loading.rs index 767a881..1a6ed08 100644 --- a/src/match_loading.rs +++ b/src/match_loading.rs @@ -8,9 +8,7 @@ use bevy_ggrs::prelude::Session; use serde::{Deserialize, Serialize}; use crate::{ - AppState, - args::Args, - fighter::{ + AppState, args::Args, fighter::{ FighterId, FighterRoster, animation::AnimKind, attack::{AttackKind, FighterAttackScriptAssets}, @@ -18,15 +16,10 @@ use crate::{ manifest::{FighterManifest, FighterManifestRegistry}, spawn_fighter, visual::FighterAnimations, - }, - input::{LocalInputAssignments, LocalInputSource, MatchInputAssignments}, - match_runtime::MatchState, - netcode::{ + }, input::{LocalInputAssignments, LocalInputSource, MatchInputAssignments}, match_runtime::MatchState, materials::fighter_material::FighterMaterial, netcode::{ GGRSCfg, session::{ConnectionStatus, SessionConfig, create_session}, - }, - scripting::FighterAttackScript, - stage::{ + }, scripting::FighterAttackScript, stage::{ self, manifest::{StageId, StageManifest, StageManifestRegistry}, }, diff --git a/src/materials/fighter_material.rs b/src/materials/fighter_material.rs new file mode 100644 index 0000000..2b6497a --- /dev/null +++ b/src/materials/fighter_material.rs @@ -0,0 +1,88 @@ +use bevy::{gltf::GltfMaterialName, pbr::{ExtendedMaterial, MaterialExtension}, prelude::*, render::render_resource::AsBindGroup, shader::ShaderRef, world_serialization::WorldInstanceReady}; + +pub type FighterMaterial = ExtendedMaterial; + +#[derive(Component)] +pub struct FighterMesh { + pub fighter: Entity +} + +#[derive(Asset, AsBindGroup, Reflect, Debug, Clone, Default)] +pub struct FighterMaterialExtension { + #[uniform(100)] + status_color: LinearRgba, + #[uniform(100)] + status_color_intensity: f32, + + // Web examples WebGL2 support: structs must be 16 byte aligned. + #[cfg(feature = "webgl2")] + #[uniform(100)] + _webgl2_padding_8b: u32, + #[cfg(feature = "webgl2")] + #[uniform(100)] + _webgl2_padding_12b: u32, + #[cfg(feature = "webgl2")] + #[uniform(100)] + _webgl2_padding_16b: u32, +} + +const SHADER_ASSET_PATH: &str = "embedded://shinespark/shaders/fighter.wgsl"; + +impl FighterMaterialExtension { + pub fn new(status_color: Color) -> Self { + Self { + status_color: status_color.into(), + status_color_intensity: 0.0, + ..default() + } + } + + pub fn update_status_color(&mut self, status_color: LinearRgba, intensity: f32) { + self.status_color = status_color; + self.status_color_intensity = intensity; + } +} + +impl MaterialExtension for FighterMaterialExtension { + fn fragment_shader() -> ShaderRef { + SHADER_ASSET_PATH.into() + } + + fn deferred_fragment_shader() -> ShaderRef { + SHADER_ASSET_PATH.into() + } +} + +pub fn bind_fighter_material( + scene_ready: On, + mut commands: Commands, + children: Query<&Children>, + //color_override: Query<&ColorOverride>, + mesh_materials: Query<(Entity, &MeshMaterial3d, &GltfMaterialName)>, + mut asset_materials: ResMut>, + original_materials: Res>, +) { + for descendant in children.iter_descendants(scene_ready.entity) { + let Ok((entity, id, material_name)) = mesh_materials.get(descendant) else { + continue; + }; + + dbg!(material_name); + + if !material_name.starts_with("Fighter_") { + continue; + } + + let Some(original_material) = original_materials.get(id.id()) else { + continue; + }; + + commands.entity(entity).insert((MeshMaterial3d(asset_materials.add(ExtendedMaterial { + base: StandardMaterial { + // can be used in forward or deferred mode + ..original_material.clone() + }, + extension: FighterMaterialExtension::new(bevy::color::palettes::css::BLUE.into()) + })), FighterMesh { fighter: scene_ready.entity })); + } +} \ No newline at end of file diff --git a/src/materials/mod.rs b/src/materials/mod.rs new file mode 100644 index 0000000..1415824 --- /dev/null +++ b/src/materials/mod.rs @@ -0,0 +1,32 @@ +use bevy::{ + asset::embedded_asset, gltf::GltfMaterialName, material::OpaqueRendererMethod, pbr::ExtendedMaterial, prelude::*, world_serialization::WorldInstanceReady, +}; + +use bevy::{ + color::palettes::basic::RED, + pbr::{MaterialExtension}, + prelude::*, + render::render_resource::*, + shader::ShaderRef, +}; + +use crate::materials::fighter_material::{FighterMaterial, FighterMaterialExtension}; + +pub mod fighter_material; + +pub struct ShinesparkMaterialPlugin; + +pub fn startup_test(asset_server: Res) { + let test: Handle = asset_server.load("embedded://shinespark/shaders/fighter.wgsl"); + dbg!(test); +} + +impl Plugin for ShinesparkMaterialPlugin { + fn build(&self, app: &mut App) { + let omit_prefix = ""; + embedded_asset!(app, omit_prefix, "shaders/fighter.wgsl"); + app.add_systems(Startup, startup_test) + .add_observer(fighter_material::bind_fighter_material) + .add_plugins(MaterialPlugin::::default()); + } +} diff --git a/src/materials/shaders/fighter.wgsl b/src/materials/shaders/fighter.wgsl new file mode 100644 index 0000000..c9c59d1 --- /dev/null +++ b/src/materials/shaders/fighter.wgsl @@ -0,0 +1,67 @@ +#import bevy_pbr::{ + pbr_fragment::pbr_input_from_standard_material, + pbr_functions::alpha_discard, +} + +#ifdef PREPASS_PIPELINE +#import bevy_pbr::{ + prepass_io::{VertexOutput, FragmentOutput}, + pbr_deferred_functions::deferred_output, +} +#else +#import bevy_pbr::{ + forward_io::{VertexOutput, FragmentOutput}, + pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing}, +} +#endif + +struct FighterExtendedMaterial { + status_color: vec4, + status_color_intensity: f32, +#ifdef SIXTEEN_BYTE_ALIGNMENT + // Web examples WebGL2 support: structs must be 16 byte aligned. + _webgl2_padding_8b: u32, + _webgl2_padding_12b: u32, + _webgl2_padding_16b: u32, +#endif +} + +@group(#{MATERIAL_BIND_GROUP}) @binding(100) +var fighter_material: FighterExtendedMaterial; + +@fragment +fn fragment( + in: VertexOutput, + @builtin(front_facing) is_front: bool, +) -> FragmentOutput { + // generate a PbrInput struct from the StandardMaterial bindings + var pbr_input = pbr_input_from_standard_material(in, is_front); + + // we can optionally modify the input before lighting and alpha_discard is applied + //pbr_input.material.base_color.b = pbr_input.material.base_color.r; + + // alpha discard + pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color); + +#ifdef PREPASS_PIPELINE + // in deferred mode we can't modify anything after that, as lighting is run in a separate fullscreen shader. + let out = deferred_output(in, pbr_input); +#else + var out: FragmentOutput; + // apply lighting + out.color = apply_pbr_lighting(pbr_input); + + // we can optionally modify the lit color before post-processing is applied + var quantize_steps: i32 = 2; + out.color = vec4(vec4(out.color * f32(quantize_steps))) / f32(quantize_steps); + + // apply in-shader post processing (fog, alpha-premultiply, and also tonemapping, debanding if the camera is non-hdr) + // note this does not include fullscreen postprocessing effects like bloom. + out.color = main_pass_post_lighting_processing(pbr_input, out.color); + + out.color = mix(out.color, fighter_material.status_color, fighter_material.status_color_intensity * 0.5); + +#endif + + return out; +} \ No newline at end of file