Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2d00ed7bb | ||
|
|
b9611c1b42 | ||
|
|
edf2927d4d |
@@ -98,4 +98,5 @@ pub enum AnimKind {
|
|||||||
AttackUpTilt,
|
AttackUpTilt,
|
||||||
AttackForwardTilt,
|
AttackForwardTilt,
|
||||||
AttackDownTilt,
|
AttackDownTilt,
|
||||||
|
AttackNeutralAir,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ pub enum AttackKind {
|
|||||||
impl AttackKind {
|
impl AttackKind {
|
||||||
pub fn get_animation(&self) -> AnimKind {
|
pub fn get_animation(&self) -> AnimKind {
|
||||||
match self {
|
match self {
|
||||||
AttackKind::NeutralAir => AnimKind::AttackJab1,
|
AttackKind::NeutralAir => AnimKind::AttackNeutralAir,
|
||||||
AttackKind::Jab => AnimKind::AttackJab1,
|
AttackKind::Jab => AnimKind::AttackJab1,
|
||||||
AttackKind::UpTilt => AnimKind::AttackUpTilt,
|
AttackKind::UpTilt => AnimKind::AttackUpTilt,
|
||||||
AttackKind::ForwardAir => AnimKind::AttackUpTilt,
|
AttackKind::ForwardAir => AnimKind::AttackUpTilt,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
use crate::fighter::state::jump::{JumpState, JumpType};
|
||||||
use crate::fighter::state::land::{LandingState, LandingType};
|
use crate::fighter::state::land::{LandingState, LandingType};
|
||||||
use crate::fighter::state::{CollisionContext, FighterState};
|
use crate::fighter::state::{CollisionContext, FighterState, InterruptContext, air_attack, air_dodge};
|
||||||
use crate::fighter::*;
|
use crate::fighter::*;
|
||||||
use crate::input::FighterCommands;
|
use crate::input::FighterCommands;
|
||||||
|
|
||||||
@@ -112,6 +113,17 @@ pub fn update_standard_motion(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn air_interrupts_common(ctx: &mut InterruptContext, airborne_state_common: &AirborneStateCommon) -> Option<FighterState> {
|
||||||
|
if ctx.input.has_command(FighterCommands::Jump) {
|
||||||
|
Some(FighterState::Jump(JumpState {
|
||||||
|
jump_type: JumpType::DoubleJump,
|
||||||
|
airborne_state_common: AirborneStateCommon::default(),
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
air_dodge::check_input(ctx).or_else(|| air_attack::check_interrupt(ctx, airborne_state_common))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn collision_interrupt(
|
pub fn collision_interrupt(
|
||||||
ctx: &mut CollisionContext,
|
ctx: &mut CollisionContext,
|
||||||
landing_type: LandingType,
|
landing_type: LandingType,
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
use bevy::log::error;
|
||||||
|
|
||||||
|
use crate::fighter::{
|
||||||
|
FighterId, attack::{AttackKind, FighterAttackScriptAssets, update_active_hitbox_list}, state::{
|
||||||
|
CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, fall::FallState, wait::WaitState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Hash)]
|
||||||
|
pub struct AirAttackState {
|
||||||
|
pub attack_kind: AttackKind,
|
||||||
|
pub airborne_state_common: AirborneStateCommon
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FighterStateImpl for AirAttackState {
|
||||||
|
const NAME: &'static str = "AirAttack";
|
||||||
|
|
||||||
|
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
||||||
|
if state_context.is_current_animation_finished() {
|
||||||
|
Some(FighterState::Fall(FallState::new(self.airborne_state_common)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, state_context: &mut UpdateContext) {
|
||||||
|
let animation_frame = state_context.get_current_animation_frame();
|
||||||
|
let script = state_context
|
||||||
|
.attack_scripts
|
||||||
|
.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
||||||
|
if let Some(script) = script {
|
||||||
|
update_active_hitbox_list(
|
||||||
|
script,
|
||||||
|
&mut state_context
|
||||||
|
.hitboxes
|
||||||
|
.0
|
||||||
|
.as_mut()
|
||||||
|
.expect("ground attack must have active hitboxes")
|
||||||
|
.active_hitboxes,
|
||||||
|
animation_frame,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
air::update_standard_motion(state_context, &mut self.airborne_state_common);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self, state_context: &mut EnterContext) {
|
||||||
|
state_context.play_animation(self.attack_kind.get_animation(), false);
|
||||||
|
let script = state_context.attack_script_assets.scripts[&self.attack_kind].clone();
|
||||||
|
state_context.hitboxes.0 = Some(crate::fighter::FighterActiveAttack {
|
||||||
|
kind: self.attack_kind,
|
||||||
|
script,
|
||||||
|
active_hitboxes: Vec::new(),
|
||||||
|
solved_hitboxes: Vec::new(),
|
||||||
|
hit_fighters: Vec::new(),
|
||||||
|
stalled: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_exit(&mut self, state_context: &mut ExitContext) {
|
||||||
|
state_context.hitboxes.0 = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_collision_interrupt(
|
||||||
|
&mut self,
|
||||||
|
state_context: &mut CollisionContext,
|
||||||
|
) -> Option<FighterState> {
|
||||||
|
air::collision_interrupt(
|
||||||
|
state_context,
|
||||||
|
crate::fighter::state::land::LandingType::Normal,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_visually_grounded<'a>(&'a self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum AirAttackStateCreateError {
|
||||||
|
#[error("Attack script: {attack:?} not found")]
|
||||||
|
AttackScriptNotFound{attack: AttackKind},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AirAttackState {
|
||||||
|
pub fn create(attack_kind: AttackKind, attack_script_assets: &FighterAttackScriptAssets, airborne_state_common: &AirborneStateCommon) -> Result<Self, AirAttackStateCreateError> {
|
||||||
|
if attack_script_assets.scripts.contains_key(&attack_kind) {
|
||||||
|
Ok(AirAttackState {
|
||||||
|
airborne_state_common: airborne_state_common.clone(),
|
||||||
|
attack_kind
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(AirAttackStateCreateError::AttackScriptNotFound { attack: attack_kind })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn check_interrupt(state_context: &InterruptContext, airborne_state_common: &AirborneStateCommon) -> Option<FighterState> {
|
||||||
|
if state_context
|
||||||
|
.input
|
||||||
|
.has_command(crate::input::FighterCommands::Attack)
|
||||||
|
{
|
||||||
|
let attack_kind = state_context
|
||||||
|
.input
|
||||||
|
.get_attack_command_type()
|
||||||
|
.to_attack_kind_airborne(state_context.facing_direction);
|
||||||
|
let state = AirAttackState::create(attack_kind, state_context.attack_script_assets, airborne_state_common);
|
||||||
|
|
||||||
|
match state {
|
||||||
|
Ok(state) => {
|
||||||
|
Some(FighterState::AirAttack(state))
|
||||||
|
},
|
||||||
|
Err(create_error) => {
|
||||||
|
error!("{}", create_error);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext};
|
use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext};
|
||||||
use crate::fighter::animation::AnimKind;
|
use crate::fighter::animation::AnimKind;
|
||||||
use crate::fighter::state::air::AirborneStateCommon;
|
use crate::fighter::state::air::AirborneStateCommon;
|
||||||
use crate::fighter::state::{FighterState, air, jump};
|
use crate::fighter::state::{FighterState, air, air_attack, jump};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Hash, Default)]
|
#[derive(Debug, Clone, Copy, Hash, Default)]
|
||||||
pub struct FallState {
|
pub struct FallState {
|
||||||
@@ -20,7 +20,7 @@ impl FighterStateImpl for FallState {
|
|||||||
const NAME: &'static str = "Fall";
|
const NAME: &'static str = "Fall";
|
||||||
|
|
||||||
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
||||||
jump::check_input_double_jump(state_context)
|
jump::check_input_double_jump(state_context).or_else(|| air_attack::check_interrupt(state_context, &self.airborne_state_common))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self, state_context: &mut EnterContext) {
|
fn on_enter(&mut self, state_context: &mut EnterContext) {
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ pub enum JumpType {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub struct JumpState {
|
pub struct JumpState {
|
||||||
jump_type: JumpType,
|
pub jump_type: JumpType,
|
||||||
airborne_state_common: AirborneStateCommon,
|
pub airborne_state_common: AirborneStateCommon,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
@@ -108,14 +108,7 @@ impl FighterStateImpl for JumpSquatState {
|
|||||||
impl FighterStateImpl for JumpState {
|
impl FighterStateImpl for JumpState {
|
||||||
const NAME: &'static str = "Jump";
|
const NAME: &'static str = "Jump";
|
||||||
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
||||||
if state_context.input.has_command(FighterCommands::Jump) {
|
air::air_interrupts_common(state_context, &self.airborne_state_common)
|
||||||
Some(FighterState::Jump(JumpState {
|
|
||||||
jump_type: JumpType::DoubleJump,
|
|
||||||
airborne_state_common: AirborneStateCommon::default(),
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
air_dodge::check_input(state_context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, state_context: &mut UpdateContext) {
|
fn update(&mut self, state_context: &mut UpdateContext) {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ pub mod run;
|
|||||||
pub mod turn;
|
pub mod turn;
|
||||||
pub mod wait;
|
pub mod wait;
|
||||||
pub mod walk;
|
pub mod walk;
|
||||||
|
pub mod air_attack;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
pub enum SurfacePolicy {
|
pub enum SurfacePolicy {
|
||||||
@@ -135,6 +136,7 @@ pub struct InterruptContext<'a> {
|
|||||||
pub surface: &'a FighterSurfaceState,
|
pub surface: &'a FighterSurfaceState,
|
||||||
pub animation: AnimationStatus,
|
pub animation: AnimationStatus,
|
||||||
pub stage_collision: &'a StageCollision,
|
pub stage_collision: &'a StageCollision,
|
||||||
|
pub attack_script_assets: &'a FighterAttackScriptAssets,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InterruptContext<'_> {
|
impl InterruptContext<'_> {
|
||||||
@@ -246,6 +248,7 @@ fighter_states! {
|
|||||||
GroundAttack => ground_attack::GroundAttackState,
|
GroundAttack => ground_attack::GroundAttackState,
|
||||||
Hitstun => hitstun::HitstunState,
|
Hitstun => hitstun::HitstunState,
|
||||||
Angel => angel::AngelState,
|
Angel => angel::AngelState,
|
||||||
|
AirAttack => air_attack::AirAttackState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(QueryData)]
|
#[derive(QueryData)]
|
||||||
@@ -292,6 +295,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
|||||||
baked_animations: &'a Assets<BakedFighterAnimations>,
|
baked_animations: &'a Assets<BakedFighterAnimations>,
|
||||||
attributes: &'a FighterAttributes,
|
attributes: &'a FighterAttributes,
|
||||||
stage_collision: &'a StageCollision,
|
stage_collision: &'a StageCollision,
|
||||||
|
attack_script_assets: &'a FighterAttackScriptAssets,
|
||||||
) -> InterruptContext<'a> {
|
) -> InterruptContext<'a> {
|
||||||
let animation = self.animation_status(animations, baked_animations);
|
let animation = self.animation_status(animations, baked_animations);
|
||||||
InterruptContext {
|
InterruptContext {
|
||||||
@@ -302,6 +306,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
|||||||
animation,
|
animation,
|
||||||
surface: &self.surface,
|
surface: &self.surface,
|
||||||
stage_collision,
|
stage_collision,
|
||||||
|
attack_script_assets: attack_script_assets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,6 +540,7 @@ pub fn state_interrupt_system(
|
|||||||
&baked_animations,
|
&baked_animations,
|
||||||
&manifest.attributes,
|
&manifest.attributes,
|
||||||
&stage_collision,
|
&stage_collision,
|
||||||
|
attack_script_assets
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use bevy_ggrs::{PlayerInputs, Rollback};
|
|||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
use strum_macros::EnumIter;
|
use strum_macros::EnumIter;
|
||||||
|
|
||||||
|
use crate::fighter::FighterFacingDirection;
|
||||||
use crate::fighter::attack::AttackKind;
|
use crate::fighter::attack::AttackKind;
|
||||||
use crate::game_settings::GameSettings;
|
use crate::game_settings::GameSettings;
|
||||||
use crate::input::FighterInputFrame;
|
use crate::input::FighterInputFrame;
|
||||||
@@ -54,6 +55,26 @@ impl FighterAttackCommandType {
|
|||||||
FighterAttackCommandType::TiltAttackRight => AttackKind::ForwardTilt,
|
FighterAttackCommandType::TiltAttackRight => AttackKind::ForwardTilt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn to_attack_kind_airborne(&self, facing_direction: &FighterFacingDirection) -> AttackKind {
|
||||||
|
match self {
|
||||||
|
FighterAttackCommandType::Neutral => AttackKind::NeutralAir,
|
||||||
|
FighterAttackCommandType::SmashAttackUp | FighterAttackCommandType::TiltAttackUp => AttackKind::UpAir,
|
||||||
|
FighterAttackCommandType::SmashAttackDown | FighterAttackCommandType::TiltAttackDown => AttackKind::DownAir,
|
||||||
|
|
||||||
|
FighterAttackCommandType::SmashAttackLeft | FighterAttackCommandType::TiltAttackLeft => {
|
||||||
|
match facing_direction {
|
||||||
|
FighterFacingDirection::Left => AttackKind::ForwardAir,
|
||||||
|
FighterFacingDirection::Right => AttackKind::BackAir,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FighterAttackCommandType::SmashAttackRight | FighterAttackCommandType::TiltAttackRight => {
|
||||||
|
match facing_direction {
|
||||||
|
FighterFacingDirection::Left => AttackKind::BackAir,
|
||||||
|
FighterFacingDirection::Right => AttackKind::ForwardAir,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Frames of life remaining for each buffered command. Indexed by
|
/// Frames of life remaining for each buffered command. Indexed by
|
||||||
|
|||||||
+121
-10
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use bevy::input::InputSystems;
|
use bevy::input::InputSystems;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_ggrs::{RunGgrsSystems, prelude::*};
|
use bevy_ggrs::{ConfirmedFrameCount, RollbackFrameCount, RunGgrsSystems, prelude::*};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState,
|
AppState,
|
||||||
@@ -15,6 +15,8 @@ pub struct MatchState {
|
|||||||
pub elapsed_frames: u32,
|
pub elapsed_frames: u32,
|
||||||
/// Indexed by the player's session handle.
|
/// Indexed by the player's session handle.
|
||||||
pub players: Vec<PlayerMatchState>,
|
pub players: Vec<PlayerMatchState>,
|
||||||
|
/// The rollback frame on which the match first finished.
|
||||||
|
pub finished_at_frame: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||||
@@ -47,6 +49,7 @@ impl MatchState {
|
|||||||
deaths: 0,
|
deaths: 0,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
finished_at_frame: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,10 +68,11 @@ impl MatchState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let MatchRules::Stock {
|
if config.players.len() > 1
|
||||||
starting_stocks: _,
|
&& let MatchRules::Stock {
|
||||||
time_limit_frames: _,
|
starting_stocks: _,
|
||||||
} = config.rules
|
time_limit_frames: _,
|
||||||
|
} = config.rules
|
||||||
{
|
{
|
||||||
let alive_fighter_count = self
|
let alive_fighter_count = self
|
||||||
.players
|
.players
|
||||||
@@ -88,6 +92,23 @@ impl MatchState {
|
|||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn advance_frame(&mut self, config: &MatchConfig, frame: i32) {
|
||||||
|
if self.finished_at_frame.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.elapsed_frames = self.elapsed_frames.saturating_add(1);
|
||||||
|
|
||||||
|
if self.is_match_finished(config) {
|
||||||
|
self.finished_at_frame = Some(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_is_confirmed(&self, confirmed_frame: i32) -> bool {
|
||||||
|
self.finished_at_frame
|
||||||
|
.is_some_and(|finished_at| confirmed_frame >= finished_at)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MatchRuntimePlugin;
|
pub struct MatchRuntimePlugin;
|
||||||
@@ -100,13 +121,20 @@ impl Plugin for MatchRuntimePlugin {
|
|||||||
.configure_sets(PreUpdate, RunGgrsSystems.run_if(ggrs_should_run))
|
.configure_sets(PreUpdate, RunGgrsSystems.run_if(ggrs_should_run))
|
||||||
.add_systems(OnEnter(AppState::InMatch), reset_pause)
|
.add_systems(OnEnter(AppState::InMatch), reset_pause)
|
||||||
.add_systems(OnExit(AppState::InMatch), reset_pause)
|
.add_systems(OnExit(AppState::InMatch), reset_pause)
|
||||||
|
.add_systems(
|
||||||
|
GgrsSchedule,
|
||||||
|
update_match_rules.in_set(crate::schedule::GameplaySet::MatchRules),
|
||||||
|
)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
PreUpdate,
|
PreUpdate,
|
||||||
(toggle_pause, check_match_finished)
|
toggle_pause
|
||||||
.chain()
|
|
||||||
.after(InputSystems)
|
.after(InputSystems)
|
||||||
.before(RunGgrsSystems)
|
.before(RunGgrsSystems)
|
||||||
.run_if(in_state(AppState::InMatch)),
|
.run_if(in_state(AppState::InMatch)),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
transition_after_confirmed_finish.run_if(in_state(AppState::InMatch)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,6 +143,10 @@ pub fn ggrs_should_run(pause: Option<Res<PauseState>>) -> bool {
|
|||||||
!pause.is_some_and(|pause| pause.paused)
|
!pause.is_some_and(|pause| pause.paused)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn match_is_ongoing(match_state: Option<Res<MatchState>>) -> bool {
|
||||||
|
match_state.is_none_or(|match_state| match_state.finished_at_frame.is_none())
|
||||||
|
}
|
||||||
|
|
||||||
fn toggle_pause(
|
fn toggle_pause(
|
||||||
key: Res<ButtonInput<KeyCode>>,
|
key: Res<ButtonInput<KeyCode>>,
|
||||||
gamepads: Query<&Gamepad>,
|
gamepads: Query<&Gamepad>,
|
||||||
@@ -143,12 +175,20 @@ fn reset_pause(mut pause: ResMut<PauseState>, mut virtual_time: ResMut<Time<Virt
|
|||||||
virtual_time.unpause();
|
virtual_time.unpause();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_match_finished(
|
fn update_match_rules(
|
||||||
match_state: Res<MatchState>,
|
mut match_state: ResMut<MatchState>,
|
||||||
match_config: Res<MatchConfig>,
|
match_config: Res<MatchConfig>,
|
||||||
|
frame: Res<RollbackFrameCount>,
|
||||||
|
) {
|
||||||
|
match_state.advance_frame(&match_config, frame.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transition_after_confirmed_finish(
|
||||||
|
match_state: Res<MatchState>,
|
||||||
|
confirmed_frame: Res<ConfirmedFrameCount>,
|
||||||
mut next_state: ResMut<NextState<AppState>>,
|
mut next_state: ResMut<NextState<AppState>>,
|
||||||
) {
|
) {
|
||||||
if match_state.is_match_finished(&match_config) {
|
if match_state.finish_is_confirmed(confirmed_frame.0) {
|
||||||
next_state.set(AppState::MatchResults);
|
next_state.set(AppState::MatchResults);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,6 +225,7 @@ mod tests {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
assert_eq!(state.elapsed_frames, 0);
|
assert_eq!(state.elapsed_frames, 0);
|
||||||
|
assert_eq!(state.finished_at_frame, None);
|
||||||
assert_eq!(state.players.len(), 2);
|
assert_eq!(state.players.len(), 2);
|
||||||
assert!(state.players.iter().all(|player| {
|
assert!(state.players.iter().all(|player| {
|
||||||
player.stocks_remaining == Some(4) && player.score == 0 && player.deaths == 0
|
player.stocks_remaining == Some(4) && player.score == 0 && player.deaths == 0
|
||||||
@@ -204,4 +245,74 @@ mod tests {
|
|||||||
.all(|player| player.stocks_remaining.is_none())
|
.all(|player| player.stocks_remaining.is_none())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn timed_match_finishes_on_its_rollback_frame() {
|
||||||
|
let config = config(MatchRules::Timed {
|
||||||
|
time_limit_frames: 2,
|
||||||
|
});
|
||||||
|
let mut state = MatchState::from_config(&config);
|
||||||
|
|
||||||
|
state.advance_frame(&config, 10);
|
||||||
|
assert_eq!(state.elapsed_frames, 1);
|
||||||
|
assert_eq!(state.finished_at_frame, None);
|
||||||
|
|
||||||
|
state.advance_frame(&config, 11);
|
||||||
|
assert_eq!(state.elapsed_frames, 2);
|
||||||
|
assert_eq!(state.finished_at_frame, Some(11));
|
||||||
|
assert!(!state.finish_is_confirmed(10));
|
||||||
|
assert!(state.finish_is_confirmed(11));
|
||||||
|
|
||||||
|
state.advance_frame(&config, 12);
|
||||||
|
assert_eq!(state.finished_at_frame, Some(11));
|
||||||
|
assert_eq!(state.elapsed_frames, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn corrected_rollback_state_does_not_keep_speculative_finish() {
|
||||||
|
let config = config(MatchRules::Stock {
|
||||||
|
starting_stocks: 1,
|
||||||
|
time_limit_frames: None,
|
||||||
|
});
|
||||||
|
let state_before_prediction = MatchState::from_config(&config);
|
||||||
|
let mut predicted_state = state_before_prediction.clone();
|
||||||
|
predicted_state.players[1].stocks_remaining = Some(0);
|
||||||
|
predicted_state.advance_frame(&config, 20);
|
||||||
|
assert_eq!(predicted_state.finished_at_frame, Some(20));
|
||||||
|
|
||||||
|
let mut corrected_state = state_before_prediction;
|
||||||
|
corrected_state.advance_frame(&config, 20);
|
||||||
|
assert_eq!(corrected_state.finished_at_frame, None);
|
||||||
|
assert!(!corrected_state.finish_is_confirmed(20));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_player_stock_match_does_not_finish_by_elimination() {
|
||||||
|
let mut config = config(MatchRules::Stock {
|
||||||
|
starting_stocks: 1,
|
||||||
|
time_limit_frames: None,
|
||||||
|
});
|
||||||
|
config.players.truncate(1);
|
||||||
|
let mut state = MatchState::from_config(&config);
|
||||||
|
|
||||||
|
state.advance_frame(&config, 1);
|
||||||
|
assert_eq!(state.finished_at_frame, None);
|
||||||
|
|
||||||
|
state.players[0].stocks_remaining = Some(0);
|
||||||
|
state.advance_frame(&config, 2);
|
||||||
|
assert_eq!(state.finished_at_frame, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_player_stock_match_can_finish_by_time_limit() {
|
||||||
|
let mut config = config(MatchRules::Stock {
|
||||||
|
starting_stocks: 1,
|
||||||
|
time_limit_frames: Some(1),
|
||||||
|
});
|
||||||
|
config.players.truncate(1);
|
||||||
|
let mut state = MatchState::from_config(&config);
|
||||||
|
|
||||||
|
state.advance_frame(&config, 7);
|
||||||
|
assert_eq!(state.finished_at_frame, Some(7));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -9,6 +9,8 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_ggrs::prelude::*;
|
use bevy_ggrs::prelude::*;
|
||||||
|
|
||||||
|
use crate::match_runtime::match_is_ongoing;
|
||||||
|
|
||||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum GameplaySet {
|
pub enum GameplaySet {
|
||||||
/// Turn this frame's netcode inputs into per-fighter input state.
|
/// Turn this frame's netcode inputs into per-fighter input state.
|
||||||
@@ -25,6 +27,8 @@ pub enum GameplaySet {
|
|||||||
Animation,
|
Animation,
|
||||||
/// Resolve attacks
|
/// Resolve attacks
|
||||||
Attacks,
|
Attacks,
|
||||||
|
/// Advance match rules and detect the end of the match.
|
||||||
|
MatchRules,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Declares the order of [`GameplaySet`] inside [`GgrsSchedule`]. Added by
|
/// Declares the order of [`GameplaySet`] inside [`GgrsSchedule`]. Added by
|
||||||
@@ -43,8 +47,10 @@ impl Plugin for GameplaySchedulePlugin {
|
|||||||
GameplaySet::Animation,
|
GameplaySet::Animation,
|
||||||
GameplaySet::Collision,
|
GameplaySet::Collision,
|
||||||
GameplaySet::Attacks,
|
GameplaySet::Attacks,
|
||||||
|
GameplaySet::MatchRules,
|
||||||
)
|
)
|
||||||
.chain(),
|
.chain()
|
||||||
|
.run_if(match_is_ongoing),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user