863 lines
34 KiB
C#
863 lines
34 KiB
C#
using Barotrauma.Extensions;
|
|
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using FarseerPhysics.Dynamics;
|
|
using FarseerPhysics.Dynamics.Contacts;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using Barotrauma.Items.Components;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
partial class LevelTrigger
|
|
{
|
|
[Flags]
|
|
public enum TriggererType
|
|
{
|
|
None = 0,
|
|
Human = 1,
|
|
Creature = 2,
|
|
Character = Human | Creature,
|
|
Submarine = 4,
|
|
Item = 8,
|
|
OtherTrigger = 16
|
|
}
|
|
|
|
public enum TriggerForceMode
|
|
{
|
|
Force, //default, apply a force to the object over time
|
|
Acceleration, //apply an acceleration to the object, ignoring it's mass
|
|
Impulse, //apply an instant force, ignoring deltaTime
|
|
LimitVelocity //clamp the velocity of the triggerer to some value
|
|
}
|
|
|
|
public Action<LevelTrigger, Entity> OnTriggered;
|
|
|
|
/// <summary>
|
|
/// Effects applied to entities that are inside the trigger
|
|
/// </summary>
|
|
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
|
|
public IEnumerable<StatusEffect> StatusEffects
|
|
{
|
|
get { return statusEffects; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attacks applied to entities that are inside the trigger
|
|
/// </summary>
|
|
private readonly List<Attack> attacks = new List<Attack>();
|
|
|
|
private readonly float cameraShake;
|
|
private Vector2 unrotatedForce;
|
|
private float forceFluctuationTimer, currentForceFluctuation = 1.0f;
|
|
|
|
private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
|
|
|
|
private readonly TriggererType triggeredBy;
|
|
private readonly Identifier triggerSpeciesOrGroup;
|
|
private readonly PropertyConditional.LogicalComparison conditionals;
|
|
|
|
private readonly float randomTriggerInterval;
|
|
private readonly float randomTriggerProbability;
|
|
private float randomTriggerTimer;
|
|
|
|
private float triggeredTimer;
|
|
private readonly HashSet<string> tags = new HashSet<string>();
|
|
|
|
//other triggers have to have at least one of these tags to trigger this one
|
|
private readonly HashSet<string> allowedOtherTriggerTags = new HashSet<string>();
|
|
|
|
/// <summary>
|
|
/// How long the trigger stays in the triggered state after triggerers have left
|
|
/// </summary>
|
|
private readonly float stayTriggeredDelay;
|
|
|
|
public LevelTrigger ParentTrigger;
|
|
|
|
public Dictionary<Entity, Vector2> TriggererPosition
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
private Vector2 worldPosition;
|
|
public Vector2 WorldPosition
|
|
{
|
|
get { return worldPosition; }
|
|
set
|
|
{
|
|
worldPosition = value;
|
|
PhysicsBody?.SetTransform(ConvertUnits.ToSimUnits(value), PhysicsBody.Rotation);
|
|
}
|
|
}
|
|
|
|
public float Rotation
|
|
{
|
|
get { return PhysicsBody == null ? 0.0f : PhysicsBody.Rotation; }
|
|
set
|
|
{
|
|
if (PhysicsBody == null) return;
|
|
PhysicsBody.SetTransform(PhysicsBody.Position, value);
|
|
CalculateDirectionalForce();
|
|
}
|
|
}
|
|
|
|
public PhysicsBody PhysicsBody { get; private set; }
|
|
|
|
public float TriggerOthersDistance { get; private set; }
|
|
|
|
public IEnumerable<Entity> Triggerers
|
|
{
|
|
get { return triggerers.AsEnumerable(); }
|
|
}
|
|
|
|
public bool IsTriggered
|
|
{
|
|
get
|
|
{
|
|
return (triggerers.Count > 0 || triggeredTimer > 0.0f) &&
|
|
(ParentTrigger == null || ParentTrigger.IsTriggered);
|
|
}
|
|
}
|
|
|
|
public Vector2 Force
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// does the force diminish by distance
|
|
/// </summary>
|
|
public bool ForceFalloff
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public float ForceFluctuationInterval
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
public float ForceFluctuationStrength
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
public float GlobalForceDecreaseInterval
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
private readonly TriggerForceMode forceMode;
|
|
public TriggerForceMode ForceMode
|
|
{
|
|
get { return forceMode; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stop applying forces to objects if they're moving faster than this
|
|
/// </summary>
|
|
public float ForceVelocityLimit
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public float ColliderRadius
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
|
|
public bool UseNetworkSyncing
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public bool NeedsNetworkSyncing
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public Identifier InfectIdentifier
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public float InfectionChance
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
private bool triggeredOnce;
|
|
private readonly bool triggerOnce;
|
|
|
|
public LevelTrigger(ContentXElement 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(element.GetAttributeBool("sensor", true));
|
|
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
|
|
|
|
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.GetAttributeIdentifier("infectidentifier", Identifier.Empty);
|
|
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.GetAttribute("force") != null && element.GetAttribute("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))
|
|
{
|
|
Identifier speciesOrGroup = triggeredByStr.ToIdentifier();
|
|
if (CharacterPrefab.Prefabs.Any(p => p.MatchesSpeciesNameOrGroup(speciesOrGroup)))
|
|
{
|
|
triggerSpeciesOrGroup = speciesOrGroup;
|
|
triggeredBy = TriggererType.Character;
|
|
}
|
|
else
|
|
{
|
|
DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type.");
|
|
}
|
|
}
|
|
if (PhysicsBody != null)
|
|
{
|
|
PhysicsBody.CollidesWith = GetCollisionCategories(triggeredBy);
|
|
}
|
|
|
|
TriggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f);
|
|
|
|
var tagsArray = element.GetAttributeStringArray("tags", Array.Empty<string>());
|
|
foreach (string tag in tagsArray)
|
|
{
|
|
tags.Add(tag.ToLowerInvariant());
|
|
}
|
|
|
|
if (triggeredBy.HasFlag(TriggererType.OtherTrigger))
|
|
{
|
|
var otherTagsArray = element.GetAttributeStringArray("allowedothertriggertags", Array.Empty<string>());
|
|
foreach (string tag in otherTagsArray)
|
|
{
|
|
allowedOtherTriggerTags.Add(tag.ToLowerInvariant());
|
|
}
|
|
}
|
|
|
|
string debugName = string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : $"LevelTrigger in {parentDebugName}";
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "statuseffect":
|
|
LoadStatusEffect(statusEffects, subElement, debugName);
|
|
break;
|
|
case "attack":
|
|
case "damage":
|
|
LoadAttack(subElement, debugName, triggerOnce, attacks);
|
|
break;
|
|
}
|
|
}
|
|
|
|
conditionals = PropertyConditional.LoadConditionals(element);
|
|
|
|
forceFluctuationTimer = Rand.Range(0.0f, ForceFluctuationInterval);
|
|
randomTriggerTimer = Rand.Range(0.0f, randomTriggerInterval);
|
|
}
|
|
|
|
public static Category GetCollisionCategories(TriggererType triggeredBy)
|
|
{
|
|
var collidesWith = Physics.CollisionNone;
|
|
if (triggeredBy.HasFlag(TriggererType.Human) || triggeredBy.HasFlag(TriggererType.Creature)) { collidesWith |= Physics.CollisionCharacter; }
|
|
if (triggeredBy.HasFlag(TriggererType.Item)) { collidesWith |= Physics.CollisionItem | Physics.CollisionProjectile; }
|
|
if (triggeredBy.HasFlag(TriggererType.Submarine)) { collidesWith |= Physics.CollisionWall; }
|
|
return collidesWith;
|
|
}
|
|
|
|
private void CalculateDirectionalForce()
|
|
{
|
|
var ca = (float)Math.Cos(-Rotation);
|
|
var sa = (float)Math.Sin(-Rotation);
|
|
|
|
Force = new Vector2(
|
|
ca * unrotatedForce.X + sa * unrotatedForce.Y,
|
|
-sa * unrotatedForce.X + ca * unrotatedForce.Y);
|
|
}
|
|
|
|
public static void LoadStatusEffect(List<StatusEffect> statusEffects, ContentXElement element, string parentDebugName)
|
|
{
|
|
statusEffects.Add(StatusEffect.Load(element, parentDebugName));
|
|
}
|
|
|
|
public static void LoadAttack(ContentXElement element, string parentDebugName, bool triggerOnce, List<Attack> attacks)
|
|
{
|
|
var attack = new Attack(element, 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);
|
|
}
|
|
|
|
private bool PhysicsBody_OnCollision(Fixture fixtureA, Fixture fixtureB, Contact contact)
|
|
{
|
|
Entity entity = GetEntity(fixtureB);
|
|
if (entity == null) { return false; }
|
|
if (!IsTriggeredByEntity(entity, triggeredBy, triggerSpeciesOrGroup: triggerSpeciesOrGroup, conditionals: conditionals, mustBeOutside: true)) { return false; }
|
|
if (!triggerers.Contains(entity))
|
|
{
|
|
if (!IsTriggered)
|
|
{
|
|
OnTriggered?.Invoke(this, entity);
|
|
}
|
|
TriggererPosition[entity] = entity.WorldPosition;
|
|
triggerers.Add(entity);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static bool IsTriggeredByEntity(
|
|
Entity entity,
|
|
TriggererType triggeredBy,
|
|
Identifier triggerSpeciesOrGroup,
|
|
PropertyConditional.LogicalComparison conditionals,
|
|
(bool mustBe, Submarine sub) mustBeOnSpecificSub = default,
|
|
bool mustBeOutside = false)
|
|
{
|
|
if (entity is Character character)
|
|
{
|
|
if (mustBeOutside && character.CurrentHull != null) { return false; }
|
|
if (mustBeOnSpecificSub.mustBe && character.Submarine != mustBeOnSpecificSub.sub) { return false; }
|
|
if (!triggerSpeciesOrGroup.IsEmpty)
|
|
{
|
|
if (character.SpeciesName != triggerSpeciesOrGroup && character.Group != triggerSpeciesOrGroup) { return false; }
|
|
}
|
|
if (character.IsHuman)
|
|
{
|
|
if (!triggeredBy.HasFlag(TriggererType.Human)) { return false; }
|
|
}
|
|
else
|
|
{
|
|
if (!triggeredBy.HasFlag(TriggererType.Creature)) { return false; }
|
|
}
|
|
}
|
|
else if (entity is Item item)
|
|
{
|
|
if (mustBeOutside && item.CurrentHull != null) { return false; }
|
|
if (mustBeOnSpecificSub.mustBe && item.Submarine != mustBeOnSpecificSub.sub) { return false; }
|
|
if (!triggeredBy.HasFlag(TriggererType.Item)) { return false; }
|
|
}
|
|
else if (entity is Submarine)
|
|
{
|
|
if (!triggeredBy.HasFlag(TriggererType.Submarine)) { return false; }
|
|
}
|
|
if (conditionals != null && entity is ISerializableEntity serializableEntity)
|
|
{
|
|
if (!PropertyConditional.CheckConditionals(serializableEntity, conditionals.Conditionals, conditionals.LogicalOperator)) { return false; }
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void PhysicsBody_OnSeparation(Fixture fixtureA, Fixture fixtureB, Contact contact)
|
|
{
|
|
Entity entity = GetEntity(fixtureB);
|
|
if (entity == null) { return; }
|
|
|
|
if (entity is Character character &&
|
|
(!character.Enabled || character.Removed) &&
|
|
triggerers.Contains(entity))
|
|
{
|
|
TriggererPosition.Remove(entity);
|
|
triggerers.Remove(entity);
|
|
return;
|
|
}
|
|
|
|
if (CheckContactsForOtherFixtures(PhysicsBody, fixtureB, entity)) { return; }
|
|
|
|
if (triggerers.Contains(entity))
|
|
{
|
|
TriggererPosition.Remove(entity);
|
|
triggerers.Remove(entity);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether any fixture of the trigger body is in contact with any fixture belonging to the physics bodies of separatingEntity
|
|
/// </summary>
|
|
/// <param name="triggerBody">Physics body of the trigger</param>
|
|
/// <param name="separatingFixture">Fixture that got separated from the trigger</param>
|
|
/// <param name="separatingEntity">Entity that got separated from the trigger</param>
|
|
/// <returns></returns>
|
|
public static bool CheckContactsForOtherFixtures(PhysicsBody triggerBody, Fixture separatingFixture, Entity separatingEntity)
|
|
{
|
|
//check if there are contacts with any other fixture of the trigger
|
|
//(the OnSeparation callback happens when two fixtures separate,
|
|
//e.g. if a body stops touching the circular fixture at the end of a capsule-shaped body)
|
|
foreach (Fixture triggerFixture in triggerBody.FarseerBody.FixtureList)
|
|
{
|
|
ContactEdge contactEdge = triggerFixture.Body.ContactList;
|
|
while (contactEdge != null)
|
|
{
|
|
if (contactEdge.Contact != null &&
|
|
contactEdge.Contact.Enabled &&
|
|
contactEdge.Contact.IsTouching)
|
|
{
|
|
//which fixture of this contact belongs to the "other" body (not the trigger itself)
|
|
Fixture otherFixture =
|
|
contactEdge.Contact.FixtureA == triggerFixture ?
|
|
contactEdge.Contact.FixtureB :
|
|
contactEdge.Contact.FixtureA;
|
|
if (otherFixture != separatingFixture)
|
|
{
|
|
var otherEntity = GetEntity(otherFixture);
|
|
if (otherEntity == separatingEntity) { return true; }
|
|
}
|
|
}
|
|
contactEdge = contactEdge.Next;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Are there any active contacts between the physics body and the target entity
|
|
/// </summary>
|
|
public static bool CheckContactsForEntity(PhysicsBody triggerBody, Entity targetEntity)
|
|
{
|
|
foreach (Fixture fixture in triggerBody.FarseerBody.FixtureList)
|
|
{
|
|
ContactEdge contactEdge = fixture.Body.ContactList == null ? null: fixture.Body.ContactList.CreateCopy();
|
|
while (contactEdge != null)
|
|
{
|
|
if (contactEdge.Contact != null &&
|
|
contactEdge.Contact.Enabled &&
|
|
contactEdge.Contact.IsTouching)
|
|
{
|
|
if ((contactEdge.Contact.FixtureA.Body == triggerBody.FarseerBody && GetEntity(contactEdge.Contact.FixtureB) == targetEntity) ||
|
|
(contactEdge.Contact.FixtureB.Body == triggerBody.FarseerBody && GetEntity(contactEdge.Contact.FixtureA) == targetEntity))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
contactEdge = contactEdge.Next;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static Entity GetEntity(Fixture fixture)
|
|
{
|
|
if (fixture.Body == null || fixture.Body.UserData == null) { return null; }
|
|
if (fixture.Body.UserData is Entity entity) { return entity; }
|
|
if (fixture.Body.UserData is Limb limb) { return limb.character; }
|
|
if (fixture.Body.UserData is SubmarineBody subBody) { return subBody.Submarine; }
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Another trigger was triggered, check if this one should react to it
|
|
/// </summary>
|
|
public void OtherTriggered(LevelTrigger otherTrigger, Entity triggerer)
|
|
{
|
|
if (!triggeredBy.HasFlag(TriggererType.OtherTrigger) || stayTriggeredDelay <= 0.0f) { return; }
|
|
|
|
//check if the other trigger has appropriate tags
|
|
if (allowedOtherTriggerTags.Count > 0)
|
|
{
|
|
if (!allowedOtherTriggerTags.Any(t => otherTrigger.tags.Contains(t))) { return; }
|
|
}
|
|
|
|
if (Vector2.DistanceSquared(WorldPosition, otherTrigger.WorldPosition) <= otherTrigger.TriggerOthersDistance * otherTrigger.TriggerOthersDistance)
|
|
{
|
|
bool wasAlreadyTriggered = IsTriggered;
|
|
triggeredTimer = stayTriggeredDelay;
|
|
if (!wasAlreadyTriggered)
|
|
{
|
|
if (!IsTriggeredByEntity(triggerer, triggeredBy, triggerSpeciesOrGroup, conditionals, mustBeOutside: true)) { return; }
|
|
if (!triggerers.Contains(triggerer))
|
|
{
|
|
if (!IsTriggered)
|
|
{
|
|
OnTriggered?.Invoke(this, triggerer);
|
|
}
|
|
TriggererPosition[triggerer] = triggerer.WorldPosition;
|
|
triggerers.Add(triggerer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
|
|
|
public void Update(float deltaTime)
|
|
{
|
|
if (ParentTrigger != null && !ParentTrigger.IsTriggered) { return; }
|
|
|
|
|
|
bool isNotClient = true;
|
|
#if CLIENT
|
|
isNotClient = GameMain.Client == null;
|
|
#endif
|
|
|
|
if (!UseNetworkSyncing || isNotClient)
|
|
{
|
|
if (GlobalForceDecreaseInterval > 0.0f && Level.Loaded?.LevelObjectManager != null &&
|
|
Level.Loaded.LevelObjectManager.GlobalForceDecreaseTimer % (GlobalForceDecreaseInterval * 2) < GlobalForceDecreaseInterval)
|
|
{
|
|
NeedsNetworkSyncing |= currentForceFluctuation > 0.0f;
|
|
currentForceFluctuation = 0.0f;
|
|
}
|
|
else if (ForceFluctuationStrength > 0.0f)
|
|
{
|
|
//no need for force fluctuation (or network updates) if the trigger limits velocity and there are no triggerers
|
|
if (forceMode != TriggerForceMode.LimitVelocity || triggerers.Any())
|
|
{
|
|
forceFluctuationTimer += deltaTime;
|
|
if (forceFluctuationTimer > ForceFluctuationInterval)
|
|
{
|
|
NeedsNetworkSyncing = true;
|
|
currentForceFluctuation = Rand.Range(1.0f - ForceFluctuationStrength, 1.0f);
|
|
forceFluctuationTimer = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (randomTriggerProbability > 0.0f)
|
|
{
|
|
randomTriggerTimer += deltaTime;
|
|
if (randomTriggerTimer > randomTriggerInterval)
|
|
{
|
|
if (Rand.Range(0.0f, 1.0f) < randomTriggerProbability)
|
|
{
|
|
NeedsNetworkSyncing = true;
|
|
triggeredTimer = stayTriggeredDelay;
|
|
}
|
|
randomTriggerTimer = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
RemoveInActiveTriggerers(PhysicsBody, triggerers);
|
|
|
|
if (stayTriggeredDelay > 0.0f)
|
|
{
|
|
if (triggerers.Count == 0)
|
|
{
|
|
triggeredTimer -= deltaTime;
|
|
}
|
|
else
|
|
{
|
|
triggeredTimer = stayTriggeredDelay;
|
|
}
|
|
}
|
|
|
|
if (triggerOnce && triggeredOnce)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (PhysicsBody != null)
|
|
{
|
|
if (currentForceFluctuation <= 0.0f && statusEffects.None() && attacks.None())
|
|
{
|
|
//no force atm, and no status effects or attacks the trigger could apply
|
|
// -> we can disable the collider and get a minor physics performance improvement
|
|
PhysicsBody.Enabled = false;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
PhysicsBody.Enabled = true;
|
|
}
|
|
}
|
|
|
|
foreach (Entity triggerer in triggerers)
|
|
{
|
|
if (triggerer.Removed) { continue; }
|
|
|
|
ApplyStatusEffects(statusEffects, worldPosition, triggerer, deltaTime, targets);
|
|
|
|
if (triggerer is IDamageable damageable)
|
|
{
|
|
ApplyAttacks(attacks, damageable, worldPosition, deltaTime);
|
|
}
|
|
else if (triggerer is Submarine submarine)
|
|
{
|
|
ApplyAttacks(attacks, worldPosition, deltaTime);
|
|
if (!InfectIdentifier.IsEmpty)
|
|
{
|
|
submarine.AttemptBallastFloraInfection(InfectIdentifier, deltaTime, InfectionChance);
|
|
}
|
|
}
|
|
|
|
if (Force.LengthSquared() > 0.01f)
|
|
{
|
|
if (triggerer is Character character)
|
|
{
|
|
ApplyForce(character.AnimController.Collider);
|
|
foreach (Limb limb in character.AnimController.Limbs)
|
|
{
|
|
if (limb.IsSevered) { continue; }
|
|
ApplyForce(limb.body);
|
|
}
|
|
}
|
|
else if (triggerer is Submarine submarine)
|
|
{
|
|
ApplyForce(submarine.SubBody.Body);
|
|
}
|
|
}
|
|
|
|
if (triggerer == Character.Controlled || triggerer == Character.Controlled?.Submarine)
|
|
{
|
|
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, cameraShake);
|
|
}
|
|
}
|
|
|
|
if (triggerOnce && triggerers.Count > 0)
|
|
{
|
|
PhysicsBody.Enabled = false;
|
|
triggeredOnce = true;
|
|
}
|
|
}
|
|
|
|
private static readonly List<Entity> triggerersToRemove = new List<Entity>();
|
|
public static void RemoveInActiveTriggerers(PhysicsBody physicsBody, HashSet<Entity> triggerers)
|
|
{
|
|
if (physicsBody == null) { return; }
|
|
|
|
triggerersToRemove.Clear();
|
|
foreach (var triggerer in triggerers)
|
|
{
|
|
if (triggerer.Removed)
|
|
{
|
|
triggerersToRemove.Add(triggerer);
|
|
}
|
|
else if (!CheckContactsForEntity(physicsBody, triggerer))
|
|
{
|
|
triggerersToRemove.Add(triggerer);
|
|
}
|
|
}
|
|
foreach (var triggerer in triggerersToRemove)
|
|
{
|
|
triggerers.Remove(triggerer);
|
|
}
|
|
}
|
|
|
|
public static void ApplyStatusEffects(List<StatusEffect> statusEffects, Vector2 worldPosition, Entity triggerer, float deltaTime, List<ISerializableEntity> targets, Item targetItem = null)
|
|
{
|
|
foreach (StatusEffect effect in statusEffects)
|
|
{
|
|
if (effect.type == ActionType.OnBroken) { return; }
|
|
Vector2? position = null;
|
|
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
|
{
|
|
position = worldPosition;
|
|
if (targetItem != null)
|
|
{
|
|
effect.Apply(effect.type, deltaTime, triggerer, targetItem.AllPropertyObjects, position);
|
|
}
|
|
}
|
|
if (triggerer is Character character)
|
|
{
|
|
effect.Apply(effect.type, deltaTime, triggerer, character, position);
|
|
if (effect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory != null)
|
|
{
|
|
foreach (Item item in character.Inventory.AllItemsMod)
|
|
{
|
|
if (item.ContainedItems == null) { continue; }
|
|
foreach (Item containedItem in item.ContainedItems)
|
|
{
|
|
effect.Apply(effect.type, deltaTime, triggerer, containedItem.AllPropertyObjects, position);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (triggerer is Item item)
|
|
{
|
|
effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position);
|
|
}
|
|
else if (triggerer is Submarine sub)
|
|
{
|
|
effect.Apply(effect.type, deltaTime, sub, Array.Empty<ISerializableEntity>(), position);
|
|
}
|
|
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
|
{
|
|
targets.Clear();
|
|
effect.AddNearbyTargets(worldPosition, targets);
|
|
effect.Apply(effect.type, deltaTime, triggerer, targets);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies attacks to a damageable.
|
|
/// </summary>
|
|
public static void ApplyAttacks(List<Attack> attacks, IDamageable damageable, Vector2 worldPosition, float deltaTime)
|
|
{
|
|
foreach (Attack attack in attacks)
|
|
{
|
|
attack.DoDamage(null, damageable, worldPosition, deltaTime, false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies attacks to structures.
|
|
/// </summary>
|
|
public static void ApplyAttacks(List<Attack> attacks, Vector2 worldPosition, float deltaTime)
|
|
{
|
|
foreach (Attack attack in attacks)
|
|
{
|
|
float structureDamage = attack.GetStructureDamage(deltaTime);
|
|
if (structureDamage > 0.0f)
|
|
{
|
|
Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f, emitWallDamageParticles: attack.EmitStructureDamageParticles);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ApplyForce(PhysicsBody body)
|
|
{
|
|
if (body == null) { return; }
|
|
|
|
float distFactor = 1.0f;
|
|
if (ForceFalloff)
|
|
{
|
|
distFactor = GetDistanceFactor(body, PhysicsBody, ColliderRadius);
|
|
if (distFactor < 0.0f) return;
|
|
}
|
|
|
|
if (MathUtils.NearlyEqual(currentForceFluctuation, 0.0f)) { return; }
|
|
|
|
switch (ForceMode)
|
|
{
|
|
case TriggerForceMode.Force:
|
|
if (ForceVelocityLimit < 1000.0f)
|
|
body.ApplyForce(Force * currentForceFluctuation * distFactor, ForceVelocityLimit);
|
|
else
|
|
body.ApplyForce(Force * currentForceFluctuation * distFactor);
|
|
break;
|
|
case TriggerForceMode.Acceleration:
|
|
if (ForceVelocityLimit < 1000.0f)
|
|
body.ApplyForce(Force * body.Mass * currentForceFluctuation * distFactor, ForceVelocityLimit);
|
|
else
|
|
body.ApplyForce(Force * body.Mass * currentForceFluctuation * distFactor);
|
|
break;
|
|
case TriggerForceMode.Impulse:
|
|
if (ForceVelocityLimit < 1000.0f)
|
|
body.ApplyLinearImpulse(Force * currentForceFluctuation * distFactor, maxVelocity: ForceVelocityLimit);
|
|
else
|
|
body.ApplyLinearImpulse(Force * currentForceFluctuation * distFactor);
|
|
break;
|
|
case TriggerForceMode.LimitVelocity:
|
|
float maxVel = ForceVelocityLimit * currentForceFluctuation * distFactor;
|
|
if (body.LinearVelocity.LengthSquared() > maxVel * maxVel)
|
|
{
|
|
body.ApplyForce(
|
|
Vector2.Normalize(-body.LinearVelocity) *
|
|
Force.Length() * body.Mass * currentForceFluctuation * distFactor,
|
|
maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
public static float GetDistanceFactor(PhysicsBody triggererBody, PhysicsBody triggerBody, float colliderRadius)
|
|
{
|
|
return 1.0f - ConvertUnits.ToDisplayUnits(Vector2.Distance(triggererBody.SimPosition, triggerBody.SimPosition)) / colliderRadius;
|
|
}
|
|
|
|
public Vector2 GetWaterFlowVelocity(Vector2 viewPosition)
|
|
{
|
|
Vector2 baseVel = GetWaterFlowVelocity();
|
|
if (baseVel.LengthSquared() < 0.1f) return Vector2.Zero;
|
|
|
|
float triggerSize = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.Radius, PhysicsBody.Width / 2.0f), PhysicsBody.Height / 2.0f));
|
|
float dist = Vector2.Distance(viewPosition, WorldPosition);
|
|
if (dist > triggerSize) return Vector2.Zero;
|
|
|
|
return baseVel * (1.0f - dist / triggerSize);
|
|
}
|
|
|
|
public Vector2 GetWaterFlowVelocity()
|
|
{
|
|
if (Force == Vector2.Zero || ForceMode == TriggerForceMode.LimitVelocity) { return Vector2.Zero; }
|
|
|
|
Vector2 vel = Force;
|
|
if (ForceMode == TriggerForceMode.Acceleration)
|
|
{
|
|
vel *= 1000.0f;
|
|
}
|
|
else if (ForceMode == TriggerForceMode.Impulse)
|
|
{
|
|
vel /= (float)Timing.Step;
|
|
}
|
|
return vel.ClampLength(ConvertUnits.ToDisplayUnits(ForceVelocityLimit)) * currentForceFluctuation;
|
|
}
|
|
|
|
public void ServerWrite(IWriteMessage msg, Client c)
|
|
{
|
|
if (ForceFluctuationStrength > 0.0f)
|
|
{
|
|
msg.WriteRangedSingle(MathHelper.Clamp(currentForceFluctuation, 0.0f, 1.0f), 0.0f, 1.0f, 8);
|
|
}
|
|
if (stayTriggeredDelay > 0.0f)
|
|
{
|
|
msg.WriteRangedSingle(MathHelper.Clamp(triggeredTimer, 0.0f, stayTriggeredDelay), 0.0f, stayTriggeredDelay, 16);
|
|
}
|
|
}
|
|
}
|
|
}
|