proxisim/src/weapon/mod.rs
2025-11-03 16:36:45 +01:00

557 lines
19 KiB
Rust

use bevy_ecs::prelude::*;
use proxisim_models::{
bundle::{
passive::{Education, EducationPartDamageBonus, FactionUpgrades, Merits},
player::{Current, PartDamageBonus, Weapons},
stat::{
AdditiveBonus, AmmoControl, ClipSize, Clips, CritRate, DamageBonus, Dexterity,
SimpleStatBonus, SimpleStatEffective, WeaponAccuracy,
},
weapon::{
Ammo, EquippedMods, Experience, Japanese, NeedsReload, Usable, Weapon, WeaponCategory,
WeaponMod, WeaponSlot,
},
},
hierarchy::{HierarchyBuilder, Parent},
};
use crate::{
effect::{Effects, TurnLimitedEffect},
log,
log::Logger,
Stages,
};
use self::bonus::{
FirstTurnBonus, MultiTurnBonus, OpponentStatusEffect, SelfStatusEffect, TurnTriggeredBonus,
};
pub mod bonus;
pub mod temp;
#[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 },
}
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 enum DamageProcEffect {
MultiTurn {
value: f32,
bonus: MultiTurnBonus,
},
OpponentEffect {
value: f32,
bonus: OpponentStatusEffect,
},
SelfEffect {
value: f32,
bonus: SelfStatusEffect,
},
}
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).set_parent(player);
commands.entity(weapons.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::HeavyDutyBrake => {
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::Clubbing => (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 education.bio2380 {
commands
.spawn(PartDamageBonus::Education(
EducationPartDamageBonus::Bio2380,
))
.set_parent(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>();
}
}
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;
log!(logger, "reload_weapon", {
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);
// running this in the snapshot layer ensures that the stat increases aren't restored at the
// end of the run
stages.snapshot.add_systems(apply_first_turn_effects);
stages.pre_fight.add_systems(apply_passives);
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);
}