public float AdjustShotHeight(Thing caster, LocalTargetInfo target, ref float shotHeight) { /* TODO: This really should determine how much the shooter needs to rise up for a *good* shot. * If we're shooting at something tall, we might not need to rise at all, if we're shooting at * something short, we might need to rise *more* than just above the cover. This at least handles * cases where we're below cover, but the taret is taller than the cover */ GetHighestCoverAndSmokeForTarget(target, out Thing cover, out float smoke); var shooterHeight = CE_Utility.GetBoundsFor(caster).max.y; var coverHeight = CE_Utility.GetBoundsFor(cover).max.y; var centerOfVisibleTarget = (CE_Utility.GetBoundsFor(target.Thing).max.y - coverHeight) / 2 + coverHeight; if (centerOfVisibleTarget > shotHeight) { if (centerOfVisibleTarget > shooterHeight) { centerOfVisibleTarget = shooterHeight; } float distance = target.Thing.Position.DistanceTo(caster.Position); // float wobble = Mathf.Atan2(UnityEngine.Random.Range(shotHeight-centerOfVisibleTarget, centerOfVisibleTarget - shotHeight), distance); float triangleHeight = centerOfVisibleTarget - shotHeight; float wobble = -Mathf.Atan2(triangleHeight, distance); // TODO: Add inaccuracy for not standing in as natural a position shotHeight = centerOfVisibleTarget; return(wobble); } return(0); }
private void ApplySuppression(Pawn pawn) { ShieldBelt shield = null; if (pawn.RaceProps.Humanlike) { // check for shield user List <Apparel> wornApparel = pawn.apparel.WornApparel; for (int i = 0; i < wornApparel.Count; i++) { var personalShield = wornApparel[i] as ShieldBelt; if (personalShield != null) { shield = personalShield; break; } } } //Add suppression CompSuppressable compSuppressable = pawn.TryGetComp <CompSuppressable>(); if (compSuppressable != null && pawn.Faction != launcher?.Faction && (shield == null || shield?.ShieldState == ShieldState.Resetting)) { suppressionAmount = def.projectile.GetDamageAmount(CE_Utility.GetWeaponFromLauncher(launcher)); var propsCE = def.projectile as ProjectilePropertiesCE; float penetrationAmount = propsCE == null ? 0f : propsCE.armorPenetration; suppressionAmount *= 1 - Mathf.Clamp(compSuppressable.ParentArmor - penetrationAmount, 0, 1); compSuppressable.AddSuppression(suppressionAmount, OriginIV3); } }
protected override bool TryCastShot() { //Reduce ammunition if (compAmmo != null) { if (!compAmmo.TryReduceAmmoCount()) { if (compAmmo.hasMagazine) { compAmmo.TryStartReload(); } return(false); } } if (base.TryCastShot()) { //Drop casings if (verbPropsCE.ejectsCasings && projectilePropsCE.dropsCasings) { CE_Utility.ThrowEmptyCasing(this.caster.DrawPos, caster.Map, ThingDef.Named(this.projectilePropsCE.casingMoteDefname)); } return(true); } return(false); }
private bool TryCollideWithRoof(IntVec3 cell) { if (!cell.Roofed(Map)) { return(false); } var bounds = CE_Utility.GetBoundsFor(cell, cell.GetRoof(Map)); float dist; if (!bounds.IntersectRay(ShotLine, out dist)) { return(false); } if (dist * dist > ExactMinusLastPos.sqrMagnitude) { return(false); } var point = ShotLine.GetPoint(dist); ExactPosition = point; landed = true; if (DebugViewSettings.drawInterceptChecks) { MoteMaker.ThrowText(cell.ToVector3Shifted(), Map, "x", Color.red); } Impact(null); return(true); }
protected override bool TryCastShot() { //Reduce ammunition if (CompAmmo != null) { if (!CompAmmo.TryReduceAmmoCount()) { if (CompAmmo.HasMagazine) { CompAmmo.TryStartReload(); } return(false); } } if (base.TryCastShot()) { //Required since Verb_Shoot does this but Verb_LaunchProjectileCE doesn't when calling base.TryCastShot() because Shoot isn't its base if (ShooterPawn != null) { ShooterPawn.records.Increment(RecordDefOf.ShotsFired); } //Drop casings if (VerbPropsCE.ejectsCasings && projectilePropsCE.dropsCasings) { CE_Utility.ThrowEmptyCasing(caster.DrawPos, caster.Map, ThingDef.Named(projectilePropsCE.casingMoteDefname)); } // This needs to here for weapons without magazine to ensure their last shot plays sounds if (CompAmmo != null && !CompAmmo.HasMagazine && CompAmmo.UseAmmo) { if (!CompAmmo.Notify_ShotFired()) { if (VerbPropsCE.muzzleFlashScale > 0.01f) { MoteMaker.MakeStaticMote(caster.Position, caster.Map, ThingDefOf.Mote_ShotFlash, VerbPropsCE.muzzleFlashScale); } if (VerbPropsCE.soundCast != null) { VerbPropsCE.soundCast.PlayOneShot(new TargetInfo(caster.Position, caster.Map)); } if (VerbPropsCE.soundCastTail != null) { VerbPropsCE.soundCastTail.PlayOneShotOnCamera(); } if (ShooterPawn != null) { if (ShooterPawn.thinker != null) { ShooterPawn.mindState.lastEngageTargetTick = Find.TickManager.TicksGame; } } } return(CompAmmo.Notify_PostShotFired()); } return(true); } return(false); }
/// <summary> /// Tries to impact the thing based on whether it intersects the given flight path. Trees have RNG chance to not collide even on intersection. /// </summary> /// <param name="thing">What to impact</param> /// <returns>True if impact occured, false otherwise</returns> private bool TryCollideWith(Thing thing) { if (thing == launcher && !canTargetSelf) { return(false); } var bounds = CE_Utility.GetBoundsFor(thing); float dist; if (!bounds.IntersectRay(ShotLine, out dist)) { return(false); } if (dist * dist > ExactMinusLastPos.sqrMagnitude) { return(false); } // Trees and bushes have RNG chance to collide var plant = thing as Plant; if (plant != null) { //TODO: Remove fillPercent dependency because all fillPercents on trees are 0.25 //Prevents trees near the shooter (e.g the shooter's cover) to be hit float chance = thing.def.fillPercent * ((thing.Position - OriginIV3).LengthHorizontal / 40); if (Controller.settings.DebugShowTreeCollisionChance) { MoteMaker.ThrowText(thing.Position.ToVector3Shifted(), thing.Map, chance.ToString()); } if (!Rand.Chance(chance)) { return(false); } } var point = ShotLine.GetPoint(dist); if (!point.InBounds(this.Map)) { Log.Error("TryCollideWith out of bounds point from ShotLine: obj " + thing.ThingID + ", proj " + this.ThingID + ", dist " + dist + ", point " + point); } ExactPosition = point; landed = true; if (DebugViewSettings.drawInterceptChecks) { MoteMaker.ThrowText(thing.Position.ToVector3Shifted(), thing.Map, "x", Color.red); } Impact(thing); return(true); }
public static float GetOptimalHuntRange(Pawn hunter, Pawn victim) { var curJob = hunter.CurJob; var victimProps = victim.RaceProps; if (victim.Downed) { // TODO: How to detect boombats? return(Mathf.Min(curJob.verbToUse.verbProps.range, victimProps.executionRange)); } var normalRange = HuntRangePerBodysize(victimProps.baseBodySize, victimProps.executionRange, curJob.verbToUse.verbProps.range); if (victimProps.manhunterOnDamageChance > 0) { // NOTE: I assumed 2 is a good number to consider for aim time of hunting weapons (since hunting weapons are likely to be sniper rifles) // We can also get aim time from the actual weapon that the hunter is using // but I didn't see the need. const int aimTime = 2; // Determine how much target can move away from the hunter while the hunter is aiming float moveOffset = CE_Utility.GetMoveSpeed(victim) * aimTime; // Get a little bit closer when target is small. (Smaller targets don't attack that hard so it's ok from safety standpoint) // For reference, Megasloth is 4, and squirrel is 0.2 float bodySizeFactor = Mathf.Clamp01(victimProps.baseBodySize); // If the hunter's shooting skill is below skillThreshold, hunter gets closer to not miss their shots // I assumed level 10 is a good enough shooter to stay at maximum distance and don't miss const float skillThreshold = 10f; float skillFactor = Mathf.Clamp01(hunter.skills.GetSkill(SkillDefOf.Shooting).Level / skillThreshold); float weaponRange = curJob.verbToUse.verbProps.range; // Add additional offset to create a safe margin just in case something weird happens const int additionalOffset = 5; float optimal = weaponRange * bodySizeFactor * skillFactor - moveOffset - additionalOffset; // In some cases optimal range gets lower than the normal range that we calculated for non-manhunting animals. // For example when the hunter has 0 shooting skill, optimal range will be negative. // In that case we just go back to normal calculated range. return(Mathf.Max(optimal, normalRange)); } return(normalRange); //Fit for an attack range per body size curve. float HuntRangePerBodysize(float x, float executionRange, float gunRange) { return(Mathf.Min(Mathf.Clamp(1 + 20 * (1 - Mathf.Exp(-0.65f * x)), executionRange, 20), gunRange)); } }
/// <summary> /// Tries to impact the thing based on whether it intersects the given flight path. Trees have RNG chance to not collide even on intersection. /// </summary> /// <param name="thing">What to impact</param> /// <returns>True if impact occured, false otherwise</returns> private bool TryCollideWith(Thing thing) { if (thing == launcher && !canTargetSelf) { return(false); } var bounds = CE_Utility.GetBoundsFor(thing); if (!bounds.IntersectRay(ShotLine, out var dist)) { return(false); } if (dist * dist > ExactMinusLastPos.sqrMagnitude) { return(false); } // Trees and bushes have RNG chance to collide if (thing is Plant) { //Prevents trees near the shooter (e.g the shooter's cover) to be hit var accuracyFactor = def.projectile.alwaysFreeIntercept ? 1 : (thing.Position - OriginIV3).LengthHorizontal / 40 * AccuracyFactor; var chance = thing.def.fillPercent * accuracyFactor; if (Controller.settings.DebugShowTreeCollisionChance) { MoteMaker.ThrowText(thing.Position.ToVector3Shifted(), thing.Map, chance.ToString()); } if (!Rand.Chance(chance)) { return(false); } } var point = ShotLine.GetPoint(dist); if (!point.InBounds(Map)) { Log.Error("TryCollideWith out of bounds point from ShotLine: obj " + thing.ThingID + ", proj " + ThingID + ", dist " + dist + ", point " + point); } ExactPosition = point; landed = true; if (Controller.settings.DebugDrawInterceptChecks) { MoteMaker.ThrowText(thing.Position.ToVector3Shifted(), thing.Map, "x", Color.red); } Impact(thing); return(true); }
public virtual ShiftVecReport ShiftVecReportFor(LocalTargetInfo target) { IntVec3 targetCell = target.Cell; ShiftVecReport report = new ShiftVecReport(); report.target = target; report.aimingAccuracy = AimingAccuracy; report.sightsEfficiency = SightsEfficiency; report.shotDist = (targetCell - caster.Position).LengthHorizontal; report.maxRange = verbProps.range; report.lightingShift = CE_Utility.GetLightingShift(caster, LightingTracker.CombatGlowAtFor(caster.Position, targetCell)); if (!caster.Position.Roofed(caster.Map) || !targetCell.Roofed(caster.Map)) //Change to more accurate algorithm? { report.weatherShift = 1 - caster.Map.weatherManager.CurWeatherAccuracyMultiplier; } report.shotSpeed = ShotSpeed; report.swayDegrees = SwayAmplitude; var spreadmult = projectilePropsCE != null ? projectilePropsCE.spreadMult : 0f; report.spreadDegrees = (EquipmentSource?.GetStatValue(StatDef.Named("ShotSpread")) ?? 0) * spreadmult; Thing cover; float smokeDensity; GetHighestCoverAndSmokeForTarget(target, out cover, out smokeDensity); report.cover = cover; report.smokeDensity = smokeDensity; if (Controller.settings.DebugVerbose) { Log.Message($"<color=red>CE</color>: <color=orange>{caster}</color> shooting <color=orange>{target.Thing}</color> <color=yellow>ShiftVecReport</color>\n" + $"1- aimingAccuracy:{report.aimingAccuracy}\n" + $"2- sightsEfficiency:{report.sightsEfficiency}\n" + $"3- maxRange:{report.maxRange}\n" + $"4- lightingShift:{report.lightingShift}\n" + $"5- spreadDegrees:{report.spreadDegrees}\n" + $"6- smokeDensity:{report.smokeDensity}\n" + $"7- swayDegrees:{report.swayDegrees}\n" + $"8- shotSpeed:{report.shotSpeed}\n" + $"9- shotDist:{report.shotDist}\n"); } return(report); }
public bool GetChargeBracket(float range, float shotHeight, float gravityFactor, out Vector2 bracket) { bracket = new Vector2(0, 0); if (Props.chargeSpeeds.Count <= 0) { Log.Error("Tried getting charge bracket from empty list."); return(false); } foreach (var speed in Props.chargeSpeeds) { var curRange = CE_Utility.MaxProjectileRange(shotHeight, speed, MaxRangeAngle, gravityFactor); if (range <= curRange) { bracket = new Vector2(speed, curRange); return(true); } } return(false); }
/// <summary> /// Checks if the shooter can hit the target from a certain position with regards to cover height /// </summary> /// <param name="root">The position from which to check</param> /// <param name="targ">The target to check for line of sight</param> /// <returns>True if shooter can hit target from root position, false otherwise</returns> public override bool CanHitTargetFrom(IntVec3 root, LocalTargetInfo targ) { //Sanity check for flyOverhead projectiles, they should not attack things under thick roofs if (projectileDef.projectile.flyOverhead) { RoofDef roofDef = caster.Map.roofGrid.RoofAt(targ.Cell); if (roofDef != null && roofDef.isThickRoof) { return(false); } return(base.CanHitTargetFrom(root, targ)); } if (base.CanHitTargetFrom(root, targ)) { //Check if target is obstructed behind cover Thing coverTarg; if (this.GetPartialCoverBetween(root.ToVector3Shifted(), targ.Cell.ToVector3Shifted(), out coverTarg)) { float targetHeight = CE_Utility.GetCollisionHeight(targ.Thing); if (targetHeight <= CE_Utility.GetCollisionHeight(coverTarg)) { return(false); } } //Check if shooter is obstructed by cover Thing coverShoot; if (this.GetPartialCoverBetween(targ.Cell.ToVector3Shifted(), root.ToVector3Shifted(), out coverShoot)) { float shotHeight = CE_Utility.GetCollisionHeight(this.caster); if (this.CasterPawn != null) { shotHeight *= shotHeightFactor; } if (shotHeight <= CE_Utility.GetCollisionHeight(coverShoot)) { return(false); } } return(true); } return(false); }
protected virtual void Explode() { ExplosionCE explosion = GenSpawn.Spawn(CE_ThingDefOf.ExplosionCE, ExactPosition.ToIntVec3(), Map) as ExplosionCE; explosion.height = ExactPosition.y; explosion.radius = def.projectile.explosionRadius; explosion.damType = def.projectile.damageDef; explosion.instigator = launcher; explosion.damAmount = def.projectile.GetDamageAmount(CE_Utility.GetWeaponFromLauncher(launcher)); explosion.weapon = equipmentDef; explosion.projectile = def; explosion.preExplosionSpawnThingDef = def.projectile.preExplosionSpawnThingDef; explosion.preExplosionSpawnChance = def.projectile.preExplosionSpawnChance; explosion.preExplosionSpawnThingCount = def.projectile.preExplosionSpawnThingCount; explosion.postExplosionSpawnThingDef = def.projectile.postExplosionSpawnThingDef; explosion.postExplosionSpawnChance = def.projectile.postExplosionSpawnChance; explosion.postExplosionSpawnThingCount = def.projectile.postExplosionSpawnThingCount; explosion.applyDamageToExplosionCellsNeighbors = def.projectile.applyDamageToExplosionCellsNeighbors; explosion.chanceToStartFire = def.projectile.explosionChanceToStartFire; explosion.damageFalloff = def.projectile.explosionDamageFalloff; explosion.StartExplosion(def.projectile.soundExplode); if (this.def.projectile.explosionEffect != null) { Effecter effecter = this.def.projectile.explosionEffect.Spawn(); effecter.Trigger(new TargetInfo(ExactPosition.ToIntVec3(), Map, false), new TargetInfo(ExactPosition.ToIntVec3(), Map, false)); effecter.Cleanup(); } //This code was disabled because it didn't run under previous circumstances. Could be enabled if necessary /* * if (map != null && base.ExactPosition.ToIntVec3().IsValid) * { * ThrowBigExplode(base.ExactPosition + Gen.RandomHorizontalVector(def.projectile.explosionRadius * 0.5f), base.Map, def.projectile.explosionRadius * 0.4f); * } */ base.Impact(null); // base.Impact() handles this.Destroy() and comp.Explode() }
/// <summary> /// Tries to impact the thing based on whether it intersects the given flight path. Trees have RNG chance to not collide even on intersection. /// </summary> /// <param name="thing">What to impact</param> /// <param name="shotLine">Projectile's path of travel</param> /// <returns>True if impact occured, false otherwise</returns> private bool TryCollideWith(Thing thing, Ray shotLine) { if (thing == launcher && !canTargetSelf) { return(false); } // Trees have RNG chance to collide if (thing.IsTree()) { float chance = thing.def.fillPercent * ((thing.Position - OriginIV3).LengthHorizontal / 40); if (Controller.settings.DebugShowTreeCollisionChance) { MoteMaker.ThrowText(thing.Position.ToVector3Shifted(), thing.Map, chance.ToString()); } if (!Rand.Chance(chance)) { return(false); } } else { var bounds = CE_Utility.GetBoundsFor(thing); if (!bounds.IntersectRay(shotLine)) { return(false); } } if (DebugViewSettings.drawInterceptChecks) { MoteMaker.ThrowText(thing.Position.ToVector3Shifted(), thing.Map, "x", Color.red); } Impact(thing); return(true); }
public override void CompTick() { base.CompTick(); // Update suppressed tick counter and check for mental breaks if (!isSuppressed) { ticksHunkered = 0; } else if (IsHunkering) { ticksHunkered++; } if (ticksHunkered > MinTicksUntilMentalBreak && Rand.Chance(ChanceBreakPerTick)) { var pawn = (Pawn)parent; if (pawn.mindState != null && !pawn.mindState.mentalStateHandler.InMentalState) { var possibleBreaks = SuppressionUtility.GetPossibleBreaks(pawn); if (possibleBreaks.Any()) { pawn.mindState.mentalStateHandler.TryStartMentalState(possibleBreaks.RandomElement()); } } } //Apply decay once per second if (ticksUntilDecay > 0) { ticksUntilDecay--; } else if (currentSuppression > 0) { //Decay global suppression if (Controller.settings.DebugShowSuppressionBuildup && Gen.IsHashIntervalTick(parent, 30)) { MoteMaker.ThrowText(parent.DrawPos, parent.Map, "-" + (SuppressionDecayRate * 30), Color.red); } currentSuppression -= Mathf.Min(SuppressionDecayRate, currentSuppression); isSuppressed = currentSuppression > 0; // Clear crouch-walking if (!isSuppressed) { isCrouchWalking = false; } //Decay location suppression locSuppressionAmount -= Mathf.Min(SuppressionDecayRate, locSuppressionAmount); } // Throw mote at set interval if (parent.IsHashIntervalTick(TicksPerMote) && CanReactToSuppression) { if (this.IsHunkering) { CE_Utility.MakeIconOverlay((Pawn)parent, CE_ThingDefOf.Mote_HunkerIcon); } else if (this.isSuppressed) { CE_Utility.MakeIconOverlay((Pawn)parent, CE_ThingDefOf.Mote_SuppressIcon); } } }
// Added targetThing to parameters so we can calculate its height private bool CanHitCellFromCellIgnoringRange(IntVec3 sourceSq, IntVec3 targetLoc, Thing targetThing = null, bool includeCorners = false) { // Vanilla checks if (this.verbProps.mustCastOnOpenGround && (!targetLoc.Standable(this.caster.Map) || this.caster.Map.thingGrid.CellContains(targetLoc, ThingCategory.Pawn))) { return(false); } if (this.verbProps.requireLineOfSight) { // Calculate shot vector Vector3 shotSource = ShotSource; Vector3 targetPos; if (targetThing != null) { Vector3 targDrawPos = targetThing.DrawPos; targetPos = new Vector3(targDrawPos.x, new CollisionVertical(targetThing).Max, targDrawPos.z); } else { targetPos = targetLoc.ToVector3Shifted(); } Ray shotLine = new Ray(shotSource, (targetPos - shotSource)); // Create validator to check for intersection with partial cover var aimMode = CompFireModes?.CurrentAimMode; Func <IntVec3, bool> validator = delegate(IntVec3 cell) { // Skip this check entirely if we're doing suppressive fire and cell is adjacent to target if (VerbPropsCE.ignorePartialLoSBlocker || aimMode == AimMode.SuppressFire) { return(true); } Thing cover = cell.GetFirstPawn(caster.Map); if (cover == null) { cover = cell.GetCover(caster.Map); } if (cover != null && !cover.IsTree() && !cover.Position.AdjacentTo8Way(sourceSq)) { Bounds bounds = CE_Utility.GetBoundsFor(cover); // Check for intersect if (bounds.IntersectRay(shotLine)) { if (Controller.settings.DebugDrawPartialLoSChecks) { caster.Map.debugDrawer.FlashCell(cell, 0, bounds.size.y.ToString()); } return(false); } else if (Controller.settings.DebugDrawPartialLoSChecks) { caster.Map.debugDrawer.FlashCell(cell, 0.7f, bounds.size.y.ToString()); } } return(true); }; // Add validator to parameters if (!includeCorners) { if (!GenSight.LineOfSight(sourceSq, targetLoc, this.caster.Map, true, validator, 0, 0)) { return(false); } } else if (!GenSight.LineOfSightToEdges(sourceSq, targetLoc, this.caster.Map, true, validator)) { return(false); } } return(true); }
// Added targetThing to parameters so we can calculate its height private bool CanHitCellFromCellIgnoringRange(IntVec3 sourceSq, IntVec3 rootCell, IntVec3 targetLoc, Thing targetThing = null, bool includeCorners = false) { // Vanilla checks if (this.verbProps.mustCastOnOpenGround && (!targetLoc.Standable(this.caster.Map) || this.caster.Map.thingGrid.CellContains(targetLoc, ThingCategory.Pawn))) { return(false); } if (this.verbProps.requireLineOfSight) { // Calculate shot vector Vector3 shotSource = ShotSource; Vector3 targetPos; if (targetThing != null) { Vector3 targDrawPos = targetThing.DrawPos; targetPos = new Vector3(targDrawPos.x, new CollisionVertical(targetThing).Max, targDrawPos.z); var targPawn = targetThing as Pawn; if (targPawn != null) { targetPos += targPawn.Drawer.leaner.LeanOffset * 0.5f; } } else { targetPos = targetLoc.ToVector3Shifted(); } Ray shotLine = new Ray(shotSource, (targetPos - shotSource)); // Create validator to check for intersection with partial cover var aimMode = CompFireModes?.CurrentAimMode; Func <IntVec3, bool> validator = delegate(IntVec3 cell) { // Skip this check entirely if we're doing suppressive fire and cell is adjacent to target if (VerbPropsCE.ignorePartialLoSBlocker || aimMode == AimMode.SuppressFire) { return(true); } Thing cover = cell.GetFirstPawn(caster.Map); if (cover == null) { cover = cell.GetCover(caster.Map); } if (cover != null && cover != ShooterPawn && cover != caster && cover != targetThing && !cover.IsPlant()) { // var tr = cover.Position.AdjacentTo8Way(sourceSq); // Log.Message("cover: " + cover.ToString() + " value: " + tr.ToString()); if (VerbPropsCE.verbClass == typeof(Verb_ShootCEOneUse) && cover.def.fillPercent < 0.8) { return(true); } Bounds bounds = CE_Utility.GetBoundsFor(cover); // Check for intersect if (bounds.IntersectRay(shotLine)) { if (Controller.settings.DebugDrawPartialLoSChecks) { caster.Map.debugDrawer.FlashCell(cell, 0, bounds.size.y.ToString()); } return(false); } else if (Controller.settings.DebugDrawPartialLoSChecks) { caster.Map.debugDrawer.FlashCell(cell, 0.7f, bounds.size.y.ToString()); } if (cover.def.fillPercent > 0.99) { return(false); } } return(true); }; // Add validator to parameters /* * if (!includeCorners) * { * if (!GenSight.LineOfSight(sourceSq, targetLoc, this.caster.Map, true, validator, 0, 0)) * { * Log.Message("4.1 CanHitCellFromCellIgnoringRange: false"); * return false; * } * } * else if (!GenSight.LineOfSightToEdges(sourceSq, targetLoc, this.caster.Map, true, validator)) * { * Log.Message("4.2 CanHitCellFromCellIgnoringRange: false"); * return false; * } */ var exactTargetSq = targetPos.ToIntVec3(); foreach (IntVec3 curCell in GenSight.PointsOnLineOfSight(sourceSq, exactTargetSq)) { if (curCell != sourceSq && curCell != exactTargetSq && !validator(curCell)) { return(false); } } int c = 0; foreach (IntVec3 curCell2 in GenSight.PointsOnLineOfSight(rootCell, exactTargetSq)) { if (curCell2 != rootCell && curCell2 != exactTargetSq && !validator(curCell2)) { c++; } if (c >= 2) { if (rootCell.z == exactTargetSq.z || rootCell.x == exactTargetSq.x) { return(false); } } } } return(true); }
/// <summary> /// Shifts the original target position in accordance with target leading, range estimation and weather/lighting effects /// </summary> protected virtual Vector3 ShiftTarget(ShiftVecReport report) { // ----------------------------------- STEP 0: Actual location Vector3 targetLoc = report.targetPawn != null?Vector3.Scale(report.targetPawn.DrawPos, new Vector3(1, 0, 1)) : report.target.Cell.ToVector3Shifted(); Vector3 sourceLoc = this.CasterPawn != null?Vector3.Scale(this.CasterPawn.DrawPos, new Vector3(1, 0, 1)) : this.caster.Position.ToVector3Shifted(); // ----------------------------------- STEP 1: Shift for visibility Vector2 circularShiftVec = report.GetRandCircularVec(); Vector3 newTargetLoc = targetLoc; newTargetLoc.x += circularShiftVec.x; newTargetLoc.z += circularShiftVec.y; // ----------------------------------- STEP 2: Estimated shot to hit location // On first shot of burst do a range estimate if (this.estimatedTargDist < 0) { this.estimatedTargDist = report.GetRandDist(); } newTargetLoc = sourceLoc + (newTargetLoc - sourceLoc).normalized * this.estimatedTargDist; // Lead a moving target newTargetLoc += report.GetRandLeadVec(); // ----------------------------------- STEP 3: Recoil, Skewing, Skill checks, Cover calculations Vector2 skewVec = new Vector2(0, 0); skewVec += this.GetSwayVec(); skewVec += this.GetRecoilVec(); // Height difference calculations for ShotAngle float heightDifference = 0; float targetableHeight = 0; // Projectiles with flyOverhead target the ground below the target and ignore cover if (!projectileDef.projectile.flyOverhead) { targetableHeight = CE_Utility.GetCollisionHeight(this.currentTarget.Thing); if (report.cover != null) { targetableHeight += CE_Utility.GetCollisionHeight(report.cover); } heightDifference += targetableHeight * 0.5f; //Optimal hit level is halfway } this.shotHeight = CE_Utility.GetCollisionHeight(this.caster); if (this.CasterPawn != null) { this.shotHeight *= shotHeightFactor; } heightDifference -= this.shotHeight; skewVec += new Vector2(0, GetShotAngle(this.shotSpeed, (newTargetLoc - sourceLoc).magnitude, heightDifference) * (180 / (float)Math.PI)); // ----------------------------------- STEP 4: Mechanical variation // Get shotvariation Vector2 spreadVec = report.GetRandSpreadVec(); skewVec += spreadVec; // Skewing - Applied after the leading calculations to not screw them up float distanceTraveled = GetDistanceTraveled(this.shotSpeed, (float)(skewVec.y * (Math.PI / 180)), this.shotHeight); newTargetLoc = sourceLoc + ((newTargetLoc - sourceLoc).normalized * distanceTraveled); newTargetLoc = sourceLoc + (Quaternion.AngleAxis(skewVec.x, Vector3.up) * (newTargetLoc - sourceLoc)); this.shotAngle = (float)(skewVec.y * (Math.PI / 180)); return(newTargetLoc); }
/// <summary> /// Tries to impact the thing based on whether it intersects the given flight path. Trees have RNG chance to not collide even on intersection. /// </summary> /// <param name="thing">What to impact</param> /// <returns>True if impact occured, false otherwise</returns> private bool TryCollideWith(Thing thing) { if (thing == launcher && !canTargetSelf) { return(false); } var bounds = CE_Utility.GetBoundsFor(thing); float dist; if (!bounds.IntersectRay(ShotLine, out dist)) { return(false); } if (dist * dist > ExactMinusLastPos.sqrMagnitude) { return(false); } // Trees and bushes have RNG chance to collide var plant = thing as Plant; if (plant != null) { //TODO: Remove fillPercent dependency because all fillPercents on trees are 0.25 //Prevents trees near the shooter (e.g the shooter's cover) to be hit //Dependence to catch the tree from armorPenetration. Large calibers have less chance. var propsCE = def.projectile as ProjectilePropertiesCE; float penetrationAmount = propsCE == null ? 0.1f : propsCE.armorPenetration; //Every projectile, which not use flyoverhead, require armorPenetration stat for calculating collision. float penetrationmultiplier = Mathf.Clamp(((thing.def.fillPercent * 3.2f) - penetrationAmount), 0.05f, 1f); // 2.5-3.5 good values for 0.20-0.30 fillpercent. float rangemultiplier = ((thing.Position - OriginIV3).LengthHorizontal / 15); // 10-20 is fine for prevent to shoot near tree. float chance = penetrationmultiplier * (rangemultiplier < 1f ? rangemultiplier : 1f); // when projectile reach 15 cells distance, we set limit for multiplier bcs chances to hit greatly increased. if (Controller.settings.DebugShowTreeCollisionChance) { MoteMaker.ThrowText(thing.Position.ToVector3Shifted(), thing.Map, chance.ToString()); } if (!Rand.Chance(chance)) { return(false); } } var point = ShotLine.GetPoint(dist); if (!point.InBounds(this.Map)) { Log.Error("TryCollideWith out of bounds point from ShotLine: obj " + thing.ThingID + ", proj " + this.ThingID + ", dist " + dist + ", point " + point); } ExactPosition = point; if (intendedTarget != null) { var fulldistance = (intendedTarget.Position - OriginIV3).LengthHorizontalSquared; var traveleddistance = (thing.Position - OriginIV3).LengthHorizontalSquared; var requireddisttotarget = fulldistance - traveleddistance; if (thing is Building && thing.def.fillPercent > 0.5f) { // Log.Message("fulldist: " + fulldistance.ToString() + " traveled: " + traveleddistance.ToString() + " required: " + requireddisttotarget.ToString()); if (traveleddistance < requireddisttotarget) { return(false); } } } landed = true; // if (Controller.settings.DebugDrawInterceptChecks) MoteMaker.ThrowText(thing.Position.ToVector3Shifted(), thing.Map, "x", Color.red); Impact(thing); return(true); }
private static void CalculateHeightRange(Thing thing, out FloatRange heightRange, out float shotHeight) { shotHeight = 0; heightRange = new FloatRange(0, 0); if (thing == null) { return; } var plant = thing as Plant; if (plant != null) { //Height matches up exactly with visual size heightRange = new FloatRange(0f, BoundsInjector.ForPlant(plant).y); return; } if (thing is Building) { if (thing is Building_Door door && door.Open) { return; //returns heightRange = (0,0) & shotHeight = 0. If not open, doors have FillCategory.Full so returns (0, WallCollisionHeight) } if (thing.def.Fillage == FillCategory.Full) { heightRange = new FloatRange(0, WallCollisionHeight); shotHeight = WallCollisionHeight; return; } float fillPercent = thing.def.fillPercent; heightRange = new FloatRange(Mathf.Min(0f, fillPercent), Mathf.Max(0f, fillPercent)); shotHeight = fillPercent; return; } float collisionHeight = 0f; float shotHeightOffset = 0; var pawn = thing as Pawn; if (pawn != null) { collisionHeight = CE_Utility.GetCollisionBodyFactors(pawn).y; shotHeightOffset = collisionHeight * (1 - BodyRegionMiddleHeight); // Humanlikes in combat crouch to reduce their profile if (pawn.IsCrouching()) { float crouchHeight = BodyRegionBottomHeight * collisionHeight; // Minimum height we can crouch down to // Find the highest adjacent cover Map map = pawn.Map; foreach (IntVec3 curCell in GenAdjFast.AdjacentCells8Way(pawn.Position)) { if (curCell.InBounds(map)) { Thing cover = curCell.GetCover(map); if (cover != null && cover.def.Fillage == FillCategory.Partial && !cover.IsPlant()) { var coverHeight = new CollisionVertical(cover).Max; if (coverHeight > crouchHeight) { crouchHeight = coverHeight; } } } } collisionHeight = Mathf.Min(collisionHeight, crouchHeight + 0.01f + shotHeightOffset); // We crouch down only so far that we can still shoot over our own cover and never beyond our own body size } } else { collisionHeight = thing.def.fillPercent; } var edificeHeight = 0f; if (thing.Map != null) { var edifice = thing.Position.GetCover(thing.Map); if (edifice != null && edifice.GetHashCode() != thing.GetHashCode() && !edifice.IsPlant()) { edificeHeight = new CollisionVertical(edifice).heightRange.max; } } float fillPercent2 = collisionHeight; heightRange = new FloatRange(Mathf.Min(edificeHeight, edificeHeight + fillPercent2), Mathf.Max(edificeHeight, edificeHeight + fillPercent2)); shotHeight = heightRange.max - shotHeightOffset; }
public string GetTextReadout() { StringBuilder stringBuilder = new StringBuilder(); if (visibilityShift > 0) { stringBuilder.AppendLine(" " + "CE_VisibilityError".Translate() + "\t" + GenText.ToStringByStyle(visibilityShift, ToStringStyle.FloatTwo) + " c"); if (lightingShift > 0) { stringBuilder.AppendLine(" " + "Darkness".Translate() + "\t" + AsPercent(lightingShift)); } if (weatherShift > 0) { stringBuilder.AppendLine(" " + "Weather".Translate() + "\t" + AsPercent(weatherShift)); } if (smokeDensity > 0) { stringBuilder.AppendLine(" " + "CE_SmokeDensity".Translate() + "\t" + AsPercent(smokeDensity)); } } if (leadShift > 0) { stringBuilder.AppendLine(" " + "CE_LeadError".Translate() + "\t" + GenText.ToStringByStyle(leadShift, ToStringStyle.FloatTwo) + " c"); } if (distShift > 0) { stringBuilder.AppendLine(" " + "CE_RangeError".Translate() + "\t" + GenText.ToStringByStyle(distShift, ToStringStyle.FloatTwo) + " c"); } if (swayDegrees > 0) { stringBuilder.AppendLine(" " + "CE_Sway".Translate() + "\t\t" + GenText.ToStringByStyle(swayDegrees, ToStringStyle.FloatTwo) + "°"); } if (spreadDegrees > 0) { stringBuilder.AppendLine(" " + "CE_Spread".Translate() + "\t\t" + GenText.ToStringByStyle(spreadDegrees, ToStringStyle.FloatTwo) + "°"); } // Don't display cover and target size if our weapon has a CEP if (circularMissRadius > 0) { stringBuilder.AppendLine(" " + "CE_MissRadius".Translate() + "\t" + GenText.ToStringByStyle(circularMissRadius, ToStringStyle.FloatTwo) + " c"); if (indirectFireShift > 0) { stringBuilder.AppendLine(" " + "CE_IndirectFire".Translate() + "\t" + GenText.ToStringByStyle(indirectFireShift, ToStringStyle.FloatTwo) + " c"); PlayerKnowledgeDatabase.KnowledgeDemonstrated(CE_ConceptDefOf.CE_MortarDirectFire, KnowledgeAmount.FrameDisplayed); // Show we learned about indirect fire penalties } } else { if (cover != null) { stringBuilder.AppendLine(" " + "CE_CoverHeight".Translate() + "\t" + new CollisionVertical(cover).Max + " c"); } if (target.Thing != null) { stringBuilder.AppendLine(" " + "CE_TargetHeight".Translate() + "\t" + GenText.ToStringByStyle(new CollisionVertical(target.Thing).HeightRange.Span, ToStringStyle.FloatTwo) + " c"); stringBuilder.AppendLine(" " + "CE_TargetWidth".Translate() + "\t" + GenText.ToStringByStyle(CE_Utility.GetCollisionWidth(target.Thing), ToStringStyle.FloatTwo) + " c"); var pawn = target.Thing as Pawn; if (pawn != null && pawn.IsCrouching()) { LessonAutoActivator.TeachOpportunity(CE_ConceptDefOf.CE_Crouching, OpportunityType.GoodToKnow); } } PlayerKnowledgeDatabase.KnowledgeDemonstrated(CE_ConceptDefOf.CE_AimingSystem, KnowledgeAmount.FrameDisplayed); // Show we learned about the aiming system } return(stringBuilder.ToString()); }
public Vector2 GetRandCircularVec() { Vector2 vec = CE_Utility.GenRandInCircle(visibilityShift + circularMissRadius + indirectFireShift); return(vec); }
public virtual void RayCast(Thing launcher, VerbProperties verbProps, Vector2 origin, float shotAngle, float shotRotation, float shotHeight = 0f, float shotSpeed = -1f, float spreadDegrees = 0f, float aperatureSize = 0.03f, Thing equipment = null) { float magicSpreadFactor = Mathf.Sin(0.06f / 2 * Mathf.Deg2Rad) + aperatureSize; float magicLaserDamageConstant = 1 / (magicSpreadFactor * magicSpreadFactor * 3.14159f); ProjectilePropertiesCE pprops = def.projectile as ProjectilePropertiesCE; shotRotation = Mathf.Deg2Rad * shotRotation + (float)(3.14159 / 2.0f); Vector3 direction = new Vector3(Mathf.Cos(shotRotation) * Mathf.Cos(shotAngle), Mathf.Sin(shotAngle), Mathf.Sin(shotRotation) * Mathf.Cos(shotAngle)); Vector3 origin3 = new Vector3(origin.x, shotHeight, origin.y); Map map = launcher.Map; Vector3 destination = direction * verbProps.range + origin3; this.shotAngle = shotAngle; this.shotHeight = shotHeight; this.shotRotation = shotRotation; this.launcher = launcher; this.origin = origin; equipmentDef = equipment?.def ?? null; Ray ray = new Ray(origin3, direction); var lbce = this as LaserBeamCE; float spreadRadius = Mathf.Sin(spreadDegrees / 2.0f * Mathf.Deg2Rad); LaserGunDef defWeapon = equipmentDef as LaserGunDef; Vector3 muzzle = ray.GetPoint((defWeapon == null ? 0.9f : defWeapon.barrelLength)); var it_bounds = CE_Utility.GetBoundsFor(intendedTarget); for (int i = 1; i < verbProps.range; i++) { float spreadArea = (i * spreadRadius + aperatureSize) * (i * spreadRadius + aperatureSize) * 3.14159f; if (pprops.damageFalloff) { lbce.DamageModifier = 1 / (magicLaserDamageConstant * spreadArea); } Vector3 tp = ray.GetPoint(i); if (tp.y > CollisionVertical.WallCollisionHeight) { break; } if (tp.y < 0) { destination = tp; landed = true; ExactPosition = tp; Position = ExactPosition.ToIntVec3(); break; } foreach (Thing thing in Map.thingGrid.ThingsListAtFast(tp.ToIntVec3())) { if (this == thing) { continue; } var bounds = CE_Utility.GetBoundsFor(thing); if (!bounds.IntersectRay(ray, out var dist)) { continue; } if (i < 2 && thing != intendedTarget) { continue; } if (thing is Plant plant) { if (!Rand.Chance(thing.def.fillPercent * plant.Growth)) { continue; } } else if (thing is Building) { if (!Rand.Chance(thing.def.fillPercent)) { continue; } } ExactPosition = tp; destination = tp; landed = true; LastPos = destination; ExactPosition = destination; Position = ExactPosition.ToIntVec3(); lbce.SpawnBeam(muzzle, destination); lbce.Impact(thing, muzzle); return; } } if (lbce != null) { lbce.SpawnBeam(muzzle, destination); Destroy(DestroyMode.Vanish); return; } }
protected override void Impact(Thing hitThing) { Map map = base.Map; BattleLogEntry_RangedImpact logEntry = null; if (logMisses || (!logMisses && hitThing != null && (hitThing is Pawn || hitThing is Building_Turret) )) { LogImpact(hitThing, out logEntry); } if (hitThing != null) { // launcher being the pawn equipping the weapon, not the weapon itself int damageAmountBase = def.projectile.GetDamageAmount(CE_Utility.GetWeaponFromLauncher(launcher)); DamageDefExtensionCE damDefCE = def.projectile.damageDef.GetModExtension <DamageDefExtensionCE>() ?? new DamageDefExtensionCE(); var projectilePropsCE = def.projectile as ProjectilePropertiesCE; DamageInfo dinfo = new DamageInfo( def.projectile.damageDef, damageAmountBase, projectilePropsCE.armorPenetration, //Armor Penetration ExactRotation.eulerAngles.y, launcher, null, def); // Set impact height BodyPartDepth partDepth = damDefCE != null && damDefCE.harmOnlyOutsideLayers ? BodyPartDepth.Outside : BodyPartDepth.Undefined; //NOTE: ExactPosition.y isn't always Height at the point of Impact! BodyPartHeight partHeight = new CollisionVertical(hitThing).GetCollisionBodyHeight(ExactPosition.y); dinfo.SetBodyRegion(partHeight, partDepth); if (damDefCE != null && damDefCE.harmOnlyOutsideLayers) { dinfo.SetBodyRegion(BodyPartHeight.Undefined, BodyPartDepth.Outside); } // Apply primary damage hitThing.TakeDamage(dinfo).AssociateWithLog(logEntry); // Apply secondary to non-pawns (pawn secondary damage is handled in the damage worker) if (!(hitThing is Pawn) && projectilePropsCE != null && !projectilePropsCE.secondaryDamage.NullOrEmpty()) { foreach (SecondaryDamage cur in projectilePropsCE.secondaryDamage) { if (hitThing.Destroyed) { break; } var secDinfo = new DamageInfo( cur.def, cur.amount, projectilePropsCE.armorPenetration, //Armor Penetration ExactRotation.eulerAngles.y, launcher, null, def ); hitThing.TakeDamage(secDinfo).AssociateWithLog(logEntry); } } } else { SoundDefOf.BulletImpact_Ground.PlayOneShot(new TargetInfo(base.Position, map, false)); //Only display a dirt hit for projectiles with a dropshadow if (base.castShadow) { MoteMaker.MakeStaticMote(ExactPosition, map, ThingDefOf.Mote_ShotHit_Dirt, 1f); } } base.Impact(hitThing); }
/// <summary> /// Takes into account the target being downed and the projectile having been fired while the target was downed, and /// the target's bodySize /// </summary> private bool ImpactThroughBodySize(Thing thing, float height) { Pawn pawn = thing as Pawn; if (pawn != null) { PersonalShield shield = null; if (pawn.RaceProps.Humanlike) { // check for shield user List <Apparel> wornApparel = pawn.apparel.WornApparel; for (int i = 0; i < wornApparel.Count; i++) { if (wornApparel[i] is PersonalShield) { shield = (PersonalShield)wornApparel[i]; break; } } } //Add suppression CompSuppressable compSuppressable = pawn.TryGetComp <CompSuppressable>(); if (compSuppressable != null) { if (shield == null || (shield != null && shield?.ShieldState == ShieldState.Resetting)) { /* * if (pawn.skills.GetSkill(SkillDefOf.Shooting).level >= 1) * { * suppressionAmount = (def.projectile.damageAmountBase * (1f - ((pawn.skills.GetSkill(SkillDefOf.Shooting).level) / 100) * 3)); * } * else suppressionAmount = def.projectile.damageAmountBase; */ suppressionAmount = def.projectile.damageAmountBase; ProjectilePropertiesCE propsCE = def.projectile as ProjectilePropertiesCE; float penetrationAmount = propsCE == null ? 0f : propsCE.armorPenetration; suppressionAmount *= 1 - Mathf.Clamp(compSuppressable.parentArmor - penetrationAmount, 0, 1); compSuppressable.AddSuppression(suppressionAmount, origin.ToIntVec3()); } } //Check horizontal distance Vector3 dest = destination; Vector3 orig = origin; Vector3 pawnPos = pawn.DrawPos; float closestDistToPawn = Math.Abs((dest.z - orig.z) * pawnPos.x - (dest.x - orig.x) * pawnPos.z + dest.x * orig.z - dest.z * orig.x) / (float) Math.Sqrt((dest.z - orig.z) * (dest.z - orig.z) + (dest.x - orig.x) * (dest.x - orig.x)); if (closestDistToPawn <= CE_Utility.GetCollisionWidth(pawn)) { //Check vertical distance float pawnHeight = CE_Utility.GetCollisionHeight(pawn); if (height < pawnHeight) { Impact(thing); return(true); } } } if (thing.def.fillPercent > 0 || thing.def.Fillage == FillCategory.Full) { if (height < CE_Utility.GetCollisionHeight(thing) || thing.def.Fillage == FillCategory.Full) { Impact(thing); return(true); } } return(false); }
private void ApplyDamagePartial(DamageInfo dinfo, Pawn pawn, ref LocalInjuryResult result) { BodyPartRecord exactPartFromDamageInfo = GetExactPartFromDamageInfo(dinfo, pawn); if (exactPartFromDamageInfo == null) { return; } // Only apply armor if we propagate damage to the outside or the body part itself is outside, secondary damage types should directly damage organs, bypassing armor bool involveArmor = !dinfo.InstantOldInjury && !result.deflected && (dinfo.Def.harmAllLayersUntilOutside || exactPartFromDamageInfo.depth == BodyPartDepth.Outside); int damageAmount = dinfo.Amount; if (involveArmor) { damageAmount = CE_Utility.GetAfterArmorDamage(pawn, dinfo.Amount, exactPartFromDamageInfo, dinfo, true, ref result.deflected); } if ((double)damageAmount < 0.001) { result.absorbed = true; return; } // Shot absorbed and converted into blunt DamageDef_CE damageDefCE = dinfo.Def as DamageDef_CE; if (damageDefCE != null && damageDefCE.deflectable && result.deflected && dinfo.Def != CE_Utility.absorbDamageDef) { // Get outer parent of struck part BodyPartRecord currentPart = exactPartFromDamageInfo; while (currentPart != null && currentPart.parent != null && currentPart.depth != BodyPartDepth.Outside) { currentPart = currentPart.parent; } DamageInfo dinfo2 = new DamageInfo(CE_Utility.absorbDamageDef, damageAmount, dinfo.Angle, dinfo.Instigator, currentPart, dinfo.WeaponGear); ApplyDamagePartial(dinfo2, pawn, ref result); return; } //Creating the Hediff HediffDef hediffDefFromDamage = HealthUtility.GetHediffDefFromDamage(dinfo.Def, pawn, exactPartFromDamageInfo); Hediff_Injury hediff_Injury = (Hediff_Injury)HediffMaker.MakeHediff(hediffDefFromDamage, pawn, null); hediff_Injury.Part = exactPartFromDamageInfo; hediff_Injury.source = dinfo.WeaponGear; hediff_Injury.sourceBodyPartGroup = dinfo.WeaponBodyPartGroup; hediff_Injury.sourceHediffDef = dinfo.WeaponLinkedHediff; hediff_Injury.Severity = (float)damageAmount; if (dinfo.InstantOldInjury) { HediffComp_GetsOld hediffComp_GetsOld = hediff_Injury.TryGetComp <HediffComp_GetsOld>(); if (hediffComp_GetsOld != null) { hediffComp_GetsOld.IsOld = true; } else { Log.Error(string.Concat(new object[] { "Tried to create instant old injury on Hediff without a GetsOld comp: ", hediffDefFromDamage, " on ", pawn })); } } result.wounded = true; result.lastHitPart = hediff_Injury.Part; if (IsHeadshot(dinfo, hediff_Injury, pawn)) { result.headshot = true; } if (dinfo.InstantOldInjury && (hediff_Injury.def.CompPropsFor(typeof(HediffComp_GetsOld)) == null || hediff_Injury.Part.def.oldInjuryBaseChance == 0f || hediff_Injury.Part.def.IsSolid(hediff_Injury.Part, pawn.health.hediffSet.hediffs) || pawn.health.hediffSet.PartOrAnyAncestorHasDirectlyAddedParts(hediff_Injury.Part))) { return; } FinalizeAndAddInjury(pawn, hediff_Injury, dinfo, ref result); CheckPropagateDamageToInnerSolidParts(dinfo, pawn, hediff_Injury, !dinfo.InstantOldInjury, damageAmount, ref result); CheckDuplicateDamageToOuterParts(dinfo, pawn, hediff_Injury, !dinfo.InstantOldInjury, damageAmount, ref result); }
// Added targetThing to parameters so we can calculate its height private bool CanHitCellFromCellIgnoringRange(Vector3 shotSource, IntVec3 targetLoc, Thing targetThing = null) { // Vanilla checks if (verbProps.mustCastOnOpenGround && (!targetLoc.Standable(caster.Map) || caster.Map.thingGrid.CellContains(targetLoc, ThingCategory.Pawn))) { return(false); } if (verbProps.requireLineOfSight) { // Calculate shot vector Vector3 targetPos; if (targetThing != null) { Vector3 targDrawPos = targetThing.DrawPos; targetPos = new Vector3(targDrawPos.x, new CollisionVertical(targetThing).Max, targDrawPos.z); var targPawn = targetThing as Pawn; if (targPawn != null) { targetPos += targPawn.Drawer.leaner.LeanOffset * 0.6f; } } else { targetPos = targetLoc.ToVector3Shifted(); } Ray shotLine = new Ray(shotSource, (targetPos - shotSource)); // Create validator to check for intersection with partial cover var aimMode = CompFireModes?.CurrentAimMode; Predicate <IntVec3> CanShootThroughCell = (IntVec3 cell) => { Thing cover = cell.GetFirstPawn(caster.Map) ?? cell.GetCover(caster.Map); if (cover != null && cover != ShooterPawn && cover != caster && cover != targetThing && !cover.IsPlant() && !(cover is Pawn && cover.HostileTo(caster))) { // Skip this check entirely if we're doing suppressive fire and cell is adjacent to target if ((VerbPropsCE.ignorePartialLoSBlocker || aimMode == AimMode.SuppressFire) && cover.def.Fillage != FillCategory.Full) { return(true); } Bounds bounds = CE_Utility.GetBoundsFor(cover); // Simplified calculations for adjacent cover for gameplay purposes if (cover.def.Fillage != FillCategory.Full && cover.AdjacentTo8WayOrInside(caster)) { // Sanity check to prevent stuff behind us blocking LoS var cellTargDist = cell.DistanceTo(targetLoc); var shotTargDist = shotSource.ToIntVec3().DistanceTo(targetLoc); if (shotTargDist > cellTargDist) { return(cover is Pawn || bounds.size.y < shotSource.y); } } // Check for intersect if (bounds.IntersectRay(shotLine)) { if (Controller.settings.DebugDrawPartialLoSChecks) { caster.Map.debugDrawer.FlashCell(cell, 0, bounds.extents.y.ToString()); } return(false); } if (Controller.settings.DebugDrawPartialLoSChecks) { caster.Map.debugDrawer.FlashCell(cell, 0.7f, bounds.extents.y.ToString()); } } return(true); }; // Add validator to parameters foreach (IntVec3 curCell in SightUtility.GetCellsOnLine(shotSource, targetLoc.ToVector3(), caster.Map)) { if (Controller.settings.DebugDrawPartialLoSChecks) { caster.Map.debugDrawer.FlashCell(curCell, 0.4f); } if (curCell != shotSource.ToIntVec3() && curCell != targetLoc && !CanShootThroughCell(curCell)) { return(false); } } } return(true); }
public string GetTextReadout() { StringBuilder stringBuilder = new StringBuilder(); if (visibilityShift > 0) { stringBuilder.AppendLine(" " + "CE_VisibilityError".Translate() + "\t" + GenText.ToStringByStyle(visibilityShift, ToStringStyle.FloatTwo) + " c"); if (lightingShift > 0) { stringBuilder.AppendLine(" " + "Darkness".Translate() + "\t" + AsPercent(lightingShift)); } if (weatherShift > 0) { stringBuilder.AppendLine(" " + "Weather".Translate() + "\t" + AsPercent(weatherShift)); } } if (leadShift > 0) { stringBuilder.AppendLine(" " + "CE_LeadError".Translate() + "\t" + GenText.ToStringByStyle(leadShift, ToStringStyle.FloatTwo) + " c"); } if (distShift > 0) { stringBuilder.AppendLine(" " + "CE_RangeError".Translate() + "\t" + GenText.ToStringByStyle(distShift, ToStringStyle.FloatTwo) + " c"); } if (swayDegrees > 0) { stringBuilder.AppendLine(" " + "CE_Sway".Translate() + "\t\t" + GenText.ToStringByStyle(swayDegrees, ToStringStyle.FloatTwo) + "°"); } if (spreadDegrees > 0) { stringBuilder.AppendLine(" " + "CE_Spread".Translate() + "\t\t" + GenText.ToStringByStyle(spreadDegrees, ToStringStyle.FloatTwo) + "°"); } // Don't display cover and target size if our weapon has a CEP if (circularMissRadius > 0) { stringBuilder.AppendLine(" " + "CE_MissRadius".Translate() + "\t" + GenText.ToStringByStyle(circularMissRadius, ToStringStyle.FloatTwo) + " c"); if (indirectFireShift > 0) { stringBuilder.AppendLine(" " + "CE_IndirectFire".Translate() + "\t" + GenText.ToStringByStyle(indirectFireShift, ToStringStyle.FloatTwo) + " c"); } } else { if (cover != null) { stringBuilder.AppendLine(" " + "CE_CoverHeight".Translate() + "\t" + GenText.ToStringByStyle(CE_Utility.GetCollisionHeight(cover), ToStringStyle.FloatTwo) + " c"); } if (target.Thing != null) { stringBuilder.AppendLine(" " + "CE_TargetHeight".Translate() + "\t" + GenText.ToStringByStyle(CE_Utility.GetCollisionHeight(target.Thing), ToStringStyle.FloatTwo) + " c"); stringBuilder.AppendLine(" " + "CE_TargetWidth".Translate() + "\t" + GenText.ToStringByStyle(CE_Utility.GetCollisionWidth(target.Thing) * 2, ToStringStyle.FloatTwo) + " c"); } } return(stringBuilder.ToString()); }