using System; using System.Collections; using System.Collections.Generic; using MessagePack; using Newtonsoft.Json; using CultMath; using static CultMath.math; [Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable] public class ChargedWeaponData : InstantWeaponData { [Inspectable, JsonProperty("chargeTime"), Key(21), RuntimeInspectable] public PerformanceStat ChargeTime = new PerformanceStat(); [Inspectable, JsonProperty("chargeEnergy"), Key(22), RuntimeInspectable] public PerformanceStat ChargeEnergy = new PerformanceStat(); [Inspectable, JsonProperty("chargeHeat"), Key(23), RuntimeInspectable] public PerformanceStat ChargeHeat = new PerformanceStat(); [Inspectable, JsonProperty("canFireEarly"), Key(24)] public bool CanFireEarly; [Inspectable, JsonProperty("failureCharge"), Key(25)] public float FailureCharge; [Inspectable, JsonProperty("failureDamage"), Key(26)] public float FailureDamage = 1; [Inspectable, JsonProperty("chargeDamage"), Key(27)] public float ChargeFiringDamageMultiplier = 1; [Inspectable, JsonProperty("chargeSpread"), Key(28)] public float ChargeFiringSpreadMultiplier = 1; [Inspectable, JsonProperty("chargeBurstCount"), Key(29)] public float ChargeFiringBurstCountMultiplier = 1; [Inspectable, JsonProperty("chargeVisibility"), Key(30)] public float ChargeFiringVisibilityMultiplier = 1; [Inspectable, JsonProperty("chargeVelocity"), Key(31)] public float ChargeFiringVelocityMultiplier = 1; [Inspectable, JsonProperty("chargeHeatMul"), Key(32)] public float ChargeFiringHeatMultiplier = 1; public override Behavior CreateInstance(EquippedItem item) { return new ChargedWeapon(this, item); } public override Behavior CreateInstance(ConsumableItemEffect item) { return new ChargedWeapon(this, item); } // The one formula of a charged weapon's damage per second at a range (see InstantWeaponData.RangeDamagePerSecond). internal static float RangeDamagePerSecond(float damage, float chargeFiringDamageMultiplier, float cooldown, float chargeTime, float rangeFactor) => damage * chargeFiringDamageMultiplier * rangeFactor / (cooldown + chargeTime); public override float RatedRangeDamagePerSecond(float range) => RangeDamagePerSecond(PerRound(Damage.Max, Count.Max), ChargeFiringDamageMultiplier, Cooldown.Max, ChargeTime.Max, RatedRangeFactor(range)); } public class ChargedWeapon : InstantWeapon { private ChargedWeaponData _data; private bool _charging; private bool _charged; private float _charge; private float _progress; public float ChargeTime { get; protected set; } public float ChargeEnergy { get; protected set; } public float ChargeHeat { get; protected set; } // Cut 4 (docs/stats-and-power-cut.md, Cut 4): "its charge cycle already is a buffer, and ChargeEnergy // should become the fill rate rather than a separate concept." ChargeEnergy was authored but never spent // by anything (Trigger() always paid the base Weapon.Energy per shot); it becomes this weapon's input // capacitor rate instead of the InstantWeapon default (Capacity / Cooldown), so a charged weapon's own // authored charge-up speed IS its sustained rate of fire, not a second, disconnected number. protected override float RateOverride => Evaluate(_data.ChargeEnergy); // The charge multiplier lives in the formula alone: Damage is lerped by charge for the shot itself (UpdateStats), so the // live reads take the uncharged damage, and a nominal weapon reads its design's rated figure at every charge. private float _unchargedDamage; public override float DamagePerSecond => _unchargedDamage * _data.ChargeFiringDamageMultiplier / (Cooldown + ChargeTime); public override float RangeDamagePerSecond(float range) { return ChargedWeaponData.RangeDamagePerSecond(_unchargedDamage, _data.ChargeFiringDamageMultiplier, Cooldown, ChargeTime, RangeFactor(range)); } public event Action OnStartCharging; public event Action OnStopCharging; public event Action OnCharged; public event Action OnFailed; public override void ResetEvents() { base.ResetEvents(); OnStartCharging = null; OnStopCharging = null; OnCharged = null; OnFailed = null; } public override float Progress => saturate(_charging ? _charge : _cooldown); public float Charge { get => _charge; } public ChargedWeapon(ChargedWeaponData data, EquippedItem item) : base(data, item) { _data = data; } public ChargedWeapon(ChargedWeaponData data, ConsumableItemEffect item) : base(data, item) { _data = data; } protected override void UpdateStats() { base.UpdateStats(); ChargeTime = Evaluate(_data.ChargeTime); ChargeEnergy = Evaluate(_data.ChargeEnergy); ChargeHeat = Evaluate(_data.ChargeHeat); _unchargedDamage = Damage; Damage *= lerp(1, _data.ChargeFiringDamageMultiplier, saturate(_charge)); Heat *= lerp(1, _data.ChargeFiringHeatMultiplier, saturate(_charge)); Spread *= lerp(1, _data.ChargeFiringSpreadMultiplier, saturate(_charge)); BurstCount *= lerp(1, _data.ChargeFiringBurstCountMultiplier, saturate(_charge)); Visibility *= lerp(1, _data.ChargeFiringVisibilityMultiplier, saturate(_charge)); Velocity *= lerp(1, _data.ChargeFiringVelocityMultiplier, saturate(_charge)); } public override bool Execute(float dt) { if (_charging) { _charge += dt / ChargeTime; if (!_charged) { AddHeat(ChargeHeat * (dt / ChargeTime)); if(_charge > 1) { _charged = true; OnCharged?.Invoke(); } } if (_data.FailureCharge > 1 && _charge > _data.FailureCharge) { _charging = false; _cooldown = 1; _coolingDown = true; _charge = 0; OnFailed?.Invoke(); CauseDamage(_data.FailureDamage); } } return base.Execute(dt); } public override void Activate() { // CanFire also holds a weapon whose released charge has a burst pending: its cooldown starts at its first round, // so a charge started before then would overlap it. // A refused weapon never starts charging: it would pay charge heat and audio for a shot Trigger drops. if(!_charging && CanFire && !FireControl.Refuses(this, Entity)) { OnStartCharging?.Invoke(); _charging = true; _charged = false; } } public override void Deactivate() { if (_charging) { if (_data.CanFireEarly || _charge > 1) { Trigger(); _charge = 0; } OnStopCharging?.Invoke(); _charging = false; } } }