feat: added parry and homerun

This commit is contained in:
TotallyNot 2025-11-05 19:40:16 +01:00
parent 71169690b7
commit 67131c7985
Signed by: pyrite
GPG key ID: 7F1BA9170CD35D15
9 changed files with 440 additions and 181 deletions

View file

@ -3,8 +3,9 @@ use proxisim_models::bundle::{
armour::{ArmourBodyPart, ArmourBodyParts, BodyPartCoverage},
bonus::{ArmourBypassBonus, DamageMitigationBonus, MiscBonus},
player::{
Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget, Defeated, Defender,
FightEndType, Health, HealthChange, PartDamageBonus, Player, PlayerStrategy, Weapons,
ActionNullification, Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget,
Defeated, Defender, FightEndType, Health, HealthChange, PartDamageBonus, PickedAction,
Player, PlayerStrategy, Weapons,
},
stat::{
AmmoControl, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, MaxHealth,
@ -33,12 +34,12 @@ use crate::{
pub mod stats;
pub mod status_effect;
fn select_weapon<'a>(
fn select_weapon(
weapons: &Weapons,
slot: WeaponSlot,
reload: bool,
usable_q: &'a Query<(Has<NeedsReload>, Option<&Children>), With<Usable>>,
) -> Option<(Entity, Option<&'a Children>)> {
usable_q: &Query<Has<NeedsReload>, With<Usable>>,
) -> Option<WeaponSlot> {
let id = match slot {
WeaponSlot::Primary => weapons.primary?,
WeaponSlot::Secondary => weapons.secondary?,
@ -48,12 +49,12 @@ fn select_weapon<'a>(
WeaponSlot::Kick => weapons.kick,
};
let (needs_reload, children) = usable_q.get(id).ok()?;
let needs_reload = usable_q.get(id).ok()?;
if !reload && needs_reload {
None
} else {
Some((id, children))
Some(slot)
}
}
@ -99,37 +100,61 @@ fn check_term_condition(
}
pub fn pick_action(
p_query: Query<(Entity, &Weapons, &PlayerStrategy), (With<Player>, Without<PickedAction>)>,
usable_q: Query<Has<NeedsReload>, With<Usable>>,
mut commands: Commands,
) {
for (player, weapons, strat) in p_query {
let slot = match strat {
PlayerStrategy::AlwaysFists => WeaponSlot::Fists,
PlayerStrategy::AlwaysKicks => {
select_weapon(weapons, WeaponSlot::Kick, true, &usable_q)
.unwrap_or(WeaponSlot::Fists)
}
PlayerStrategy::PrimaryMelee { reload } => {
select_weapon(weapons, WeaponSlot::Primary, *reload, &usable_q)
.or_else(|| select_weapon(weapons, WeaponSlot::Melee, true, &usable_q))
.unwrap_or(WeaponSlot::Fists)
}
PlayerStrategy::InOrder { order, reload } => order
.iter()
.find_map(|slot| select_weapon(weapons, *slot, *reload, &usable_q))
.unwrap_or(WeaponSlot::Fists),
};
commands.entity(player).insert(PickedAction(slot));
}
}
pub fn prepare_weapon(
mut p_query: Query<
(Entity, &Weapons, &PlayerStrategy, &mut CombatTurns),
(Entity, &Weapons, &PickedAction, &mut CombatTurns),
(With<Current>, With<Player>),
>,
target_q: Query<Entity, With<CurrentTarget>>,
usable_q: Query<(Has<NeedsReload>, Option<&Children>), With<Usable>>,
child_q: Query<Option<&Children>>,
weapon_trigger_q: Query<&TurnTriggeredEffect>,
mut commands: Commands,
mut effects: Effects,
metrics: Res<Metrics>,
) {
let (current, weapons, strat, mut turns) = p_query.single_mut().unwrap();
let (weapon, children) = match strat {
PlayerStrategy::AlwaysFists => (weapons.fists, None),
PlayerStrategy::AlwaysKicks => select_weapon(weapons, WeaponSlot::Kick, true, &usable_q)
.unwrap_or_else(|| (weapons.fists, Default::default())),
PlayerStrategy::PrimaryMelee { reload } => {
select_weapon(weapons, WeaponSlot::Primary, *reload, &usable_q)
.or_else(|| select_weapon(weapons, WeaponSlot::Melee, true, &usable_q))
.unwrap_or_else(|| (weapons.fists, Default::default()))
}
PlayerStrategy::InOrder { order, reload } => order
.iter()
.find_map(|slot| select_weapon(weapons, *slot, *reload, &usable_q))
.unwrap_or_else(|| (weapons.fists, Default::default())),
let (current, weapons, picked_action, mut turns) = p_query.single_mut().unwrap();
let weapon = match picked_action.0 {
WeaponSlot::Primary => weapons.primary.unwrap_or(weapons.fists),
WeaponSlot::Secondary => weapons.secondary.unwrap_or(weapons.fists),
WeaponSlot::Melee => weapons.melee.unwrap_or(weapons.fists),
WeaponSlot::Temporary => weapons.temporary.unwrap_or(weapons.fists),
WeaponSlot::Fists => weapons.fists,
WeaponSlot::Kick => weapons.kick,
};
metrics.increment_counter(Some(current), "turn", 1);
metrics.increment_counter(Some(weapon), "turn", 1);
commands.entity(weapon).insert(Current);
let children = child_q.get(weapon).unwrap();
let target = target_q.single().unwrap();
if let Some(children) = children {
@ -297,6 +322,46 @@ fn check_damage_bonuses(
}
}
#[derive(SystemParam)]
struct NullificationParams<'w, 's> {
nullification_q: Query<'w, 's, &'static ActionNullification>,
}
fn check_action_nullification(
params: &NullificationParams,
shared: &mut SharedParams,
target_children: &Children,
target_action: &PickedAction,
player_action: &PickedAction,
) -> Option<&'static str> {
for bonus in params.nullification_q.iter_many(target_children) {
match bonus {
ActionNullification::Parry { chance }
if player_action.0 == WeaponSlot::Melee
&& target_action.0 == WeaponSlot::Melee
&& (*chance >= 1.0 || shared.rng.random_bool(*chance)) =>
{
return Some("parry");
}
ActionNullification::Homerun { chance }
if player_action.0 == WeaponSlot::Temporary
&& target_action.0 == WeaponSlot::Melee
&& (*chance >= 1.0 || shared.rng.random_bool(*chance)) =>
{
return Some("homerun");
}
ActionNullification::GentsStripClub
if player_action.0 == WeaponSlot::Melee && shared.rng.random_bool(0.25) =>
{
return Some("dodged");
}
_ => (),
}
}
None
}
#[derive(SystemParam)]
struct DeferredDamageParams<'w, 's> {
damage_q: Query<'w, 's, (Entity, &'static DeferredDamage)>,
@ -379,6 +444,7 @@ struct EntityParams<'w, 's> {
&'static SimpleStatEffective<DamageBonus>,
&'static SimpleStatEffective<MaxHealth>,
&'static Children,
&'static PickedAction,
&'static mut Health,
Has<Attacker>,
),
@ -395,6 +461,8 @@ struct EntityParams<'w, 's> {
&'static EffectiveStat<Defence>,
&'static SimpleStatEffective<MaxHealth>,
&'static ArmourBodyParts,
&'static Children,
&'static PickedAction,
&'static mut Health,
),
(With<CurrentTarget>, Without<Current>),
@ -405,11 +473,12 @@ struct EntityParams<'w, 's> {
// of multi turn bonuses
#[allow(clippy::too_many_arguments)]
fn use_damaging_weapon(
(mut shared, mut bonus_mitigation, deferred, misc_bonus): (
(mut shared, mut bonus_mitigation, deferred, misc_bonus, nullification): (
SharedParams,
BonusMitigationParams,
DeferredDamageParams,
DamageBonusParams,
NullificationParams,
),
mut entities: EntityParams,
armour_q: Query<&ArmourBodyPart>,
@ -440,11 +509,20 @@ fn use_damaging_weapon(
p_dmg_bonus,
p_max_health,
p_children,
p_action,
mut p_health,
attacker,
) = entities.player_q.single_mut().unwrap();
let (target, target_dex, target_def, target_max_health, armour_parts, mut health) =
entities.target_q.single_mut().unwrap();
let (
target,
target_dex,
target_def,
target_max_health,
armour_parts,
t_children,
t_action,
mut health,
) = entities.target_q.single_mut().unwrap();
if check_deferred_damage(
&deferred,
@ -549,6 +627,26 @@ fn use_damaging_weapon(
return;
};
} else {
if multi_attack_proc.is_none()
&& let Some(kind) = check_action_nullification(
&nullification,
&mut shared,
t_children,
t_action,
p_action,
)
{
log!(shared.logger, "nullified", {
actor: player,
recipient: player,
weapon: weapon,
rounds,
kind,
});
return;
}
let body_part = if !non_targeted {
shared.rng.sample(crit)
} else {
@ -641,7 +739,7 @@ fn use_damaging_weapon(
shared.metrics.record_histogram(Some(player), "dmg", dmg_i);
shared.metrics.record_histogram(Some(weapon), "dmg", dmg_i);
if dmg_i > 0 {
if multi_attack_proc.is_none() && dmg_i > 0 {
for effect in damage_proc_q.iter_many(children) {
match *effect {
DamageProcEffect::MultiTurn { value, bonus } => {
@ -930,6 +1028,24 @@ fn process_health_changes(
}
}
fn remove_action_end_of_turn(
attacker_turn: Query<Has<Attacker>, (With<Current>, With<Player>)>,
picked_q: Query<Entity, With<PickedAction>>,
mut commands: Commands,
) {
if !attacker_turn.single().unwrap() {
for player in picked_q {
commands.entity(player).remove::<PickedAction>();
}
}
}
fn remove_picked_actions(picked_q: Query<Entity, With<PickedAction>>, mut commands: Commands) {
for player in picked_q {
commands.entity(player).remove::<PickedAction>();
}
}
pub(crate) fn configure(stages: &mut Stages) {
stats::configure(stages);
status_effect::configure(stages);
@ -937,19 +1053,28 @@ pub(crate) fn configure(stages: &mut Stages) {
stages.add_event::<ChooseWeapon>();
stages.add_event::<HealthChange>();
stages.equip.add_systems(designate_first);
stages.pre_fight.add_systems(restore_health);
stages.pre_turn.add_systems(pick_action);
stages.turn.add_systems(use_damaging_weapon);
stages.pre_fight.add_systems((restore_health, pick_action));
stages.pre_turn.add_systems(prepare_weapon);
stages
.turn
.add_systems((use_damaging_weapon, remove_action_end_of_turn));
stages
.post_turn
.add_systems((check_term_condition, change_roles, process_health_changes))
.add_systems((
check_term_condition,
change_roles,
process_health_changes,
pick_action,
))
.add_systems(
check_stalemate
.after(check_term_condition)
.before(change_roles),
);
stages.post_fight.add_systems(record_post_fight_stats);
stages
.post_fight
.add_systems((record_post_fight_stats, remove_picked_actions));
stages
.restore
.add_systems((restore_initial_state, restore_health));
.add_systems((restore_initial_state, restore_health, pick_action));
}