557 lines
19 KiB
Rust
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);
|
|
}
|