initial commit

This commit is contained in:
TotallyNot 2023-12-31 21:26:43 +01:00
commit 86f9333aec
21 changed files with 6449 additions and 0 deletions

955
src/weapon/mod.rs Normal file
View file

@ -0,0 +1,955 @@
use bevy_ecs::prelude::*;
use macros::LogMessage;
use rand::Rng as _;
use crate::{
effect::{Effects, TurnLimitedEffect},
hierarchy::{HierarchyBuilder, Parent},
log::Logger,
metrics::Metrics,
passives::{Education, FactionUpgrades, Merits},
player::{
stats::{
AdditiveBonus, AmmoControl, ClipSize, Clips, CritRate, DamageBonus, Dexterity,
EffectiveStat, SimpleStatBonus, SimpleStatBundle, SimpleStatEffective, Speed,
WeaponAccuracy,
},
BodyPart, Current, CurrentTarget, InitiateHit, Player, Weapons,
},
Id, Name, Rng, Stages,
};
use self::{
bonus::{FirstTurnBonus, MultiTurnBonus, TurnTriggeredBonus},
temp::{NonTargeted, Uses},
};
pub mod bonus;
pub mod temp;
#[derive(Component)]
pub struct Usable;
#[derive(Component)]
pub struct Weapon;
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponSlot {
Primary,
Secondary,
Melee,
Temporary,
Fists,
Kick,
}
#[derive(Component, Debug, Clone, Copy)]
pub enum WeaponVerb {
Hit,
Kicked,
Fired,
Threw,
Exploded,
}
#[derive(Component, Clone, Copy)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponCategory {
HeavyArtillery,
MachineGun,
Rifle,
Smg,
Shotgun,
Pistol,
Club,
Piercing,
Slashing,
Mechanical,
Temporary,
HandToHand,
}
#[derive(Component, Debug)]
pub struct DamageStat(pub f32);
#[derive(Component)]
pub struct Japanese;
#[derive(Component)]
pub struct Ammo(pub u16);
#[derive(Component)]
pub struct RateOfFire(pub [u16; 2]);
#[derive(Component)]
pub struct NeedsReload;
#[derive(Clone, Copy)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponMod {
ReflexSight,
HolographicSight,
AcogSight,
ThermalSight,
Laser1mw,
Laser5mw,
Laser30mw,
Laser100mw,
SmallSuppressor,
StandardSuppressor,
LargeSuppressor,
ExtendedMags,
HighCapacityMags,
ExtraClip,
ExtraClip2,
AdjustableTrigger,
HairTrigger,
Bipod,
Tripod,
CustomGrip,
SkeetChoke,
ImprovedChoke,
FullChoke,
RecoilPad,
StandardBrake,
HeavyDutyBreak,
TacticalBrake,
SmallLight,
PrecisionLight,
TacticalIlluminator,
}
#[derive(Clone, Copy)]
pub enum TurnTriggeredMod {
Bipod,
Tripod,
SmallLight,
PrecisionLight,
TacticalIlluminator,
}
impl TurnTriggeredMod {
pub fn trigger(self, effects: &mut Effects, current: Entity, target: Entity) {
match self {
Self::Bipod => {
effects.spawn_and_insert(
AdditiveBonus::<Dexterity>::new("bipod", -0.3),
current,
TurnLimitedEffect::new(current, 0),
);
}
Self::Tripod => {
effects.spawn_and_insert(
AdditiveBonus::<Dexterity>::new("tripod", -0.3),
current,
TurnLimitedEffect::new(current, 0),
);
}
Self::SmallLight => {
effects.spawn_and_insert(
SimpleStatBonus::<WeaponAccuracy>::new("small light", -3.0 / 50.0),
target,
TurnLimitedEffect::new(target, 1),
);
}
Self::PrecisionLight => {
effects.spawn_and_insert(
SimpleStatBonus::<WeaponAccuracy>::new("precision light", -4.0 / 50.0),
target,
TurnLimitedEffect::new(target, 1),
);
}
Self::TacticalIlluminator => {
effects.spawn_and_insert(
SimpleStatBonus::<WeaponAccuracy>::new("tactical illuminator", -5.0 / 50.0),
target,
TurnLimitedEffect::new(target, 1),
);
}
}
}
}
/// Effects that are triggered by selecting a weapon
#[derive(Component)]
pub enum TurnTriggeredEffect {
Mod(TurnTriggeredMod),
Bonus {
value: f32,
bonus: TurnTriggeredBonus,
},
}
impl TurnTriggeredEffect {
pub fn trigger(&self, effects: &mut Effects, current: Entity, target: Entity) {
match self {
TurnTriggeredEffect::Mod(weapon_mod) => {
weapon_mod.trigger(effects, current, target);
}
TurnTriggeredEffect::Bonus { value, bonus } => {
bonus.trigger(*value, effects, current, target)
}
}
}
}
#[derive(Clone, Copy)]
pub enum FirstTurnMod {
AdjustableTrigger,
HairTrigger,
}
impl FirstTurnMod {
fn spawn(self, effects: &mut Effects, weapon: Entity, owner: Entity) {
match self {
Self::AdjustableTrigger => effects.spawn_and_insert(
SimpleStatBonus::<WeaponAccuracy>::new("adjustable trigger", 5.0 / 50.0),
weapon,
TurnLimitedEffect::new(owner, 1),
),
Self::HairTrigger => effects.spawn_and_insert(
SimpleStatBonus::<WeaponAccuracy>::new("hair trigger", 7.5 / 50.0),
weapon,
TurnLimitedEffect::new(owner, 1),
),
};
}
}
#[derive(Component)]
pub enum FirstTurnEffect {
Mod(FirstTurnMod),
Bonus { value: f32, bonus: FirstTurnBonus },
}
#[derive(Component)]
pub enum DamageProcEffect {
MultiTurn { value: f32, bonus: MultiTurnBonus },
}
impl FirstTurnEffect {
fn spawn(&self, effects: &mut Effects, weapon: Entity, owner: Entity) {
match self {
Self::Mod(weapon_mod) => weapon_mod.spawn(effects, weapon, owner),
Self::Bonus { value, bonus } => bonus.spawn(effects, weapon, owner, *value),
};
}
}
#[derive(Component)]
pub struct EquippedMods(pub Vec<WeaponMod>);
#[derive(Component)]
pub struct Experience(pub f32);
#[derive(LogMessage)]
pub struct ReloadWeapon {
#[log(player)]
pub actor: Entity,
#[log(weapon)]
pub weapon: Entity,
}
#[derive(LogMessage)]
pub struct MissTarget {
#[log(player)]
pub actor: Entity,
#[log(player)]
pub recipient: Entity,
#[log(weapon)]
pub weapon: Entity,
pub rounds: Option<u16>,
}
#[derive(Bundle)]
pub struct WeaponBundle {
pub usable: Usable,
pub weapon: Weapon,
pub name: Name,
pub id: Id,
pub verb: WeaponVerb,
pub slot: WeaponSlot,
}
#[derive(Bundle)]
pub struct DamagingWeaponBundle {
pub crit_rate: SimpleStatBundle<CritRate>,
pub dmg: DamageStat,
pub acc: SimpleStatBundle<WeaponAccuracy>,
pub dmg_bonus: SimpleStatBundle<DamageBonus>,
pub equipped_mods: EquippedMods,
pub experience: Experience,
pub category: WeaponCategory,
}
#[derive(Bundle)]
pub struct AmmoWeaponBundle {
pub ammo: Ammo,
pub clips: SimpleStatBundle<Clips>,
pub clip_size: SimpleStatBundle<ClipSize>,
pub rate_of_fire: RateOfFire,
pub ammo_control: SimpleStatBundle<AmmoControl>,
}
impl WeaponBundle {
pub fn new(name: String, id: usize, verb: WeaponVerb, slot: WeaponSlot) -> Self {
Self {
usable: Usable,
weapon: Weapon,
name: Name(name),
id: Id(id),
verb,
slot,
}
}
pub fn fists(id: usize) -> Self {
Self::new("Fists".to_owned(), id, WeaponVerb::Hit, WeaponSlot::Fists)
}
pub fn kick(id: usize) -> Self {
Self::new("Kick".to_owned(), id, WeaponVerb::Kicked, WeaponSlot::Kick)
}
}
impl DamagingWeaponBundle {
pub fn new(
dmg: f32,
acc: f32,
mods: Vec<WeaponMod>,
exp: f32,
category: WeaponCategory,
) -> Self {
Self {
crit_rate: SimpleStatBundle::new(0),
dmg: DamageStat(dmg / 10.0),
acc: SimpleStatBundle::new((acc - 50.0) / 50.0),
dmg_bonus: SimpleStatBundle::new(1.0),
equipped_mods: EquippedMods(mods),
experience: Experience(exp),
category,
}
}
pub fn fists() -> Self {
// NOTE: The accuracy value is taken from the attack page. The damage value here differs
// from the one in Proxima's simulator, but in some quick tests 10.0 proofed to be a better fit
// This might have changed in the weapon damage update
Self::new(10.0, 50.0, Vec::default(), 0.0, WeaponCategory::HandToHand)
}
pub fn kick() -> Self {
// NOTE: The accuracy value is taken from the attack page. The damage value here differs
// from the one in Proxima's simulator, but in some quick tests 30.0 proofed to be a better fit
// This might have changed in the weapon damage update
Self::new(30.0, 40.0, Vec::default(), 0.0, WeaponCategory::HandToHand)
}
}
impl AmmoWeaponBundle {
pub fn new(clips: u16, clip_size: u16, rof: [u16; 2]) -> Self {
Self {
ammo: Ammo(clip_size),
clips: SimpleStatBundle::new(clips - 1),
clip_size: SimpleStatBundle::new(clip_size),
rate_of_fire: RateOfFire(rof),
ammo_control: SimpleStatBundle::new(0.0),
}
}
}
fn set_owner(weapons_q: Query<(Entity, &Weapons)>, mut commands: Commands) {
for (player, weapons) in weapons_q.iter() {
if let Some(primary) = weapons.primary {
commands.entity(primary).set_parent(player);
}
if let Some(secondary) = weapons.secondary {
commands.entity(secondary).set_parent(player);
}
if let Some(melee) = weapons.melee {
commands.entity(melee).set_parent(player);
}
if let Some(temp) = weapons.temporary {
commands.entity(temp).set_parent(player);
}
commands.entity(weapons.fists.unwrap()).set_parent(player);
if let Some(kick) = weapons.kick {
commands.entity(kick).set_parent(player);
}
}
}
fn apply_passives(
weapon_q: Query<(
Entity,
&EquippedMods,
&Experience,
&WeaponCategory,
&WeaponSlot,
Has<Japanese>,
&Parent,
)>,
player_q: Query<(&Merits, &Education, &FactionUpgrades)>,
mut effects: Effects,
mut commands: Commands,
) {
for (weapon, mods, exp, cat, slot, japanese, player) in weapon_q.iter() {
let exp = (exp.0 / 10.0).round();
if exp > 0.0 {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new("experience", exp * 0.2 / 50.0),
weapon,
);
effects.spawn(
SimpleStatBonus::<DamageBonus>::new("experience", exp / 100.0),
weapon,
);
}
for w_mod in &mods.0 {
match w_mod {
WeaponMod::ReflexSight => {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new("reflex sight", 1.0 / 50.0),
weapon,
);
}
WeaponMod::HolographicSight => {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new("holographic sight", 1.25 / 50.0),
weapon,
);
}
WeaponMod::AcogSight => {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new("ACOG sight", 1.5 / 50.0),
weapon,
);
}
WeaponMod::ThermalSight => {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new("thermal sight", 1.75 / 50.0),
weapon,
);
}
WeaponMod::Laser1mw => {
effects.spawn(SimpleStatBonus::<CritRate>::new("1mw laser", 4), weapon);
}
WeaponMod::Laser5mw => {
effects.spawn(SimpleStatBonus::<CritRate>::new("5mw laser", 6), weapon);
}
WeaponMod::Laser30mw => {
effects.spawn(SimpleStatBonus::<CritRate>::new("30mw laser", 8), weapon);
}
WeaponMod::Laser100mw => {
effects.spawn(SimpleStatBonus::<CritRate>::new("100mw laser", 10), weapon);
}
WeaponMod::SmallSuppressor => {
effects.spawn(
SimpleStatBonus::<DamageBonus>::new("small suppressor", -0.05),
weapon,
);
}
WeaponMod::StandardSuppressor => {
effects.spawn(
SimpleStatBonus::<DamageBonus>::new("standard suppressor", -0.05),
weapon,
);
}
WeaponMod::LargeSuppressor => {
effects.spawn(
SimpleStatBonus::<DamageBonus>::new("large suppressor", -0.05),
weapon,
);
}
WeaponMod::ExtendedMags => {
effects.spawn(
SimpleStatBonus::<ClipSize>::new("extended mags", 1.2),
weapon,
);
}
WeaponMod::HighCapacityMags => {
effects.spawn(
SimpleStatBonus::<ClipSize>::new("high capacity mags", 1.3),
weapon,
);
}
WeaponMod::CustomGrip => {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new("custom grip", 0.75),
weapon,
);
}
WeaponMod::SkeetChoke => {
effects.spawn(
SimpleStatBonus::<DamageBonus>::new("skeet choke", 0.06),
weapon,
);
}
WeaponMod::ImprovedChoke => {
effects.spawn(
SimpleStatBonus::<DamageBonus>::new("improved choke", 0.08),
weapon,
);
}
WeaponMod::FullChoke => {
effects.spawn(
SimpleStatBonus::<DamageBonus>::new("full choke", 0.10),
weapon,
);
}
WeaponMod::ExtraClip => {
effects.spawn(SimpleStatBonus::<Clips>::new("extra clip", 1), weapon);
}
WeaponMod::ExtraClip2 => {
effects.spawn(SimpleStatBonus::<Clips>::new("extra clip x2", 2), weapon);
}
WeaponMod::RecoilPad => {
effects.spawn(
SimpleStatBonus::<AmmoControl>::new("recoil pad", 0.24),
weapon,
);
}
WeaponMod::StandardBrake => {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new("standard brake", 1.00 / 50.0),
weapon,
);
}
WeaponMod::HeavyDutyBreak => {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new("heavy duty brake", 1.25 / 50.0),
weapon,
);
}
WeaponMod::TacticalBrake => {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new("tactical brake", 1.50 / 50.0),
weapon,
);
}
WeaponMod::Bipod => {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new("bipod", 1.75 / 50.0),
weapon,
);
commands
.spawn(TurnTriggeredEffect::Mod(TurnTriggeredMod::Bipod))
.set_parent(weapon);
}
WeaponMod::Tripod => {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new("bipod", 2.00 / 50.0),
weapon,
);
commands
.spawn(TurnTriggeredEffect::Mod(TurnTriggeredMod::Tripod))
.set_parent(weapon);
}
WeaponMod::SmallLight => {
commands
.spawn(TurnTriggeredEffect::Mod(TurnTriggeredMod::SmallLight))
.set_parent(weapon);
}
WeaponMod::PrecisionLight => {
commands
.spawn(TurnTriggeredEffect::Mod(TurnTriggeredMod::PrecisionLight))
.set_parent(weapon);
}
WeaponMod::TacticalIlluminator => {
commands
.spawn(TurnTriggeredEffect::Mod(
TurnTriggeredMod::TacticalIlluminator,
))
.set_parent(weapon);
}
WeaponMod::AdjustableTrigger => {
commands
.spawn(FirstTurnEffect::Mod(FirstTurnMod::AdjustableTrigger))
.set_parent(weapon);
}
WeaponMod::HairTrigger => {
commands
.spawn(FirstTurnEffect::Mod(FirstTurnMod::HairTrigger))
.set_parent(weapon);
}
}
}
let (merits, education, faction) = player_q.get(player.get()).unwrap();
let (mastery, edu_acc) = match cat {
WeaponCategory::HeavyArtillery => (
merits.heavy_artillery_mastery,
education.cbt2860.then_some("CBT2860"),
),
WeaponCategory::MachineGun => (
merits.machine_gun_mastery,
education.cbt2830.then_some("CTB2830"),
),
WeaponCategory::Rifle => (merits.rifle_mastery, education.cbt2850.then_some("CBT2850")),
WeaponCategory::Smg => (merits.smg_mastery, education.cbt2830.then_some("CBT2830")),
WeaponCategory::Shotgun => (
merits.shotgun_mastery,
education.cbt2125.then_some("CBT2125"),
),
WeaponCategory::Pistol => (
merits.pistol_mastery,
education.cbt2840.then_some("CBT2840"),
),
WeaponCategory::Club => (merits.club_mastery, None),
WeaponCategory::Piercing => (merits.piercing_mastery, None),
WeaponCategory::Slashing => (merits.slashing_mastery, None),
WeaponCategory::Mechanical => (merits.mechanical_mastery, None),
WeaponCategory::Temporary => {
if education.gen2119 {
effects.spawn(SimpleStatBonus::<DamageBonus>::new("GEN2119", 0.05), weapon);
}
(
merits.temporary_mastery,
education.gen2116.then_some("GEN2116"),
)
}
WeaponCategory::HandToHand => (0, None),
};
if let Some(label) = edu_acc {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new(label, 1.0 / 50.0),
weapon,
);
}
// NOTE: Even though it says "weapons" in the description, this also applies to h2h
if education.bio2350 {
effects.spawn(SimpleStatBonus::<DamageBonus>::new("BIO2350", 0.01), weapon);
}
if education.def3770 && *slot == WeaponSlot::Fists {
effects.spawn(SimpleStatBonus::<DamageBonus>::new("DEF3770", 1.0), weapon);
}
if education.his2170 && *slot == WeaponSlot::Melee {
effects.spawn(SimpleStatBonus::<DamageBonus>::new("HIS2170", 0.02), weapon);
}
if matches!(*slot, WeaponSlot::Primary | WeaponSlot::Secondary) {
if education.mth2310 {
effects.spawn(SimpleStatBonus::<AmmoControl>::new("MTH2310", 0.05), weapon);
}
if education.mth3330 {
effects.spawn(SimpleStatBonus::<AmmoControl>::new("MTH3330", 0.20), weapon);
}
}
if education.his2160 && japanese {
effects.spawn(SimpleStatBonus::<DamageBonus>::new("HIS2160", 0.10), weapon);
}
if mastery > 0 {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new("mastery", (mastery as f32) * 0.2 / 50.0),
weapon,
);
effects.spawn(
SimpleStatBonus::<DamageBonus>::new("mastery", (mastery as f32) / 100.0),
weapon,
);
}
if faction.dmg > 0 {
effects.spawn(
SimpleStatBonus::<DamageBonus>::new("faction", (faction.dmg as f32) * 0.01),
weapon,
);
}
if faction.acc > 0 {
effects.spawn(
SimpleStatBonus::<WeaponAccuracy>::new(
"faction",
(faction.dmg as f32) * 0.2 / 50.0,
),
weapon,
);
}
}
}
fn unset_current(weapon_q: Query<Entity, (With<Current>, With<Weapon>)>, mut commands: Commands) {
for weapon in weapon_q.iter() {
commands.entity(weapon).remove::<Current>();
}
}
// TODO: Move all mitigation aspects of this into the player system
pub fn use_damaging_weapon(
mut rng: ResMut<Rng>,
weapon_q: Query<
(
Entity,
&DamageStat,
&SimpleStatEffective<WeaponAccuracy>,
&SimpleStatEffective<DamageBonus>,
&SimpleStatEffective<CritRate>,
Has<NonTargeted>,
),
(With<Weapon>, With<Current>, Without<NeedsReload>),
>,
player_q: Query<
(
Entity,
&EffectiveStat<Speed>,
&SimpleStatEffective<CritRate>,
&SimpleStatEffective<WeaponAccuracy>,
&SimpleStatEffective<DamageBonus>,
),
(With<Player>, With<Current>),
>,
target_q: Query<(Entity, &EffectiveStat<Dexterity>), With<CurrentTarget>>,
(mut ammo_q, mut temp_q): (
Query<(
&mut Ammo,
&SimpleStatEffective<Clips>,
&RateOfFire,
&SimpleStatEffective<AmmoControl>,
)>,
Query<&mut Uses>,
),
mut hit_events: EventWriter<InitiateHit>,
(mut logger, mut commands, metrics): (Logger, Commands, Res<Metrics>),
) {
let Ok((weapon, dmg, acc, dmg_bonus, crit, non_targeted)) = weapon_q.get_single() else {
return;
};
let (player, player_spd, player_crit, acc_bonus, p_dmg_bonus) = player_q.single();
let (target, target_dex) = target_q.single();
if let Ok(mut uses) = temp_q.get_mut(weapon) {
uses.0 -= 1;
if uses.0 == 0 {
commands.entity(weapon).remove::<Usable>();
}
}
let spd_dex_ratio = (player_spd.value / target_dex.value).clamp(1.0 / 64.0, 64.0);
let base_hit_chance = if spd_dex_ratio < 1.0 {
0.5 * (8.0 * spd_dex_ratio.sqrt() - 1.0) / 7.0
} else {
1.0 - 0.5 * (8.0 / spd_dex_ratio.sqrt() - 1.0) / 7.0
};
let mut acc_eff = acc + acc_bonus;
let mut ammo = ammo_q
.get_mut(weapon)
.ok()
.map(|(ammo, clips, rof, ammo_ctrl)| {
let ammo_ctrl = 1.0 - (ammo_ctrl).value;
let rof_eff = ((rof.0[0] as f32) * ammo_ctrl)..((rof.0[1] as f32) * ammo_ctrl);
(ammo, clips, rof_eff)
});
enum MultiAttack {
Blindfire,
Rage(usize),
Fury(usize),
DoubleTap { fired_first: bool },
}
let mut multi_attack_proc = None;
let crit = player_crit + crit;
loop {
let rounds = ammo.as_mut().map(|(ref mut ammo, clips, rof)| {
let rounds = (rng.gen_range(rof.clone()).round() as u16).clamp(1, ammo.0);
metrics.increment_counter(player, "rounds_fired", rounds.into());
metrics.increment_counter(weapon, "rounds_fired", rounds.into());
ammo.0 -= rounds;
if ammo.0 == 0 {
if clips.value == 0 {
commands.entity(weapon).remove::<Usable>();
} else {
commands.entity(weapon).insert(NeedsReload);
}
}
rounds
});
let hit_chance = if base_hit_chance < 0.5 {
base_hit_chance + acc_eff.value * base_hit_chance
} else {
base_hit_chance + acc_eff.value * (1.0 - base_hit_chance)
};
if hit_chance <= 1.0 && !rng.gen_bool(hit_chance as f64) {
logger.log(|| MissTarget {
weapon,
actor: player,
recipient: target,
rounds,
});
metrics.increment_counter(player, "miss", 1);
metrics.increment_counter(weapon, "miss", 1);
if multi_attack_proc.is_none() {
return;
};
} else {
let body_part = if !non_targeted {
rng.sample(crit)
} else {
BodyPart::Stomach
};
hit_events.send(InitiateHit {
body_part,
weapon,
rounds,
dmg: dmg.0,
dmg_bonus_weapon: dmg_bonus.value,
dmg_bonus_player: p_dmg_bonus.value,
hit_chance,
crit_rate: crit.value,
});
}
match multi_attack_proc {
Some(MultiAttack::Blindfire) => acc_eff.value -= 5.0 / 50.0,
Some(MultiAttack::Rage(turns @ 1..)) => {
multi_attack_proc = Some(MultiAttack::Rage(turns - 1))
}
Some(MultiAttack::Fury(turns @ 1..)) => {
multi_attack_proc = Some(MultiAttack::Fury(turns - 1))
}
Some(MultiAttack::DoubleTap { fired_first: false }) => {
multi_attack_proc = Some(MultiAttack::DoubleTap { fired_first: true })
}
_ => break,
}
}
/*
let hit_chance = if base_hit_chance < 0.5 {
base_hit_chance + acc_eff.value * base_hit_chance
} else {
base_hit_chance + acc_eff.value * (1.0 - base_hit_chance)
};
if hit_chance <= 1.0 && !rng.gen_bool(hit_chance as f64) {
logger.log(|| MissTarget {
weapon,
actor: player,
recipient: target,
rounds: rounds.map(|(rounds, _)| rounds),
});
metrics.increment_counter(player, "miss", 1);
metrics.increment_counter(weapon, "miss", 1);
return;
}
let crit = player_crit + crit;
let body_part = if !non_targeted {
rng.sample(crit)
} else {
BodyPart::Stomach
};
let def_str_ratio = (target_def.value / player_str.value).clamp(1.0 / 32.0, 14.0);
let mitigation = if def_str_ratio < 1.0 {
0.5 * def_str_ratio.log(32.0) + 0.5
} else {
0.5 * def_str_ratio.log(14.0) + 0.5
};
let dmg_intrinsic = 7.0 * (player_str.value / 10.0).log10().powi(2)
+ 27.0 * (player_str.value / 10.0).log10()
+ 30.0;
init_hit.send(InitiateHit {
body_part,
weapon,
rounds: rounds.map(|(rounds, _)| rounds),
crit_rate: crit.value,
dmg: dmg.0,
dmg_bonus_weapon: (dmg_bonus + p_dmg_bonus).value,
dmg_intrinsic,
def_mitigation: mitigation,
hit_chance,
}); */
}
fn reload_weapon(
mut weapon_q: Query<
(
Entity,
&Parent,
&mut Ammo,
&mut SimpleStatEffective<Clips>,
&SimpleStatEffective<ClipSize>,
),
(With<NeedsReload>, With<Current>),
>,
mut commands: Commands,
mut logger: Logger,
) {
for (weapon, player, mut ammo, mut clips, clip_size) in weapon_q.iter_mut() {
ammo.0 = clip_size.value;
clips.value -= 1;
logger.log(|| ReloadWeapon {
actor: player.get(),
weapon,
});
commands.entity(weapon).remove::<NeedsReload>();
}
}
fn restore_usability(weapon_q: Query<Entity, With<Weapon>>, mut commands: Commands) {
for weapon in weapon_q.iter() {
commands.entity(weapon).insert(Usable);
}
}
fn restore_ammo(
mut ammo_q: Query<(Entity, &mut Ammo, &SimpleStatEffective<ClipSize>)>,
mut commands: Commands,
) {
for (weapon, mut ammo, clip_size) in ammo_q.iter_mut() {
ammo.0 = clip_size.value;
commands.entity(weapon).remove::<NeedsReload>();
}
}
fn apply_first_turn_effects(
effect_q: Query<(&Parent, &FirstTurnEffect)>,
player_q: Query<&Parent>,
mut effects: Effects,
) {
for (weapon, effect) in effect_q.iter() {
let player = player_q.get(weapon.get()).unwrap();
effect.spawn(&mut effects, weapon.get(), player.get());
}
}
pub(crate) fn configure(stages: &mut Stages) {
stages.equip.add_systems(set_owner);
stages.pre_fight.add_systems((
apply_passives,
apply_first_turn_effects
.after(apply_passives)
.after(bonus::prepare_bonuses),
));
stages.turn.add_systems(reload_weapon);
stages.post_turn.add_systems(unset_current);
stages
.restore
.add_systems((restore_ammo, restore_usability, apply_first_turn_effects));
temp::configure(stages);
bonus::configure(stages);
}