Release v0.15.12.0

This commit is contained in:
Joonas Rikkonen
2021-10-27 18:50:57 +03:00
parent bf95e82d80
commit 234fb6bc06
450 changed files with 26042 additions and 10457 deletions
@@ -0,0 +1,92 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
abstract class AbilityCondition
{
protected CharacterTalent characterTalent;
protected Character character;
protected bool invert;
public virtual bool AllowClientSimulation => true;
public AbilityCondition(CharacterTalent characterTalent, XElement conditionElement)
{
this.characterTalent = characterTalent;
character = characterTalent.Character;
invert = conditionElement.GetAttributeBool("invert", false);
}
public abstract bool MatchesCondition(AbilityObject abilityObject);
public abstract bool MatchesCondition();
// tools
protected enum TargetType
{
Any = 0,
Enemy = 1,
Ally = 2,
NotSelf = 3,
Alive = 4,
Monster = 5,
InFriendlySubmarine = 6,
};
protected List<TargetType> ParseTargetTypes(string[] targetTypeStrings)
{
List<TargetType> targetTypes = new List<TargetType>();
foreach (string targetTypeString in targetTypeStrings)
{
TargetType targetType = TargetType.Any;
if (!Enum.TryParse(targetTypeString, true, out targetType))
{
DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")");
}
targetTypes.Add(targetType);
}
return targetTypes;
}
protected bool IsViableTarget(IEnumerable<TargetType> targetTypes, Character targetCharacter)
{
if (targetCharacter == null) { return false; }
bool isViable = true;
foreach (TargetType targetType in targetTypes)
{
if (!IsViableTarget(targetType, targetCharacter))
{
isViable = false;
break;
}
}
return isViable;
}
private bool IsViableTarget(TargetType targetType, Character targetCharacter)
{
switch (targetType)
{
case TargetType.Enemy:
return !HumanAIController.IsFriendly(character, targetCharacter);
case TargetType.Ally:
return HumanAIController.IsFriendly(character, targetCharacter);
case TargetType.NotSelf:
return targetCharacter != character;
case TargetType.Alive:
return !targetCharacter.IsDead;
case TargetType.Monster:
return !targetCharacter.IsHuman;
case TargetType.InFriendlySubmarine:
return targetCharacter.Submarine != null && targetCharacter.Submarine.TeamID == character.TeamID;
default:
return true;
}
}
}
}
@@ -0,0 +1,29 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionAffliction : AbilityConditionData
{
private readonly string[] afflictions;
public AbilityConditionAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction)
{
return afflictions.Any(a => a == affliction.Identifier);
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityAttackResult));
return false;
}
}
}
}
@@ -0,0 +1,79 @@
using Barotrauma.Items.Components;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionAttackData : AbilityConditionData
{
private enum WeaponType
{
Any = 0,
Melee = 1,
Ranged = 2
};
private readonly string itemIdentifier;
private readonly string[] tags;
private readonly WeaponType weapontype;
public AbilityConditionAttackData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
itemIdentifier = conditionElement.GetAttributeString("itemidentifier", "");
tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true);
switch (conditionElement.GetAttributeString("weapontype", ""))
{
case "melee":
weapontype = WeaponType.Melee;
break;
case "ranged":
weapontype = WeaponType.Ranged;
break;
}
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is AbilityAttackData attackData)
{
Item item = attackData?.SourceAttack?.SourceItem;
if (item == null)
{
DebugConsole.AddWarning($"Source Item was not found in {this} for talent {characterTalent.DebugIdentifier}!");
return false;
}
if (!string.IsNullOrEmpty(itemIdentifier))
{
if (item.prefab.Identifier != itemIdentifier)
{
return false;
}
}
if (tags.Any())
{
if (!tags.All(t => item.HasTag(t)))
{
return false;
}
}
switch (weapontype)
{
case WeaponType.Melee:
return item.GetComponent<MeleeWeapon>() != null;
case WeaponType.Ranged:
return item.GetComponent<RangedWeapon>() != null;
}
return true;
}
else
{
LogAbilityConditionError(abilityObject, typeof(AbilityAttackData));
return false;
}
}
}
}
@@ -0,0 +1,38 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionAttackResult : AbilityConditionData
{
private readonly List<TargetType> targetTypes;
private readonly string[] afflictions;
public AbilityConditionAttackResult(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true));
afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityAttackResult)?.AttackResult is AttackResult attackResult)
{
if (!IsViableTarget(targetTypes, attackResult.HitLimb?.character)) { return false; }
if (afflictions.Any())
{
if (attackResult.Afflictions == null || !afflictions.Any(a => attackResult.Afflictions.Select(c => c.Identifier).Contains(a))) { return false; }
}
return true;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityAttackResult));
return false;
}
}
}
}
@@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionCharacter : AbilityConditionData
{
private readonly List<TargetType> targetTypes;
public AbilityConditionCharacter(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true));
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityCharacter)?.Character is Character character)
{
if (!IsViableTarget(targetTypes, character)) { return false; }
return true;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityCharacter));
return false;
}
}
}
}
@@ -0,0 +1,36 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
abstract class AbilityConditionData : AbilityCondition
{
/// <summary>
/// Some conditions rely on specific ability data that is integrally connected to the AbilityEffectType.
/// This is done in order to avoid having to create duplicate ability behavior, such as if an ability needs to trigger
/// a common ability effect but in specific circumstances. These conditions could also be partially replaced by
/// more explicit AbilityEffectType enums, but this would introduce bloat and overhead to integral game logic
/// when instead said logic can be made to only run when required using these conditions.
///
/// These conditions will return an error if used outside their limited intended use.
/// </summary>
public AbilityConditionData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData)
{
DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject}");
}
protected abstract bool MatchesConditionSpecific(AbilityObject abilityObject);
public override bool MatchesCondition()
{
DebugConsole.ThrowError("Used data-reliant ability condition in a state-based ability! This is not allowed.");
return false;
}
public override bool MatchesCondition(AbilityObject abilityObject)
{
if (abilityObject is null) { return invert; }
return invert ? !MatchesConditionSpecific(abilityObject) : MatchesConditionSpecific(abilityObject);
}
}
}
@@ -0,0 +1,22 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionEvasiveManeuvers : AbilityConditionData
{
public AbilityConditionEvasiveManeuvers(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilitySubmarine)?.Submarine is Submarine submarine && (abilityObject as IAbilityCharacter)?.Character is Character attackingCharacter)
{
return submarine.TeamID == character.TeamID && character.Submarine == submarine && attackingCharacter.TeamID != character.TeamID;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilitySubmarine));
return false;
}
}
}
}
@@ -0,0 +1,60 @@
using Barotrauma.Items.Components;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionIsAiming : AbilityConditionDataless
{
private enum WeaponType
{
Any = 0,
Melee = 1,
Ranged = 2
};
private readonly bool hittingCountsAsAiming;
private readonly WeaponType weapontype;
public AbilityConditionIsAiming(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
hittingCountsAsAiming = conditionElement.GetAttributeBool("hittingcountsasaiming", false);
switch (conditionElement.GetAttributeString("weapontype", ""))
{
case "melee":
weapontype = WeaponType.Melee;
break;
case "ranged":
weapontype = WeaponType.Ranged;
break;
}
}
protected override bool MatchesConditionSpecific()
{
if (character.AnimController is HumanoidAnimController animController)
{
foreach (Item item in character.HeldItems)
{
switch (weapontype)
{
case WeaponType.Melee:
var meleeWeapon = item.GetComponent<MeleeWeapon>();
if (meleeWeapon != null)
{
if (animController.IsAimingMelee || (meleeWeapon.Hitting && hittingCountsAsAiming)) { return true; }
}
break;
case WeaponType.Ranged:
if (animController.IsAiming && item.GetComponent<RangedWeapon>() != null) { return true; }
break;
default:
if (animController.IsAiming || animController.IsAimingMelee) { return true; }
break;
}
}
}
return false;
}
}
}
@@ -0,0 +1,49 @@
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionItem : AbilityConditionData
{
private readonly string[] identifiers;
private readonly string[] tags;
public AbilityConditionItem(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
identifiers = conditionElement.GetAttributeStringArray("identifiers", Array.Empty<string>(), convertToLowerInvariant: true);
tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
ItemPrefab itemPrefab = null;
if ((abilityObject as IAbilityItemPrefab)?.ItemPrefab is ItemPrefab abilityItemPrefab)
{
itemPrefab = abilityItemPrefab;
}
else if ((abilityObject as IAbilityItem)?.Item is Item abilityItem)
{
itemPrefab = abilityItem.Prefab;
}
if (itemPrefab != null)
{
if (identifiers.Any())
{
if (!identifiers.Any(t => itemPrefab.Identifier == t))
{
return false;
}
}
return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p));
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityItemPrefab));
return false;
}
}
}
}
@@ -0,0 +1,23 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionItemOutsideSubmarine : AbilityConditionData
{
public AbilityConditionItemOutsideSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityItem)?.Item is Item item)
{
return item.Submarine == null || item.Submarine.TeamID != character.Info.TeamID;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityItem));
return false;
}
}
}
}
@@ -0,0 +1,23 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionItemWreck : AbilityConditionData
{
public AbilityConditionItemWreck(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityItem)?.Item is Item item)
{
return item.Submarine?.Info?.IsWreck ?? false;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityItem));
return false;
}
}
}
}
@@ -0,0 +1,41 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionLocation : AbilityConditionData
{
private readonly bool? hasOutpost;
private readonly string[] locationIdentifiers;
public AbilityConditionLocation(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
if (conditionElement.Attribute("hasoutpost") != null)
{
hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false);
}
locationIdentifiers = conditionElement.GetAttributeStringArray("locationtype", new string[0]);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is IAbilityLocation abilityLocation)
{
if (locationIdentifiers.Any())
{
if (!locationIdentifiers.Contains(abilityLocation.Location.Type.Identifier)) { return false; }
}
if (hasOutpost.HasValue)
{
if (hasOutpost.Value != abilityLocation.Location.HasOutpost()) { return false; }
}
return true;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityItemPrefab));
return false;
}
}
}
}
@@ -0,0 +1,38 @@
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionMission : AbilityConditionData
{
private readonly MissionType missionType;
public AbilityConditionMission(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
string missionTypeString = conditionElement.GetAttributeString("missiontype", "None");
if (!Enum.TryParse(missionTypeString, out missionType))
{
DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - \"" + missionTypeString + "\" is not a valid mission type.");
return;
}
if (missionType == MissionType.None)
{
DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - mission type cannot be none.");
return;
}
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityMission)?.Mission is Mission mission)
{
return mission.Prefab.Type == missionType;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
return false;
}
}
}
}
@@ -0,0 +1,33 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionReduceAffliction : AbilityConditionData
{
private readonly string[] allowedTypes;
private readonly string identifier;
public AbilityConditionReduceAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
allowedTypes = conditionElement.GetAttributeStringArray("allowedtypes", new string[0], convertToLowerInvariant: true);
identifier = conditionElement.GetAttributeString("identifier", "");
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction)
{
if (allowedTypes.Find(c => c == affliction.Prefab.AfflictionType) == null) { return false; }
if (!string.IsNullOrEmpty(identifier) && affliction.Prefab.Identifier != identifier) { return false; }
return true;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityAffliction));
return false;
}
}
}
}
@@ -0,0 +1,32 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionSkill : AbilityConditionData
{
private readonly string skillIdentifier;
public AbilityConditionSkill(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
skillIdentifier = conditionElement.GetAttributeString("skillidentifier", "").ToLowerInvariant();
}
private bool MatchesConditionSpecific(string skillIdentifier)
{
return this.skillIdentifier == skillIdentifier;
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityString)?.String is string skillIdentifier)
{
return MatchesConditionSpecific(skillIdentifier);
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityString));
return false;
}
}
}
}
@@ -0,0 +1,19 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionAboveVitality : AbilityConditionDataless
{
private readonly float vitalityPercentage;
public AbilityConditionAboveVitality(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
vitalityPercentage = conditionElement.GetAttributeFloat("vitalitypercentage", 0f);
}
protected override bool MatchesConditionSpecific()
{
return character.HealthPercentage / 100f > vitalityPercentage;
}
}
}
@@ -0,0 +1,19 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionAlliesAboveVitality : AbilityConditionDataless
{
float vitalityPercentage;
public AbilityConditionAlliesAboveVitality(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
vitalityPercentage = conditionElement.GetAttributeFloat("vitalitypercentage", 0f);
}
protected override bool MatchesConditionSpecific()
{
return Character.GetFriendlyCrew(character).All(c => c.HealthPercentage / 100f >= vitalityPercentage);
}
}
}
@@ -0,0 +1,26 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionCoauthor : AbilityConditionDataless
{
private readonly string jobIdentifier;
public AbilityConditionCoauthor(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
jobIdentifier = conditionElement.GetAttributeString("jobidentifier", string.Empty);
}
protected override bool MatchesConditionSpecific()
{
if (character.SelectedCharacter is Character otherCharacter)
{
if (!otherCharacter.HasJob(jobIdentifier)) { return false; }
if (!(character.SelectedBy == otherCharacter)) { return false; }
return true;
}
return false;
}
}
}
@@ -0,0 +1,18 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionCrouched : AbilityConditionDataless
{
public AbilityConditionCrouched(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
}
protected override bool MatchesConditionSpecific()
{
return character.AnimController is HumanoidAnimController humanoidAnimController && humanoidAnimController.Crouching;
}
}
}
@@ -0,0 +1,20 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
abstract class AbilityConditionDataless : AbilityCondition
{
public AbilityConditionDataless(CharacterTalent characterTalent, XElement conditionElement) : base (characterTalent, conditionElement) { }
protected abstract bool MatchesConditionSpecific();
public override bool MatchesCondition()
{
return invert ? !MatchesConditionSpecific() : MatchesConditionSpecific();
}
public override bool MatchesCondition(AbilityObject abilityObject)
{
return invert ? !MatchesConditionSpecific() : MatchesConditionSpecific();
}
}
}
@@ -0,0 +1,31 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionHasAffliction : AbilityConditionDataless
{
private string afflictionIdentifier;
private float minimumPercentage;
public AbilityConditionHasAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
afflictionIdentifier = conditionElement.GetAttributeString("afflictionidentifier", "");
minimumPercentage = conditionElement.GetAttributeFloat("minimumpercentage", 0f);
}
protected override bool MatchesConditionSpecific()
{
if (!string.IsNullOrEmpty(afflictionIdentifier))
{
var affliction = character.CharacterHealth.GetAffliction(afflictionIdentifier);
if (affliction == null) { return false; }
return minimumPercentage <= affliction.Strength / affliction.Prefab.MaxStrength;
}
return false;
}
}
}
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionHasDifferentJobs : AbilityConditionDataless
{
private readonly int amount;
public AbilityConditionHasDifferentJobs(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
amount = conditionElement.GetAttributeInt("amount", 0);
}
protected override bool MatchesConditionSpecific()
{
IEnumerable<Character> crewmembers = Character.GetFriendlyCrew(character);
int differentCrewAmount = crewmembers.Select(c => c.Info?.Job?.Prefab.Identifier).Distinct().Count();
return differentCrewAmount >= amount;
}
}
}
@@ -0,0 +1,57 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionHasItem : AbilityConditionDataless
{
// not used for anything atm, will be used for clown subclass
private readonly string[] tags;
private InvSlotType? invSlotType;
bool requireAll;
private List<Item> items = new List<Item>();
public AbilityConditionHasItem(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true);
requireAll = conditionElement.GetAttributeBool("requireall", false);
//this.invSlotType = invSlotType;
}
protected override bool MatchesConditionSpecific()
{
items.Clear();
if (tags.Any())
{
foreach (string tag in tags)
{
// there is a better method, should use that instead
if (character.GetEquippedItem(tag, invSlotType) is Item foundItem)
{
items.Add(foundItem);
}
}
}
else
{
if (character.GetEquippedItem(null, invSlotType) is Item foundItem)
{
items.Add(foundItem);
}
}
if (requireAll)
{
return (items.Count >= tags.Count());
}
else
{
return items.Any();
}
}
}
}
@@ -0,0 +1,29 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionHasPermanentStat : AbilityConditionDataless
{
private readonly string statIdentifier;
private readonly StatTypes statType;
private readonly float min;
public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
statIdentifier = conditionElement.GetAttributeString("statidentifier", string.Empty);
if (string.IsNullOrEmpty(statIdentifier))
{
DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!");
}
string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier);
min = conditionElement.GetAttributeFloat("min", 0f);
}
protected override bool MatchesConditionSpecific()
{
return character.Info.GetSavedStatValue(statType, statIdentifier) >= min;
}
}
}
@@ -0,0 +1,24 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionHasSkill : AbilityConditionDataless
{
private readonly string skillIdentifier;
private readonly float minValue;
public AbilityConditionHasSkill(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
skillIdentifier = conditionElement.GetAttributeString("skillidentifier", string.Empty);
minValue = conditionElement.GetAttributeFloat("minvalue", 0f);
}
protected override bool MatchesConditionSpecific()
{
return character.GetSkillLevel(skillIdentifier) >= minValue;
}
}
}
@@ -0,0 +1,31 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionHasStatusTag : AbilityConditionDataless
{
private readonly string tag;
public AbilityConditionHasStatusTag(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
tag = conditionElement.GetAttributeString("tag", "");
if (string.IsNullOrEmpty(tag))
{
DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag.");
}
}
protected override bool MatchesConditionSpecific()
{
if (!string.IsNullOrEmpty(tag))
{
return
StatusEffect.DurationList.Any(d => d.Targets.Contains(character) && d.Parent.HasTag(tag)) ||
DelayedEffect.DelayList.Any(d => d.Targets.Contains(character) && d.Parent.HasTag(tag));
}
return false;
}
}
}
@@ -0,0 +1,20 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionHasVelocity : AbilityConditionDataless
{
private readonly float velocity;
public AbilityConditionHasVelocity(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
velocity = conditionElement.GetAttributeFloat("velocity", 0f);
}
protected override bool MatchesConditionSpecific()
{
return character.AnimController.Collider.LinearVelocity.LengthSquared() > velocity * velocity;
}
}
}
@@ -0,0 +1,15 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionInFriendlySubmarine : AbilityConditionDataless
{
public AbilityConditionInFriendlySubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific()
{
return character.Submarine?.TeamID == character.TeamID;
}
}
}
@@ -0,0 +1,15 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionInHull : AbilityConditionDataless
{
public AbilityConditionInHull(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific()
{
return character.CurrentHull != null;
}
}
}
@@ -0,0 +1,15 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionInWater : AbilityConditionDataless
{
public AbilityConditionInWater(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific()
{
return character.InWater;
}
}
}
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionLevelsBehindHighest : AbilityConditionDataless
{
private readonly int levelsBehind;
public AbilityConditionLevelsBehindHighest(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
levelsBehind = conditionElement.GetAttributeInt("levelsbehind", 0);
}
protected override bool MatchesConditionSpecific()
{
return Character.GetFriendlyCrew(character).Where(c => c.Info != null && (c.Info.GetCurrentLevel() - character.Info.GetCurrentLevel() >= levelsBehind)).Any();
}
}
}
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionNoCrewDied : AbilityConditionDataless
{
public AbilityConditionNoCrewDied(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
}
protected override bool MatchesConditionSpecific()
{
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
{
return !campaign.CrewHasDied;
}
return true;
}
}
}
@@ -0,0 +1,17 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionOnMission : AbilityConditionDataless
{
public AbilityConditionOnMission(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
}
protected override bool MatchesConditionSpecific()
{
return Level.Loaded?.Type != LevelData.LevelType.Outpost;
}
}
}
@@ -0,0 +1,18 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionRagdolled : AbilityConditionDataless
{
public AbilityConditionRagdolled(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
}
protected override bool MatchesConditionSpecific()
{
return character.IsRagdolled || character.Stun > 0f || character.IsIncapacitated;
}
}
}
@@ -0,0 +1,15 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionRunning : AbilityConditionDataless
{
public AbilityConditionRunning(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific()
{
return character.AnimController is HumanoidAnimController animController && animController.IsMovingFast;
}
}
}
@@ -0,0 +1,20 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionServerRandom : AbilityConditionDataless
{
private readonly float randomChance = 0f;
public override bool AllowClientSimulation => false;
public AbilityConditionServerRandom(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
randomChance = conditionElement.GetAttributeFloat("randomchance", 1f);
}
protected override bool MatchesConditionSpecific()
{
return randomChance >= Rand.Range(0f, 1f, Rand.RandSync.Unsynced);
}
}
}
@@ -0,0 +1,21 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionShipFlooded : AbilityConditionDataless
{
private readonly float floodPercentage;
public AbilityConditionShipFlooded(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
floodPercentage = conditionElement.GetAttributeFloat("floodpercentage", 0f);
}
protected override bool MatchesConditionSpecific()
{
if (!character.IsInFriendlySub) { return false; }
float currentFloodPercentage = character.Submarine.GetHulls(false).Average(h => h.WaterPercentage);
return currentFloodPercentage / 100 > floodPercentage;
}
}
}
@@ -0,0 +1,52 @@
namespace Barotrauma.Abilities
{
interface IAbilityItemPrefab
{
public ItemPrefab ItemPrefab { get; set; }
}
interface IAbilityItem
{
public Item Item { get; set; }
}
interface IAbilityValue
{
public float Value { get; set; }
}
interface IAbilityMission
{
public Mission Mission { get; set; }
}
interface IAbilityLocation
{
public Location Location { get; set; }
}
interface IAbilityCharacter
{
public Character Character { get; set; }
}
interface IAbilityString
{
public string String { get; set; }
}
interface IAbilityAffliction
{
public Affliction Affliction { get; set; }
}
interface IAbilityAttackResult
{
public AttackResult AttackResult { get; set; }
}
interface IAbilitySubmarine
{
public Submarine Submarine { get; set; }
}
}
@@ -0,0 +1,187 @@
using System.Collections.Generic;
namespace Barotrauma.Abilities
{
abstract class AbilityObject
{
// kept as blank for now, as we are using a composition and only using this object to enforce parameter types
}
class AbilityCharacter : AbilityObject, IAbilityCharacter
{
public AbilityCharacter(Character character)
{
Character = character;
}
public Character Character { get; set; }
}
class AbilityItem : AbilityObject, IAbilityItem
{
public AbilityItem(Item item)
{
Item = item;
}
public Item Item { get; set; }
}
class AbilityValue : AbilityObject, IAbilityValue
{
public AbilityValue(float value)
{
Value = value;
}
public float Value { get; set; }
}
class AbilityAffliction : AbilityObject, IAbilityAffliction
{
public AbilityAffliction(Affliction affliction)
{
Affliction = affliction;
}
public Affliction Affliction { get; set; }
}
class AbilityAfflictionCharacter : AbilityObject, IAbilityAffliction, IAbilityCharacter
{
public AbilityAfflictionCharacter(Affliction affliction, Character character)
{
Affliction = affliction;
Character = character;
}
public Character Character { get; set; }
public Affliction Affliction { get; set; }
}
class AbilityValueItem : AbilityObject, IAbilityValue, IAbilityItemPrefab
{
public AbilityValueItem(float value, ItemPrefab itemPrefab)
{
Value = value;
ItemPrefab = itemPrefab;
}
public float Value { get; set; }
public ItemPrefab ItemPrefab { get; set; }
}
class AbilityItemPrefabItem : AbilityObject, IAbilityItem, IAbilityItemPrefab
{
public AbilityItemPrefabItem(Item item, ItemPrefab itemPrefab)
{
Item = item;
ItemPrefab = itemPrefab;
}
public Item Item { get; set; }
public ItemPrefab ItemPrefab { get; set; }
}
class AbilityValueString : AbilityObject, IAbilityValue, IAbilityString
{
public AbilityValueString(float value, string abilityString)
{
Value = value;
String = abilityString;
}
public float Value { get; set; }
public string String { get; set; }
}
class AbilityStringCharacter : AbilityObject, IAbilityCharacter, IAbilityString
{
public AbilityStringCharacter(string abilityString, Character character)
{
String = abilityString;
Character = character;
}
public Character Character { get; set; }
public string String { get; set; }
}
class AbilityValueAffliction : AbilityObject, IAbilityValue, IAbilityAffliction
{
public AbilityValueAffliction(float value, Affliction affliction)
{
Value = value;
Affliction = affliction;
}
public float Value { get; set; }
public Affliction Affliction { get; set; }
}
class AbilityValueMission : AbilityObject, IAbilityValue, IAbilityMission
{
public AbilityValueMission(float value, Mission mission)
{
Value = value;
Mission = mission;
}
public float Value { get; set; }
public Mission Mission { get; set; }
}
class AbilityLocation : AbilityObject, IAbilityLocation
{
public AbilityLocation(Location location)
{
Location = location;
}
public Location Location { get; set; }
}
// this is an exception class that should only be passed in this form, so classes that use it should cast into it directly
class AbilityAttackData : AbilityObject, IAbilityCharacter
{
public float DamageMultiplier { get; set; } = 1f;
public float AddedPenetration { get; set; } = 0f;
public List<Affliction> Afflictions { get; set; }
public Attack SourceAttack { get; }
public Character Character { get; set; }
public Character Attacker { get; set; }
public AbilityAttackData(Attack sourceAttack, Character character)
{
SourceAttack = sourceAttack;
Character = character;
}
}
class AbilityApplyTreatment : AbilityObject, IAbilityCharacter, IAbilityItem
{
public Character Character { get; set; }
public Character User { get; set; }
public Item Item { get; set; }
public AbilityApplyTreatment(Character user, Character target, Item item)
{
Character = target;
User = user;
Item = item;
}
}
class AbilityAttackResult : AbilityObject, IAbilityAttackResult
{
public AttackResult AttackResult { get; set; }
public AbilityAttackResult(AttackResult attackResult)
{
AttackResult = attackResult;
}
}
class AbilityCharacterSubmarine : AbilityObject, IAbilityCharacter, IAbilitySubmarine
{
public AbilityCharacterSubmarine(Character character, Submarine submarine)
{
Character = character;
Submarine = submarine;
}
public Character Character { get; set; }
public Submarine Submarine { get; set; }
}
}
@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
abstract class CharacterAbility
{
public CharacterAbilityGroup CharacterAbilityGroup { get; }
public CharacterTalent CharacterTalent { get; }
public Character Character { get; }
public bool RequiresAlive { get; }
public virtual bool AllowClientSimulation => false;
public virtual bool AppliesEffectOnIntervalUpdate => false;
private const float DefaultEffectTime = 1.0f;
// currently resets if the character dies. would need to be stored in a dictionary of sorts to maintain through death
/// <summary>
/// Used primarily for StatusEffects. Default to constant outside interval abilities.
/// </summary>
protected float EffectDeltaTime => CharacterAbilityGroup is CharacterAbilityGroupInterval abilityGroupInterval ? abilityGroupInterval.TimeSinceLastUpdate : DefaultEffectTime;
public CharacterAbility(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement)
{
CharacterAbilityGroup = characterAbilityGroup;
CharacterTalent = characterAbilityGroup.CharacterTalent;
Character = CharacterTalent.Character;
RequiresAlive = abilityElement.GetAttributeBool("requiresalive", true);
}
public bool IsViable()
{
if (!AllowClientSimulation && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
if (RequiresAlive && Character.IsDead) { return false; }
return true;
}
public virtual void InitializeAbility(bool addingFirstTime) { }
public virtual void UpdateCharacterAbility(bool conditionsMatched, float timeSinceLastUpdate)
{
// may need a separate Update for changing state on non-interval-based abilities
if (AppliesEffectOnIntervalUpdate)
{
if (conditionsMatched)
{
ApplyEffect();
}
}
else
{
VerifyState(conditionsMatched, timeSinceLastUpdate);
}
}
protected virtual void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups.");
}
public void ApplyAbilityEffect(AbilityObject abilityObject)
{
if (abilityObject is null)
{
ApplyEffect();
}
else
{
ApplyEffect(abilityObject);
}
}
protected virtual void ApplyEffect()
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect");
}
protected virtual void ApplyEffect(AbilityObject abilityObject)
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect");
}
protected void LogabilityObjectMismatch()
{
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type.");
}
// XML
public static CharacterAbility Load(XElement abilityElement, CharacterAbilityGroup characterAbilityGroup, bool errorMessages = true)
{
Type abilityType;
string type = abilityElement.Name.ToString().ToLowerInvariant();
try
{
abilityType = Type.GetType("Barotrauma.Abilities." + type + "", false, true);
if (abilityType == null)
{
if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")");
return null;
}
}
catch (Exception e)
{
if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", e);
return null;
}
object[] args = { characterAbilityGroup, abilityElement };
CharacterAbility characterAbility;
try
{
characterAbility = (CharacterAbility)Activator.CreateInstance(abilityType, args);
}
catch (TargetInvocationException e)
{
DebugConsole.ThrowError("Error while creating an instance of a CharacterAbility of the type " + abilityType + ".", e.InnerException);
return null;
}
return characterAbility;
}
}
}
@@ -0,0 +1,61 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyForce : CharacterAbility
{
private readonly float force;
private readonly float maxVelocity;
private readonly string afflictionIdentifier;
private readonly HashSet<LimbType> limbTypes = new HashSet<LimbType>();
public override bool AppliesEffectOnIntervalUpdate => true;
public CharacterAbilityApplyForce(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
force = abilityElement.GetAttributeFloat("force", 0f);
maxVelocity = abilityElement.GetAttributeFloat("maxvelocity", 10f);
afflictionIdentifier = abilityElement.GetAttributeString("afflictionidentifier", "");
string[] limbTypesStr = abilityElement.GetAttributeStringArray("limbtypes", new string[0]);
foreach (string limbTypeStr in limbTypesStr)
{
if (Enum.TryParse(limbTypeStr, out LimbType limbType))
{
limbTypes.Add(limbType);
}
else
{
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - \"{limbTypeStr}\" is not a valid limb type.");
}
}
}
protected override void ApplyEffect()
{
float strength = 1.0f;
if (!string.IsNullOrEmpty(afflictionIdentifier))
{
Affliction affliction = Character.CharacterHealth.GetAffliction(afflictionIdentifier);
if (affliction == null) { return; }
strength = affliction.Strength / affliction.Prefab.MaxStrength;
}
foreach (Limb limb in Character.AnimController.Limbs)
{
if (limb.IsSevered || limb.Removed) { continue; }
if (limbTypes.Any())
{
if (!limbTypes.Contains(limb.type)) { continue; }
}
if (Character.AnimController.TargetMovement.LengthSquared() < 0.001f) { continue; }
limb.body.ApplyForce(Vector2.Normalize(limb.Mass * Character.AnimController.TargetMovement) * force * strength, maxVelocity);
}
}
}
}
@@ -0,0 +1,88 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyStatusEffects : CharacterAbility
{
public override bool AppliesEffectOnIntervalUpdate => true;
public override bool AllowClientSimulation => true;
protected readonly List<StatusEffect> statusEffects;
private readonly bool nearbyCharactersAppliesToSelf;
private readonly bool nearbyCharactersAppliesToAllies;
private readonly bool applyToSelected;
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects"));
applyToSelected = abilityElement.GetAttributeBool("applytoselected", false);
nearbyCharactersAppliesToSelf = abilityElement.GetAttributeBool("nearbycharactersappliestoself", true);
nearbyCharactersAppliesToAllies = abilityElement.GetAttributeBool("nearbycharactersappliestoallies", true);
}
protected void ApplyEffectSpecific(Character targetCharacter)
{
foreach (var statusEffect in statusEffects)
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
// currently used to spawn items on the targeted character
statusEffect.SetUser(targetCharacter);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(statusEffect.GetNearbyTargets(targetCharacter.WorldPosition, targets));
if (!nearbyCharactersAppliesToSelf)
{
targets.RemoveAll(c => c == Character);
}
if (!nearbyCharactersAppliesToAllies)
{
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character));
}
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter);
}
else
{
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character);
}
}
}
protected override void ApplyEffect()
{
if (applyToSelected && Character.SelectedCharacter is Character selectedCharacter)
{
ApplyEffectSpecific(selectedCharacter);
}
else
{
ApplyEffectSpecific(Character);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter)
{
ApplyEffectSpecific(targetCharacter);
}
else
{
ApplyEffect();
}
}
}
}
@@ -0,0 +1,35 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyStatusEffectsToAllies : CharacterAbilityApplyStatusEffects
{
private readonly bool allowSelf;
private readonly float maxDistance = float.MaxValue;
public CharacterAbilityApplyStatusEffectsToAllies(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
allowSelf = abilityElement.GetAttributeBool("allowself", true);
maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue);
}
protected override void ApplyEffect()
{
IEnumerable<Character> chosenCharacters = Character.GetFriendlyCrew(Character).Where(c => allowSelf || c != Character);
foreach (Character character in chosenCharacters)
{
if (maxDistance < float.MaxValue)
{
if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; }
}
ApplyEffectSpecific(character);
}
}
}
}
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyStatusEffectsToAttacker : CharacterAbilityApplyStatusEffects
{
public CharacterAbilityApplyStatusEffectsToAttacker(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as AbilityAttackData)?.Attacker is Character attacker)
{
ApplyEffectSpecific(attacker);
}
}
}
}
@@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyStatusEffectsToLastOrderedCharacter : CharacterAbilityApplyStatusEffects
{
public CharacterAbilityApplyStatusEffectsToLastOrderedCharacter(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
}
protected override void ApplyEffect()
{
if (IsViableTarget(Character.LastOrderedCharacter))
{
ApplyEffectSpecific(Character.LastOrderedCharacter);
}
if (Character.HasAbilityFlag(AbilityFlags.AllowSecondOrderedTarget) && IsViableTarget(Character.SecondLastOrderedCharacter))
{
ApplyEffectSpecific(Character.SecondLastOrderedCharacter);
}
}
private bool IsViableTarget(Character targetCharacter)
{
if (targetCharacter == null || targetCharacter.Removed) { return false; }
if (targetCharacter == Character) { return false; }
return true;
}
}
}
@@ -0,0 +1,35 @@
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyStatusEffectsToNearestAlly : CharacterAbilityApplyStatusEffects
{
protected float squaredMaxDistance;
public CharacterAbilityApplyStatusEffectsToNearestAlly(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
squaredMaxDistance = MathF.Pow(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue), 2);
}
protected override void ApplyEffect()
{
Character closestCharacter = null;
float closestDistance = float.MaxValue;
foreach (Character crewCharacter in Character.GetFriendlyCrew(Character))
{
if (crewCharacter != Character && Vector2.DistanceSquared(Character.WorldPosition, crewCharacter.WorldPosition) is float tempDistance && tempDistance < closestDistance)
{
closestCharacter = crewCharacter;
closestDistance = tempDistance;
}
}
if (closestDistance < squaredMaxDistance)
{
ApplyEffectSpecific(closestCharacter);
}
}
}
}
@@ -0,0 +1,41 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyStatusEffectsToRandomAlly : CharacterAbilityApplyStatusEffects
{
private readonly float squaredMaxDistance;
private readonly bool allowDifferentSub;
private readonly bool allowSelf;
public override bool AllowClientSimulation => false;
public CharacterAbilityApplyStatusEffectsToRandomAlly(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
squaredMaxDistance = MathF.Pow(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue), 2);
allowDifferentSub = abilityElement.GetAttributeBool("mustbeonsamesub", true);
allowSelf = abilityElement.GetAttributeBool("allowself", true);
}
protected override void ApplyEffect()
{
Character chosenCharacter = null;
chosenCharacter = Character.GetFriendlyCrew(Character).Where(c =>
(allowSelf || c != Character) &&
(allowDifferentSub || c.Submarine == Character.Submarine) &&
Vector2.DistanceSquared(Character.WorldPosition, c.WorldPosition) is float tempDistance &&
tempDistance < squaredMaxDistance).GetRandom();
if (chosenCharacter == null) { return; }
ApplyEffectSpecific(chosenCharacter);
}
}
}
@@ -0,0 +1,27 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGainSimultaneousSkill : CharacterAbility
{
private string skillIdentifier;
public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
skillIdentifier = abilityElement.GetAttributeString("skillidentifier", "").ToLowerInvariant();
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityValue)?.Value is float skillIncrease)
{
Character.Info?.IncreaseSkillLevel(skillIdentifier, skillIncrease);
}
else
{
LogabilityObjectMismatch();
}
}
}
}
@@ -0,0 +1,44 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveAffliction : CharacterAbility
{
private readonly string afflictionId;
private readonly float strength;
private readonly string multiplyStrengthBySkill;
private readonly bool setValue;
public CharacterAbilityGiveAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
afflictionId = abilityElement.GetAttributeString("afflictionid", abilityElement.GetAttributeString("affliction", string.Empty));
strength = abilityElement.GetAttributeFloat("strength", 0f);
multiplyStrengthBySkill = abilityElement.GetAttributeString("multiplystrengthbyskill", string.Empty);
setValue = abilityElement.GetAttributeBool("setvalue", false);
if (string.IsNullOrEmpty(afflictionId))
{
DebugConsole.ThrowError("Error in CharacterAbilityGiveAffliction - affliction identifier not set.");
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is IAbilityCharacter character)
{
var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier.Equals(afflictionId, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\".");
return;
}
float strength = this.strength;
if (!string.IsNullOrEmpty(multiplyStrengthBySkill))
{
strength *= Character.GetSkillLevel(multiplyStrengthBySkill);
}
character.Character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(strength), allowStacking: !setValue);
}
}
}
}
@@ -0,0 +1,20 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveFlag : CharacterAbility
{
private readonly AbilityFlags abilityFlag;
// this and resistance giving should probably be moved directly to charactertalent attributes, as they don't need to interact with either ability group types
public CharacterAbilityGiveFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
abilityFlag = CharacterAbilityGroup.ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier);
}
public override void InitializeAbility(bool addingFirstTime)
{
Character.AddAbilityFlag(abilityFlag);
}
}
}
@@ -0,0 +1,46 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveMoney : CharacterAbility
{
public override bool AppliesEffectOnIntervalUpdate => true;
private readonly int amount;
private readonly string scalingStatIdentifier;
public CharacterAbilityGiveMoney(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
scalingStatIdentifier = abilityElement.GetAttributeString("scalingstatidentifier", string.Empty);
}
private void ApplyEffectSpecific(Character targetCharacter)
{
float multiplier = 1f;
if (!string.IsNullOrEmpty(scalingStatIdentifier))
{
multiplier = 0 + Character.Info.GetSavedStatValue(StatTypes.None, scalingStatIdentifier);
}
targetCharacter.GiveMoney((int)(multiplier * amount));
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter)
{
ApplyEffectSpecific(targetCharacter);
}
else
{
ApplyEffectSpecific(Character);
}
}
protected override void ApplyEffect()
{
ApplyEffectSpecific(Character);
}
}
}
@@ -0,0 +1,64 @@
using Barotrauma.Extensions;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGivePermanentStat : CharacterAbility
{
private readonly string statIdentifier;
private readonly StatTypes statType;
private readonly float value;
private readonly float maxValue;
private readonly bool targetAllies;
private readonly bool removeOnDeath;
private readonly bool giveOnAddingFirstTime;
private readonly bool setValue;
//private readonly float maximumValue;
public override bool AllowClientSimulation => true;
public override bool AppliesEffectOnIntervalUpdate => true;
public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant();
string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier);
value = abilityElement.GetAttributeFloat("value", 0f);
maxValue = abilityElement.GetAttributeFloat("maxvalue", float.MaxValue);
targetAllies = abilityElement.GetAttributeBool("targetallies", false);
removeOnDeath = abilityElement.GetAttributeBool("removeondeath", true);
giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None);
setValue = abilityElement.GetAttributeBool("setvalue", false);
}
public override void InitializeAbility(bool addingFirstTime)
{
if (giveOnAddingFirstTime && addingFirstTime)
{
ApplyEffectSpecific();
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffectSpecific();
}
protected override void ApplyEffect()
{
ApplyEffectSpecific();
}
private void ApplyEffectSpecific()
{
if (targetAllies)
{
Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue));
}
else
{
Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
}
}
}
}
@@ -0,0 +1,26 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveResistance : CharacterAbility
{
private readonly string resistanceId;
private readonly float multiplier;
public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
resistanceId = abilityElement.GetAttributeString("resistanceid", abilityElement.GetAttributeString("resistance", string.Empty));
multiplier = abilityElement.GetAttributeFloat("multiplier", 1f); // rename this to resistance for consistency
if (string.IsNullOrEmpty(resistanceId))
{
DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set.");
}
}
public override void InitializeAbility(bool addingFirstTime)
{
Character.ChangeAbilityResistance(resistanceId, multiplier);
}
}
}
@@ -0,0 +1,21 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveStat : CharacterAbility
{
private readonly StatTypes statType;
private readonly float value;
public CharacterAbilityGiveStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier);
value = abilityElement.GetAttributeFloat("value", 0f);
}
public override void InitializeAbility(bool addingFirstTime)
{
Character.ChangeStat(statType, value);
}
}
}
@@ -0,0 +1,23 @@
using Barotrauma.Extensions;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveTalentPoints : CharacterAbility
{
private readonly int amount;
public CharacterAbilityGiveTalentPoints(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
}
public override void InitializeAbility(bool addingFirstTime)
{
if (addingFirstTime && Character.Info != null)
{
Character.Info.AdditionalTalentPoints += amount;
}
}
}
}
@@ -0,0 +1,60 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityIncreaseSkill : CharacterAbility
{
public override bool AppliesEffectOnIntervalUpdate => true;
private readonly string skillIdentifier;
private readonly float skillIncrease;
public CharacterAbilityIncreaseSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
skillIdentifier = abilityElement.GetAttributeString("skillidentifier", "").ToLowerInvariant();
skillIncrease = abilityElement.GetAttributeFloat("skillincrease", 0f);
if (string.IsNullOrEmpty(skillIdentifier))
{
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill identifier not defined in CharacterAbilityIncreaseSkill.");
}
if (MathUtils.NearlyEqual(skillIncrease, 0))
{
DebugConsole.AddWarning($"Possible error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill increase set to 0.");
}
}
protected override void ApplyEffect()
{
ApplyEffectSpecific(Character);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityCharacter)?.Character is Character character)
{
ApplyEffectSpecific(character);
}
else
{
ApplyEffectSpecific(Character);
}
}
private void ApplyEffectSpecific(Character character)
{
if (skillIdentifier.Equals("random"))
{
var skill = character.Info?.Job?.Skills?.GetRandom();
if (skill == null) { return; }
character.Info?.IncreaseSkillLevel(skill.Identifier, skillIncrease);
}
else
{
character.Info?.IncreaseSkillLevel(skillIdentifier, skillIncrease);
}
}
}
}
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyAffliction : CharacterAbility
{
private readonly string[] afflictionIdentifiers;
private readonly float addedMultiplier;
public CharacterAbilityModifyAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
afflictionIdentifiers = abilityElement.GetAttributeStringArray("afflictionidentifiers", new string[0], convertToLowerInvariant: true);
addedMultiplier = abilityElement.GetAttributeFloat("addedmultiplier", 0f);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction)
{
foreach (string afflictionIdentifier in afflictionIdentifiers)
{
if (affliction.Identifier == afflictionIdentifier)
{
affliction.Strength *= 1 + addedMultiplier;
}
}
}
else
{
LogabilityObjectMismatch();
}
}
}
}
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyAttackData : CharacterAbility
{
private readonly List<Affliction> afflictions;
private readonly float addedDamageMultiplier;
private readonly float addedPenetration;
private readonly bool implode;
public CharacterAbilityModifyAttackData(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
if (abilityElement.GetChildElement("afflictions") is XElement afflictionElements)
{
afflictions = CharacterAbilityGroup.ParseAfflictions(CharacterTalent, afflictionElements);
}
addedDamageMultiplier = abilityElement.GetAttributeFloat("addeddamagemultiplier", 0f);
addedPenetration = abilityElement.GetAttributeFloat("addedpenetration", 0f);
implode = abilityElement.GetAttributeBool("implode", false);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is AbilityAttackData attackData)
{
if (attackData.Afflictions == null)
{
attackData.Afflictions = afflictions;
}
else
{
attackData.Afflictions.AddRange(afflictions);
}
attackData.DamageMultiplier += addedDamageMultiplier;
attackData.AddedPenetration += addedPenetration;
if (implode)
{
// might have issues, as the method used to be private and only used for pressure death
attackData.Character?.Implode();
}
}
else
{
LogabilityObjectMismatch();
}
}
}
}
@@ -0,0 +1,34 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyFlag : CharacterAbility
{
private readonly AbilityFlags abilityFlag;
private bool lastState;
public CharacterAbilityModifyFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
abilityFlag = CharacterAbilityGroup.ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
if (conditionsMatched != lastState)
{
if (conditionsMatched)
{
Character.AddAbilityFlag(abilityFlag);
}
else
{
Character.RemoveAbilityFlag(abilityFlag);
}
lastState = conditionsMatched;
}
}
}
}
@@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyReduceAffliction : CharacterAbility
{
float addedAmountMultiplier;
public CharacterAbilityModifyReduceAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
addedAmountMultiplier = abilityElement.GetAttributeFloat("addedamountmultiplier", 0f);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is AbilityValueAffliction afflictionReduceAmount)
{
afflictionReduceAmount.Affliction.Strength -= addedAmountMultiplier * afflictionReduceAmount.Value;
}
else
{
LogabilityObjectMismatch();
}
}
}
}
@@ -0,0 +1,32 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyResistance : CharacterAbility
{
private readonly string resistanceId;
private readonly float resistance;
bool lastState;
// should probably be split to different classes
public CharacterAbilityModifyResistance(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
resistanceId = abilityElement.GetAttributeString("resistanceid", "");
resistance = abilityElement.GetAttributeFloat("resistance", 1f);
if (string.IsNullOrEmpty(resistanceId))
{
DebugConsole.ThrowError("Error in CharacterAbilityModifyResistance - resistance identifier not set.");
}
}
public override void UpdateCharacterAbility(bool conditionsMatched, float timeSinceLastUpdate)
{
if (conditionsMatched != lastState)
{
Character.ChangeAbilityResistance(resistanceId, conditionsMatched ? resistance : 1 / resistance);
lastState = conditionsMatched;
}
}
}
}
@@ -0,0 +1,26 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyStat : CharacterAbility
{
private readonly StatTypes statType;
private readonly float value;
bool lastState;
public CharacterAbilityModifyStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier);
value = abilityElement.GetAttributeFloat("value", 0f);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
if (conditionsMatched != lastState)
{
Character.ChangeStat(statType, conditionsMatched ? value : -value);
lastState = conditionsMatched;
}
}
}
}
@@ -0,0 +1,34 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyStatToFlooding : CharacterAbility
{
private readonly StatTypes statType;
private readonly float maxValue;
private float lastValue = 0f;
public CharacterAbilityModifyStatToFlooding(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier);
maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
Character.ChangeStat(statType, -lastValue);
if (conditionsMatched && Character.IsInFriendlySub)
{
float currentFloodPercentage = Character.Submarine.GetHulls(false).Average(h => h.WaterPercentage);
lastValue = currentFloodPercentage / 100f * maxValue;
Character.ChangeStat(statType, lastValue);
}
else
{
lastValue = 0f;
}
}
}
}
@@ -0,0 +1,35 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyStatToLevel : CharacterAbility
{
private readonly StatTypes statType;
private readonly float statPerLevel;
private readonly int maxLevel;
private float lastValue = 0f;
public CharacterAbilityModifyStatToLevel(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier);
statPerLevel = abilityElement.GetAttributeFloat("statperlevel", 0f);
maxLevel = abilityElement.GetAttributeInt("maxlevel", int.MaxValue);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
Character.ChangeStat(statType, -lastValue);
if (conditionsMatched)
{
int level = MathHelper.Min(Character?.Info.GetCurrentLevel() ?? 0, maxLevel);
lastValue = statPerLevel * level;
Character.ChangeStat(statType, lastValue);
}
else
{
lastValue = 0f;
}
}
}
}
@@ -0,0 +1,52 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyStatToSkill : CharacterAbility
{
private readonly StatTypes statType;
private readonly float maxValue;
private readonly string skillIdentifier;
private readonly bool useAll;
private float lastValue = 0f;
public CharacterAbilityModifyStatToSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier);
maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f);
skillIdentifier = abilityElement.GetAttributeString("skillidentifier", string.Empty);
useAll = skillIdentifier == "all";
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
Character.ChangeStat(statType, -lastValue);
if (conditionsMatched)
{
float skillTotal = 0f;
if (useAll && Character.Info?.Job != null)
{
foreach (Skill skill in Character.Info.Job.Skills)
{
skillTotal += Character.GetSkillLevel(skill.Identifier);
}
skillTotal /= Character.Info.Job.Skills.Count;
}
else
{
skillTotal = Character.GetSkillLevel(skillIdentifier);
}
lastValue = skillTotal / 100f * maxValue;
Character.ChangeStat(statType, lastValue);
}
else
{
lastValue = 0f;
}
}
}
}
@@ -0,0 +1,25 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyValue : CharacterAbility
{
private readonly float addedValue;
private readonly float multiplyValue;
public CharacterAbilityModifyValue(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f);
multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is IAbilityValue abilityValue)
{
abilityValue.Value += addedValue;
abilityValue.Value *= multiplyValue;
}
}
}
}
@@ -0,0 +1,46 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityPutItem : CharacterAbility
{
private readonly string itemIdentifier;
private readonly int amount;
public override bool AppliesEffectOnIntervalUpdate => true;
public CharacterAbilityPutItem(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
itemIdentifier = abilityElement.GetAttributeString("itemidentifier", "");
amount = abilityElement.GetAttributeInt("amount", 1);
}
protected override void ApplyEffect()
{
if (string.IsNullOrEmpty(itemIdentifier))
{
DebugConsole.ThrowError("Cannot put item in inventory - itemIdentifier not defined.");
return;
}
ItemPrefab itemPrefab = ItemPrefab.Find(null, itemIdentifier);
if (itemPrefab == null)
{
DebugConsole.ThrowError("Cannot put item in inventory - item prefab " + itemIdentifier + " not found.");
return;
}
for (int i = 0; i < amount; i++)
{
if (GameMain.GameSession?.RoundEnding ?? true)
{
Item item = new Item(itemPrefab, Character.WorldPosition, Character.Submarine);
Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any });
}
else
{
Entity.Spawner.AddToSpawnQueue(itemPrefab, Character.Inventory);
}
}
}
}
}
@@ -0,0 +1,30 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityResetPermanentStat : CharacterAbility
{
private readonly string statIdentifier;
public override bool AppliesEffectOnIntervalUpdate => true;
public override bool AllowClientSimulation => true;
public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant();
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffectSpecific();
}
protected override void ApplyEffect()
{
ApplyEffectSpecific();
}
private void ApplyEffectSpecific()
{
Character?.Info.ResetSavedStatValue(statIdentifier);
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityRevive : CharacterAbility
{
public override bool AppliesEffectOnIntervalUpdate => true;
public CharacterAbilityRevive(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
}
private void ApplyEffectSpecific()
{
Character.Revive(removeAllAfflictions: false);
}
protected override void ApplyEffect()
{
ApplyEffectSpecific();
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffectSpecific();
}
}
}
@@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilitySpawnItemsToContainer : CharacterAbility
{
// currently used only for spawning items to containers
private readonly List<StatusEffect> statusEffects;
private readonly List<Item> openedContainers = new List<Item>();
private readonly float randomChance;
private readonly bool oncePerContainer;
public CharacterAbilitySpawnItemsToContainer(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects"));
randomChance = abilityElement.GetAttributeFloat("randomchance", 1f);
oncePerContainer = abilityElement.GetAttributeBool("oncepercontainer", false);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityItem)?.Item is Item item)
{
if (oncePerContainer)
{
if (openedContainers.Contains(item)) { return; }
openedContainers.Add(item);
}
if (randomChance < Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) { return; }
foreach (var statusEffect in statusEffects)
{
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, item, item);
}
}
else
{
LogabilityObjectMismatch();
}
}
}
}
@@ -0,0 +1,31 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityUnlockTree : CharacterAbility
{
public CharacterAbilityUnlockTree(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
}
public override void InitializeAbility(bool addingFirstTime)
{
if (!addingFirstTime) { return; }
if (!TalentTree.JobTalentTrees.TryGetValue(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; }
var subTree = talentTree.TalentSubTrees.Find(t => t.TalentOptionStages.Any(ts => ts.Talents.Contains(CharacterTalent.Prefab)));
if (subTree != null)
{
foreach (var talentOption in subTree.TalentOptionStages)
{
foreach (var talent in talentOption.Talents)
{
if (talent == CharacterTalent.Prefab) { continue; }
Character.GiveTalent(talent);
}
}
}
}
}
}
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityAlienHoarder : CharacterAbility
{
private readonly float addedDamageMultiplierPerItem;
private readonly float maxAddedDamageMultiplier;
private readonly string[] tags;
public CharacterAbilityAlienHoarder(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
addedDamageMultiplierPerItem = abilityElement.GetAttributeFloat("addeddamagemultiplierperitem", 0f);
maxAddedDamageMultiplier = abilityElement.GetAttributeFloat("maxaddedddamagemultiplier", float.MaxValue);
tags = abilityElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is AbilityAttackData attackData)
{
float totalAddedDamageMultiplier = 0f;
foreach (Item item in Character.Inventory.AllItems)
{
if (tags.Any(t => item.Prefab.Tags.Any(p => t == p)))
{
totalAddedDamageMultiplier += addedDamageMultiplierPerItem;
}
}
attackData.DamageMultiplier += Math.Min(totalAddedDamageMultiplier, maxAddedDamageMultiplier);
}
else
{
LogabilityObjectMismatch();
}
}
}
}
@@ -0,0 +1,21 @@
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityApprenticeship : CharacterAbility
{
public CharacterAbilityApprenticeship(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is AbilitySkillGain abilitySkillGain && !abilitySkillGain.GainedFromApprenticeship && abilitySkillGain.Character != Character)
{
Character.Info?.IncreaseSkillLevel(abilitySkillGain.String, 1.0f, gainedFromApprenticeship: true);
}
}
}
}
@@ -0,0 +1,43 @@
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityAtmosMachine : CharacterAbility
{
private readonly float addedValue;
private readonly float multiplyValue;
private readonly string[] tags;
private readonly int maxMultiplyCount;
public CharacterAbilityAtmosMachine(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f);
multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f);
tags = abilityElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
maxMultiplyCount = abilityElement.GetAttributeInt("maxmultiplycount", int.MaxValue);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is IAbilityValue abilityValue)
{
int multiplyCount = 0;
foreach (Item item in Item.ItemList)
{
if (item.Prefab.Tags.Any(t => tags.Contains(t)))
{
multiplyCount++;
if (multiplyCount == maxMultiplyCount)
{
break;
}
}
}
abilityValue.Value += addedValue * multiplyCount;
}
}
}
}
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityBountyHunter : CharacterAbility
{
private float vitalityPercentage;
public CharacterAbilityBountyHunter(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
vitalityPercentage = abilityElement.GetAttributeFloat("vitalitypercentage", 0f);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityCharacter)?.Character is Character character)
{
Character.GiveMoney((int)(vitalityPercentage * character.MaxVitality));
}
}
}
}
@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityByTheBook : CharacterAbility
{
private readonly int moneyAmount;
private readonly int experienceAmount;
private readonly int max;
public CharacterAbilityByTheBook(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
moneyAmount = abilityElement.GetAttributeInt("moneyamount", 0);
experienceAmount = abilityElement.GetAttributeInt("experienceamount", 0);
max = abilityElement.GetAttributeInt("max", 0);
}
protected override void ApplyEffect()
{
IEnumerable<Character> enemyCharacters = Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.None);
int timesGiven = 0;
foreach (Character enemyCharacter in enemyCharacters)
{
if (!enemyCharacter.IsHuman) { continue; }
if (enemyCharacter.Submarine == null || enemyCharacter.Submarine != Submarine.MainSub) { continue; }
if (enemyCharacter.IsDead) { continue; }
if (!enemyCharacter.LockHands) { continue; }
if (timesGiven > max) { continue; }
Character.GiveMoney(moneyAmount);
foreach (Character character in Character.GetFriendlyCrew(Character))
{
character.Info?.GiveExperience(experienceAmount);
}
timesGiven++;
}
}
}
}
@@ -0,0 +1,31 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityInsurancePolicy : CharacterAbility
{
public override bool AppliesEffectOnIntervalUpdate => true;
private readonly int moneyPerMission;
private static List<Client> clientsAlreadyUsed = new List<Client>();
public CharacterAbilityInsurancePolicy(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
moneyPerMission = abilityElement.GetAttributeInt("moneypermission", 0);
}
protected override void ApplyEffect()
{
if (Character?.Info is CharacterInfo info)
{
Character.GiveMoney(moneyPerMission * info.MissionsCompletedSinceDeath);
}
}
}
}
@@ -0,0 +1,26 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityMultitasker : CharacterAbility
{
private string lastSkillIdentifier;
public CharacterAbilityMultitasker(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityString)?.String is string skillIdentifier)
{
if (skillIdentifier != lastSkillIdentifier)
{
lastSkillIdentifier = skillIdentifier;
Character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f);
}
}
}
}
}
@@ -0,0 +1,44 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityPsychoClown : CharacterAbility
{
private StatTypes statType;
private float maxValue;
private string afflictionIdentifier;
private float lastValue = 0f;
public CharacterAbilityPsychoClown(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier);
maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f);
afflictionIdentifier = abilityElement.GetAttributeString("afflictionidentifier", "");
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
// managing state this way seems liable to cause bugs, maybe instead create abstraction to reset these values more safely
// talents cannot be removed while in active play because of the lack of this, for example
Character.ChangeStat(statType, -lastValue);
if (conditionsMatched)
{
var affliction = Character.CharacterHealth.GetAffliction(afflictionIdentifier);
float afflictionStrength = 0f;
if (affliction != null)
{
afflictionStrength = affliction.Strength / affliction.Prefab.MaxStrength;
}
lastValue = afflictionStrength * maxValue;
Character.ChangeStat(statType, lastValue);
}
else
{
lastValue = 0f;
}
}
}
}
@@ -0,0 +1,39 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityRegenerateLoot : CharacterAbility
{
// separate random chance used for the ability itself to prevent the player
// from opening/reopening a container until it spawns loot
private readonly float randomChance;
// not maintained through death, so it's possible for players to respawn and re-loot chests
// seems like a minor issue for now
private readonly List<Item> openedContainers = new List<Item>();
public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
randomChance = abilityElement.GetAttributeFloat("randomchance", 1f);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityItem)?.Item is Item item)
{
if (openedContainers.Contains(item)) { return; }
openedContainers.Add(item);
if (randomChance < Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) { return; }
if (item.GetComponent<ItemContainer>() is ItemContainer itemContainer)
{
AutoItemPlacer.RegenerateLoot(item.Submarine, itemContainer);
}
}
}
}
}
@@ -0,0 +1,43 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityTandemFire : CharacterAbilityApplyStatusEffectsToNearestAlly
{
// this should just be its own class, misleading to inherit here
private string tag;
public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
tag = abilityElement.GetAttributeString("tag", "");
}
protected override void ApplyEffect()
{
if (Character.SelectedConstruction == null || !Character.SelectedConstruction.HasTag(tag)) { return; }
Character closestCharacter = null;
float closestDistance = float.MaxValue;
foreach (Character crewCharacter in Character.GetFriendlyCrew(Character))
{
if (crewCharacter != Character && Vector2.DistanceSquared(Character.SimPosition, Character.GetRelativeSimPosition(crewCharacter)) is float tempDistance && tempDistance < closestDistance)
{
closestCharacter = crewCharacter;
closestDistance = tempDistance;
}
}
if (closestCharacter.SelectedConstruction == null || !closestCharacter.SelectedConstruction.HasTag(tag)) { return; }
if (closestDistance < squaredMaxDistance)
{
ApplyEffectSpecific(Character);
ApplyEffectSpecific(closestCharacter);
}
}
}
}
@@ -0,0 +1,222 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
abstract class CharacterAbilityGroup
{
public CharacterTalent CharacterTalent { get; }
public Character Character { get; }
// currently only used to turn off simulation if random conditions are in use
public bool IsActive { get; private set; } = true;
public readonly AbilityEffectType AbilityEffectType;
protected int maxTriggerCount { get; }
protected int timesTriggered = 0;
// add support for OR conditions?
protected readonly List<AbilityCondition> abilityConditions = new List<AbilityCondition>();
// separate dictionaries for each type of characterability?
protected readonly List<CharacterAbility> characterAbilities = new List<CharacterAbility>();
public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, XElement abilityElementGroup)
{
AbilityEffectType = abilityEffectType;
CharacterTalent = characterTalent;
Character = CharacterTalent.Character;
maxTriggerCount = abilityElementGroup.GetAttributeInt("maxtriggercount", int.MaxValue);
foreach (XElement subElement in abilityElementGroup.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "abilities":
LoadAbilities(subElement);
break;
case "conditions":
LoadConditions(subElement);
break;
}
}
}
public void ActivateAbilityGroup(bool addingFirstTime)
{
foreach (var characterAbility in characterAbilities)
{
characterAbility.InitializeAbility(addingFirstTime);
}
}
public void LoadConditions(XElement conditionElements)
{
foreach (XElement conditionElement in conditionElements.Elements())
{
AbilityCondition newCondition = ConstructCondition(CharacterTalent, conditionElement);
if (newCondition == null)
{
DebugConsole.ThrowError($"AbilityCondition was not found in talent {CharacterTalent.DebugIdentifier}!");
return;
}
if (!newCondition.AllowClientSimulation && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
IsActive = false;
}
abilityConditions.Add(newCondition);
}
}
public void AddAbility(CharacterAbility characterAbility)
{
if (characterAbility == null)
{
DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!");
return;
}
characterAbilities.Add(characterAbility);
}
// XML
private AbilityCondition ConstructCondition(CharacterTalent characterTalent, XElement conditionElement, bool errorMessages = true)
{
AbilityCondition newCondition = null;
Type conditionType;
string type = conditionElement.Name.ToString().ToLowerInvariant();
try
{
conditionType = Type.GetType("Barotrauma.Abilities." + type + "", false, true);
if (conditionType == null)
{
if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")");
return null;
}
}
catch (Exception e)
{
if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")", e);
return null;
}
object[] args = { characterTalent, conditionElement };
try
{
newCondition = (AbilityCondition)Activator.CreateInstance(conditionType, args);
}
catch (TargetInvocationException e)
{
DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ".", e.InnerException);
return null;
}
if (newCondition == null)
{
DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ", instance was null");
return null;
}
return newCondition;
}
private void LoadAbilities(XElement abilityElements)
{
foreach (XElement abilityElementGroup in abilityElements.Elements())
{
AddAbility(ConstructAbility(abilityElementGroup, CharacterTalent));
}
}
private CharacterAbility ConstructAbility(XElement abilityElement, CharacterTalent characterTalent)
{
CharacterAbility newAbility = CharacterAbility.Load(abilityElement, this);
if (newAbility == null)
{
DebugConsole.ThrowError($"Unable to create an ability for {characterTalent.DebugIdentifier}!");
return null;
}
return newAbility;
}
public static List<StatusEffect> ParseStatusEffects(CharacterTalent characterTalent, XElement statusEffectElements)
{
if (statusEffectElements == null)
{
DebugConsole.ThrowError("StatusEffect list was not found in talent " + characterTalent.DebugIdentifier);
return null;
}
List<StatusEffect> statusEffects = new List<StatusEffect>();
foreach (XElement statusEffectElement in statusEffectElements.Elements())
{
var statusEffect = StatusEffect.Load(statusEffectElement, characterTalent.DebugIdentifier);
statusEffects.Add(statusEffect);
}
return statusEffects;
}
public static StatTypes ParseStatType(string statTypeString, string debugIdentifier)
{
if (!Enum.TryParse(statTypeString, true, out StatTypes statType))
{
DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in CharacterTalent (" + debugIdentifier + ")");
}
return statType;
}
public static List<Affliction> ParseAfflictions(CharacterTalent characterTalent, XElement afflictionElements)
{
if (afflictionElements == null)
{
DebugConsole.ThrowError("Affliction list was not found in talent " + characterTalent.DebugIdentifier);
return null;
}
List<Affliction> afflictions = new List<Affliction>();
// similar logic to affliction creation in statuseffects
// might be worth unifying
foreach (XElement afflictionElement in afflictionElements.Elements())
{
string afflictionIdentifier = afflictionElement.GetAttributeString("identifier", "").ToLowerInvariant();
AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier);
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in CharacterTalent (" + characterTalent.DebugIdentifier + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found.");
continue;
}
Affliction afflictionInstance = afflictionPrefab.Instantiate(afflictionElement.GetAttributeFloat(1.0f, "amount", "strength"));
afflictionInstance.Probability = afflictionElement.GetAttributeFloat(1.0f, "probability");
afflictions.Add(afflictionInstance);
}
return afflictions;
}
public static AbilityFlags ParseFlagType(string flagTypeString, string debugIdentifier)
{
AbilityFlags flagType = AbilityFlags.None;
if (!Enum.TryParse(flagTypeString, true, out flagType))
{
DebugConsole.ThrowError("Invalid flag type type \"" + flagTypeString + "\" in CharacterTalent (" + debugIdentifier + ")");
}
return flagType;
}
}
}
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGroupEffect : CharacterAbilityGroup
{
public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, XElement abilityElementGroup) :
base(abilityEffectType, characterTalent, abilityElementGroup) { }
public void CheckAbilityGroup(AbilityObject abilityObject)
{
if (!IsActive) { return; }
if (IsApplicable(abilityObject))
{
foreach (var characterAbility in characterAbilities)
{
if (characterAbility.IsViable())
{
characterAbility.ApplyAbilityEffect(abilityObject);
}
}
timesTriggered++;
}
}
private bool IsApplicable(AbilityObject abilityObject)
{
if (timesTriggered >= maxTriggerCount) { return false; }
return abilityConditions.All(c => c.MatchesCondition(abilityObject));
}
}
}
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGroupInterval : CharacterAbilityGroup
{
private float interval { get; set; }
public float TimeSinceLastUpdate { get; private set; }
private float effectDelay;
private float effectDelayTimer;
public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, XElement abilityElementGroup) :
base(abilityEffectType, characterTalent, abilityElementGroup)
{
// too many overlapping intervals could cause hitching? maybe randomize a little
interval = abilityElementGroup.GetAttributeFloat("interval", 0f);
effectDelay = abilityElementGroup.GetAttributeFloat("effectdelay", 0f);
}
public void UpdateAbilityGroup(float deltaTime)
{
if (!IsActive) { return; }
TimeSinceLastUpdate += deltaTime;
if (TimeSinceLastUpdate >= interval)
{
bool conditionsMatched = IsApplicable();
effectDelayTimer = conditionsMatched ? effectDelayTimer + TimeSinceLastUpdate : 0f;
conditionsMatched &= effectDelayTimer >= effectDelay;
foreach (var characterAbility in characterAbilities)
{
if (characterAbility.IsViable())
{
characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate);
}
}
if (conditionsMatched)
{
timesTriggered++;
}
TimeSinceLastUpdate = 0;
}
}
private bool IsApplicable()
{
if (timesTriggered >= maxTriggerCount) { return false; }
return abilityConditions.All(c => c.MatchesCondition());
}
}
}
@@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Abilities;
namespace Barotrauma
{
class CharacterTalent
{
public Character Character { get; }
public string DebugIdentifier { get; }
public readonly TalentPrefab Prefab;
public bool AddedThisRound = true;
private readonly Dictionary<AbilityEffectType, List<CharacterAbilityGroupEffect>> characterAbilityGroupEffectDictionary = new Dictionary<AbilityEffectType, List<CharacterAbilityGroupEffect>>();
private readonly List<CharacterAbilityGroupInterval> characterAbilityGroupIntervals = new List<CharacterAbilityGroupInterval>();
// works functionally but a missing recipe is not represented on GUI side. this might be better placed in the character class itself, though it might be fine here as well
public List<string> UnlockedRecipes { get; } = new List<string>();
public CharacterTalent(TalentPrefab talentPrefab, Character character)
{
Character = character;
Prefab = talentPrefab;
XElement element = talentPrefab.ConfigElement;
DebugIdentifier = talentPrefab.OriginalName;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "abilitygroupeffect":
LoadAbilityGroupEffect(subElement);
break;
case "abilitygroupinterval":
LoadAbilityGroupInterval(subElement);
break;
case "addedrecipe":
if (subElement.GetAttributeString("itemidentifier", string.Empty) is string recipeIdentifier && recipeIdentifier != string.Empty)
{
UnlockedRecipes.Add(recipeIdentifier);
}
else
{
DebugConsole.ThrowError("No recipe identifier defined for talent " + DebugIdentifier);
}
break;
}
}
}
public virtual void UpdateTalent(float deltaTime)
{
foreach (var characterAbilityGroupInterval in characterAbilityGroupIntervals)
{
characterAbilityGroupInterval.UpdateAbilityGroup(deltaTime);
}
}
public void CheckTalent(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
{
if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityGroups))
{
foreach (var characterAbilityGroup in characterAbilityGroups)
{
characterAbilityGroup.CheckAbilityGroup(abilityObject);
}
}
}
public void ActivateTalent(bool addingFirstTime)
{
foreach (var characterAbilityGroups in characterAbilityGroupEffectDictionary.Values)
{
foreach (var characterAbilityGroup in characterAbilityGroups)
{
characterAbilityGroup.ActivateAbilityGroup(addingFirstTime);
}
}
}
// XML logic
private void LoadAbilityGroupInterval(XElement abilityGroup)
{
characterAbilityGroupIntervals.Add(new CharacterAbilityGroupInterval(AbilityEffectType.Undefined, this, abilityGroup));
}
private void LoadAbilityGroupEffect(XElement abilityGroup)
{
AbilityEffectType abilityEffectType = ParseAbilityEffectType(this, abilityGroup.GetAttributeString("abilityeffecttype", "none"));
AddAbilityGroupEffect(new CharacterAbilityGroupEffect(abilityEffectType, this, abilityGroup), abilityEffectType);
}
public void AddAbilityGroupEffect(CharacterAbilityGroupEffect characterAbilityGroup, AbilityEffectType abilityEffectType = AbilityEffectType.None)
{
if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityList))
{
characterAbilityList.Add(characterAbilityGroup);
}
else
{
List<CharacterAbilityGroupEffect> characterAbilityGroups = new List<CharacterAbilityGroupEffect>();
characterAbilityGroups.Add(characterAbilityGroup);
characterAbilityGroupEffectDictionary.Add(abilityEffectType, characterAbilityGroups);
}
}
public static AbilityEffectType ParseAbilityEffectType(CharacterTalent characterTalent, string abilityEffectTypeString)
{
if (!Enum.TryParse(abilityEffectTypeString, true, out AbilityEffectType abilityEffectType))
{
DebugConsole.ThrowError("Invalid ability effect type \"" + abilityEffectTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")");
}
if (abilityEffectType == AbilityEffectType.Undefined)
{
DebugConsole.ThrowError("Ability effect type not defined in CharacterTalent (" + characterTalent.DebugIdentifier + ")");
}
return abilityEffectType;
}
}
}
@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
class TalentPrefab : IPrefab, IDisposable, IHasUintIdentifier
{
public string Identifier { get; private set; }
public string OriginalName => Identifier;
public ContentPackage ContentPackage { get; private set; }
public string FilePath { get; private set; }
public string DisplayName { get; private set; }
public string Description { get; private set; }
public readonly Sprite Icon;
public static readonly PrefabCollection<TalentPrefab> TalentPrefabs = new PrefabCollection<TalentPrefab>();
public XElement ConfigElement
{
get;
private set;
}
public TalentPrefab(XElement element, string filePath)
{
FilePath = filePath;
ConfigElement = element;
Identifier = element.GetAttributeString("identifier", "noidentifier");
DisplayName = TextManager.Get("talentname." + Identifier, returnNull: true) ?? Identifier;
this.CalculatePrefabUIntIdentifier(TalentPrefabs);
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "icon":
Icon = new Sprite(subElement);
break;
case "description":
string tempDescription = Description;
TextManager.ConstructDescription(ref tempDescription, subElement);
Description = tempDescription;
break;
}
}
if (string.IsNullOrEmpty(Description))
{
if (element.Attribute("description") != null)
{
string description = element.GetAttributeString("description", string.Empty);
Description = TextManager.Get(description, returnNull: true) ?? description;
}
else
{
Description = TextManager.Get("talentdescription." + Identifier, returnNull: true) ?? string.Empty;
}
}
#if DEBUG
if (!TextManager.ContainsTag("talentname." + Identifier))
{
DebugConsole.AddWarning($"Name for the talent \"{Identifier}\" not found in the text files.");
}
if (string.IsNullOrEmpty(Description))
{
DebugConsole.AddWarning($"Description for the talent \"{Identifier}\" not configured");
}
if (Description.Contains('['))
{
DebugConsole.ThrowError($"Description for the talent \"{Identifier}\" contains brackets - was some variable not replaced correctly? ({Description})");
}
#endif
}
private bool disposed = false;
public void Dispose()
{
if (disposed) { return; }
disposed = true;
TalentPrefabs.Remove(this);
}
/// <summary>
/// Unique identifier that's generated by hashing the prefab's string identifier.
/// Used to reduce the amount of bytes needed to write talent data into network messages in multiplayer.
/// </summary>
public uint UIntIdentifier { get; set; }
public static void RemoveByFile(string filePath) => TalentPrefabs.RemoveByFile(filePath);
public static void LoadFromFile(ContentFile file)
{
DebugConsole.Log("Loading talent prefab: " + file.Path);
RemoveByFile(file.Path);
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var rootElement = doc.Root;
switch (rootElement.Name.ToString().ToLowerInvariant())
{
case "talent":
TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), false);
break;
case "talents":
foreach (var element in rootElement.Elements())
{
if (element.IsOverride())
{
var itemElement = element.GetChildElement("talent");
if (itemElement != null)
{
TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), true);
}
else
{
DebugConsole.ThrowError($"Cannot find a talent element from the children of the override element defined in {file.Path}");
}
}
else
{
TalentPrefabs.Add(new TalentPrefab(element, file.Path), false);
}
}
break;
default:
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}");
break;
}
}
public static void LoadAll(IEnumerable<ContentFile> files)
{
DebugConsole.Log("Loading talent prefabs: ");
foreach (ContentFile file in files)
{
LoadFromFile(file);
}
}
}
}
@@ -0,0 +1,276 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class TalentTree
{
public enum TalentTreeStageState
{
Invalid,
Locked,
Unlocked,
Available,
Highlighted
}
public static readonly Dictionary<string, TalentTree> JobTalentTrees = new Dictionary<string, TalentTree>();
public readonly List<TalentSubTree> TalentSubTrees = new List<TalentSubTree>();
public XElement ConfigElement
{
get;
private set;
}
public TalentTree(XElement element, string filePath)
{
ConfigElement = element;
string jobIdentifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant();
if (string.IsNullOrEmpty(jobIdentifier))
{
DebugConsole.ThrowError($"No job defined for talent tree in \"{filePath}\"!");
return;
}
foreach (XElement subTreeElement in element.GetChildElements("subtree"))
{
TalentSubTrees.Add(new TalentSubTree(subTreeElement));
}
// talents found and unlocked using the identifier wihin the talent tree, so no duplicates may occur
HashSet<string> duplicateSet = new HashSet<string>();
foreach (string talent in TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier))))
{
TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talent, StringComparison.OrdinalIgnoreCase));
if (talentPrefab == null)
{
DebugConsole.AddWarning($"Talent tree for job {jobIdentifier} contains non-existent talent {talent}! Talent tree not added.");
return;
}
if (!duplicateSet.Add(talent))
{
DebugConsole.ThrowError($"Talent tree for job {jobIdentifier} contains duplicate talent {talent}! Talent tree not added.");
return;
}
}
if (!JobTalentTrees.TryAdd(jobIdentifier, this))
{
DebugConsole.ThrowError($"Could not add talent tree for job {jobIdentifier}! A talent tree for this job is already likely defined");
}
}
public bool TalentIsInTree(string talentIdentifier)
{
return TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier))).Any(c => c == talentIdentifier);
}
public static void LoadFromFile(ContentFile file)
{
DebugConsole.Log("Loading talent tree: " + file.Path);
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var rootElement = doc.Root;
switch (rootElement.Name.ToString().ToLowerInvariant())
{
case "talenttree":
new TalentTree(rootElement, file.Path);
break;
case "talenttrees":
foreach (var element in rootElement.Elements())
{
if (element.IsOverride())
{
var treeElement = element.GetChildElement("talenttree");
if (treeElement != null)
{
new TalentTree(rootElement, file.Path);
}
else
{
DebugConsole.ThrowError($"Cannot find a talent tree element from the children of the override element defined in {file.Path}");
}
}
else
{
new TalentTree(element, file.Path);
}
}
break;
default:
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}");
break;
}
}
public static void LoadAll(IEnumerable<ContentFile> files)
{
DebugConsole.Log("Loading talent tree: ");
foreach (ContentFile file in files)
{
LoadFromFile(file);
}
}
public static bool IsViableTalentForCharacter(Character character, string talentIdentifier)
{
return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? Enumerable.Empty<string>());
}
// i hate this function - markus
public static TalentTreeStageState GetTalentOptionStageState(Character character, string subTreeIdentifier, int index, List<string> selectedTalents)
{
if (character?.Info?.Job.Prefab is null) { return TalentTreeStageState.Invalid; }
if (!JobTalentTrees.TryGetValue(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentTreeStageState.Invalid; }
TalentSubTree subTree = talentTree.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier);
if (subTree == null) { return TalentTreeStageState.Invalid; }
TalentOption targetTalentOption = subTree.TalentOptionStages[index];
if (targetTalentOption.Talents.Any(t => character.HasTalent(t.Identifier)))
{
return TalentTreeStageState.Unlocked;
}
if (targetTalentOption.Talents.Any(t => selectedTalents.Contains(t.Identifier)))
{
return TalentTreeStageState.Highlighted;
}
bool hasTalentInLastTier = true;
bool isLastTalentPurchased = true;
int lastindex = index - 1;
if (lastindex >= 0)
{
TalentOption lastLatentOption = subTree.TalentOptionStages[lastindex];
hasTalentInLastTier = lastLatentOption.Talents.Any(HasTalent);
isLastTalentPurchased = lastLatentOption.Talents.Any(t => character.HasTalent(t.Identifier));
}
if (!hasTalentInLastTier)
{
return TalentTreeStageState.Locked;
}
bool hasPointsForNewTalent = character.Info.GetTotalTalentPoints() - selectedTalents.Count > 0;
if (hasPointsForNewTalent)
{
return isLastTalentPurchased ? TalentTreeStageState.Highlighted : TalentTreeStageState.Available;
}
return TalentTreeStageState.Locked;
bool HasTalent(TalentPrefab t)
{
return selectedTalents.Contains(t.Identifier);
}
}
public static bool IsViableTalentForCharacter(Character character, string talentIdentifier, IEnumerable<string> selectedTalents)
{
if (character?.Info?.Job.Prefab == null) { return false; }
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count() <= 0) { return false; }
if (!JobTalentTrees.TryGetValue(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
foreach (var subTree in talentTree.TalentSubTrees)
{
foreach (var talentOptionStage in subTree.TalentOptionStages)
{
bool hasTalentInThisTier = talentOptionStage.Talents.Any(t => selectedTalents.Contains(t.Identifier));
if (!hasTalentInThisTier)
{
if (talentOptionStage.Talents.Any(t => t.Identifier == talentIdentifier))
{
return true;
}
else
{
break;
}
}
}
}
return false;
}
public static List<string> CheckTalentSelection(Character controlledCharacter, IEnumerable<string> selectedTalents)
{
List<string> viableTalents = new List<string>();
bool canStillUnlock = true;
// keep trying to unlock talents until none of the talents are unlockable
while (canStillUnlock && selectedTalents.Any())
{
canStillUnlock = false;
foreach (string talent in selectedTalents)
{
if (IsViableTalentForCharacter(controlledCharacter, talent, viableTalents))
{
viableTalents.Add(talent);
canStillUnlock = true;
}
}
}
return viableTalents;
}
}
class TalentSubTree
{
public string Identifier { get; }
public string DisplayName { get; }
public readonly List<TalentOption> TalentOptionStages = new List<TalentOption>();
public TalentSubTree(XElement subTreeElement)
{
Identifier = subTreeElement.GetAttributeString("identifier", "");
DisplayName = TextManager.Get("talenttree." + Identifier, returnNull: true) ?? Identifier;
foreach (XElement talentOptionsElement in subTreeElement.GetChildElements("talentoptions"))
{
TalentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier));
}
}
}
class TalentOption
{
public readonly List<TalentPrefab> Talents = new List<TalentPrefab>();
public TalentOption(XElement talentOptionsElement, string debugIdentifier)
{
foreach (XElement talentOptionElement in talentOptionsElement.GetChildElements("talentoption"))
{
string identifier = talentOptionElement.GetAttributeString("identifier", string.Empty);
if (!TalentPrefab.TalentPrefabs.ContainsKey(identifier))
{
DebugConsole.ThrowError($"Error in talent tree \"{debugIdentifier}\" - could not find a talent with the identifier \"{identifier}\".");
return;
}
Talents.Add(TalentPrefab.TalentPrefabs[identifier]);
}
}
}
}