public static void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource) { if (attack.Range <= 0.0f) { return; } //long range for the broad distance check, because large characters may still be in range even if their collider isn't float broadRange = Math.Max(attack.Range * 10.0f, 10000.0f); foreach (Character c in Character.CharacterList) { if (!c.Enabled || Math.Abs(c.WorldPosition.X - worldPosition.X) > broadRange || Math.Abs(c.WorldPosition.Y - worldPosition.Y) > broadRange) { continue; } Vector2 explosionPos = worldPosition; if (c.Submarine != null) { explosionPos -= c.Submarine.Position; } Hull hull = Hull.FindHull(ConvertUnits.ToDisplayUnits(explosionPos), null, false); bool underWater = hull == null || explosionPos.Y < hull.Surface; explosionPos = ConvertUnits.ToSimUnits(explosionPos); Dictionary <Limb, float> distFactors = new Dictionary <Limb, float>(); foreach (Limb limb in c.AnimController.Limbs) { float dist = Vector2.Distance(limb.WorldPosition, worldPosition); //calculate distance from the "outer surface" of the physics body //doesn't take the rotation of the limb into account, but should be accurate enough for this purpose float limbRadius = Math.Max(Math.Max(limb.body.width * 0.5f, limb.body.height * 0.5f), limb.body.radius); dist = Math.Max(0.0f, dist - FarseerPhysics.ConvertUnits.ToDisplayUnits(limbRadius)); if (dist > attack.Range) { continue; } float distFactor = 1.0f - dist / attack.Range; //solid obstacles between the explosion and the limb reduce the effect of the explosion by 90% if (Submarine.CheckVisibility(limb.SimPosition, explosionPos) != null) { distFactor *= 0.1f; } distFactors.Add(limb, distFactor); List <Affliction> modifiedAfflictions = new List <Affliction>(); foreach (Affliction affliction in attack.Afflictions) { modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / c.AnimController.Limbs.Length)); } c.LastDamageSource = damageSource; Character attacker = null; if (damageSource is Item item) { attacker = item.GetComponent <Projectile>()?.User; if (attacker == null) { attacker = item.GetComponent <MeleeWeapon>()?.User; } } //use a position slightly from the limb's position towards the explosion //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods Vector2 hitPos = limb.WorldPosition + (worldPosition - limb.WorldPosition) / dist * 0.01f; c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker); if (attack.StatusEffects != null && attack.StatusEffects.Any()) { attack.SetUser(attacker); var statusEffectTargets = new List <ISerializableEntity>() { c, limb }; foreach (StatusEffect statusEffect in attack.StatusEffects) { statusEffect.Apply(ActionType.OnUse, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(ActionType.Always, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(underWater ? ActionType.InWater : ActionType.NotInWater, 1.0f, damageSource, statusEffectTargets); } } if (limb.WorldPosition != worldPosition && force > 0.0f) { Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition); if (!MathUtils.IsValid(limbDiff)) { limbDiff = Rand.Vector(1.0f); } Vector2 impulse = limbDiff * distFactor * force; Vector2 impulsePoint = limb.SimPosition - limbDiff * limbRadius; limb.body.ApplyLinearImpulse(impulse, impulsePoint, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } } //sever joints if (c.IsDead && attack.SeverLimbsProbability > 0.0f) { foreach (Limb limb in c.AnimController.Limbs) { if (!distFactors.ContainsKey(limb)) { continue; } foreach (LimbJoint joint in c.AnimController.LimbJoints) { if (joint.IsSevered || (joint.LimbA != limb && joint.LimbB != limb)) { continue; } if (Rand.Range(0.0f, 1.0f) < attack.SeverLimbsProbability * distFactors[limb]) { c.AnimController.SeverLimbJoint(joint); } } } } } }
public static void ApplyExplosionForces(Vector2 worldPosition, Attack attack, float force) { if (attack.Range <= 0.0f) { return; } foreach (Character c in Character.CharacterList) { Vector2 explosionPos = worldPosition; if (c.Submarine != null) { explosionPos -= c.Submarine.Position; } explosionPos = ConvertUnits.ToSimUnits(explosionPos); Dictionary <Limb, float> distFactors = new Dictionary <Limb, float>(); foreach (Limb limb in c.AnimController.Limbs) { float dist = Vector2.Distance(limb.WorldPosition, worldPosition); //calculate distance from the "outer surface" of the physics body //doesn't take the rotation of the limb into account, but should be accurate enough for this purpose float limbRadius = Math.Max(Math.Max(limb.body.width * 0.5f, limb.body.height * 0.5f), limb.body.radius); dist = Math.Max(0.0f, dist - FarseerPhysics.ConvertUnits.ToDisplayUnits(limbRadius)); if (dist > attack.Range) { continue; } float distFactor = 1.0f - dist / attack.Range; //solid obstacles between the explosion and the limb reduce the effect of the explosion by 90% if (Submarine.CheckVisibility(limb.SimPosition, explosionPos) != null) { distFactor *= 0.1f; } distFactors.Add(limb, distFactor); c.AddDamage(limb.WorldPosition, DamageType.None, attack.GetDamage(1.0f) / c.AnimController.Limbs.Length * distFactor, attack.GetBleedingDamage(1.0f) / c.AnimController.Limbs.Length * distFactor, attack.Stun * distFactor, false); if (limb.WorldPosition != worldPosition && force > 0.0f) { Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition); Vector2 impulsePoint = limb.SimPosition - limbDiff * limbRadius; limb.body.ApplyLinearImpulse(limbDiff * distFactor * force, impulsePoint); } } //sever joints if (c.IsDead && attack.SeverLimbsProbability > 0.0f) { foreach (Limb limb in c.AnimController.Limbs) { if (!distFactors.ContainsKey(limb)) { continue; } foreach (LimbJoint joint in c.AnimController.LimbJoints) { if (joint.IsSevered || (joint.LimbA != limb && joint.LimbB != limb)) { continue; } if (Rand.Range(0.0f, 1.0f) < attack.SeverLimbsProbability * distFactors[limb]) { c.AnimController.SeverLimbJoint(joint); } } } } } }
public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams) { this.ragdoll = ragdoll; this.character = character; this.limbParams = limbParams; wearingItems = new List <WearableSprite>(); dir = Direction.Right; body = new PhysicsBody(limbParams); type = limbParams.Type; if (limbParams.IgnoreCollisions) { body.CollisionCategories = Category.None; body.CollidesWith = Category.None; ignoreCollisions = true; } else { //limbs don't collide with each other body.CollisionCategories = Physics.CollisionCharacter; body.CollidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking; } body.UserData = this; pullJoint = new FixedMouseJoint(body.FarseerBody, ConvertUnits.ToSimUnits(limbParams.PullPos * Scale)) { Enabled = false, MaxForce = ((type == LimbType.LeftHand || type == LimbType.RightHand) ? 400.0f : 150.0f) * body.Mass }; GameMain.World.AddJoint(pullJoint); var element = limbParams.Element; if (element.Attribute("mouthpos") != null) { MouthPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("mouthpos", Vector2.Zero)); } body.BodyType = BodyType.Dynamic; body.FarseerBody.AngularDamping = LimbAngularDamping; damageModifiers = new List <DamageModifier>(); foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "attack": attack = new Attack(subElement, (character == null ? "null" : character.Name) + ", limb " + type); if (attack.DamageRange <= 0) { switch (body.BodyShape) { case PhysicsBody.Shape.Circle: attack.DamageRange = body.radius; break; case PhysicsBody.Shape.Capsule: attack.DamageRange = body.height / 2 + body.radius; break; case PhysicsBody.Shape.Rectangle: attack.DamageRange = new Vector2(body.width / 2.0f, body.height / 2.0f).Length(); break; } attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange); } break; case "damagemodifier": damageModifiers.Add(new DamageModifier(subElement, character.Name)); break; } } SerializableProperties = SerializableProperty.GetProperties(this); InitProjSpecific(element); }
public Limb(Character character, XElement element, float scale = 1.0f) { this.character = character; wearingItems = new List <WearableSprite>(); dir = Direction.Right; doesFlip = ToolBox.GetAttributeBool(element, "flip", false); this.scale = scale; body = new PhysicsBody(element, scale); if (ToolBox.GetAttributeBool(element, "ignorecollisions", false)) { body.CollisionCategories = Category.None; body.CollidesWith = Category.None; ignoreCollisions = true; } else { //limbs don't collide with each other body.CollisionCategories = Physics.CollisionCharacter; body.CollidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem; } body.UserData = this; refJointIndex = -1; Vector2 pullJointPos = Vector2.Zero; if (element.Attribute("type") != null) { try { type = (LimbType)Enum.Parse(typeof(LimbType), element.Attribute("type").Value, true); } catch { type = LimbType.None; DebugConsole.ThrowError("Error in " + element + "! \"" + element.Attribute("type").Value + "\" is not a valid limb type"); } pullJointPos = ToolBox.GetAttributeVector2(element, "pullpos", Vector2.Zero) * scale; pullJointPos = ConvertUnits.ToSimUnits(pullJointPos); stepOffset = ToolBox.GetAttributeVector2(element, "stepoffset", Vector2.Zero) * scale; stepOffset = ConvertUnits.ToSimUnits(stepOffset); refJointIndex = ToolBox.GetAttributeInt(element, "refjoint", -1); } else { type = LimbType.None; } pullJoint = new FixedMouseJoint(body.FarseerBody, pullJointPos); pullJoint.Enabled = false; pullJoint.MaxForce = ((type == LimbType.LeftHand || type == LimbType.RightHand) ? 400.0f : 150.0f) * body.Mass; GameMain.World.AddJoint(pullJoint); steerForce = ToolBox.GetAttributeFloat(element, "steerforce", 0.0f); //maxHealth = Math.Max(ToolBox.GetAttributeFloat(element, "health", 100.0f),1.0f); armorSector = ToolBox.GetAttributeVector2(element, "armorsector", Vector2.Zero); armorSector.X = MathHelper.ToRadians(armorSector.X); armorSector.Y = MathHelper.ToRadians(armorSector.Y); armorValue = Math.Max(ToolBox.GetAttributeFloat(element, "armor", 0.0f), 0.0f); if (element.Attribute("mouthpos") != null) { MouthPos = ConvertUnits.ToSimUnits(ToolBox.GetAttributeVector2(element, "mouthpos", Vector2.Zero)); } body.BodyType = BodyType.Dynamic; body.FarseerBody.AngularDamping = LimbAngularDamping; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": string spritePath = subElement.Attribute("texture").Value; string spritePathWithTags = spritePath; if (character.Info != null) { spritePath = spritePath.Replace("[GENDER]", (character.Info.Gender == Gender.Female) ? "f" : ""); spritePath = spritePath.Replace("[HEADID]", character.Info.HeadSpriteId.ToString()); if (character.Info.HeadSprite != null && character.Info.SpriteTags.Any()) { string tags = ""; character.Info.SpriteTags.ForEach(tag => tags += "[" + tag + "]"); spritePathWithTags = Path.Combine( Path.GetDirectoryName(spritePath), Path.GetFileNameWithoutExtension(spritePath) + tags + Path.GetExtension(spritePath)); } } if (File.Exists(spritePathWithTags)) { sprite = new Sprite(subElement, "", spritePathWithTags); } else { sprite = new Sprite(subElement, "", spritePath); } break; case "damagedsprite": string damagedSpritePath = subElement.Attribute("texture").Value; if (character.Info != null) { damagedSpritePath = damagedSpritePath.Replace("[GENDER]", (character.Info.Gender == Gender.Female) ? "f" : ""); damagedSpritePath = damagedSpritePath.Replace("[HEADID]", character.Info.HeadSpriteId.ToString()); } damagedSprite = new Sprite(subElement, "", damagedSpritePath); break; case "attack": attack = new Attack(subElement); break; } } InitProjSpecific(element); }
public override AttackResult AddDamage(IDamageable attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false) { AttackResult result = base.AddDamage(attacker, worldPosition, attack, deltaTime, playSound); aiController.OnAttacked(attacker, result.Damage + result.Bleeding); return(result); }
public override AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb limb = null, string identifier = "") { AttackResult result = base.ApplyAttack(attacker, worldPosition, attack, deltaTime, playSound, limb, identifier); aiController.OnAttacked(attacker, result.Damage + result.Bleeding); return(result); }
private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker) { if (attack.Range <= 0.0f) { return; } //long range for the broad distance check, because large characters may still be in range even if their collider isn't float broadRange = Math.Max(attack.Range * 10.0f, 10000.0f); foreach (Character c in Character.CharacterList) { if (!c.Enabled || Math.Abs(c.WorldPosition.X - worldPosition.X) > broadRange || Math.Abs(c.WorldPosition.Y - worldPosition.Y) > broadRange) { continue; } if (onlyInside && c.Submarine == null) { continue; } else if (onlyOutside && c.Submarine != null) { continue; } Vector2 explosionPos = worldPosition; if (c.Submarine != null) { explosionPos -= c.Submarine.Position; } Hull hull = Hull.FindHull(explosionPos, null, false); bool underWater = hull == null || explosionPos.Y < hull.Surface; explosionPos = ConvertUnits.ToSimUnits(explosionPos); Dictionary <Limb, float> distFactors = new Dictionary <Limb, float>(); Dictionary <Limb, float> damages = new Dictionary <Limb, float>(); List <Affliction> modifiedAfflictions = new List <Affliction>(); foreach (Limb limb in c.AnimController.Limbs) { if (limb.IsSevered || limb.IgnoreCollisions || !limb.body.Enabled) { continue; } float dist = Vector2.Distance(limb.WorldPosition, worldPosition); //calculate distance from the "outer surface" of the physics body //doesn't take the rotation of the limb into account, but should be accurate enough for this purpose float limbRadius = limb.body.GetMaxExtent(); dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius)); if (dist > attack.Range) { continue; } float distFactor = 1.0f - dist / attack.Range; //solid obstacles between the explosion and the limb reduce the effect of the explosion if (!ignoreCover) { distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition); } distFactors.Add(limb, distFactor); modifiedAfflictions.Clear(); foreach (Affliction affliction in attack.Afflictions.Keys) { //previously the damage would be divided by the number of limbs (the intention was to prevent characters with more limbs taking more damage from explosions) //that didn't work well on large characters like molochs and endworms: the explosions tend to only damage one or two of their limbs, and since the characters //have lots of limbs, they tended to only take a fraction of the damage they should //now we just divide by 10, which keeps the damage to normal-sized characters roughly the same as before and fixes the large characters modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / 10)); } c.LastDamageSource = damageSource; if (attacker == null) { if (damageSource is Item item) { attacker = item.GetComponent <Projectile>()?.User; if (attacker == null) { attacker = item.GetComponent <MeleeWeapon>()?.User; } } } //use a position slightly from the limb's position towards the explosion //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods Vector2 dir = worldPosition - limb.WorldPosition; Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f; AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker); damages.Add(limb, attackResult.Damage); if (attack.StatusEffects != null && attack.StatusEffects.Any()) { attack.SetUser(attacker); var statusEffectTargets = new List <ISerializableEntity>() { c, limb }; foreach (StatusEffect statusEffect in attack.StatusEffects) { statusEffect.Apply(ActionType.OnUse, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(ActionType.Always, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(underWater ? ActionType.InWater : ActionType.NotInWater, 1.0f, damageSource, statusEffectTargets); } } if (limb.WorldPosition != worldPosition && !MathUtils.NearlyEqual(force, 0.0f)) { Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition); if (!MathUtils.IsValid(limbDiff)) { limbDiff = Rand.Vector(1.0f); } Vector2 impulse = limbDiff * distFactor * force; Vector2 impulsePoint = limb.SimPosition - limbDiff * limbRadius; limb.body.ApplyLinearImpulse(impulse, impulsePoint, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.2f); } } if (c == Character.Controlled && !c.IsDead && playTinnitus) { Limb head = c.AnimController.GetLimb(LimbType.Head); if (head != null && damages.TryGetValue(head, out float headDamage) && headDamage > 0.0f && distFactors.TryGetValue(head, out float headFactor)) { PlayTinnitusProjSpecific(headFactor); } } //sever joints if (attack.SeverLimbsProbability > 0.0f) { foreach (Limb limb in c.AnimController.Limbs) { if (limb.character.Removed || limb.Removed) { continue; } if (limb.IsSevered) { continue; } if (!c.IsDead && !limb.CanBeSeveredAlive) { continue; } if (distFactors.TryGetValue(limb, out float distFactor)) { if (damages.TryGetValue(limb, out float damage)) { c.TrySeverLimbJoints(limb, attack.SeverLimbsProbability * distFactor, damage, allowBeheading: true); } } } } } }
private Item GetWeapon(IEnumerable <ItemComponent> weaponList, out ItemComponent weaponComponent) { weaponComponent = null; float bestPriority = 0; float lethalDmg = -1; bool enemyIsClose = EnemyIsClose(); foreach (var weapon in weaponList) { float priority = weapon.CombatPriority; if (!IsLoaded(weapon)) { if (weapon is RangedWeapon && enemyIsClose) { // Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo). continue; } else { // Halve the priority for weapons that don't have proper ammunition loaded. priority /= 2; } } if (Enemy.Stun > 1) { // Enemy is stunned, reduce the priority of stunner weapons. Attack attack = GetAttackDefinition(weapon); if (attack != null) { lethalDmg = attack.GetTotalDamage(); float max = lethalDmg + 1; if (weapon.Item.HasTag("stunner")) { priority = max; } else { float stunDmg = ApproximateStunDamage(weapon, attack); float diff = stunDmg - lethalDmg; priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max); } } } else if (Mode == CombatMode.Arrest) { // Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons. if (weapon.Item.HasTag("stunner")) { priority *= 2; } else { Attack attack = GetAttackDefinition(weapon); if (attack != null) { lethalDmg = attack.GetTotalDamage(); float stunDmg = ApproximateStunDamage(weapon, attack); float diff = stunDmg - lethalDmg; if (diff < 0) { priority /= 2; } } } } else if (weapon is MeleeWeapon && weapon.Item.HasTag("stunner") && !CanMeleeStunnerStun(weapon)) { Attack attack = GetAttackDefinition(weapon); priority = attack?.GetTotalDamage() ?? priority / 2; } if (priority > bestPriority) { weaponComponent = weapon; bestPriority = priority; } } if (weaponComponent == null) { return(null); } if (bestPriority < 1) { return(null); } if (Mode == CombatMode.Arrest) { if (weaponComponent.Item.HasTag("stunner")) { isLethalWeapon = false; } else { if (lethalDmg < 0) { lethalDmg = GetLethalDamage(weaponComponent); } isLethalWeapon = lethalDmg > 1; } if (allowHoldFire && !hasAimed && holdFireTimer <= 0) { holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f); } } return(weaponComponent.Item);
public LevelTrigger(XElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "") { TriggererPosition = new Dictionary <Entity, Vector2>(); worldPosition = position; if (element.Attributes("radius").Any() || element.Attributes("width").Any() || element.Attributes("height").Any()) { PhysicsBody = new PhysicsBody(element, scale) { CollisionCategories = Physics.CollisionLevel, CollidesWith = Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionProjectile | Physics.CollisionWall, }; PhysicsBody.FarseerBody.OnCollision += PhysicsBody_OnCollision; PhysicsBody.FarseerBody.OnSeparation += PhysicsBody_OnSeparation; PhysicsBody.FarseerBody.SetIsSensor(true); PhysicsBody.FarseerBody.BodyType = BodyType.Static; PhysicsBody.FarseerBody.BodyType = BodyType.Kinematic; ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f)); PhysicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation); } cameraShake = element.GetAttributeFloat("camerashake", 0.0f); InfectIdentifier = element.GetAttributeString("infectidentifier", null); InfectionChance = element.GetAttributeFloat("infectionchance", 0.05f); triggerOnce = element.GetAttributeBool("triggeronce", false); stayTriggeredDelay = element.GetAttributeFloat("staytriggereddelay", 0.0f); randomTriggerInterval = element.GetAttributeFloat("randomtriggerinterval", 0.0f); randomTriggerProbability = element.GetAttributeFloat("randomtriggerprobability", 0.0f); UseNetworkSyncing = element.GetAttributeBool("networksyncing", false); unrotatedForce = element.Attribute("force") != null && element.Attribute("force").Value.Contains(',') ? element.GetAttributeVector2("force", Vector2.Zero) : new Vector2(element.GetAttributeFloat("force", 0.0f), 0.0f); ForceFluctuationInterval = element.GetAttributeFloat("forcefluctuationinterval", 0.01f); ForceFluctuationStrength = Math.Max(element.GetAttributeFloat("forcefluctuationstrength", 0.0f), 0.0f); ForceFalloff = element.GetAttributeBool("forcefalloff", true); GlobalForceDecreaseInterval = element.GetAttributeFloat("globalforcedecreaseinterval", 0.0f); ForceVelocityLimit = ConvertUnits.ToSimUnits(element.GetAttributeFloat("forcevelocitylimit", float.MaxValue)); string forceModeStr = element.GetAttributeString("forcemode", "Force"); if (!Enum.TryParse(forceModeStr, out forceMode)) { DebugConsole.ThrowError("Error in LevelTrigger config: \"" + forceModeStr + "\" is not a valid force mode."); } CalculateDirectionalForce(); string triggeredByStr = element.GetAttributeString("triggeredby", "Character"); if (!Enum.TryParse(triggeredByStr, out triggeredBy)) { DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type."); } UpdateCollisionCategories(); TriggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f); var tagsArray = element.GetAttributeStringArray("tags", new string[0]); foreach (string tag in tagsArray) { tags.Add(tag.ToLower()); } if (triggeredBy.HasFlag(TriggererType.OtherTrigger)) { var otherTagsArray = element.GetAttributeStringArray("allowedothertriggertags", new string[0]); foreach (string tag in otherTagsArray) { allowedOtherTriggerTags.Add(tag.ToLower()); } } foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "statuseffect": statusEffects.Add(StatusEffect.Load(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in " + parentDebugName)); break; case "attack": case "damage": var attack = new Attack(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in " + parentDebugName); if (!triggerOnce) { var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step); attack.Afflictions.Clear(); foreach (Affliction affliction in multipliedAfflictions) { attack.Afflictions.Add(affliction, null); } } attacks.Add(attack); break; } } forceFluctuationTimer = Rand.Range(0.0f, ForceFluctuationInterval); randomTriggerTimer = Rand.Range(0.0f, randomTriggerInterval); }
private Item GetWeapon(IEnumerable <ItemComponent> weaponList, out ItemComponent weaponComponent) { weaponComponent = null; float bestPriority = 0; float lethalDmg = -1; foreach (var weapon in weaponList) { // By default, the bots won't go offensive with bad weapons, unless they are close to the enemy or ordered to fight enemies. // NPC characters ignore this check. if ((initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest) && character.TeamID != Character.TeamType.FriendlyNPC) { if (!objectiveManager.IsCurrentOrder <AIObjectiveFightIntruders>() && !EnemyIsClose()) { if (weapon.CombatPriority < goodWeaponPriority) { continue; } } } float priority = weapon.CombatPriority; if (!IsLoaded(weapon)) { if (weapon is RangedWeapon && EnemyIsClose()) { // Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo). continue; } else { // Halve the priority for weapons that don't have proper ammunition loaded. priority /= 2; } } if (Enemy.Stun > 1) { // Enemy is stunned, reduce the priority of stunner weapons. Attack attack = GetAttackDefinition(weapon); if (attack != null) { lethalDmg = attack.GetTotalDamage(); float max = lethalDmg + 1; if (weapon.Item.HasTag("stunner")) { priority = max; } else { float stunDmg = ApproximateStunDamage(weapon, attack); float diff = stunDmg - lethalDmg; priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max); } } } else if (Mode == CombatMode.Arrest) { // Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons. if (weapon.Item.HasTag("stunner")) { priority *= 2; } else { Attack attack = GetAttackDefinition(weapon); if (attack != null) { lethalDmg = attack.GetTotalDamage(); float stunDmg = ApproximateStunDamage(weapon, attack); float diff = stunDmg - lethalDmg; if (diff < 0) { priority /= 2; } } } } if (priority > bestPriority) { weaponComponent = weapon; bestPriority = priority; } } if (weaponComponent == null) { return(null); } if (bestPriority < 1) { return(null); } if (Mode == CombatMode.Arrest) { if (weaponComponent.Item.HasTag("stunner")) { isLethalWeapon = false; } else { if (lethalDmg < 0) { lethalDmg = GetLethalDamage(weaponComponent); } isLethalWeapon = lethalDmg > 1; } if (allowHoldFire && !hasAimed && holdFireTimer <= 0) { holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f); } } return(weaponComponent.Item); bool EnemyIsClose() => character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500; Attack GetAttackDefinition(ItemComponent weapon) { Attack attack = null; if (weapon is MeleeWeapon meleeWeapon) { attack = meleeWeapon.Attack; } else if (weapon is RangedWeapon rangedWeapon) { attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack; } return(attack); } float GetLethalDamage(ItemComponent weapon) { float lethalDmg = 0; Attack attack = GetAttackDefinition(weapon); if (attack != null) { lethalDmg = attack.GetTotalDamage(); } return(lethalDmg); } float ApproximateStunDamage(ItemComponent weapon, Attack attack) { // Try to reduce the priority using the actual damage values and status effects. // This is an approximation, because we can't check the status effect conditions here. // The result might be incorrect if there is a high stun effect that's only applied in certain conditions. var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type == ActionType.OnUse && se.HasRequiredItems(character)); if (weapon.statusEffectLists != null && weapon.statusEffectLists.TryGetValue(ActionType.OnUse, out List <StatusEffect> hitEffects)) { statusEffects = statusEffects.Concat(hitEffects); } float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == "stun" ? a.Strength : 0); float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se => { float stunAmount = 0; var stunAffliction = se.Afflictions.Find(a => a.Identifier == "stun"); if (stunAffliction != null) { stunAmount = stunAffliction.Strength; } return(stunAmount); }); return(attack.Stun + afflictionsStun + effectsStun); } }