Unstable 0.1500.0.0

This commit is contained in:
Markus Isberg
2021-08-26 21:08:21 +09:00
parent 265a2e7ab3
commit 501e02c026
245 changed files with 9775 additions and 2034 deletions
@@ -0,0 +1,89 @@
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(object abilityData);
public abstract bool MatchesCondition();
// tools
protected enum TargetType
{
Any = 0,
Enemy = 1,
Ally = 2,
NotSelf = 3,
Alive = 4,
Monster = 5,
};
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;
default:
return true;
}
}
}
}
@@ -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 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(object abilityData)
{
if (abilityData is AttackData 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(abilityData, typeof(AttackData));
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(object abilityData)
{
if (abilityData is AttackResult attackResult)
{
if (!IsViableTarget(targetTypes, attackResult.HitLimb?.character)) { return false; }
if (afflictions.Any())
{
if (!afflictions.Any(a => attackResult.Afflictions.Select(c => c.Identifier).Contains(a))) { return false; }
}
return true;
}
else
{
LogAbilityConditionError(abilityData, typeof(AttackData));
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(object abilityData)
{
if (abilityData is Character character)
{
if (!IsViableTarget(targetTypes, character)) { return false; }
return true;
}
else
{
LogAbilityConditionError(abilityData, typeof(Character));
return false;
}
}
}
}
@@ -0,0 +1,39 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
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<T>(T abilityData, Type expectedData)
{
DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityData}");
}
protected abstract bool MatchesConditionSpecific(object abilityData);
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(object abilityData)
{
if (abilityData is null) { return invert; }
return invert ? !MatchesConditionSpecific(abilityData) : MatchesConditionSpecific(abilityData);
}
}
}
@@ -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(object abilityData)
{
if (abilityData is Submarine submarine)
{
return submarine.TeamID == character.TeamID && character.Submarine == submarine;
}
else
{
LogAbilityConditionError(abilityData, typeof(Submarine));
return false;
}
}
}
}
@@ -0,0 +1,27 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionHandsomeStranger : AbilityConditionData
{
string skillIdentifier;
public AbilityConditionHandsomeStranger(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
skillIdentifier = conditionElement.GetAttributeString("skillidentifier", "").ToLowerInvariant();
}
protected override bool MatchesConditionSpecific(object abilityData)
{
if (abilityData is string skillIdentifier)
{
return this.skillIdentifier == skillIdentifier;
}
else
{
LogAbilityConditionError(abilityData, typeof(string));
return false;
}
}
}
}
@@ -0,0 +1,50 @@
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionItem : AbilityConditionData
{
private readonly string identifier;
private readonly string[] tags;
public AbilityConditionItem(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
identifier = conditionElement.GetAttributeString("identifier", string.Empty).ToLowerInvariant();
tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
}
protected override bool MatchesConditionSpecific(object abilityData)
{
ItemPrefab item = null;
if (abilityData is Item tempItem)
{
item = tempItem.Prefab;
}
// this and other instances of this type of casting will be refactored
else if (abilityData is (ItemPrefab itemPrefab, object _))
{
item = itemPrefab;
}
if (item != null)
{
if (!string.IsNullOrEmpty(identifier))
{
if (item.Identifier != identifier)
{
return false;
}
}
return tags.Any(t => item.Tags.Any(p => t == p));
}
else
{
LogAbilityConditionError(abilityData, typeof(Item));
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(object abilityData)
{
if (abilityData is (Affliction affliction, float reduceAmount))
{
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(abilityData, typeof((Affliction, float)));
return false;
}
}
}
}
@@ -0,0 +1,22 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionScavenger : AbilityConditionData
{
public AbilityConditionScavenger(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(object abilityData)
{
if (abilityData is Item item)
{
return item.Submarine != character.Submarine;
}
else
{
LogAbilityConditionError(abilityData, typeof(Item));
return false;
}
}
}
}
@@ -0,0 +1,20 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionAboveVitality : AbilityConditionDataless
{
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,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,24 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
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(object abilityData)
{
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,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,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(object abilityData)
{
if (abilityData is (Mission mission, AbilityValue missionAbilityValue))
{
return mission.Prefab.Type == missionType;
}
else
{
LogAbilityConditionError(abilityData, typeof((Mission, AbilityValue)));
return false;
}
}
}
}
@@ -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,24 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionServerRandom : AbilityConditionDataless
{
private 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.Submarine == null || character.Submarine.TeamID != character.TeamID) { return false; }
float currentFloodPercentage = character.Submarine.GetHulls(false).Average(h => h.WaterPercentage);
return currentFloodPercentage / 100 > floodPercentage;
}
}
}
@@ -0,0 +1,142 @@
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 virtual bool RequiresAlive => true;
public virtual bool AllowClientSimulation => false;
public virtual bool AppliesEffectOnIntervalUpdate => false;
private const float DefaultEffectTime = 1.0f;
/// <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;
}
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($"Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups.");
}
public void ApplyAbilityEffect(object abilityData)
{
if (abilityData is null)
{
ApplyEffect();
}
else
{
ApplyEffect(abilityData);
}
}
protected virtual void ApplyEffect()
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect");
}
protected virtual void ApplyEffect(object abilityData)
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect");
}
protected void LogAbilityDataMismatch()
{
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;
}
DebugConsole.AddWarning("Instantiated " + characterAbility + " for talent " + characterAbilityGroup.CharacterTalent.DebugIdentifier);
return characterAbility;
}
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;
}
public static float DistanceToSquaredDistance(float distance)
{
return distance * distance;
}
}
}
@@ -0,0 +1,34 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyForce : CharacterAbility
{
private readonly float impulseStrength;
private readonly float maxVelocity;
private readonly string afflictionIdentifier;
public override bool AppliesEffectOnIntervalUpdate => true;
public CharacterAbilityApplyForce(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
impulseStrength = abilityElement.GetAttributeFloat("impulsestrength", 0f);
maxVelocity = abilityElement.GetAttributeFloat("maxvelocity", 10f);
afflictionIdentifier = abilityElement.GetAttributeString("afflictionidentifier", "");
}
protected override void ApplyEffect()
{
Affliction affliction = Character.CharacterHealth.GetAffliction(afflictionIdentifier);
if (affliction == null) { return; }
foreach (Limb limb in Character.AnimController.Limbs)
{
limb.body.ApplyForce(Vector2.Normalize(limb.Mass * Character.AnimController.TargetMovement) * impulseStrength * (affliction.Strength / affliction.Prefab.MaxStrength), maxVelocity);
}
}
}
}
@@ -0,0 +1,43 @@
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;
public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects"));
}
protected void ApplyEffectSpecific(Character targetCharacter)
{
foreach (var statusEffect in statusEffects)
{
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter);
}
}
protected override void ApplyEffect()
{
ApplyEffectSpecific(Character);
}
protected override void ApplyEffect(object abilityData)
{
if (abilityData is Character targetCharacter)
{
ApplyEffectSpecific(targetCharacter);
}
else
{
ApplyEffect();
}
}
}
}
@@ -0,0 +1,36 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyStatusEffectsToNearestAlly : CharacterAbilityApplyStatusEffects
{
protected float squaredMaxDistance;
public CharacterAbilityApplyStatusEffectsToNearestAlly(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
squaredMaxDistance = DistanceToSquaredDistance(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue));
}
protected override void ApplyEffect()
{
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 (closestDistance < squaredMaxDistance)
{
ApplyEffectSpecific(closestCharacter);
}
}
}
}
@@ -0,0 +1,40 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
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 = DistanceToSquaredDistance(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue));
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.SimPosition, Character.GetRelativeSimPosition(c)) is float tempDistance &&
tempDistance < squaredMaxDistance).GetRandom();
if (chosenCharacter == null) { return; }
ApplyEffectSpecific(chosenCharacter);
}
}
}
@@ -0,0 +1,20 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveFlag : CharacterAbility
{
private 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 = ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier);
}
public override void InitializeAbility(bool addingFirstTime)
{
Character.AddAbilityFlag(abilityFlag);
}
}
}
@@ -0,0 +1,21 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveMissionCount : CharacterAbility
{
private readonly int amount;
public CharacterAbilityGiveMissionCount(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
}
public override void InitializeAbility(bool addingFirstTime)
{
if (!addingFirstTime) { return; }
if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; }
campaign.Settings.AddedMissionCount += amount;
}
}
}
@@ -0,0 +1,22 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveMoney : CharacterAbility
{
public override bool AppliesEffectOnIntervalUpdate => true;
private int amount;
public CharacterAbilityGiveMoney(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
}
protected override void ApplyEffect()
{
Character.GiveMoney(amount);
}
}
}
@@ -0,0 +1,49 @@
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 bool targetAllies;
private readonly bool removeOnDeath;
//private readonly float maximumValue;
public override bool AppliesEffectOnIntervalUpdate => true;
public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant();
statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier);
value = abilityElement.GetAttributeFloat("value", 0f);
targetAllies = abilityElement.GetAttributeBool("targetallies", false);
removeOnDeath = abilityElement.GetAttributeBool("removeondeath", true);
//maximumValue = abilityElement.GetAttributeFloat("maximumvalue", float.MaxValue);
}
protected override void ApplyEffect(object abilityData)
{
ApplyEffectSpecific();
}
protected override void ApplyEffect()
{
ApplyEffectSpecific();
}
private void ApplyEffectSpecific()
{
if (targetAllies)
{
Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath));
}
else
{
Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath);
}
}
}
}
@@ -0,0 +1,21 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveResistance : CharacterAbility
{
private string resistanceId;
private float resistance;
public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
resistanceId = abilityElement.GetAttributeString("resistanceid", "");
resistance = abilityElement.GetAttributeFloat("resistance", 1f);
}
public override void InitializeAbility(bool addingFirstTime)
{
Character.ChangeAbilityResistance(resistanceId, resistance);
}
}
}
@@ -0,0 +1,22 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveStat : CharacterAbility
{
private StatTypes statType;
private float value;
// 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 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,41 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityIncreaseSkill : CharacterAbility
{
public override bool AppliesEffectOnIntervalUpdate => true;
private string skillIdentifier;
private float skillIncrease;
public CharacterAbilityIncreaseSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
skillIdentifier = abilityElement.GetAttributeString("skillidentifier", "").ToLowerInvariant();
skillIncrease = abilityElement.GetAttributeFloat("skillincrease", 0f);
}
protected override void ApplyEffect()
{
ApplyEffectSpecific(Character);
}
protected override void ApplyEffect(object abilityData)
{
if (abilityData is Character character)
{
ApplyEffectSpecific(character);
}
else
{
ApplyEffectSpecific(Character);
}
}
private void ApplyEffectSpecific(Character character)
{
character.Info?.IncreaseSkillLevel(skillIdentifier, skillIncrease, character.Position + Vector2.UnitY * 175.0f);
}
}
}
@@ -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(object abilityData)
{
if (abilityData is Affliction affliction)
{
foreach (string afflictionIdentifier in afflictionIdentifiers)
{
if (affliction.Identifier == afflictionIdentifier)
{
affliction.Strength *= 1 + addedMultiplier;
}
}
}
else
{
LogAbilityDataMismatch();
}
}
}
}
@@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyAttackData : CharacterAbility
{
private readonly List<Affliction> afflictions;
float addedDamageMultiplier;
float addedPenetration;
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);
}
protected override void ApplyEffect(object abilityData)
{
if (abilityData is AttackData attackData)
{
if (attackData.Afflictions == null)
{
attackData.Afflictions = afflictions;
}
else
{
attackData.Afflictions.AddRange(afflictions);
}
attackData.DamageMultiplier += addedDamageMultiplier;
attackData.AddedPenetration += addedPenetration;
}
else
{
LogAbilityDataMismatch();
}
}
}
}
@@ -0,0 +1,34 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyFlag : CharacterAbility
{
private AbilityFlags abilityFlag;
private bool lastState;
public CharacterAbilityModifyFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
abilityFlag = 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(object abilityData)
{
if (abilityData is (Affliction affliction, float reduceAmount))
{
affliction.Strength -= addedAmountMultiplier * reduceAmount;
}
else
{
LogAbilityDataMismatch();
}
}
}
}
@@ -0,0 +1,27 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyResistance : CharacterAbility
{
private string resistanceId;
private 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);
}
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,46 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyValue : CharacterAbility
{
private float addedValue;
private float multiplierValue;
public CharacterAbilityModifyValue(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f);
multiplierValue = abilityElement.GetAttributeFloat("multipliervalue", 1f);
}
protected override void ApplyEffect(object abilityData)
{
if (abilityData is AbilityValue abilityValue)
{
ApplyEffectSpecific(abilityValue);
}
else if (abilityData is (object _, AbilityValue tupleAbilityValue))
{
ApplyEffectSpecific(tupleAbilityValue);
}
}
private void ApplyEffectSpecific(AbilityValue abilityValue)
{
abilityValue.Value += addedValue;
abilityValue.Value *= multiplierValue;
}
}
// this seems like a real silly way to have to pass values by reference into these same interfaces
// if more of these are required, maybe there should be an additional set of interfaces to easily pass values by reference instead
class AbilityValue
{
public float Value { get; set; }
public AbilityValue(float value)
{
Value = value;
}
}
}
@@ -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,29 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityResetPermanentStat : CharacterAbility
{
private readonly string statIdentifier;
public override bool RequiresAlive => false;
public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant();
}
protected override void ApplyEffect(object abilityData)
{
ApplyEffectSpecific();
}
protected override void ApplyEffect()
{
ApplyEffectSpecific();
}
private void ApplyEffectSpecific()
{
Character?.Info.ResetSavedStatValue(statIdentifier);
}
}
}
@@ -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(object abilityData)
{
if (abilityData is (string skillIdentifier, Character character) && character != Character)
{
character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f, character.Position + Vector2.UnitY * 175.0f);
}
}
}
}
@@ -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(object abilityData)
{
if (abilityData is Character character)
{
Character.GiveMoney((int)(vitalityPercentage * character.MaxVitality));
}
}
}
}
@@ -0,0 +1,30 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityIndustrialRevolution : CharacterAbility
{
float addedFabricationSpeed;
public CharacterAbilityIndustrialRevolution(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
addedFabricationSpeed = abilityElement.GetAttributeFloat("addedfabricationspeed", 0f);
}
public override void UpdateCharacterAbility(bool conditionsMatched, float timeSinceLastUpdate)
{
if (conditionsMatched)
{
// not necessarily the cleanest or performant way, but at least this shouldn't break anything.
// must be done every frame in order to work.
if (Character.SelectedConstruction?.GetComponent<Fabricator>() is Fabricator fabricator && fabricator.IsActive)
{
fabricator.FabricationSpeedMultiplier += addedFabricationSpeed;
}
}
}
}
}
@@ -0,0 +1,54 @@
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;
public override bool RequiresAlive => false;
private readonly int moneyPerLevel;
private bool hasOccurred = false;
private static List<Client> clientsAlreadyUsed = new List<Client>();
public CharacterAbilityInsurancePolicy(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
moneyPerLevel = abilityElement.GetAttributeInt("moneyperlevel", 0);
}
protected override void ApplyEffect()
{
if (Character?.Info is CharacterInfo info && !hasOccurred)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
foreach (Client client in GameMain.NetworkMember.ConnectedClients)
{
if (client.Character == Character && clientsAlreadyUsed.Contains(client)) { return; }
}
}
Character.GiveMoney(moneyPerLevel * info.GetCurrentLevel());
hasOccurred = true;
// this is an ugly way to do this, but this effect should not occur more than once per round for a client
// this seemed like the simplest way to do it since characters are instantiated from scratch each time
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
foreach (Client client in GameMain.NetworkMember.ConnectedClients)
{
if (client.Character == Character)
{
clientsAlreadyUsed.Add(client);
}
}
}
}
}
}
}
@@ -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(object abilityData)
{
if (abilityData is string skillIdentifier)
{
if (skillIdentifier != lastSkillIdentifier)
{
lastSkillIdentifier = skillIdentifier;
Character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f, Character.Position + Vector2.UnitY * 175.0f);
}
}
}
}
}
@@ -0,0 +1,44 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityPsychoClown : CharacterAbility
{
private StatTypes statType;
private float value;
private string afflictionIdentifier;
private float lastValue = 0f;
public CharacterAbilityPsychoClown(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier);
value = abilityElement.GetAttributeFloat("value", 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 * value;
Character.ChangeStat(statType, lastValue);
}
else
{
lastValue = 0f;
}
}
}
}
@@ -0,0 +1,30 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityRegenerateLoot : CharacterAbility
{
List<Item> openedContainers = new List<Item>();
public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
}
protected override void ApplyEffect(object abilityData)
{
if (abilityData is Item item && !openedContainers.Contains(item))
{
openedContainers.Add(item);
if (item.GetComponent<ItemContainer>() is ItemContainer itemContainer)
{
AutoItemPlacer.RegenerateLoot(item.Submarine, itemContainer);
}
}
}
}
}
@@ -0,0 +1,42 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityStonewall : CharacterAbility
{
private readonly List<StatusEffect> statusEffects;
private readonly List<StatusEffect> statusEffectsReset;
private int maxEnemyCount;
private float squaredDistance;
public CharacterAbilityStonewall(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects"));
statusEffectsReset = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffectsreset"));
maxEnemyCount = abilityElement.GetAttributeInt("maxenemycount", 0);
squaredDistance = DistanceToSquaredDistance(abilityElement.GetAttributeFloat("distance", 0));
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
int numberOfEnemiesInRange = Character.CharacterList.Where(c => !HumanAIController.IsFriendly(Character, c) && !c.IsDead && Vector2.DistanceSquared(Character.SimPosition, Character.GetRelativeSimPosition(c)) < squaredDistance).Count();
foreach (var statusEffect in statusEffectsReset)
{
statusEffect.Apply(ActionType.OnAbility, 1f, Character, Character);
}
if (conditionsMatched && numberOfEnemiesInRange > 0)
{
foreach (var statusEffect in statusEffects)
{
statusEffect.Apply(ActionType.OnAbility, Math.Min(numberOfEnemiesInRange, maxEnemyCount), Character, Character);
}
}
}
}
}
@@ -0,0 +1,42 @@
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
{
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 || !Character.SelectedConstruction.HasTag(tag)) { return; }
if (closestDistance < squaredMaxDistance)
{
ApplyEffectSpecific(Character);
ApplyEffectSpecific(closestCharacter);
}
}
}
}
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityTaskmaster : CharacterAbility
{
private readonly List<StatusEffect> statusEffects;
private readonly List<StatusEffect> statusEffectsRemove;
private Character lastCharacter;
public CharacterAbilityTaskmaster(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects"));
statusEffectsRemove = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffectsremove"));
}
protected override void ApplyEffect(object abilityData)
{
if (abilityData is Character targetCharacter)
{
if (targetCharacter == Character) { return; }
foreach (var statusEffect in statusEffectsRemove)
{
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, lastCharacter);
}
foreach (var statusEffect in statusEffects)
{
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter);
}
lastCharacter = targetCharacter;
}
}
}
}
@@ -0,0 +1,206 @@
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;
// 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(CharacterTalent characterTalent, XElement abilityElementGroup)
{
CharacterTalent = characterTalent;
Character = CharacterTalent.Character;
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)
{
StatTypes statType;
if (!Enum.TryParse(statTypeString, true, out 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;
}
}
}
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGroupEffect : CharacterAbilityGroup
{
public CharacterAbilityGroupEffect(CharacterTalent characterTalent, XElement abilityElementGroup) : base(characterTalent, abilityElementGroup) { }
public void CheckAbilityGroup(object abilityData)
{
if (!IsActive) { return; }
if (IsApplicable(abilityData))
{
foreach (var characterAbility in characterAbilities)
{
if (characterAbility.IsViable())
{
characterAbility.ApplyAbilityEffect(abilityData);
}
}
}
}
private bool IsApplicable(object abilityData)
{
return abilityConditions.All(c => c.MatchesCondition(abilityData));
}
}
}
@@ -0,0 +1,48 @@
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(CharacterTalent characterTalent, XElement abilityElementGroup) : base(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);
}
}
TimeSinceLastUpdate = 0;
}
}
private bool IsApplicable()
{
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;
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, object abilityData)
{
if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityGroups))
{
foreach (var characterAbilityGroup in characterAbilityGroups)
{
characterAbilityGroup.CheckAbilityGroup(abilityData);
}
}
}
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)
{
string name = abilityGroup.Name.ToString().ToLowerInvariant();
characterAbilityGroupIntervals.Add(new CharacterAbilityGroupInterval(this, abilityGroup));
}
private void LoadAbilityGroupEffect(XElement abilityGroup)
{
AbilityEffectType abilityEffectType = ParseAbilityEffectType(this, abilityGroup.GetAttributeString("abilityeffecttype", "none"));
AddAbilityGroupEffect(new CharacterAbilityGroupEffect(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)
{
AbilityEffectType abilityEffectType = AbilityEffectType.Undefined;
if (!Enum.TryParse(abilityEffectTypeString, true, out 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,102 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
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 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");
this.CalculatePrefabUIntIdentifier(TalentPrefabs);
}
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,219 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class TalentTree
{
public static readonly Dictionary<string, TalentTree> JobTalentTrees = new Dictionary<string, TalentTree>();
public readonly List<TalentSubTree> TalentSubTrees = new List<TalentSubTree>();
private static HashSet<string> subtreeTalents = new HashSet<string>();
public XElement ConfigElement
{
get;
private set;
}
public TalentTree(XElement element, string filePath)
{
ConfigElement = element;
string jobIdentifier = element.GetAttributeString("jobidentifier", "");
if (string.IsNullOrEmpty(jobIdentifier))
{
DebugConsole.ThrowError("No job defined for talent tree!");
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 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.ToString()}' 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>());
}
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 readonly List<TalentOption> TalentOptionStages = new List<TalentOption>();
public TalentSubTree(XElement subTreeElement)
{
Identifier = subTreeElement.GetAttributeString("identifier", "");
foreach (XElement talentOptionsElement in subTreeElement.GetChildElements("talentoptions"))
{
TalentOptionStages.Add(new TalentOption(talentOptionsElement));
}
}
}
class TalentOption
{
public readonly List<Talent> Talents = new List<Talent>();
public TalentOption(XElement talentOptionsElement)
{
foreach (XElement talentOptionElement in talentOptionsElement.GetChildElements("talentoption"))
{
Talents.Add(new Talent(talentOptionElement));
}
}
}
class Talent
{
public readonly string Identifier;
public readonly Sprite Icon;
public Talent(XElement talentOptionElement)
{
Identifier = talentOptionElement.GetAttributeString("identifier", "");
foreach (XElement subElement in talentOptionElement.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "icon":
Icon = new Sprite(subElement);
break;
}
}
}
}
}