613 lines
23 KiB
C#
613 lines
23 KiB
C#
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using FarseerPhysics.Dynamics;
|
|
using FarseerPhysics.Dynamics.Contacts;
|
|
using Lidgren.Network;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
partial class LevelTrigger
|
|
{
|
|
[Flags]
|
|
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;
|
|
|
|
private PhysicsBody physicsBody;
|
|
|
|
/// <summary>
|
|
/// Effects applied to entities that are inside the trigger
|
|
/// </summary>
|
|
private List<StatusEffect> statusEffects = new List<StatusEffect>();
|
|
|
|
/// <summary>
|
|
/// Attacks applied to entities that are inside the trigger
|
|
/// </summary>
|
|
private List<Attack> attacks = new List<Attack>();
|
|
|
|
private float cameraShake;
|
|
private Vector2 unrotatedForce;
|
|
private float forceFluctuationTimer, currentForceFluctuation = 1.0f;
|
|
|
|
private HashSet<Entity> triggerers = new HashSet<Entity>();
|
|
|
|
private TriggererType triggeredBy;
|
|
|
|
private float randomTriggerInterval;
|
|
private float randomTriggerProbability;
|
|
private float randomTriggerTimer;
|
|
|
|
private float triggeredTimer;
|
|
|
|
//how far away this trigger can activate other triggers from
|
|
private float triggerOthersDistance;
|
|
|
|
private HashSet<string> tags = new HashSet<string>();
|
|
|
|
//other triggers have to have at least one of these tags to trigger this one
|
|
private HashSet<string> allowedOtherTriggerTags = new HashSet<string>();
|
|
|
|
/// <summary>
|
|
/// How long the trigger stays in the triggered state after triggerers have left
|
|
/// </summary>
|
|
private 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 { return physicsBody; }
|
|
}
|
|
|
|
public float TriggerOthersDistance
|
|
{
|
|
get { return triggerOthersDistance; }
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private 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 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.IsSensor = true;
|
|
physicsBody.FarseerBody.IsStatic = true;
|
|
physicsBody.FarseerBody.IsKinematic = true;
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step);
|
|
attack.Afflictions.Clear();
|
|
foreach (Affliction affliction in multipliedAfflictions)
|
|
{
|
|
attack.Afflictions.Add(affliction);
|
|
}
|
|
attacks.Add(attack);
|
|
break;
|
|
}
|
|
}
|
|
|
|
forceFluctuationTimer = Rand.Range(0.0f, ForceFluctuationInterval);
|
|
randomTriggerTimer = Rand.Range(0.0f, randomTriggerInterval);
|
|
}
|
|
|
|
private void UpdateCollisionCategories()
|
|
{
|
|
if (physicsBody == null) return;
|
|
|
|
var collidesWith = Physics.CollisionNone;
|
|
if (triggeredBy.HasFlag(TriggererType.Character) || 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;
|
|
|
|
physicsBody.CollidesWith = 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);
|
|
}
|
|
|
|
private bool PhysicsBody_OnCollision(Fixture fixtureA, Fixture fixtureB, FarseerPhysics.Dynamics.Contacts.Contact contact)
|
|
{
|
|
Entity entity = GetEntity(fixtureB);
|
|
if (entity == null) return false;
|
|
|
|
if (entity is Character character)
|
|
{
|
|
if (character.CurrentHull != null) return false;
|
|
if (character.ConfigPath == Character.HumanConfigFile)
|
|
{
|
|
if (!triggeredBy.HasFlag(TriggererType.Human)) return false;
|
|
}
|
|
else
|
|
{
|
|
if (!triggeredBy.HasFlag(TriggererType.Creature)) return false;
|
|
}
|
|
}
|
|
else if (entity is Item item)
|
|
{
|
|
if (item.CurrentHull != null) return false;
|
|
if (!triggeredBy.HasFlag(TriggererType.Item)) return false;
|
|
}
|
|
else if (entity is Submarine)
|
|
{
|
|
if (!triggeredBy.HasFlag(TriggererType.Submarine)) return false;
|
|
}
|
|
|
|
if (!triggerers.Contains(entity))
|
|
{
|
|
if (!IsTriggered)
|
|
{
|
|
OnTriggered?.Invoke(this, entity);
|
|
}
|
|
TriggererPosition[entity] = entity.WorldPosition;
|
|
triggerers.Add(entity);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void PhysicsBody_OnSeparation(Fixture fixtureA, Fixture fixtureB)
|
|
{
|
|
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;
|
|
}
|
|
|
|
//check if there are any other contacts with the entity
|
|
//(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)
|
|
ContactEdge contactEdge = fixtureA.Body.ContactList;
|
|
while (contactEdge != null)
|
|
{
|
|
if (contactEdge.Contact != null &&
|
|
contactEdge.Contact.IsTouching)
|
|
{
|
|
var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ?
|
|
contactEdge.Contact.FixtureB :
|
|
contactEdge.Contact.FixtureA);
|
|
if (otherEntity == entity) return;
|
|
}
|
|
contactEdge = contactEdge.Next;
|
|
}
|
|
|
|
if (triggerers.Contains(entity))
|
|
{
|
|
TriggererPosition.Remove(entity);
|
|
triggerers.Remove(entity);
|
|
}
|
|
}
|
|
|
|
private 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(LevelObject levelObject, LevelTrigger otherTrigger)
|
|
{
|
|
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)
|
|
{
|
|
OnTriggered?.Invoke(this, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Update(float deltaTime)
|
|
{
|
|
if (ParentTrigger != null && !ParentTrigger.IsTriggered) return;
|
|
|
|
triggerers.RemoveWhere(t => t.Removed);
|
|
|
|
bool isNotClient = true;
|
|
#if CLIENT
|
|
isNotClient = GameMain.Client == null;
|
|
#endif
|
|
|
|
if (!UseNetworkSyncing || isNotClient)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (stayTriggeredDelay > 0.0f)
|
|
{
|
|
if (triggerers.Count == 0)
|
|
{
|
|
triggeredTimer -= deltaTime;
|
|
}
|
|
else
|
|
{
|
|
triggeredTimer = stayTriggeredDelay;
|
|
}
|
|
}
|
|
|
|
foreach (Entity triggerer in triggerers)
|
|
{
|
|
foreach (StatusEffect effect in statusEffects)
|
|
{
|
|
if (triggerer is Character)
|
|
{
|
|
effect.Apply(effect.type, deltaTime, triggerer, (Character)triggerer);
|
|
}
|
|
else if (triggerer is Item)
|
|
{
|
|
effect.Apply(effect.type, deltaTime, triggerer, ((Item)triggerer).AllPropertyObjects);
|
|
}
|
|
}
|
|
|
|
if (triggerer is IDamageable damageable)
|
|
{
|
|
foreach (Attack attack in attacks)
|
|
{
|
|
attack.DoDamage(null, damageable, WorldPosition, deltaTime, false);
|
|
}
|
|
}
|
|
else if (triggerer is Submarine submarine)
|
|
{
|
|
foreach (Attack attack in attacks)
|
|
{
|
|
float structureDamage = attack.GetStructureDamage(deltaTime);
|
|
if (structureDamage > 0.0f)
|
|
{
|
|
Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Force.LengthSquared() > 0.01f)
|
|
{
|
|
if (triggerer is Character character)
|
|
{
|
|
ApplyForce(character.AnimController.Collider, deltaTime);
|
|
foreach (Limb limb in character.AnimController.Limbs)
|
|
{
|
|
ApplyForce(limb.body, deltaTime);
|
|
}
|
|
}
|
|
else if (triggerer is Submarine submarine)
|
|
{
|
|
ApplyForce(submarine.SubBody.Body, deltaTime);
|
|
}
|
|
}
|
|
|
|
if (triggerer == Character.Controlled || triggerer == Character.Controlled?.Submarine)
|
|
{
|
|
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, cameraShake);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ApplyForce(PhysicsBody body, float deltaTime)
|
|
{
|
|
float distFactor = 1.0f;
|
|
if (ForceFalloff)
|
|
{
|
|
distFactor = 1.0f - ConvertUnits.ToDisplayUnits(Vector2.Distance(body.SimPosition, PhysicsBody.SimPosition)) / ColliderRadius;
|
|
if (distFactor < 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, 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);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
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) 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(NetBuffer 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);
|
|
}
|
|
}
|
|
}
|
|
}
|