private static float GetAccuracy(Thing weapon, VerbProperties verb, ProjectileProperties projectile, float dist, Pawn pawn = null) { float forcedMissRadius = CalculateAdjustedForcedMissDist(verb.ForcedMissRadius, dist); float baseAimOn = verb.GetHitChanceFactor(weapon, dist); if (pawn != null) { baseAimOn *= ShotReport.HitFactorFromShooter(pawn, dist); } int affectedCellCount = (verb.CausesExplosion) ? GenRadial.NumCellsInRadius(projectile.explosionRadius) : 1; float accuracy; if (forcedMissRadius > 0.5f) { int affectableCellCount = GenRadial.NumCellsInRadius(forcedMissRadius); accuracy = (float)affectedCellCount / affectableCellCount; } else { float medianToWildRadius = ShootTuning.MissDistanceFromAimOnChanceCurves.Evaluate(baseAimOn, 0.5f); float indirectHitChance = (float)(affectedCellCount - 1) / GenRadial.NumCellsInRadius(medianToWildRadius); accuracy = baseAimOn + (1f - baseAimOn) * indirectHitChance; } return(Mathf.Clamp01(accuracy)); }
/// <summary> /// Gets the adjusted hit chance factor of a shot. This is equivalent to shootVerb.GetHitChanceFactor() unless /// a shooter is provided, in which case it will also be adjusted based on the shooter's hit chance. /// /// This value can be greater than 1.0 in the case of weapons with overcapped accuracy. /// </summary> /// <returns>The adjusted hit chance factor.</returns> /// <param name="range">The range of the shot.</param> /// <param name="shooter">(Optional) The turret or pawn shooting the weapon.</param> public float GetAdjustedHitChanceFactor(float range, Thing shooter = null) { float hitChance = shootVerb.GetHitChanceFactor(weapon, range); if (shooter != null) { hitChance *= ShotReport.HitFactorFromShooter(shooter, range); } return(hitChance); }
public override string GetExplanationFinalizePart(StatRequest req, ToStringNumberSense numberSense, float finalVal) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 5; i <= 45; i += 5) { float f = ShotReport.HitFactorFromShooter(finalVal, i); stringBuilder.AppendLine("distance".Translate().CapitalizeFirst() + " " + i.ToString() + ": " + f.ToStringPercent("F1")); } stringBuilder.AppendLine(base.GetExplanationFinalizePart(req, numberSense, finalVal)); return(stringBuilder.ToString()); }
public float GetAimOnTargetChance() { var distance = (Target - Origin).LengthHorizontal; var factorFromShooterAndDist = ShotReport.HitFactorFromShooter(Caster, distance); var factorFromEquipment = _weaponVerb.verbProps.GetHitChanceFactor( _weaponVerb.EquipmentSource, distance); var factorFromWeather = 1f; if (!Caster.Position.Roofed(CasterMap) || !Target.Roofed(CasterMap)) { factorFromWeather = CasterMap.weatherManager.CurWeatherAccuracyMultiplier; } ThingDef coveringGas = null; foreach (var point in GenSight.PointsOnLineOfSight(Origin, Target)) { if (!point.CanBeSeenOver(CasterMap)) { break; } Thing gas = point.GetGas(CasterMap); if (IsThisGasMorePenalisingThan(gas, coveringGas)) { coveringGas = gas.def; } } var factorFromCoveringGas = 1f - (coveringGas?.gas.accuracyPenalty ?? 0f); var result = factorFromShooterAndDist * factorFromEquipment * factorFromWeather * factorFromCoveringGas; if (result < 0.0201f) { result = 0.0201f; } return(result); }
public override float GetValueUnfinalized(StatRequest req, bool applyPostProcess = true) { if (!(req.Def is ThingDef def)) { return(0.0f); } CompEquippable equipComp = null; CompLootAffixableThing lootComp = null; if (req.HasThing) { var thing = (ThingWithComps)req.Thing; equipComp = thing.TryGetComp <CompEquippable>(); lootComp = thing.TryGetComp <CompLootAffixableThing>(); } Verb verb = equipComp?.AllVerbs.First(v => v.verbProps.isPrimary); VerbProperties verbProps = verb != null ? verb.verbProps : def.Verbs.First(vp => vp.isPrimary); Pawn attacker = req.HasThing ? GetCurrentWeaponUser(req.Thing) : null; var projProps = verbProps.defaultProjectile.projectile; var projModifier = (LootAffixModifier_VerbPropertiesChange_Def)lootComp?.AllModifiers.FirstOrFallback( lam => lam.AppliesTo == ModifierTarget.VerbProperties && lam is LootAffixModifier_VerbPropertiesChange_Def lamVPCD && lamVPCD.affectedField == "defaultProjectile" ); ThingDef modProjectile = projModifier != null ? (ThingDef)projModifier.resolvedDef : null; var modProjProps = modProjectile?.projectile; float chance = modProjProps != null ? 1f - projModifier.GetRealChance(lootComp.parent) : 1f; if (chance <= 0.05f) { chance = 1f; // already permanently set to "base" verbProps } float baseDamage = projProps.damageDef.harmsHealth == false ? 0f : req.HasThing ? projProps.GetDamageAmount(req.Thing) : req.StuffDef != null?projProps.GetDamageAmount(def.GetStatValueAbstract(StatDefOf.RangedWeapon_DamageMultiplier, req.StuffDef)) : projProps.GetDamageAmount(def.GetStatValueAbstract(StatDefOf.RangedWeapon_DamageMultiplier)) ; float damage = baseDamage * verbProps.burstShotCount * chance; if (chance < 1f) { float modChance = 1f - chance; float modBaseDamage = modProjProps.damageDef.harmsHealth == false ? 0f : modProjProps.GetDamageAmount(req.Thing) ; damage += modBaseDamage * verbProps.burstShotCount * modChance; } // FIXME: Confirm warmupTime (and AimingDelayFactor) is used in a full shot cycle // FIXME: warmupTime * this.CasterPawn.GetStatValue(StatDefOf.AimingDelayFactor, true)).SecondsToTicks() float secondsSpent = 0; if (verb != null) { secondsSpent = verbProps.AdjustedFullCycleTime(verb, attacker); } else { secondsSpent = verbProps.warmupTime + ((verbProps.burstShotCount - 1) * verbProps.ticksBetweenBurstShots).TicksToSeconds(); secondsSpent += req.HasThing ? req.Thing.GetStatValue(StatDefOf.RangedWeapon_Cooldown, true) : req.StuffDef != null?def.GetStatValueAbstract(StatDefOf.RangedWeapon_Cooldown, req.StuffDef) : def.GetStatValueAbstract(StatDefOf.RangedWeapon_Cooldown) ; } // Every integer range possible as an average float avgAccuracy = 0; for (int i = 3; i <= verbProps.range; i++) { float rngAccuracy = verbProps.GetHitChanceFactor(req.Thing, i); if (attacker != null) { rngAccuracy *= ShotReport.HitFactorFromShooter(attacker, i); } avgAccuracy += rngAccuracy; } if (verbProps.range >= 3) { avgAccuracy /= verbProps.range - 2; } return(secondsSpent == 0 ? 0.0f : damage / secondsSpent * avgAccuracy); }
public override string GetExplanationUnfinalized(StatRequest req, ToStringNumberSense numberSense) { if (!(req.Def is ThingDef def)) { return(null); } /* Damage section */ CompEquippable equipComp = null; CompLootAffixableThing lootComp = null; if (req.HasThing) { var thing = (ThingWithComps)req.Thing; equipComp = thing.TryGetComp <CompEquippable>(); lootComp = thing.TryGetComp <CompLootAffixableThing>(); } Verb verb = equipComp?.AllVerbs.First(v => v.verbProps.isPrimary); VerbProperties verbProps = verb != null ? verb.verbProps : def.Verbs.First(vp => vp.isPrimary); Pawn attacker = req.HasThing ? GetCurrentWeaponUser(req.Thing) : null; var projProps = verbProps.defaultProjectile.projectile; var projModifier = (LootAffixModifier_VerbPropertiesChange_Def)lootComp?.AllModifiers.FirstOrFallback( lam => lam.AppliesTo == ModifierTarget.VerbProperties && lam is LootAffixModifier_VerbPropertiesChange_Def lamVPCD && lamVPCD.affectedField == "defaultProjectile" ); ThingDef modProjectile = projModifier != null ? (ThingDef)projModifier.resolvedDef : null; var modProjProps = modProjectile?.projectile; float chance = modProjProps != null ? 1f - projModifier.GetRealChance(lootComp.parent) : 1f; if (chance <= 0.05f) { chance = 1f; // already permanently set to "base" verbProps } string chanceStr = GenText.ToStringPercent(chance); float baseDamage = projProps.damageDef.harmsHealth == false ? 0f : req.HasThing ? projProps.GetDamageAmount(req.Thing) : req.StuffDef != null?projProps.GetDamageAmount(def.GetStatValueAbstract(StatDefOf.RangedWeapon_DamageMultiplier, req.StuffDef)) : projProps.GetDamageAmount(def.GetStatValueAbstract(StatDefOf.RangedWeapon_DamageMultiplier)) ; float damage = baseDamage * verbProps.burstShotCount * chance; string reportText = "Damage".Translate() + ":\n"; if (chance < 1f) { reportText += " " + "RimLoot_StatsReport_ProjectileWithChance".Translate( verbProps.defaultProjectile.Named("PROJECTILE"), chanceStr.Named("chance") ) + "\n"; reportText += string.Format(" {0}: {1} * {2} * {3} = {4}\n\n", "Damage".Translate(), baseDamage.ToStringDecimalIfSmall(), verbProps.burstShotCount, chanceStr, damage.ToStringDecimalIfSmall() ); float modChance = 1f - chance; string modChanceStr = GenText.ToStringPercent(modChance); float modBaseDamage = modProjProps.damageDef.harmsHealth == false ? 0f : modProjProps.GetDamageAmount(req.Thing) ; float modDamage = modBaseDamage * verbProps.burstShotCount * modChance; reportText += " " + "RimLoot_StatsReport_ProjectileWithChance".Translate( modProjectile.Named("PROJECTILE"), modChanceStr.Named("chance") ) + "\n"; reportText += string.Format(" {0}: {1} * {2} * {3} = {4}\n\n", "Damage".Translate(), modBaseDamage.ToStringDecimalIfSmall(), verbProps.burstShotCount, modChanceStr, modDamage.ToStringDecimalIfSmall() ); reportText += string.Format("{0}: {1}\n\n", "StatsReport_TotalValue".Translate(), (damage + modDamage).ToStringDecimalIfSmall()); } else { reportText += " " + "RimLoot_StatsReport_Projectile".Translate(verbProps.defaultProjectile.Named("PROJECTILE")) + "\n"; reportText += string.Format(" {0}: {1} * {2} = {3}\n\n", "Damage".Translate(), baseDamage.ToStringDecimalIfSmall(), verbProps.burstShotCount, damage.ToStringDecimalIfSmall() ); } /* Seconds per attack */ float secondsSpent = 0; float cooldown = req.HasThing ? req.Thing.GetStatValue(StatDefOf.RangedWeapon_Cooldown, true) : req.StuffDef != null?def.GetStatValueAbstract(StatDefOf.RangedWeapon_Cooldown, req.StuffDef) : def.GetStatValueAbstract(StatDefOf.RangedWeapon_Cooldown) ; float burstShotTime = ((verbProps.burstShotCount - 1) * verbProps.ticksBetweenBurstShots).TicksToSeconds(); if (verb != null) { secondsSpent = verbProps.AdjustedFullCycleTime(verb, attacker); } else { secondsSpent = verbProps.warmupTime + cooldown + burstShotTime; } reportText += GenText.ToTitleCaseSmart("SecondsPerAttackLower".Translate()) + ":\n"; reportText += string.Format(" {0}: {1}\n", "WarmupTime".Translate(), "PeriodSeconds".Translate(verbProps.warmupTime.ToStringDecimalIfSmall())); if (burstShotTime > 0) { reportText += string.Format(" {0}: {1}\n", "BurstShotFireRate".Translate(), "PeriodSeconds".Translate(burstShotTime.ToStringDecimalIfSmall())); } reportText += string.Format(" {0}: {1}\n", "CooldownTime".Translate(), "PeriodSeconds".Translate(cooldown.ToStringDecimalIfSmall())); reportText += string.Format("{0}: {1}\n\n", "StatsReport_TotalValue".Translate(), "PeriodSeconds".Translate(secondsSpent.ToStringDecimalIfSmall())); /* Average accuracy */ // Every integer range possible as an average float wpnAccuracy = 0; float pawnAccuracy = 0; for (int i = 3; i <= verbProps.range; i++) { wpnAccuracy += verbProps.GetHitChanceFactor(req.Thing, i); if (attacker != null) { pawnAccuracy += ShotReport.HitFactorFromShooter(attacker, i); } } if (verbProps.range >= 3) { wpnAccuracy /= verbProps.range - 2; if (attacker != null) { pawnAccuracy /= verbProps.range - 2; } } reportText += "AverageAccuracy".Translate() + ":\n"; reportText += string.Format(" {0}: {1}\n", "ShootReportWeapon".Translate(), wpnAccuracy.ToStringPercent("F1")); if (pawnAccuracy > 0) { reportText += string.Format(" {0}: {1}\n", "ShootReportShooterAbility".Translate(), pawnAccuracy.ToStringPercent("F1")); } return(reportText); }
public static string CombatPart(Pawn pawn, Comp_NightVision comp) { var strHelper = new CombatStatStrHelper(); const float NoLight = 0f; const float FullLight = 1f; float noLightFactor = comp.FactorFromGlow(glow: NoLight); float fullLightFactor = comp.FactorFromGlow(glow: FullLight); strHelper.AddMainHeader(); if (Settings.CombatStore.RangedHitEffectsEnabled.Value) { if (ShowRangedEffectsForPawn(pawn)) { strHelper.AddLine(Str_Combat.ShootTargetAtGlow()); for (var i = 1; i <= 4; i++) { float hit = ShotReport.HitFactorFromShooter(caster: pawn, distance: i * 5); strHelper.AddLine( Str_Combat.ShotChanceTransform( distance: i * 5, hitChance: hit, nvResult: CombatHelpers.HitChanceGlowTransform(hitChance: hit, attGlowFactor: noLightFactor), psResult: CombatHelpers.HitChanceGlowTransform(hitChance: hit, attGlowFactor: fullLightFactor) ) ); } strHelper.NextLine(); } else { // TODO add line reporting why effects are not appearing } } if (Settings.CombatStore.MeleeHitEffectsEnabled.Value) { if (ShowMeleeEffectsForPawn(pawn)) { float pawnDodgeVal = pawn.GetStatValue(stat: Defs_Rimworld.MeleeDodgeStat); float meleeHit = pawn.GetStatValue(stat: Defs_Rimworld.MeleeHitStat, applyPostProcess: true); var caps = Settings.Store.MultiplierCaps; strHelper.AddLine(Str_Combat.StrikeTargetAtGlow()); strHelper.AddLine( Str_Combat.StrikeChanceTransform( hitChance: meleeHit, nvResult: CombatHelpers.HitChanceGlowTransform(hitChance: meleeHit, attGlowFactor: noLightFactor), psResult: CombatHelpers.HitChanceGlowTransform(hitChance: meleeHit, attGlowFactor: fullLightFactor) ) ); strHelper.NextLine(); strHelper.AddSurpriseAttackHeader(); /////////////////////////////////////////// //// Surprise attack stats at 0% light //// float noLightSurpAttChance = CombatHelpers.SurpriseAttackChance(atkGlowFactor: noLightFactor, defGlowFactor: caps.min); // attack vs pawn with minimum LM strHelper.AddSurpriseAttackRow(NoLight, noLightFactor, caps.min); // skip if we need more room to show dodge stats if (pawnDodgeVal.IsTrivial()) { //skip if chance was 0% vs pawn with min LM (as it won't be different if (noLightSurpAttChance.IsNonTrivial()) { // attack vs pawn with normal LM noLightSurpAttChance = CombatHelpers.SurpriseAttackChance(atkGlowFactor: noLightFactor, defGlowFactor: 1f); strHelper.AddSurpriseAttackRow(NoLight, noLightFactor, 1); // skip as above if (noLightSurpAttChance.IsNonTrivial()) { // attack vs pawn with max LM strHelper.AddSurpriseAttackRow(0, noLightFactor, caps.max); } } } //////////////////////////////////////////// ///////////////////////////////////////////// //// Surprise attack stats at 100% light //// // skip if we need more room to show dodge stats if (pawnDodgeVal.IsTrivial()) { // attack vs pawn with min LM strHelper.AddSurpriseAttackRow(fullLightFactor, fullLightFactor, caps.min); } // attack vs pawn with normal LM strHelper.AddSurpriseAttackRow(fullLightFactor, fullLightFactor, 1f); strHelper.NextLine(); ///////////////////////////////////////////// ///////////////////////////////////////////// //// Dodge Stats //// strHelper.AddDodgeHeader(); // This pawns chance to dodge when attacked // attacked by pawn with min LM in no light strHelper.AddDodgeRow(NoLight, caps.min, noLightFactor, pawnDodgeVal); // skip if pawns dodge value is zero if (pawnDodgeVal.IsNonTrivial()) { // attacked by pawn with normal LM in no light strHelper.AddDodgeRow(NoLight, 1f, noLightFactor, pawnDodgeVal); // attacked by pawn with max LM in no light strHelper.AddDodgeRow(NoLight, caps.max, noLightFactor, pawnDodgeVal); // attacked by pawn with min LM in no light strHelper.AddDodgeRow(FullLight, caps.min, fullLightFactor, pawnDodgeVal); } //attacked by pawn with normal LM in full light strHelper.AddDodgeRow(FullLight, 1, fullLightFactor, pawnDodgeVal); } } return(strHelper.ToString()); }
private static float BaseArcheryEffectivenessFor(Pawn pawn) { float accuracy = ShotReport.HitFactorFromShooter(pawn, archeryDistance); return(accuracy * MentalBreakThresholdMultiplierFor(pawn, 0.25f) * ArcheryEffectivenessNormaliser); }