741e26251d
Added tags to Status Effects, which also allows for fancy stuff like checking for tags over duration elements for more interesting interactions. Adds "stackable" variable which dictates whether or not the same duration/delay effect can be applied to the same target(s) at the same time. This is a bit imperfect at the moment. Adds Chloromydride which is a non-stackable chem stabilizing critical health which stops its effects once the health is stabilized. TODO: Remove target from Targets if he already has it, and if targets becomes empty, return; TODO: Conditional tag-checking and status effect tag-checking, plus more "special" checks like SpeciesName and other non-serialized options. TODO: StatusEffect Cancel component to stop delayed/duration effects
606 lines
23 KiB
C#
606 lines
23 KiB
C#
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
#if CLIENT
|
|
using Barotrauma.Particles;
|
|
#endif
|
|
|
|
namespace Barotrauma
|
|
{
|
|
class DurationListElement
|
|
{
|
|
public StatusEffect Parent;
|
|
public Entity Entity;
|
|
public List<ISerializableEntity> Targets;
|
|
public float StartTimer;
|
|
}
|
|
partial class PropertyConditional
|
|
{
|
|
public string Attribute;
|
|
public string Operator;
|
|
public object Value;
|
|
|
|
public PropertyConditional(string Attribute, string Operator, object Value)
|
|
{
|
|
this.Attribute = Attribute;
|
|
this.Operator = Operator;
|
|
this.Value = Value;
|
|
}
|
|
|
|
public bool Matches(SerializableProperty property)
|
|
{
|
|
if (property.GetValue() == null)
|
|
{
|
|
DebugConsole.ThrowError("Couldn't compare " + Value.ToString() + " (" + Value.GetType() + ") to property \"" + property.Name + "- property.GetValue() returns null!!");
|
|
return false;
|
|
}
|
|
Type type = property.GetValue().GetType();
|
|
|
|
float? floatValue = null;
|
|
float? floatProperty = null;
|
|
if (type == typeof(float) || type == typeof(int))
|
|
{
|
|
floatValue = Convert.ToSingle(Value);
|
|
floatProperty = Convert.ToSingle(property.GetValue());
|
|
}
|
|
|
|
switch (Operator)
|
|
{
|
|
case "==":
|
|
if (property.GetValue().Equals(floatValue == null ? floatValue : Value))
|
|
return true;
|
|
break;
|
|
case "!=":
|
|
if (property.GetValue().Equals(floatValue == null ? floatValue : Value))
|
|
return true;
|
|
break;
|
|
case ">":
|
|
if (floatValue == null)
|
|
{
|
|
DebugConsole.ThrowError("Couldn't compare " + Value.ToString() + " (" + Value.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! "
|
|
+ "Make sure the type of the value set in the config files matches the type of the property.");
|
|
}
|
|
else if (floatProperty > floatValue)
|
|
return true;
|
|
break;
|
|
case "<":
|
|
if (floatValue == null)
|
|
{
|
|
DebugConsole.ThrowError("Couldn't compare " + Value.ToString() + " (" + Value.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! "
|
|
+ "Make sure the type of the value set in the config files matches the type of the property.");
|
|
}
|
|
else if (floatProperty < floatValue)
|
|
return true;
|
|
break;
|
|
case ">=":
|
|
if (floatValue == null)
|
|
{
|
|
DebugConsole.ThrowError("Couldn't compare " + Value.ToString() + " (" + Value.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! "
|
|
+ "Make sure the type of the value set in the config files matches the type of the property.");
|
|
}
|
|
else if (floatProperty >= floatValue)
|
|
return true;
|
|
break;
|
|
case "<=":
|
|
if (floatValue == null)
|
|
{
|
|
DebugConsole.ThrowError("Couldn't compare " + Value.ToString() + " (" + Value.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! "
|
|
+ "Make sure the type of the value set in the config files matches the type of the property.");
|
|
}
|
|
else if (floatProperty <= floatValue)
|
|
return true;
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
partial class StatusEffect
|
|
{
|
|
[Flags]
|
|
public enum TargetType
|
|
{
|
|
This = 1, Parent = 2, Character = 4, Contained = 8, Nearby = 16, UseTarget = 32, Hull = 64
|
|
}
|
|
|
|
private TargetType targetTypes;
|
|
private HashSet<string> targetNames;
|
|
|
|
private List<RelatedItem> requiredItems;
|
|
|
|
#if CLIENT
|
|
private List<ParticleEmitter> particleEmitters;
|
|
|
|
private Sound sound;
|
|
private bool loopSound;
|
|
#endif
|
|
|
|
public string[] propertyNames;
|
|
private object[] propertyEffects;
|
|
|
|
List<PropertyConditional> propertyConditionals;
|
|
|
|
private bool setValue;
|
|
|
|
private bool disableDeltaTime;
|
|
|
|
private HashSet<string> onContainingNames;
|
|
private HashSet<string> tags;
|
|
|
|
private readonly float duration;
|
|
public static List<DurationListElement> DurationList = new List<DurationListElement>();
|
|
|
|
public bool CheckConditionalAlways; //Always do the conditional checks for the duration/delay. If false, only check conditional on apply.
|
|
|
|
public bool Stackable; //Can the same status effect be applied several times to the same targets?
|
|
|
|
private readonly bool useItem;
|
|
|
|
public readonly ActionType type;
|
|
|
|
private Explosion explosion;
|
|
|
|
public readonly float FireSize;
|
|
|
|
public TargetType Targets
|
|
{
|
|
get { return targetTypes; }
|
|
}
|
|
|
|
public HashSet<string> TargetNames
|
|
{
|
|
get { return targetNames; }
|
|
}
|
|
|
|
public HashSet<string> OnContainingNames
|
|
{
|
|
get { return onContainingNames; }
|
|
}
|
|
|
|
public string Tags
|
|
{
|
|
get { return string.Join(",", tags); }
|
|
set
|
|
{
|
|
tags.Clear();
|
|
if (value == null) return;
|
|
|
|
string[] newTags = value.Split(',');
|
|
foreach (string tag in newTags)
|
|
{
|
|
string newTag = tag.Trim();
|
|
if (!tags.Contains(newTag)) tags.Add(newTag);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
public static StatusEffect Load(XElement element)
|
|
{
|
|
if (element.Attribute("delay")!=null)
|
|
{
|
|
return new DelayedEffect(element);
|
|
}
|
|
|
|
return new StatusEffect(element);
|
|
}
|
|
|
|
protected StatusEffect(XElement element)
|
|
{
|
|
requiredItems = new List<RelatedItem>();
|
|
tags = new HashSet<string>(element.GetAttributeString("tags", "").Split(','));
|
|
|
|
#if CLIENT
|
|
particleEmitters = new List<ParticleEmitter>();
|
|
#endif
|
|
|
|
IEnumerable<XAttribute> attributes = element.Attributes();
|
|
List<XAttribute> propertyAttributes = new List<XAttribute>();
|
|
propertyConditionals = new List<PropertyConditional>();
|
|
|
|
foreach (XAttribute attribute in attributes)
|
|
{
|
|
switch (attribute.Name.ToString())
|
|
{
|
|
case "type":
|
|
try
|
|
{
|
|
type = (ActionType)Enum.Parse(typeof(ActionType), attribute.Value, true);
|
|
}
|
|
|
|
catch
|
|
{
|
|
string[] split = attribute.Value.Split('=');
|
|
type = (ActionType)Enum.Parse(typeof(ActionType), split[0], true);
|
|
|
|
string[] containingNames = split[1].Split(',');
|
|
onContainingNames = new HashSet<string>();
|
|
for (int i = 0; i < containingNames.Length; i++)
|
|
{
|
|
onContainingNames.Add(containingNames[i].Trim());
|
|
}
|
|
}
|
|
|
|
break;
|
|
case "target":
|
|
string[] Flags = attribute.Value.Split(',');
|
|
foreach (string s in Flags)
|
|
{
|
|
targetTypes |= (TargetType)Enum.Parse(typeof(TargetType), s, true);
|
|
}
|
|
|
|
break;
|
|
case "disabledeltatime":
|
|
disableDeltaTime = attribute.GetAttributeBool(false);
|
|
break;
|
|
case "setvalue":
|
|
setValue = attribute.GetAttributeBool(false);
|
|
break;
|
|
case "targetnames":
|
|
string[] names = attribute.Value.Split(',');
|
|
targetNames = new HashSet<string>();
|
|
for (int i=0; i < names.Length; i++ )
|
|
{
|
|
targetNames.Add(names[i].Trim());
|
|
}
|
|
break;
|
|
case "duration":
|
|
duration = attribute.GetAttributeFloat(0.0f);
|
|
break;
|
|
case "stackable":
|
|
Stackable = attribute.GetAttributeBool(true);
|
|
break;
|
|
case "checkconditionalalways":
|
|
CheckConditionalAlways = attribute.GetAttributeBool(false);
|
|
break;
|
|
case "sound":
|
|
DebugConsole.ThrowError("Error in StatusEffect " + element.Parent.Name.ToString() +
|
|
" - sounds should be defined as child elements of the StatusEffect, not as attributes.");
|
|
break;
|
|
default:
|
|
propertyAttributes.Add(attribute);
|
|
break;
|
|
}
|
|
}
|
|
|
|
int count = propertyAttributes.Count;
|
|
propertyNames = new string[count];
|
|
propertyEffects = new object[count];
|
|
|
|
int n = 0;
|
|
foreach (XAttribute attribute in propertyAttributes)
|
|
{
|
|
propertyNames[n] = attribute.Name.ToString().ToLowerInvariant();
|
|
propertyEffects[n] = XMLExtensions.GetAttributeObject(attribute);
|
|
n++;
|
|
}
|
|
|
|
foreach (XElement subElement in element.Elements())
|
|
{
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "explosion":
|
|
explosion = new Explosion(subElement);
|
|
break;
|
|
case "fire":
|
|
FireSize = subElement.GetAttributeFloat("size",10.0f);
|
|
break;
|
|
case "use":
|
|
case "useitem":
|
|
useItem = true;
|
|
break;
|
|
case "requireditem":
|
|
case "requireditems":
|
|
RelatedItem newRequiredItem = RelatedItem.Load(subElement);
|
|
|
|
if (newRequiredItem == null) continue;
|
|
|
|
requiredItems.Add(newRequiredItem);
|
|
break;
|
|
case "conditional":
|
|
IEnumerable<XAttribute> conditionalAttributes = subElement.Attributes();
|
|
foreach(XAttribute attribute in conditionalAttributes)
|
|
{
|
|
string attributeString = XMLExtensions.GetAttributeObject(attribute).ToString();
|
|
string atStr = attributeString;
|
|
string[] splitString = atStr.Split(' ');
|
|
string op = splitString[0];
|
|
if (splitString.Length > 0)
|
|
{
|
|
for (int i=1; i<splitString.Length; i++)
|
|
{
|
|
atStr = splitString[i] + (i > 1 && i < splitString.Length ? " " : "");
|
|
}
|
|
}
|
|
//thanks xml for not letting me use < or > in attributes :(
|
|
switch (op)
|
|
{
|
|
case "e":
|
|
case "eq":
|
|
case "equals":
|
|
op = "==";
|
|
break;
|
|
case "ne":
|
|
case "neq":
|
|
case "notequals":
|
|
case "!e":
|
|
case "!eq":
|
|
case "!equals":
|
|
op = "!=";
|
|
break;
|
|
case "gt":
|
|
case "greaterthan":
|
|
op = ">";
|
|
break;
|
|
case "lt":
|
|
case "lessthan":
|
|
op = "<";
|
|
break;
|
|
case "gte":
|
|
case "gteq":
|
|
case "greaterthanequals":
|
|
op = ">=";
|
|
break;
|
|
case "lte":
|
|
case "lteq":
|
|
case "lessthanequals":
|
|
op = "<=";
|
|
break;
|
|
default:
|
|
if (op != "==" && op != "!=" && op != ">" && op != "<" && op != ">=" && op != "<=") //Didn't use escape strings or anything
|
|
{
|
|
atStr = attributeString; //We probably don't even have an operator
|
|
op = "==";
|
|
}
|
|
break;
|
|
}
|
|
propertyConditionals.Add(new PropertyConditional(attribute.Name.ToString().ToLowerInvariant(), op, atStr));
|
|
}
|
|
break;
|
|
#if CLIENT
|
|
case "particleemitter":
|
|
particleEmitters.Add(new ParticleEmitter(subElement));
|
|
break;
|
|
case "sound":
|
|
sound = Sound.Load(subElement);
|
|
loopSound = subElement.GetAttributeBool("loop", false);
|
|
break;
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual bool HasRequiredItems(Entity entity)
|
|
{
|
|
if (requiredItems == null) return true;
|
|
foreach (RelatedItem requiredItem in requiredItems)
|
|
{
|
|
Item item = entity as Item;
|
|
if (item != null)
|
|
{
|
|
if (!requiredItem.CheckRequirements(null, item)) return false;
|
|
}
|
|
Character character = entity as Character;
|
|
if (character != null)
|
|
{
|
|
if (!requiredItem.CheckRequirements(character, null)) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public virtual bool HasRequiredConditions(List<ISerializableEntity> targets)
|
|
{
|
|
if (!propertyConditionals.Any()) return true;
|
|
foreach (ISerializableEntity target in targets)
|
|
{
|
|
foreach (PropertyConditional pc in propertyConditionals)
|
|
{
|
|
string op = pc.Operator;
|
|
object value = pc.Value;
|
|
|
|
//TODO: Somehow check for non-serialized properties like SpeciesName for even more robust conditional conditioning
|
|
if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(pc.Attribute, out SerializableProperty property)) continue;
|
|
|
|
if (!pc.Matches(property))
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target)
|
|
{
|
|
if (this.type != type || !HasRequiredItems(entity)) return;
|
|
|
|
if (targetNames != null && !targetNames.Contains(target.Name)) return;
|
|
|
|
if (duration > 0.0f && !Stackable && DurationList.Find(d => d.Parent == this && d.Entity == entity && d.Targets.Contains(target)) != null) return;
|
|
|
|
List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
|
targets.Add(target);
|
|
|
|
if (!HasRequiredConditions(targets)) return;
|
|
|
|
Apply(type, deltaTime, entity, targets);
|
|
}
|
|
|
|
public virtual void Apply(ActionType type, float deltaTime, Entity entity, List<ISerializableEntity> targets)
|
|
{
|
|
if (this.type != type || !HasRequiredItems(entity) || !HasRequiredConditions(targets)) return;
|
|
|
|
Apply(deltaTime, entity, targets);
|
|
}
|
|
|
|
protected void Apply(float deltaTime, Entity entity, List<ISerializableEntity> targets)
|
|
{
|
|
#if CLIENT
|
|
if (sound != null)
|
|
{
|
|
if (loopSound)
|
|
{
|
|
if (!Sounds.SoundManager.IsPlaying(sound))
|
|
{
|
|
sound.Play(entity.WorldPosition);
|
|
}
|
|
else
|
|
{
|
|
sound.UpdatePosition(entity.WorldPosition);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sound.Play(entity.WorldPosition);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (useItem)
|
|
{
|
|
foreach (Item item in targets.FindAll(t => t is Item).Cast<Item>())
|
|
{
|
|
item.Use(deltaTime, targets.FirstOrDefault(t => t is Character) as Character);
|
|
}
|
|
}
|
|
|
|
if (duration > 0.0f)
|
|
{
|
|
DurationListElement element = new DurationListElement();
|
|
element.Parent = this;
|
|
element.StartTimer = duration;
|
|
element.Entity = entity;
|
|
element.Targets = targets;
|
|
|
|
DurationList.Add(element);
|
|
}
|
|
else
|
|
{
|
|
foreach (ISerializableEntity target in targets)
|
|
{
|
|
for (int i = 0; i < propertyNames.Length; i++)
|
|
{
|
|
SerializableProperty property;
|
|
|
|
if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(propertyNames[i], out property)) continue;
|
|
|
|
ApplyToProperty(property, propertyEffects[i], deltaTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (explosion != null) explosion.Explode(entity.WorldPosition);
|
|
|
|
|
|
Hull hull = null;
|
|
if (entity is Character)
|
|
{
|
|
hull = ((Character)entity).AnimController.CurrentHull;
|
|
}
|
|
else if (entity is Item)
|
|
{
|
|
hull = ((Item)entity).CurrentHull;
|
|
}
|
|
|
|
if (FireSize > 0.0f)
|
|
{
|
|
var fire = new FireSource(entity.WorldPosition, hull);
|
|
|
|
fire.Size = new Vector2(FireSize, fire.Size.Y);
|
|
}
|
|
|
|
#if CLIENT
|
|
foreach (ParticleEmitter emitter in particleEmitters)
|
|
{
|
|
emitter.Emit(deltaTime, entity.WorldPosition, hull);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private void ApplyToProperty(SerializableProperty property, object value, float deltaTime)
|
|
{
|
|
if (disableDeltaTime || setValue) deltaTime = 1.0f;
|
|
|
|
Type type = value.GetType();
|
|
if (type == typeof(float) ||
|
|
(type == typeof(int) && property.GetValue() is float))
|
|
{
|
|
float floatValue = Convert.ToSingle(value) * deltaTime;
|
|
|
|
if (!setValue) floatValue += (float)property.GetValue();
|
|
property.TrySetValue(floatValue);
|
|
}
|
|
else if (type == typeof(int) && value is int)
|
|
{
|
|
int intValue = (int)((int)value * deltaTime);
|
|
if (!setValue) intValue += (int)property.GetValue();
|
|
property.TrySetValue(intValue);
|
|
}
|
|
else if (type == typeof(bool) && value is bool)
|
|
{
|
|
property.TrySetValue((bool)value);
|
|
}
|
|
else if (type == typeof(string))
|
|
{
|
|
property.TrySetValue((string)value);
|
|
}
|
|
else
|
|
{
|
|
DebugConsole.ThrowError("Couldn't apply value " + value.ToString() + " (" + type + ") to property \"" + property.Name + "\" (" + property.GetValue().GetType() + ")! "
|
|
+ "Make sure the type of the value set in the config files matches the type of the property.");
|
|
}
|
|
}
|
|
|
|
public static void UpdateAll(float deltaTime)
|
|
{
|
|
DelayedEffect.Update(deltaTime);
|
|
for (int i = DurationList.Count - 1; i >= 0; i--)
|
|
{
|
|
DurationListElement element = DurationList[i];
|
|
|
|
if (element.Parent.CheckConditionalAlways && !element.Parent.HasRequiredConditions(element.Targets))
|
|
{
|
|
DurationList.Remove(element);
|
|
continue;
|
|
}
|
|
|
|
foreach (ISerializableEntity target in element.Targets)
|
|
{
|
|
for (int n = 0; n < element.Parent.propertyNames.Length; n++)
|
|
{
|
|
SerializableProperty property;
|
|
|
|
if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(element.Parent.propertyNames[n], out property)) continue;
|
|
|
|
element.Parent.ApplyToProperty(property, element.Parent.propertyEffects[n], CoroutineManager.UnscaledDeltaTime);
|
|
}
|
|
}
|
|
|
|
element.StartTimer -= deltaTime;
|
|
|
|
if (element.StartTimer > 0.0f) continue;
|
|
DurationList.Remove(element);
|
|
}
|
|
}
|
|
|
|
public static void StopAll()
|
|
{
|
|
CoroutineManager.StopCoroutines("statuseffect");
|
|
}
|
|
|
|
public void AddTag(string tag)
|
|
{
|
|
if (tags.Contains(tag)) return;
|
|
tags.Add(tag);
|
|
}
|
|
|
|
public bool HasTag(string tag)
|
|
{
|
|
if (tag == null) return true;
|
|
|
|
return (tags.Contains(tag) || tags.Contains(tag.ToLowerInvariant()));
|
|
}
|
|
}
|
|
}
|