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

@ -59,7 +59,7 @@ pub enum WeaponBonusType {
Roshambo, Roshambo,
Throttle, Throttle,
// Attack nullification types // Action nullification types
Homerun, Homerun,
Parry, Parry,

View file

@ -21,6 +21,9 @@ pub struct Defeated;
#[derive(Component)] #[derive(Component)]
pub struct Current; pub struct Current;
#[derive(Component)]
pub struct PickedAction(pub WeaponSlot);
#[derive(Component)] #[derive(Component)]
pub struct CurrentTarget; pub struct CurrentTarget;
@ -139,6 +142,13 @@ pub enum HealthChange {
DoubleEdged { dmg: u32 }, DoubleEdged { dmg: u32 },
} }
#[derive(Component, Debug)]
pub enum ActionNullification {
Parry { chance: f64 },
Homerun { chance: f64 },
GentsStripClub,
}
impl PartDamageBonus { impl PartDamageBonus {
pub fn dmg_bonus(&self, part: BodyPart) -> Option<f32> { pub fn dmg_bonus(&self, part: BodyPart) -> Option<f32> {
match self { match self {

View file

@ -305,11 +305,11 @@ impl WeaponDto {
commands.insert(EquippedMods(mods)); commands.insert(EquippedMods(mods));
if let Some(bonus) = &self.bonuses[0] { if let Some(bonus) = &self.bonuses[0] {
commands.insert(children![WeaponBonusBundle::new(bonus.bonus, bonus.value)]); commands.with_child(WeaponBonusBundle::new(bonus.bonus, bonus.value));
} }
if let Some(bonus) = &self.bonuses[1] { if let Some(bonus) = &self.bonuses[1] {
commands.insert(children![WeaponBonusBundle::new(bonus.bonus, bonus.value)]); commands.with_child(WeaponBonusBundle::new(bonus.bonus, bonus.value));
} }
commands.id() commands.id()

View file

@ -10,15 +10,16 @@
"strategy": { "strategy": {
"type": "in_order", "type": "in_order",
"order": [ "order": [
"secondary" "temporary",
"melee"
], ],
"reload": false "reload": true
}, },
"education": { "education": {
"bio2350": true, "bio2350": true,
"bio2380": true, "bio2380": true,
"bio2410": true, "bio2410": true,
"cbt2790": false, "cbt2790": true,
"cbt2820": true, "cbt2820": true,
"cbt2830": true, "cbt2830": true,
"cbt2840": true, "cbt2840": true,
@ -27,33 +28,33 @@
"cbt2125": true, "cbt2125": true,
"gen2116": true, "gen2116": true,
"gen2119": true, "gen2119": true,
"haf2104": false, "haf2104": true,
"haf2105": false, "haf2105": true,
"haf2106": false, "haf2106": true,
"haf2107": false, "haf2107": true,
"haf2108": false, "haf2108": true,
"haf2109": false, "haf2109": true,
"his2160": true, "his2160": true,
"his2170": true, "his2170": true,
"mth2240": false, "mth2240": true,
"mth2250": false, "mth2250": true,
"mth2260": false, "mth2260": true,
"mth2320": false, "mth2320": true,
"mth2310": true, "mth2310": true,
"mth3330": true, "mth3330": true,
"psy2640": false, "psy2640": true,
"psy2650": false, "psy2650": true,
"psy2660": false, "psy2660": true,
"psy2670": false, "psy2670": true,
"def2710": false, "def2710": true,
"def2730": false, "def2730": true,
"def2740": false, "def2740": true,
"def2750": false, "def2750": true,
"def2760": false, "def2760": true,
"def3770": true, "def3770": true,
"spt2480": true, "spt2480": true,
"spt2490": false, "spt2490": true,
"spt2500": false "spt2500": true
}, },
"merits": { "merits": {
"life": 10, "life": 10,
@ -86,148 +87,142 @@
}, },
"weapons": { "weapons": {
"primary": null, "primary": null,
"secondary": { "secondary": null,
"id": 490, "melee": {
"name": "Blunderbuss", "id": 247,
"kind": "secondary", "name": "Katana",
"cat": "shotgun", "kind": "melee",
"base_dmg": 46, "cat": "slashing",
"base_acc": 24, "base_dmg": 52,
"dmg": 46, "base_acc": 55,
"acc": 24, "dmg": 52,
"ammo": { "acc": 55,
"clip_size": 1, "ammo": null,
"rate_of_fire": [
1,
1
]
},
"mods": [ "mods": [
null, null,
null null
], ],
"bonuses": [ "bonuses": [
{ null,
"bonus": "bleed",
"value": 100
},
null null
], ],
"compatible_mods": [ "compatible_mods": [],
"laser1mw", "experience": 0,
"laser5mw", "japanese": true
"laser30mw", },
"laser100mw", "temporary": {
"extra_clip", "id": 463,
"extra_clip2", "name": "Epinephrine",
"adjustable_trigger", "kind": "temporary",
"hair_trigger", "cat": "temporary",
"custom_grip", "base_dmg": 0,
"skeet_choke", "base_acc": 0,
"improved_choke", "dmg": 0,
"full_choke", "acc": 0,
"small_light", "ammo": null,
"precision_light", "mods": [
"tactical_illuminator" null,
null
], ],
"bonuses": [
null,
null
],
"compatible_mods": [],
"experience": 0, "experience": 0,
"japanese": false "japanese": false
}, }
"melee": null,
"temporary": null
}, },
"armour": { "armour": {
"helmet": { "helmet": {
"slot": "head", "slot": "head",
"id": 1355, "id": 675,
"name": "Vanguard Respirator", "name": "Marauder Face Mask",
"base_armour": 48, "base_armour": 40,
"armour": 48, "armour": 40,
"coverage": { "coverage": {
"body": 2.819999933242798, "body": 11.489999771118164,
"heart": 0, "heart": 0,
"stomach": 0, "stomach": 0,
"chest": 0, "chest": 0,
"arm": 0, "arm": 0,
"groin": 0, "groin": 0,
"leg": 0, "leg": 0,
"throat": 0, "throat": 60.56999969482422,
"hand": 0, "hand": 0,
"foot": 0, "foot": 0,
"head": 39.599998474121094 "head": 100
}, },
"immunities": [ "immunities": [
"pepper_spray", "pepper_spray"
"nerve_gas",
"tear_gas"
], ],
"bonus": { "bonus": {
"kind": "irrepressible", "kind": "invulnerable",
"value": null "value": 4
} }
}, },
"body": { "body": {
"slot": "body", "slot": "body",
"id": 1356, "id": 676,
"name": "Vanguard Body", "name": "Marauder Body",
"base_armour": 48, "base_armour": 52,
"armour": 48, "armour": 52,
"coverage": { "coverage": {
"body": 44.279998779296875, "body": 42.470001220703125,
"heart": 100, "heart": 100,
"stomach": 100, "stomach": 100,
"chest": 100, "chest": 100,
"arm": 100, "arm": 100,
"groin": 35.38999938964844, "groin": 0,
"leg": 0.2800000011920929, "leg": 0,
"throat": 83.52999877929688, "throat": 84.72000122070312,
"hand": 0.12999999523162842, "hand": 5.050000190734863,
"foot": 0, "foot": 0,
"head": 0 "head": 0
}, },
"immunities": [], "immunities": [],
"bonus": { "bonus": {
"kind": "irrepressible", "kind": "imperviable",
"value": null "value": 4
} }
}, },
"pants": { "pants": {
"slot": "legs", "slot": "legs",
"id": 1357, "id": 677,
"name": "Vanguard Pants", "name": "Marauder Pants",
"base_armour": 48, "base_armour": 52,
"armour": 48, "armour": 52,
"coverage": { "coverage": {
"body": 24.959999084472656, "body": 20.989999771118164,
"heart": 0, "heart": 0,
"stomach": 0, "stomach": 0.5199999809265137,
"chest": 0, "chest": 0,
"arm": 0, "arm": 0,
"groin": 99.06999969482422, "groin": 100,
"leg": 100, "leg": 96.69000244140625,
"throat": 0, "throat": 0,
"hand": 0, "hand": 0,
"foot": 25.200000762939453, "foot": 0,
"head": 0 "head": 0
}, },
"immunities": [], "immunities": [],
"bonus": { "bonus": {
"kind": "irrepressible", "kind": "imperviable",
"value": null "value": 4
} }
}, },
"gloves": { "gloves": {
"slot": "hands", "slot": "hands",
"id": 1359, "id": 679,
"name": "Vanguard Gloves", "name": "Marauder Gloves",
"base_armour": 48, "base_armour": 52,
"armour": 48, "armour": 52,
"coverage": { "coverage": {
"body": 14.399999618530273, "body": 14.369999885559082,
"heart": 0, "heart": 0,
"stomach": 0, "stomach": 0,
"chest": 0, "chest": 0,
"arm": 0.7300000190734863, "arm": 0.5,
"groin": 0, "groin": 0,
"leg": 0, "leg": 0,
"throat": 0, "throat": 0,
@ -237,24 +232,24 @@
}, },
"immunities": [], "immunities": [],
"bonus": { "bonus": {
"kind": "irrepressible", "kind": "invulnerable",
"value": null "value": 4
} }
}, },
"boots": { "boots": {
"slot": "feet", "slot": "feet",
"id": 1358, "id": 678,
"name": "Vanguard Boots", "name": "Marauder Boots",
"base_armour": 48, "base_armour": 52,
"armour": 48, "armour": 52,
"coverage": { "coverage": {
"body": 15.130000114440918, "body": 15.649999618530273,
"heart": 0, "heart": 0,
"stomach": 0, "stomach": 0,
"chest": 0, "chest": 0,
"arm": 0, "arm": 0,
"groin": 0, "groin": 0,
"leg": 5.829999923706055, "leg": 9.529999732971191,
"throat": 0, "throat": 0,
"hand": 0, "hand": 0,
"foot": 100, "foot": 100,
@ -262,9 +257,12 @@
}, },
"immunities": [], "immunities": [],
"bonus": { "bonus": {
"kind": "irrepressible", "kind": "invulnerable",
"value": null "value": 4
} }
} }
},
"property": {
"damage": true
} }
} }

View file

@ -10,7 +10,8 @@
"strategy": { "strategy": {
"type": "in_order", "type": "in_order",
"order": [ "order": [
"primary" "primary",
"melee"
], ],
"reload": false "reload": false
}, },
@ -85,9 +86,92 @@
"side_effects": 10 "side_effects": 10
}, },
"weapons": { "weapons": {
"primary": null, "primary": {
"id": 399,
"name": "ArmaLite M-15A4",
"kind": "primary",
"cat": "rifle",
"base_dmg": 68,
"base_acc": 57,
"dmg": 68,
"acc": 57,
"ammo": {
"ammo_kind": "standard",
"clip_size": 15,
"rate_of_fire": [
3,
5
]
},
"mods": [
null,
null
],
"bonuses": [
null,
null
],
"compatible_mods": [
"reflex_sight",
"holographic_sight",
"acog_sight",
"thermal_sight",
"laser1mw",
"laser5mw",
"laser30mw",
"laser100mw",
"small_suppressor",
"standard_suppressor",
"large_suppressor",
"extended_mags",
"high_capacity_mags",
"extra_clip",
"extra_clip2",
"bipod",
"tripod",
"adjustable_trigger",
"hair_trigger",
"custom_grip",
"recoil_pad",
"standard_brake",
"heavy_duty_brake",
"tactical_brake",
"small_light",
"precision_light",
"tactical_illuminator"
],
"experience": 0,
"japanese": false
},
"secondary": null, "secondary": null,
"melee": null, "melee": {
"id": 237,
"name": "Kodachi",
"kind": "melee",
"cat": "slashing",
"base_dmg": 62,
"base_acc": 56,
"dmg": 62,
"acc": 56,
"ammo": null,
"mods": [
null,
null
],
"bonuses": [
{
"bonus": "parry",
"value": 100
},
{
"bonus": "homerun",
"value": 100
}
],
"compatible_mods": [],
"experience": 0,
"japanese": true
},
"temporary": null "temporary": null
}, },
"armour": { "armour": {
@ -121,27 +205,27 @@
}, },
"body": { "body": {
"slot": "body", "slot": "body",
"id": 681, "id": 676,
"name": "EOD Apron", "name": "Marauder Body",
"base_armour": 55, "base_armour": 52,
"armour": 55, "armour": 52,
"coverage": { "coverage": {
"body": 55.2400016784668, "body": 42.470001220703125,
"heart": 100, "heart": 100,
"stomach": 100, "stomach": 100,
"chest": 100, "chest": 100,
"arm": 100, "arm": 100,
"groin": 100, "groin": 0,
"leg": 17.579999923706055, "leg": 0,
"throat": 100, "throat": 84.72000122070312,
"hand": 16.8799991607666, "hand": 5.050000190734863,
"foot": 0, "foot": 0,
"head": 4.679999828338623 "head": 0
}, },
"immunities": [], "immunities": [],
"bonus": { "bonus": {
"kind": "impassable", "kind": "imperviable",
"value": 0 "value": null
} }
}, },
"pants": { "pants": {

View file

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

View file

@ -1,11 +1,12 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::bundle::{ use proxisim_models::bundle::{
bonus::{ArmourBypassBonus, BonusPartDamageBonus, BonusValue, MiscBonus, WeaponBonusType}, bonus::{ArmourBypassBonus, BonusPartDamageBonus, BonusValue, MiscBonus, WeaponBonusType},
player::PartDamageBonus, player::{ActionNullification, PartDamageBonus},
stat::{ stat::{
AdditiveBonus, AmmoControl, Clips, CritRate, DamageBonus, SimpleStatBonus, AdditiveBonus, AmmoControl, Clips, CritRate, DamageBonus, SimpleStatBonus,
SimpleStatEffective, Speed, Strength, WeaponAccuracy, SimpleStatEffective, Speed, Strength, WeaponAccuracy,
}, },
weapon::Weapon,
}; };
use crate::{ use crate::{
@ -197,10 +198,11 @@ impl BonusPartDamageBonus {
pub(crate) fn prepare_bonuses( pub(crate) fn prepare_bonuses(
bonus_q: Query<(&ChildOf, &WeaponBonusType, &BonusValue)>, bonus_q: Query<(&ChildOf, &WeaponBonusType, &BonusValue)>,
clips_q: Query<&SimpleStatEffective<Clips>>, clips_q: Query<&SimpleStatEffective<Clips>>,
weapon_q: Query<&ChildOf, With<Weapon>>,
mut effects: Effects, mut effects: Effects,
mut commands: Commands, mut commands: Commands,
) { ) {
for (weapon, bonus, value) in bonus_q.iter() { for (weapon, bonus, value) in bonus_q {
match bonus { match bonus {
WeaponBonusType::Berserk => { WeaponBonusType::Berserk => {
effects.spawn( effects.spawn(
@ -495,6 +497,23 @@ pub(crate) fn prepare_bonuses(
chance: value.0 as f64 / 100.0, chance: value.0 as f64 / 100.0,
}); });
} }
WeaponBonusType::Parry => {
let player = weapon_q.get(weapon.parent()).unwrap();
commands
.entity(player.parent())
.with_child(ActionNullification::Parry {
chance: value.0 as f64 / 100.0,
});
}
WeaponBonusType::Homerun => {
let player = weapon_q.get(weapon.parent()).unwrap();
commands
.entity(player.parent())
.with_child(ActionNullification::Homerun {
chance: value.0 as f64 / 100.0,
});
}
val => unimplemented!("{val:?}"), val => unimplemented!("{val:?}"),
} }
} }

View file

@ -540,12 +540,15 @@ pub(crate) fn configure(stages: &mut Stages) {
// running this in the snapshot layer ensures that the stat increases aren't restored at the // running this in the snapshot layer ensures that the stat increases aren't restored at the
// end of the run // end of the run
stages.snapshot.add_systems(apply_first_turn_effects); stages.snapshot.add_systems(apply_first_turn_effects);
stages.equip.add_systems(apply_passives); stages
.equip
.add_systems((apply_passives, restore_usability));
stages.turn.add_systems(reload_weapon); stages.turn.add_systems(reload_weapon);
stages.post_turn.add_systems(unset_current); stages.post_turn.add_systems(unset_current);
stages stages
.restore .post_fight
.add_systems((restore_ammo, restore_usability, apply_first_turn_effects)); .add_systems((restore_usability, restore_ammo));
stages.restore.add_systems(apply_first_turn_effects);
temp::configure(stages); temp::configure(stages);
bonus::configure(stages); bonus::configure(stages);

View file

@ -1,11 +1,12 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::bundle::{ use proxisim_models::bundle::{
player::{Current, CurrentTarget, HealthChange, Player}, player::{ActionNullification, Current, CurrentTarget, HealthChange, PickedAction, Player},
weapon::{BuffingTemp, DebuffingTemp, Usable, Uses}, weapon::{BuffingTemp, DebuffingTemp, Usable, Uses, WeaponSlot},
}; };
use rand::Rng as _;
use crate::{ use crate::{
Stages, Rng, Stages,
effect::Effects, effect::Effects,
log, log,
log::Logger, log::Logger,
@ -19,15 +20,39 @@ use crate::{
pub struct AssociatedWeapon(pub Entity); pub struct AssociatedWeapon(pub Entity);
fn use_debuffing_temp( fn use_debuffing_temp(
mut temp_q: Query<(Entity, &DebuffingTemp, &mut Uses), With<Current>>, mut temp_q: Query<(Entity, &DebuffingTemp, &mut Uses, &ChildOf), With<Current>>,
target_q: Query<Entity, With<CurrentTarget>>, target_q: Query<(Entity, &Children, &PickedAction), With<CurrentTarget>>,
nullification_q: Query<&ActionNullification>,
mut effects: Effects, mut effects: Effects,
mut commands: Commands, mut commands: Commands,
mut rng: ResMut<Rng>,
mut logger: Logger,
) { ) {
let Ok((weapon, temp, mut uses)) = temp_q.single_mut() else { let Ok((weapon, temp, mut uses, player)) = temp_q.single_mut() else {
return; return;
}; };
let target = target_q.single().unwrap(); let (target, children, picked_action) = target_q.single().unwrap();
uses.0 -= 1;
if uses.0 == 0 {
commands.entity(weapon).remove::<Usable>();
}
for bonus in nullification_q.iter_many(children) {
if let ActionNullification::Homerun { chance } = bonus
&& picked_action.0 == WeaponSlot::Melee
&& (*chance >= 1.0 || rng.random_bool(*chance))
{
log!(logger, "nullified", {
actor: player.parent(),
recipient: target,
kind: "homerun",
weapon,
});
return;
}
}
match temp { match temp {
DebuffingTemp::TearGas => effects.spawn_and_insert( DebuffingTemp::TearGas => effects.spawn_and_insert(
@ -56,11 +81,6 @@ fn use_debuffing_temp(
AssociatedWeapon(weapon), AssociatedWeapon(weapon),
), ),
}; };
uses.0 -= 1;
if uses.0 == 0 {
commands.entity(weapon).remove::<Usable>();
}
} }
fn use_buffing_temp( fn use_buffing_temp(