Compare commits

...
3 Commits
Author SHA1 Message Date
EIREXE c0c88d5dbd Kill warnings
CI / production-build (pull_request) Successful in 15m36s
CI / build-and-test (pull_request) Successful in 7m46s
2026-09-27 21:31:21 +02:00
EIREXE a83caf9153 Fix airdodge duration & tests 2026-09-27 21:30:44 +02:00
EIREXE d1782afe51 Add pulsing effect when invulnerable/intangible, make airdodge make the player intangible.
CI / production-build (pull_request) Failing after 4m48s
CI / build-and-test (pull_request) Failing after 5m3s
2026-09-27 20:34:21 +02:00
14 changed files with 311 additions and 43 deletions
+1 -1
View File
@@ -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]
+4 -1
View File
@@ -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<FighterCheckpoint> {
bone_matrices: fighter.get::<FighterBoneMatrices>()?.clone(),
surface: fighter.get::<FighterSurfaceState>()?.clone(),
pending_respawn: fighter.get::<FighterPendingRespawn>()?.clone(),
invulnerability_state: fighter.get::<FighterInvulnerabilityState>()?.clone(),
})
}
+3
View File
@@ -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 {
+18 -3
View File
@@ -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<FighterManifest>,
@@ -229,6 +239,8 @@ impl Plugin for FighterPlugin {
.checksum_component_with_hash::<FighterPendingRespawn>()
.rollback_component_with_copy::<FighterHitstop>()
.checksum_component_with_hash::<FighterHitstop>()
.rollback_component_with_copy::<FighterInvulnerabilityState>()
.checksum_component_with_hash::<FighterInvulnerabilityState>()
.rollback_component_with_copy::<FighterKnockbackVelocity>()
.checksum_component_with_hash::<FighterKnockbackVelocity>()
.rollback_resource_with_reflect::<StageCollision>()
@@ -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::<GameSettings>)
)
.add_systems(
FixedPostUpdate,
debug::hurtbox_debug.run_if(crate::debug_tools::hurtboxes_enabled),
+2
View File
@@ -168,6 +168,8 @@ mod tests {
air_dodge_duration: 0,
air_dodge_velocity: FGi32::ZERO,
air_dodge_decay: FGi32::ZERO,
air_dodge_intangible_duration: 29,
air_dodge_intangible_start: 4,
}
}
+16 -5
View File
@@ -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<FighterState> {
+6 -4
View File
@@ -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,
+64 -1
View File
@@ -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<BakedFighterAnimations>,
}
#[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<FighterMaterial>)>,
mut materials: ResMut<Assets<FighterMaterial>>,
game_settings: Res<GameSettings>,
) {
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);
}
}
+3
View File
@@ -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)]
+23 -18
View File
@@ -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::<AppState>()
.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(
+3 -10
View File
@@ -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, netcode::{
GGRSCfg,
session::{ConnectionStatus, SessionConfig, create_session},
},
scripting::FighterAttackScript,
stage::{
}, scripting::FighterAttackScript, stage::{
self,
manifest::{StageId, StageManifest, StageManifestRegistry},
},
+77
View File
@@ -0,0 +1,77 @@
use bevy::{gltf::GltfMaterialName, pbr::{ExtendedMaterial, MaterialExtension}, prelude::*, render::render_resource::AsBindGroup, shader::ShaderRef, world_serialization::WorldInstanceReady};
pub type FighterMaterial = ExtendedMaterial<StandardMaterial, FighterMaterialExtension>;
#[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,
}
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<WorldInstanceReady>,
mut commands: Commands,
children: Query<&Children>,
//color_override: Query<&ColorOverride>,
mesh_materials: Query<(Entity, &MeshMaterial3d<StandardMaterial>, &GltfMaterialName)>,
mut asset_materials: ResMut<Assets<FighterMaterial>>,
original_materials: Res<Assets<StandardMaterial>>,
) {
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 }));
}
}
+24
View File
@@ -0,0 +1,24 @@
use bevy::{
asset::embedded_asset, prelude::*,
};
pub mod fighter_material;
pub struct ShinesparkMaterialPlugin;
pub fn startup_test(asset_server: Res<AssetServer>) {
let test: Handle<Shader> = 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::<fighter_material::FighterMaterial>::default());
}
}
+67
View File
@@ -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<f32>,
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<uniform> 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<f32>(vec4<u32>(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;
}