Add the ability to respawn and improve controller port mapping (#72)
Reviewed-on: #72 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #72.
This commit is contained in:
+119
-12
@@ -19,7 +19,7 @@ use crate::{
|
||||
spawn_fighter,
|
||||
visual::FighterAnimations,
|
||||
},
|
||||
input::{LocalInputAssignments, LocalInputSource},
|
||||
input::{LocalInputAssignments, LocalInputSource, MatchInputAssignments},
|
||||
match_runtime::MatchState,
|
||||
netcode::{
|
||||
GGRSCfg,
|
||||
@@ -34,7 +34,10 @@ use crate::{
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MatchPlayer {
|
||||
/// Dense index used by GGRS and rollback-simulated player state.
|
||||
pub handle: usize,
|
||||
/// Physical controller port selected in the menu.
|
||||
pub controller_port: usize,
|
||||
pub fighter: FighterId,
|
||||
}
|
||||
|
||||
@@ -45,6 +48,22 @@ pub struct MatchConfig {
|
||||
pub rules: MatchRules,
|
||||
}
|
||||
|
||||
impl MatchConfig {
|
||||
/// Maps a GGRS handle to the controller port used by gameplay.
|
||||
///
|
||||
/// Multiplayer uses the stable, dense GGRS handle as the port. A
|
||||
/// single-player match preserves the physical controller port selected in
|
||||
/// the menu (for example, port 3 remains port 3).
|
||||
pub fn controller_port_for_handle(&self, handle: usize) -> Option<usize> {
|
||||
let player = self.players.iter().find(|player| player.handle == handle)?;
|
||||
Some(if self.players.len() > 1 {
|
||||
handle
|
||||
} else {
|
||||
player.controller_port
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MatchRules {
|
||||
Stock {
|
||||
@@ -227,6 +246,7 @@ pub fn initiate_default_match(
|
||||
let players = (0..args.players)
|
||||
.map(|handle| MatchPlayer {
|
||||
handle,
|
||||
controller_port: handle,
|
||||
fighter: FighterId::TestFighter,
|
||||
})
|
||||
.collect();
|
||||
@@ -262,6 +282,7 @@ pub fn initiate_default_match(
|
||||
pub fn prepare_match(
|
||||
mut commands: Commands,
|
||||
config: Res<MatchConfig>,
|
||||
input_assignments: Res<LocalInputAssignments>,
|
||||
assets: Res<MatchAssets>,
|
||||
gltfs: Res<Assets<Gltf>>,
|
||||
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
||||
@@ -276,7 +297,7 @@ pub fn prepare_match(
|
||||
if !valid_player_handles(&config.players) {
|
||||
fail_match(
|
||||
&mut next_state,
|
||||
"player handles must be unique and non-empty",
|
||||
"player handles must be dense, unique, and non-empty",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -484,6 +505,11 @@ pub fn prepare_match(
|
||||
}
|
||||
};
|
||||
|
||||
commands.insert_resource(create_match_input_assignments(
|
||||
&input_assignments,
|
||||
&config.players,
|
||||
));
|
||||
|
||||
let root = commands.spawn((Name::new("Match"), MatchRoot)).id();
|
||||
|
||||
stage::spawn_stage(
|
||||
@@ -503,6 +529,9 @@ pub fn prepare_match(
|
||||
fighter_roster.push(spawn_fighter(
|
||||
&mut commands,
|
||||
player.handle,
|
||||
config
|
||||
.controller_port_for_handle(player.handle)
|
||||
.expect("validated match config contains every player handle"),
|
||||
spawn_point,
|
||||
manifest_handle.clone(),
|
||||
model_scale,
|
||||
@@ -533,16 +562,24 @@ fn valid_player_handles(players: &[MatchPlayer]) -> bool {
|
||||
|
||||
let mut handles = players.iter().map(|player| player.handle).collect::<Vec<_>>();
|
||||
handles.sort_unstable();
|
||||
handles.windows(2).all(|handles| handles[0] != handles[1])
|
||||
&& handles.last().is_none_or(|handle| *handle != usize::MAX)
|
||||
handles.into_iter().eq(0..players.len())
|
||||
}
|
||||
|
||||
fn player_count_for_handles(players: &[MatchPlayer]) -> usize {
|
||||
players
|
||||
.iter()
|
||||
.map(|player| player.handle)
|
||||
.max()
|
||||
.map_or(0, |handle| handle + 1)
|
||||
players.len()
|
||||
}
|
||||
|
||||
fn create_match_input_assignments(
|
||||
assignments: &LocalInputAssignments,
|
||||
players: &[MatchPlayer],
|
||||
) -> MatchInputAssignments {
|
||||
let mut match_assignments = vec![None; players.len()];
|
||||
for player in players {
|
||||
match_assignments[player.handle] = assignments
|
||||
.get_from_slot(player.controller_port)
|
||||
.copied();
|
||||
}
|
||||
MatchInputAssignments(match_assignments)
|
||||
}
|
||||
|
||||
pub fn cleanup_match(
|
||||
@@ -563,6 +600,7 @@ pub fn cleanup_match(
|
||||
commands.remove_resource::<MatchAssets>();
|
||||
commands.remove_resource::<MatchConfig>();
|
||||
commands.remove_resource::<MatchState>();
|
||||
commands.remove_resource::<MatchInputAssignments>();
|
||||
commands.remove_resource::<FighterRoster>();
|
||||
}
|
||||
|
||||
@@ -577,10 +615,12 @@ mod tests {
|
||||
players: vec![
|
||||
MatchPlayer {
|
||||
handle: 0,
|
||||
controller_port: 0,
|
||||
fighter: FighterId::TestFighter,
|
||||
},
|
||||
MatchPlayer {
|
||||
handle: 1,
|
||||
controller_port: 1,
|
||||
fighter: FighterId::TestFighter,
|
||||
},
|
||||
],
|
||||
@@ -598,10 +638,12 @@ mod tests {
|
||||
let players = [
|
||||
MatchPlayer {
|
||||
handle: 0,
|
||||
controller_port: 0,
|
||||
fighter: FighterId::TestFighter,
|
||||
},
|
||||
MatchPlayer {
|
||||
handle: 0,
|
||||
controller_port: 1,
|
||||
fighter: FighterId::TestFighter,
|
||||
},
|
||||
];
|
||||
@@ -610,20 +652,85 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_handles_may_have_gaps() {
|
||||
fn player_handles_must_not_have_gaps() {
|
||||
let players = [
|
||||
MatchPlayer {
|
||||
handle: 0,
|
||||
controller_port: 0,
|
||||
fighter: FighterId::TestFighter,
|
||||
},
|
||||
MatchPlayer {
|
||||
handle: 2,
|
||||
controller_port: 3,
|
||||
fighter: FighterId::TestFighter,
|
||||
},
|
||||
];
|
||||
|
||||
assert!(valid_player_handles(&players));
|
||||
assert_eq!(player_count_for_handles(&players), 3);
|
||||
assert!(!valid_player_handles(&players));
|
||||
assert_eq!(player_count_for_handles(&players), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controller_ports_use_ggrs_handles_only_in_multiplayer() {
|
||||
let single_player = MatchConfig {
|
||||
stage: StageId::TestStage,
|
||||
players: vec![MatchPlayer {
|
||||
handle: 0,
|
||||
controller_port: 3,
|
||||
fighter: FighterId::TestFighter,
|
||||
}],
|
||||
rules: MatchRules::default(),
|
||||
};
|
||||
assert_eq!(single_player.controller_port_for_handle(0), Some(3));
|
||||
|
||||
let multiplayer = MatchConfig {
|
||||
stage: StageId::TestStage,
|
||||
players: vec![
|
||||
MatchPlayer {
|
||||
handle: 0,
|
||||
controller_port: 0,
|
||||
fighter: FighterId::TestFighter,
|
||||
},
|
||||
MatchPlayer {
|
||||
handle: 1,
|
||||
controller_port: 3,
|
||||
fighter: FighterId::TestFighter,
|
||||
},
|
||||
],
|
||||
rules: MatchRules::default(),
|
||||
};
|
||||
assert_eq!(multiplayer.controller_port_for_handle(0), Some(0));
|
||||
assert_eq!(multiplayer.controller_port_for_handle(1), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_input_assignments_map_ports_to_ggrs_handles_without_mutating_ports() {
|
||||
let players = [
|
||||
MatchPlayer {
|
||||
handle: 0,
|
||||
controller_port: 0,
|
||||
fighter: FighterId::TestFighter,
|
||||
},
|
||||
MatchPlayer {
|
||||
handle: 1,
|
||||
controller_port: 3,
|
||||
fighter: FighterId::TestFighter,
|
||||
},
|
||||
];
|
||||
let assignments = LocalInputAssignments(vec![
|
||||
(0, LocalInputSource::Keyboard),
|
||||
(3, LocalInputSource::Gamepad(Entity::PLACEHOLDER)),
|
||||
]);
|
||||
|
||||
let match_assignments = create_match_input_assignments(&assignments, &players);
|
||||
|
||||
assert_eq!(match_assignments.get_for_handle(0), Some(LocalInputSource::Keyboard));
|
||||
assert_eq!(
|
||||
match_assignments.get_for_handle(1),
|
||||
Some(LocalInputSource::Gamepad(Entity::PLACEHOLDER))
|
||||
);
|
||||
assert_eq!(assignments[0].0, 0);
|
||||
assert_eq!(assignments[1].0, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user