#nullable enable using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; namespace Barotrauma { class TraitorEventPrefab : EventPrefab { class MissionRequirement { public Identifier MissionIdentifier; public Identifier MissionTag; public MissionType MissionType; public MissionRequirement(XElement element, TraitorEventPrefab prefab) { MissionIdentifier = element.GetAttributeIdentifier(nameof(MissionIdentifier), Identifier.Empty); MissionTag = element.GetAttributeIdentifier(nameof(MissionTag), Identifier.Empty); MissionType = element.GetAttributeEnum(nameof(MissionType), MissionType.None); if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionType == MissionType.None) { DebugConsole.ThrowError($"Error in traitor event \"{prefab.Identifier}\". Mission requirement with no {nameof(MissionIdentifier)}, {nameof(MissionTag)} or {nameof(MissionType)}.", contentPackage: prefab.ContentPackage); } } public bool Match(Mission mission) { if (mission == null) { return MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionType == MissionType.None; } if (!MissionIdentifier.IsEmpty) { return mission.Prefab.Identifier == MissionIdentifier; } else if (!MissionTag.IsEmpty) { return mission.Prefab.Tags.Contains(MissionTag); } else if (MissionType != MissionType.None) { return mission.Prefab.Type == MissionType; } return false; } } class LevelRequirement { private enum LevelType { LocationConnection, Outpost, Any } private readonly LevelType levelType; public ImmutableArray LocationTypes { get; } /// /// Minimimum difficulty of the level for this event to get selected. Defaults to 0. /// private readonly float minDifficulty; /// /// Minimimum difficulty of the level for this event to get selected. Defaults to 5 or , whichever is lower. /// private readonly float minDifficultyInCampaign; //feels a little weird to have something this specific here, but couldn't think of a better way to implement this public ImmutableArray RequiredItemConditionals; public LevelRequirement(ContentXElement element, TraitorEventPrefab prefab) { levelType = element.GetAttributeEnum(nameof(LevelType), LevelType.Any); LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty()).ToImmutableArray(); minDifficulty = element.GetAttributeFloat(nameof(minDifficulty), 0.0f); minDifficultyInCampaign = element.GetAttributeFloat(nameof(minDifficultyInCampaign), Math.Max(minDifficulty, 5.0f)); List requiredItemConditionals = new List(); foreach (var subElement in element.Elements()) { if (subElement.NameAsIdentifier() == "itemconditional") { requiredItemConditionals.AddRange(PropertyConditional.FromXElement(subElement)); } } RequiredItemConditionals = requiredItemConditionals.ToImmutableArray(); } public bool Match(Level level) { if (level?.LevelData == null) { return false; } switch (levelType) { case LevelType.LocationConnection: if (level.LevelData.Type != LevelData.LevelType.LocationConnection) { return false; } break; case LevelType.Outpost: if (level.LevelData.Type != LevelData.LevelType.Outpost) { return false; } break; } if (GameMain.GameSession?.Campaign != null) { if (level.Difficulty < minDifficultyInCampaign) { return false; } } else { if (level.Difficulty < minDifficulty) { return false; } } if (level.StartLocation == null) { if (LocationTypes.Any()) { return false; } } else { if (LocationTypes.Any() && !LocationTypes.Contains(level.StartLocation.Type.Identifier)) { return false; } } if (RequiredItemConditionals.Any()) { bool matchFound = false; foreach (var item in Item.ItemList) { if (RequiredItemConditionals.All(c => item.ConditionalMatches(c))) { matchFound = true; break; } } if (!matchFound) { return false; } } return true; } } class ReputationRequirement { public Identifier Faction; public Identifier CompareToFaction; public float CompareToValue; public readonly PropertyConditional.ComparisonOperatorType Operator; public ReputationRequirement(XElement element, TraitorEventPrefab prefab) { Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty); string conditionStr = element.GetAttributeString("reputation", string.Empty); string[] splitString = conditionStr.Split(' '); string value; if (splitString.Length > 0) { //the first part of the string is the operator, skip it value = string.Join(" ", splitString.Skip(1)); } else { DebugConsole.ThrowError( $"{conditionStr} in {prefab.Identifier} is too short."+ "It should start with an operator followed by a faction identifier or a floating point value."); return; } Operator = PropertyConditional.GetComparisonOperatorType(splitString[0]); if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var floatVal)) { CompareToValue = floatVal; } else { CompareToFaction = value.ToIdentifier(); } } public bool Match(CampaignMode campaign) { var faction1 = campaign.GetFaction(Faction); if (faction1 == null) { DebugConsole.ThrowError($"Could not find the faction {Faction}."); return false; } if (!CompareToFaction.IsEmpty) { var faction2 = campaign.GetFaction(Faction); if (faction2 == null) { DebugConsole.ThrowError($"Could not find the faction {CompareToFaction}."); return false; } return PropertyConditional.CompareFloat(faction1.Reputation.Value, faction2.Reputation.Value, Operator); } return PropertyConditional.CompareFloat(faction1.Reputation.Value, CompareToValue, Operator); } } public readonly Sprite? Icon; public readonly Color IconColor; public const int MinDangerLevel = 1; public const int MaxDangerLevel = 3; public ImmutableHashSet Tags; private readonly ImmutableArray reputationRequirements; private readonly ImmutableArray missionRequirements; private readonly ImmutableArray levelRequirements; public bool HasReputationRequirements => reputationRequirements.Any(); public bool HasMissionRequirements => missionRequirements.Any(); public bool HasLevelRequirements => levelRequirements.Any(); /// /// An event with one of these tags must've been completed previously for this event to trigger. /// public ImmutableHashSet RequiredCompletedTags; public readonly int DangerLevel; /// /// An event of this danger level (or higher) must have been selected previously for this event to trigger. /// It does not matter whether the event was completed successfully or not. Defaults to one less than the DangerLevel of this event. /// public readonly int RequiredPreviousDangerLevel; /// /// An event of a lower danger level must have been completed on the previous round for this event to trigger. /// Defaults to false (no requirements) /// public readonly bool RequirePreviousDangerLevelCompleted; /// /// Minimum number of non-spectating human players on the server for the event to get selected. /// public readonly int MinPlayerCount; /// /// Number of players to assign as a "secondary traitor". /// If both this and are defined, this is treated as a minimum number of secondary traitors. /// public readonly int SecondaryTraitorAmount; /// /// Percentage of players to assign as a "secondary traitor". /// public readonly float SecondaryTraitorPercentage; /// /// Does accusing a secondary traitor count as correctly identifying the traitor? /// public readonly bool AllowAccusingSecondaryTraitor; /// /// Money penalty if the crew votes a wrong player as the traitor /// public readonly int MoneyPenaltyForUnfoundedTraitorAccusation; /// /// Is this event chainable, i.e. does the same traitor get another, higher-lvl one if they complete this one successfully? /// public readonly bool IsChainable; public readonly float StealPercentageOfExperience; public TraitorEventPrefab(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) : base(element, file, fallbackIdentifier) { DangerLevel = MathHelper.Clamp(element.GetAttributeInt(nameof(DangerLevel), MinDangerLevel), min: MinDangerLevel, max: MaxDangerLevel); RequiredPreviousDangerLevel = MathHelper.Clamp(element.GetAttributeInt(nameof(RequiredPreviousDangerLevel), def: DangerLevel - 1), min: 0, max: MaxDangerLevel - 1); RequirePreviousDangerLevelCompleted = element.GetAttributeBool(nameof(RequirePreviousDangerLevelCompleted), false); MinPlayerCount = element.GetAttributeInt(nameof(MinPlayerCount), 0); SecondaryTraitorAmount = element.GetAttributeInt(nameof(SecondaryTraitorAmount), 0); SecondaryTraitorPercentage = element.GetAttributeFloat(nameof(SecondaryTraitorPercentage), 0.0f); AllowAccusingSecondaryTraitor = element.GetAttributeBool(nameof(AllowAccusingSecondaryTraitor), true); MoneyPenaltyForUnfoundedTraitorAccusation = element.GetAttributeInt(nameof(MoneyPenaltyForUnfoundedTraitorAccusation), 100); Tags = element.GetAttributeIdentifierImmutableHashSet(nameof(Tags), ImmutableHashSet.Empty); RequiredCompletedTags = element.GetAttributeIdentifierImmutableHashSet(nameof(RequiredCompletedTags), ImmutableHashSet.Empty); StealPercentageOfExperience = element.GetAttributeFloat(nameof(StealPercentageOfExperience), 0.0f); IsChainable = element.GetAttributeBool(nameof(IsChainable), true); List reputationRequirements = new List(); List levelRequirements = new List(); List missionRequirements = new List(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "reputationrequirement": reputationRequirements.Add(new ReputationRequirement(subElement!, this)); break; case "missionrequirement": missionRequirements.Add(new MissionRequirement(subElement!, this)); break; case "levelrequirement": levelRequirements.Add(new LevelRequirement(subElement!, this)); break; case "icon": Icon = new Sprite(subElement); IconColor = subElement.GetAttributeColor("color", Color.White); break; } } this.reputationRequirements = reputationRequirements.ToImmutableArray(); this.levelRequirements = levelRequirements.ToImmutableArray(); this.missionRequirements = missionRequirements.ToImmutableArray(); } public bool ReputationRequirementsMet(CampaignMode? campaign) { if (campaign == null) { //no requirements in the campaign return true; } foreach (ReputationRequirement requirement in reputationRequirements) { if (!requirement.Match(campaign)) { return false; } } return true; } public bool MissionRequirementsMet(GameSession? gameSession) { if (gameSession == null) { return false; } foreach (MissionRequirement requirement in missionRequirements) { if (gameSession.Missions.None(m => requirement.Match(m))) { return false; } } return true; } public bool LevelRequirementsMet(Level? level) { if (level == null) { return false; } //by default (if no requirements are specified) traitor events happen in LocationConnections. if (levelRequirements.None() && level.Type != LevelData.LevelType.LocationConnection) { return false; } foreach (LevelRequirement requirement in levelRequirements) { if (!requirement.Match(level)) { return false; } } return true; } public override void Dispose() { Icon?.Remove(); } public override string ToString() { return $"{nameof(TraitorEventPrefab)} ({Identifier})"; } } }