relaod mechanic

This commit is contained in:
2025-04-02 02:15:43 +08:00
parent 9aaa015bd6
commit 01f8128e5f
7 changed files with 82 additions and 16 deletions

View File

@@ -82,6 +82,7 @@ fn on_trigger_state(
player_rot: Query<&Transform, With<PlayerRig>>, player_rot: Query<&Transform, With<PlayerRig>>,
player_transform: Query<&Transform, With<Player>>, player_transform: Query<&Transform, With<Player>>,
mut active_heads: ResMut<ActiveHeads>, mut active_heads: ResMut<ActiveHeads>,
time: Res<Time>,
) { ) {
if matches!(trigger.event(), TriggerState::Active) { if matches!(trigger.event(), TriggerState::Active) {
let Some(state) = active_heads.current() else { let Some(state) = active_heads.current() else {
@@ -107,7 +108,7 @@ fn on_trigger_state(
pos: transform.translation, pos: transform.translation,
}; };
active_heads.use_ammo(); active_heads.use_ammo(time.elapsed_secs());
match state.ability { match state.ability {
HeadAbility::Thrown => commands.trigger(TriggerThrow(trigger_state)), HeadAbility::Thrown => commands.trigger(TriggerThrow(trigger_state)),

View File

@@ -154,6 +154,7 @@ fn sync(
head: head.0, head: head.0,
health: hp.health(), health: hp.health(),
ammo: 1., ammo: 1.,
reloading: None,
}); });
} }
} }

View File

@@ -262,6 +262,7 @@ fn swap_head_inputs(
backpack: Res<Backpack>, backpack: Res<Backpack>,
mut commands: Commands, mut commands: Commands,
mut state: ResMut<BackpackUiState>, mut state: ResMut<BackpackUiState>,
time: Res<Time>,
) { ) {
if state.count == 0 { if state.count == 0 {
return; return;
@@ -292,17 +293,17 @@ fn swap_head_inputs(
if changed { if changed {
commands.trigger(PlaySound::Selection); commands.trigger(PlaySound::Selection);
sync(&backpack, &mut state); sync(&backpack, &mut state, time.elapsed_secs());
} }
} }
fn sync_on_change(backpack: Res<Backpack>, mut state: ResMut<BackpackUiState>) { fn sync_on_change(backpack: Res<Backpack>, mut state: ResMut<BackpackUiState>, time: Res<Time>) {
if backpack.is_changed() { if backpack.is_changed() || backpack.reloading() {
sync(&backpack, &mut state); sync(&backpack, &mut state, time.elapsed_secs());
} }
} }
fn sync(backpack: &Res<Backpack>, state: &mut ResMut<BackpackUiState>) { fn sync(backpack: &Res<Backpack>, state: &mut ResMut<BackpackUiState>, time: f32) {
state.count = backpack.heads.len(); state.count = backpack.heads.len();
state.scroll = state.scroll.min(state.count.saturating_sub(HEAD_SLOTS)); state.scroll = state.scroll.min(state.count.saturating_sub(HEAD_SLOTS));
@@ -316,7 +317,7 @@ fn sync(backpack: &Res<Backpack>, state: &mut ResMut<BackpackUiState>) {
for i in 0..HEAD_SLOTS { for i in 0..HEAD_SLOTS {
if let Some(head) = backpack.heads.get(i + state.scroll) { if let Some(head) = backpack.heads.get(i + state.scroll) {
state.heads[i] = Some(UiHeadState::from(*head)); state.heads[i] = Some(UiHeadState::new(*head, time));
} else { } else {
state.heads[i] = None; state.heads[i] = None;
} }

View File

@@ -15,6 +15,18 @@ pub struct Backpack {
pub heads: Vec<HeadState>, pub heads: Vec<HeadState>,
} }
impl Backpack {
pub fn reloading(&self) -> bool {
for head in &self.heads {
if !head.has_ammo() {
return true;
}
}
false
}
}
#[derive(Event)] #[derive(Event)]
pub struct BackbackSwapEvent(pub usize); pub struct BackbackSwapEvent(pub usize);

View File

@@ -6,6 +6,7 @@ pub struct UiHeadState {
pub head: usize, pub head: usize,
pub health: f32, pub health: f32,
pub ammo: f32, pub ammo: f32,
pub reloading: Option<f32>,
} }
impl UiHeadState { impl UiHeadState {
@@ -16,14 +17,23 @@ impl UiHeadState {
pub fn ammo_used(&self) -> f32 { pub fn ammo_used(&self) -> f32 {
1. - self.ammo 1. - self.ammo
} }
}
impl From<HeadState> for UiHeadState { pub fn reloading(&self) -> Option<f32> {
fn from(value: HeadState) -> Self { self.reloading
}
pub(crate) fn new(value: HeadState, time: f32) -> Self {
let reloading = if value.has_ammo() {
None
} else {
Some((time - value.last_use) / value.reload_duration)
};
Self { Self {
head: value.head, head: value.head,
ammo: value.ammo as f32 / value.ammo_max as f32, ammo: value.ammo as f32 / value.ammo_max as f32,
health: value.health as f32 / value.health_max as f32, health: value.health as f32 / value.health_max as f32,
reloading,
} }
} }
} }

View File

@@ -182,11 +182,18 @@ fn update_ammo(
if res.is_changed() { if res.is_changed() {
for (mut gradient, HeadImage(head)) in gradients.iter_mut() { for (mut gradient, HeadImage(head)) in gradients.iter_mut() {
if let Some(head) = res.heads[*head] { if let Some(head) = res.heads[*head] {
let ammo_used = head.ammo_used();
let Gradient::Conic(gradient) = &mut gradient.0[0] else { let Gradient::Conic(gradient) = &mut gradient.0[0] else {
continue; continue;
}; };
let angle = PI * 2. * ammo_used;
let progress = if let Some(reloading) = head.reloading() {
1. - reloading
} else {
head.ammo_used()
};
let angle = progress * PI * 2.;
gradient.stops[1].angle = Some(angle); gradient.stops[1].angle = Some(angle);
gradient.stops[2].angle = Some(angle); gradient.stops[2].angle = Some(angle);
} }
@@ -203,11 +210,13 @@ fn update_health(res: Res<UiActiveHeads>, mut query: Query<(&mut Node, &HeadDama
} }
} }
fn sync(active: Res<ActiveHeads>, mut state: ResMut<UiActiveHeads>) { fn sync(active: Res<ActiveHeads>, mut state: ResMut<UiActiveHeads>, time: Res<Time>) {
if active.is_changed() { if active.is_changed() || active.reloading() {
state.current_slot = active.slot(); state.current_slot = active.slot();
for i in 0..HEAD_SLOTS { for i in 0..HEAD_SLOTS {
state.heads[i] = active.head(i).map(UiHeadState::from); state.heads[i] = active
.head(i)
.map(|state| UiHeadState::new(state, time.elapsed_secs()));
} }
} }
} }

View File

@@ -25,6 +25,8 @@ pub struct HeadState {
pub health_max: u32, pub health_max: u32,
pub ammo: u32, pub ammo: u32,
pub ammo_max: u32, pub ammo_max: u32,
pub reload_duration: f32,
pub last_use: f32,
} }
impl HeadState { impl HeadState {
@@ -36,6 +38,8 @@ impl HeadState {
ammo, ammo,
ammo_max: ammo, ammo_max: ammo,
ability: HeadAbility::None, ability: HeadAbility::None,
reload_duration: 5.,
last_use: 0.,
} }
} }
@@ -61,12 +65,13 @@ impl ActiveHeads {
self.heads[self.current_slot] self.heads[self.current_slot]
} }
pub fn use_ammo(&mut self) { pub fn use_ammo(&mut self, time: f32) {
let Some(head) = &mut self.heads[self.current_slot] else { let Some(head) = &mut self.heads[self.current_slot] else {
error!("cannot use ammo of empty head"); error!("cannot use ammo of empty head");
return; return;
}; };
head.last_use = time;
head.ammo = head.ammo.saturating_sub(1); head.ammo = head.ammo.saturating_sub(1);
} }
@@ -77,6 +82,16 @@ impl ActiveHeads {
pub fn head(&self, slot: usize) -> Option<HeadState> { pub fn head(&self, slot: usize) -> Option<HeadState> {
self.heads[slot] self.heads[slot]
} }
pub fn reloading(&self) -> bool {
for head in self.heads {
if head.map(|head| head.ammo == 0).unwrap_or(false) {
return true;
}
}
false
}
} }
#[derive(Event, Reflect)] #[derive(Event, Reflect)]
@@ -103,6 +118,7 @@ pub fn plugin(app: &mut App) {
}); });
app.add_systems(OnEnter(GameState::Playing), setup); app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(Update, reload.run_if(in_state(GameState::Playing)));
app.add_observer(on_select_active_head); app.add_observer(on_select_active_head);
app.add_observer(on_swap_backpack); app.add_observer(on_swap_backpack);
@@ -117,6 +133,22 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.insert_resource(HeadsImages { heads }); commands.insert_resource(HeadsImages { heads });
} }
fn reload(mut active: ResMut<ActiveHeads>, time: Res<Time>) {
if !active.reloading() {
return;
}
for head in active.heads.iter_mut() {
let Some(head) = head else {
continue;
};
if !head.has_ammo() && (head.last_use + head.reload_duration <= time.elapsed_secs()) {
head.ammo = head.ammo_max;
}
}
}
fn on_select_active_head( fn on_select_active_head(
trigger: Trigger<SelectActiveHead>, trigger: Trigger<SelectActiveHead>,
mut commands: Commands, mut commands: Commands,