v1.4.4.1 (Blood in the Water Update)

This commit is contained in:
Regalis11
2024-04-24 18:09:05 +03:00
parent 89b91d1c3e
commit ff1b8951a7
397 changed files with 15250 additions and 6479 deletions
@@ -1,4 +1,4 @@
using Barotrauma.Items.Components;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -137,18 +137,21 @@ namespace Barotrauma
}
}
var monsterSet = ToolBox.SelectWeightedRandom(monsterSets, m => m.Commonness, Rand.RandSync.Unsynced);
foreach ((CharacterPrefab monsterSpecies, Point monsterCountRange) in monsterSet.MonsterPrefabs)
if (monsterSets.Any())
{
int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1);
for (int i = 0; i < amount; i++)
var monsterSet = ToolBox.SelectWeightedRandom(monsterSets, m => m.Commonness, Rand.RandSync.Unsynced);
foreach ((CharacterPrefab monsterSpecies, Point monsterCountRange) in monsterSet.MonsterPrefabs)
{
CoroutineManager.Invoke(() =>
int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1);
for (int i = 0; i < amount; i++)
{
//round ended before the coroutine finished
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
Entity.Spawner.AddCharacterToSpawnQueue(monsterSpecies.Identifier, spawnPos);
}, Rand.Range(0f, amount));
CoroutineManager.Invoke(() =>
{
//round ended before the coroutine finished
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
Entity.Spawner.AddCharacterToSpawnQueue(monsterSpecies.Identifier, spawnPos);
}, Rand.Range(0f, amount));
}
}
}
@@ -15,6 +15,9 @@ namespace Barotrauma
private readonly Dictionary<Item, int> inventorySlotIndices = new Dictionary<Item, int>();
private readonly Dictionary<Item, byte> parentItemContainerIndices = new Dictionary<Item, byte>();
/// <summary>
/// Percentage of items (0.0 - 1.0) needed to be delivered to complete the mission.
/// </summary>
private float requiredDeliveryAmount;
private readonly List<(ContentXElement element, ItemContainer container)> itemsToSpawn = new List<(ContentXElement element, ItemContainer container)>();
@@ -86,7 +89,7 @@ namespace Barotrauma
bool isPriorMission = true;
foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions)
{
if (!(mission is CargoMission otherMission)) { continue; }
if (mission is not CargoMission otherMission) { continue; }
if (mission == this) { isPriorMission = false; }
previouslySelectedMissions.Add(otherMission);
if (!isPriorMission) { continue; }
@@ -99,7 +102,8 @@ namespace Barotrauma
{
int maxCount = subElement.GetAttributeInt("maxcount", 10);
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; }
ItemPrefab itemPrefab = FindItemPrefab(subElement);
// For logging purposes
FindItemPrefab(subElement);
while (itemsToSpawn.Count < maxItemCount)
{
itemsToSpawn.Add((subElement, null));
@@ -121,7 +125,7 @@ namespace Barotrauma
bool isPriorMission = true;
foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions)
{
if (!(mission is CargoMission otherMission)) { continue; }
if (mission is not CargoMission otherMission) { continue; }
if (mission == this) { isPriorMission = false; }
previouslySelectedMissions.Add(otherMission);
if (!isPriorMission) { continue; }
@@ -161,27 +165,53 @@ namespace Barotrauma
itemsToSpawn.Add((itemConfig.Elements().First(), null));
}
// Calculate the current total reward, since it might differ from the
// prefab total reward depending on the current actual crate count.
calculatedReward = 0;
bool crateValuesUniform = true;
int? prevCrateReward = null;
foreach (var (element, container) in itemsToSpawn)
{
int price = element.GetAttributeInt("reward", Prefab.Reward / itemsToSpawn.Count);
if (rewardPerCrate.HasValue)
int currentCrateReward = element.GetAttributeInt("reward", 0);
calculatedReward += currentCrateReward;
// Apparently crates can have varying values, so we need to check
// here if that is the case, stopping checks on the first discrepancy
if (crateValuesUniform)
{
if (price != rewardPerCrate.Value) { rewardPerCrate = -1; }
if (prevCrateReward.HasValue)
{
if (prevCrateReward.Value != currentCrateReward)
{
crateValuesUniform = false;
}
}
prevCrateReward = currentCrateReward;
}
else
{
rewardPerCrate = price;
}
calculatedReward += price;
}
if (rewardPerCrate.HasValue && rewardPerCrate < 0) { rewardPerCrate = null; }
if (crateValuesUniform)
{
// If rewardPerCrate is set, it will be displayed in the client UI as eg. "123 mk x 5"
rewardPerCrate = calculatedReward / itemsToSpawn.Count;
}
else
{
// If rewardPerCrate is null, the client UI will display just the total reward
rewardPerCrate = null;
}
// Apply the mission reward campaign setting multiplier to the per-crate price, too
if (GameMain.GameSession?.Campaign is CampaignMode campaign && rewardPerCrate is int confirmedRewardPerCrate)
{
rewardPerCrate = (int)Math.Round(confirmedRewardPerCrate * campaign.Settings.MissionRewardMultiplier);
}
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(currentSub))}‖end‖";
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
}
public override int GetReward(Submarine sub)
public override int GetBaseReward(Submarine sub)
{
// If we are not at the location of the mission, skip the calculation of the reward
if (GameMain.GameSession?.StartLocation != Locations[0])
@@ -272,7 +302,7 @@ namespace Barotrauma
item.FindHull();
items.Add(item);
if (parent != null && parent.GetComponent<ItemContainer>() != null)
if (parent?.GetComponent<ItemContainer>() != null)
{
parentInventoryIDs.Add(item, parent.ID);
parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(parent.GetComponent<ItemContainer>()));
@@ -61,7 +61,7 @@ namespace Barotrauma
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
}
public override int GetReward(Submarine sub)
public override int GetBaseReward(Submarine sub)
{
if (sub != missionSub)
{
@@ -160,14 +160,13 @@ namespace Barotrauma
if (terroristChance > 0f)
{
int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count);
int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count);
terroristCount = Math.Clamp(terroristCount, 1, characters.Count);
terroristCharacters.Clear();
characters.GetRange(0, terroristCount).ForEach(c => terroristCharacters.Add(c));
terroristCharacters.ForEach(c => c.IsHostileEscortee = true);
terroristDistanceSquared = Vector2.DistanceSquared(Level.Loaded.StartPosition, Level.Loaded.EndPosition) * Rand.Range(0.35f, 0.65f);
#if DEBUG
DebugConsole.AddWarning("Terrorists will trigger at range " + Math.Sqrt(terroristDistanceSquared));
foreach (Character character in terroristCharacters)
@@ -251,6 +250,7 @@ namespace Barotrauma
// decoupled from range check to prevent from weirdness if players handcuff a terrorist and move backwards
foreach (Character character in terroristCharacters)
{
character.IsHostileEscortee = true;
if (character.HasTeamChange(TerroristTeamChangeIdentifier))
{
// already triggered
@@ -259,7 +259,7 @@ namespace Barotrauma
}
else if (owner is Character c)
{
return c.Info != null && GameMain.GameSession.CrewManager.CharacterInfos.Contains(c.Info);
return c.Info != null && GameMain.GameSession.CrewManager.GetCharacterInfos().Contains(c.Info);
}
return false;
}
@@ -183,33 +183,32 @@ namespace Barotrauma
completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier})");
}
for (int n = 0; n < 2; n++)
{
string locationName = $"‖color:gui.orange‖{locations[n].DisplayName}‖end‖";
if (description != null) { description = description.Replace("[location" + (n + 1) + "]", locationName); }
if (successMessage != null) { successMessage = successMessage.Replace("[location" + (n + 1) + "]", locationName); }
if (failureMessage != null) { failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locationName); }
for (int m = 0; m < messages.Length; m++)
{
messages[m] = messages[m].Replace("[location" + (n + 1) + "]", locationName);
}
}
string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖";
if (description != null)
{
descriptionWithoutReward = description;
description = description.Replace("[reward]", rewardText);
}
if (successMessage != null) { successMessage = successMessage.Replace("[reward]", rewardText); }
if (failureMessage != null) { failureMessage = failureMessage.Replace("[reward]", rewardText); }
descriptionWithoutReward = ReplaceVariablesInMissionMessage(description, sub, replaceReward: false);
description = ReplaceVariablesInMissionMessage(description, sub);
successMessage = ReplaceVariablesInMissionMessage(successMessage, sub);
failureMessage = ReplaceVariablesInMissionMessage(failureMessage, sub);
for (int m = 0; m < messages.Length; m++)
{
messages[m] = messages[m].Replace("[reward]", rewardText);
messages[m] = ReplaceVariablesInMissionMessage(messages[m], sub);
}
Messages = messages.ToImmutableArray();
}
public LocalizedString ReplaceVariablesInMissionMessage(LocalizedString message, Submarine sub, bool replaceReward = true)
{
for (int locationIndex = 0; locationIndex < 2; locationIndex++)
{
string locationName = $"‖color:gui.orange‖{Locations[locationIndex].DisplayName}‖end‖";
message = message.Replace("[location" + (locationIndex + 1) + "]", locationName);
}
if (replaceReward)
{
string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖";
message = message.Replace("[reward]", rewardText);
}
return message;
}
public virtual void SetLevel(LevelData level) { }
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
@@ -254,11 +253,30 @@ namespace Barotrauma
return null;
}
public virtual int GetReward(Submarine sub)
/// <summary>
/// Calculates the base reward, can be overridden for different mission types
/// </summary>
public virtual int GetBaseReward(Submarine sub)
{
return Prefab.Reward;
}
/// <summary>
/// Calculates the available reward, taking into account universal modifiers such as campaign settings
/// </summary>
public int GetReward(Submarine sub)
{
int reward = GetBaseReward(sub);
// Some modifiers should apply universally to all implementations of GetBaseReward
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
{
reward = (int)Math.Round(reward * campaign.Settings.MissionRewardMultiplier);
}
return reward;
}
public void Start(Level level)
{
state = 0;
@@ -353,7 +371,7 @@ namespace Barotrauma
}
if (GameMain.GameSession?.EventManager != null)
{
var newEvent = eventPrefab.CreateInstance();
var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed);
GameMain.GameSession.EventManager.ActivateEvent(newEvent);
}
}
@@ -455,9 +473,13 @@ namespace Barotrauma
foreach (var reputationReward in ReputationRewards)
{
var reputationGainMultiplier = new AbilityMissionReputationGainMultiplier(this, 1f, character: null);
foreach (var c in crewCharacters) { c.CheckTalents(AbilityEffectType.OnCrewGainMissionReputation, reputationGainMultiplier); }
float amount = reputationReward.Amount * reputationGainMultiplier.Value;
if (reputationReward.FactionIdentifier == "location")
{
OriginLocation.Reputation?.AddReputation(reputationReward.Amount);
OriginLocation.Reputation?.AddReputation(amount);
TryGiveReputationForOpposingFaction(OriginLocation.Faction, reputationReward.AmountForOpposingFaction);
}
else
@@ -465,7 +487,7 @@ namespace Barotrauma
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.FactionIdentifier);
if (faction != null)
{
faction.Reputation.AddReputation(reputationReward.Amount);
faction.Reputation.AddReputation(amount);
TryGiveReputationForOpposingFaction(faction, reputationReward.AmountForOpposingFaction);
}
}
@@ -664,5 +686,19 @@ namespace Barotrauma
public Mission Mission { get; set; }
public Character Character { get; set; }
}
class AbilityMissionReputationGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission, IAbilityCharacter
{
public AbilityMissionReputationGainMultiplier(Mission mission, float reputationMultiplier, Character character)
{
Value = reputationMultiplier;
Mission = mission;
Character = character;
}
public float Value { get; set; }
public Mission Mission { get; set; }
public Character Character { get; set; }
}
}
@@ -110,6 +110,7 @@ namespace Barotrauma
public readonly int Reward;
// The titles and bodies of the popup messages during the mission, shown when the state of the mission changes. The order matters.
public readonly ImmutableArray<LocalizedString> Headers;
public readonly ImmutableArray<LocalizedString> Messages;
@@ -187,23 +188,25 @@ namespace Barotrauma
Tags = element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToImmutableHashSet();
string nameTag = element.GetAttributeString("name", "");
Name = TextManager.Get($"MissionName.{TextIdentifier}");
if (!string.IsNullOrEmpty(nameTag))
{
Name = Name
.Fallback(TextManager.Get(nameTag))
.Fallback(nameTag);
}
Name = GetText(element.GetAttributeString("name", ""), "MissionName");
Description = GetText(element.GetAttributeString("description", ""), "MissionDescription");
string descriptionTag = element.GetAttributeString("description", "");
Description =
TextManager.Get($"MissionDescription.{TextIdentifier}");
if (!string.IsNullOrEmpty(descriptionTag))
LocalizedString GetText(string textTag, string textTagPrefix)
{
Description = Description
.Fallback(TextManager.Get(descriptionTag))
.Fallback(descriptionTag);
if (string.IsNullOrEmpty(textTag))
{
return TextManager.Get($"{textTagPrefix}.{TextIdentifier}");
}
else
{
return
//prefer finding a text based on the specific text tag defined in the mission config
TextManager.Get(textTag)
//2nd option: the "default" format (MissionName.SomeMission)
.Fallback(TextManager.Get($"{textTagPrefix}.{TextIdentifier}"))
//last option: use the text in the xml as-is with no localization
.Fallback(textTag);
}
}
Reward = element.GetAttributeInt("reward", 1);
@@ -372,6 +375,12 @@ namespace Barotrauma
DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - mission type cannot be none.");
return;
}
#if DEBUG
if (Type == MissionType.Monster && SonarLabel.IsNullOrEmpty())
{
DebugConsole.AddWarning($"Potential error in mission prefab \"{Identifier}\" - sonar label not set.");
}
#endif
if (CoOpMissionClasses.ContainsKey(Type))
{
@@ -68,7 +68,7 @@ namespace Barotrauma
}
}
public override int GetReward(Submarine sub)
public override int GetBaseReward(Submarine sub)
{
return alternateReward;
}
@@ -262,13 +262,13 @@ namespace Barotrauma
enemySub.EnableMaintainPosition();
enemySub.TeamID = CharacterTeamType.None;
//make the enemy sub withstand atleast the same depth as the player sub
enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth);
enemySub.SetCrushDepth(Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth));
if (Level.Loaded != null)
{
//...and the depth of the patrol positions + 1000 m
foreach (var patrolPos in patrolPositions)
{
enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Level.Loaded.GetRealWorldDepth(patrolPos.Y) + 1000);
enemySub.SetCrushDepth(Math.Max(enemySub.RealWorldCrushDepth, Level.Loaded.GetRealWorldDepth(patrolPos.Y) + 1000));
}
}
enemySub.ImmuneToBallastFlora = true;
@@ -394,11 +394,11 @@ namespace Barotrauma
DebugConsole.NewMessage("Patrol pos: " + patrolPos);
}
#endif
enemySub.SetPosition(spawnPos);
if (!IsClient)
{
InitPirateShip();
}
enemySub.SetPosition(spawnPos);
// flipping the sub on the frame it is moved into place must be done after it's been moved, or it breaks item connections in the submarine
// creating the pirates has to be done after the sub has been flipped, or it seems to break the AI pathing
@@ -10,10 +10,13 @@ namespace Barotrauma
{
partial class SalvageMission : Mission
{
private class Target
{
public Item Item;
/// <summary>
/// The target this item spawns inside (usually a crate for example).
/// </summary>
public Target ParentTarget;
/// <summary>
/// Note that the integer values matter here:
@@ -29,14 +32,20 @@ namespace Barotrauma
}
public readonly ItemPrefab ItemPrefab;
/// <summary>
/// Where the target can be spawned to. E.g. MainPath or Wreck.
/// </summary>
public readonly Level.PositionType SpawnPositionType;
public readonly Identifier ContainerTag;
public readonly Identifier ExistingItemTag;
public readonly bool RemoveItem;
public readonly LocalizedString SonarLabel;
/// <summary>
/// Can the mission continue before this target has been retrieved? Can be used if you want the targets to be retrieved in a specific order.
/// </summary>
public readonly bool AllowContinueBeforeRetrieved;
/// <summary>
@@ -51,6 +60,13 @@ namespace Barotrauma
{
get
{
//if placing the item inside the parent (e.g. some item inside a crate) failed,
//consider this item retrieved (= essentially ignoring the item, it's not necessary to retrieve)
if (PlacingInsideParentTargetFailed)
{
return true;
}
return RequiredRetrievalState switch
{
RetrievalState.None => true,
@@ -78,20 +94,29 @@ namespace Barotrauma
public bool Interacted;
private readonly SalvageMission mission;
public readonly bool RequireInsideOriginalContainer;
public Item OriginalContainer;
/// <summary>
/// Means that the item could not be placed inside the container it was intended to spawn inside (probably meaning the mission has been misconfigured to e.g. spawn more items inside a crate than what the crate can hold).
/// </summary>
public bool PlacingInsideParentTargetFailed;
/// <summary>
/// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list.
/// </summary>
public readonly List<List<StatusEffect>> StatusEffects = new List<List<StatusEffect>>();
public Target(ContentXElement element, SalvageMission mission)
public Target(ContentXElement element, SalvageMission mission, Target parentTarget)
{
this.mission = mission;
ParentTarget = parentTarget;
ContainerTag = element.GetAttributeIdentifier("containertag", Identifier.Empty);
RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", RetrievalState.RetrievedToSub);
AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", false);
HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", false);
RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", parentTarget?.RequiredRetrievalState ?? RetrievalState.RetrievedToSub);
AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", parentTarget != null);
HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", parentTarget?.HideLabelAfterRetrieved ?? false);
RequireInsideOriginalContainer = element.GetAttributeBool("requireinsideoriginalcontainer", false);
string sonarLabelTag = element.GetAttributeString("sonarlabel", "");
if (!string.IsNullOrEmpty(sonarLabelTag))
{
@@ -126,6 +151,7 @@ namespace Barotrauma
if (ItemPrefab == null)
{
string itemTag = element.GetAttributeString("itemtag", "");
//NOTE: using unsynced random here is fine, the clients receive the info of what item spawned from the server
ItemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab;
}
if (ItemPrefab == null && ExistingItemTag.IsEmpty)
@@ -135,7 +161,7 @@ namespace Barotrauma
}
}
SpawnPositionType = element.GetAttributeEnum("spawntype", Level.PositionType.Cave | Level.PositionType.Ruin);
SpawnPositionType = element.GetAttributeEnum("spawntype", parentTarget?.SpawnPositionType ?? (Level.PositionType.Cave | Level.PositionType.Ruin));
foreach (var subElement in element.Elements())
{
@@ -149,12 +175,15 @@ namespace Barotrauma
break;
}
case "chooserandom":
StatusEffects.Add(new List<StatusEffect>());
foreach (var effectElement in subElement.Elements())
if (subElement.Elements().Any(static e => e.NameAsIdentifier() == "statuseffect"))
{
var newEffect = StatusEffect.Load(effectElement, parentDebugName: mission.Prefab.Name.Value);
if (newEffect == null) { continue; }
StatusEffects.Last().Add(newEffect);
StatusEffects.Add(new List<StatusEffect>());
foreach (var effectElement in subElement.Elements())
{
var newEffect = StatusEffect.Load(effectElement, parentDebugName: mission.Prefab.Name.Value);
if (newEffect == null) { continue; }
StatusEffects.Last().Add(newEffect);
}
}
break;
}
@@ -170,7 +199,24 @@ namespace Barotrauma
private readonly List<Target> targets = new List<Target>();
public bool AnyTargetNeedsToBeRetrievedToSub => targets.Any(t => t.RequiredRetrievalState == Target.RetrievalState.RetrievedToSub && !t.Retrieved);
/// <summary>
/// What percentage of targets need to be retrieved for the mission to complete (0.0 - 1.0). Defaults to 0.98.
/// </summary>
private readonly float requiredDeliveryAmount;
/// <summary>
/// Message displayed when at least one of the targets is retrieved, but the mission is not complete yet.
/// </summary>
private LocalizedString partiallyRetrievedMessage;
/// <summary>
/// Message displayed when all targets have been retrieved.
/// </summary>
private LocalizedString allRetrievedMessage;
public bool AnyTargetNeedsToBeRetrievedToSub => targets.Any(static t => t.RequiredRetrievalState == Target.RetrievalState.RetrievedToSub && !t.Retrieved);
private readonly MTRandom rng;
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
@@ -179,8 +225,23 @@ namespace Barotrauma
foreach (var target in targets)
{
if (target.Retrieved && target.HideLabelAfterRetrieved) { continue; }
if (target.Item != null)
if (target.Item != null && !target.Item.Removed)
{
if (target.Item.ParentInventory?.Owner is Item parentItem)
{
bool insideParentItem = false;
foreach (var parentTarget in targets)
{
if (parentTarget.Item == parentItem && !parentTarget.SonarLabel.IsNullOrEmpty())
{
insideParentItem = true;
break;
}
}
//if the item is inside another target that has it's own sonar label, no need to show one on this item
if (insideParentItem) { continue; }
}
yield return (
target.SonarLabel ?? Prefab.SonarLabel,
target.Item.GetRootInventoryOwner()?.WorldPosition ?? target.Item.WorldPosition);
@@ -193,17 +254,82 @@ namespace Barotrauma
public SalvageMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
requiredDeliveryAmount = prefab.ConfigElement.GetAttributeFloat(nameof(requiredDeliveryAmount), 0.98f);
//LevelData may not be instantiated at this point, in that case use the name identifier of the location
rng = new MTRandom(ToolBox.StringToInt(
locations[0].LevelData?.Seed ?? locations[0].NameIdentifier.Value +
locations[1].LevelData?.Seed ?? locations[1].NameIdentifier.Value));
partiallyRetrievedMessage = GetMessage(nameof(partiallyRetrievedMessage));
allRetrievedMessage = GetMessage(nameof(allRetrievedMessage));
foreach (ContentXElement subElement in prefab.ConfigElement.Elements())
{
if (subElement.NameAsIdentifier() == "target")
if (subElement.NameAsIdentifier() == "target" ||
subElement.NameAsIdentifier() == "chooserandom")
{
targets.Add(new Target(subElement, this));
LoadTarget(subElement, parentTarget: null);
}
}
if (!targets.Any())
{
targets.Add(new Target(prefab.ConfigElement, this));
targets.Add(new Target(prefab.ConfigElement, this, parentTarget: null));
}
LocalizedString GetMessage(string attributeName)
{
if (prefab.ConfigElement.GetAttribute(attributeName) != null)
{
string msgTag = prefab.ConfigElement.GetAttributeString(attributeName, string.Empty);
return ReplaceVariablesInMissionMessage(TextManager.Get(msgTag).Fallback(msgTag), sub);
}
return string.Empty;
}
}
private void LoadTarget(ContentXElement element, Target parentTarget)
{
ContentXElement chosenElement = element;
if (element.NameAsIdentifier() == "chooserandom")
{
/* chooserandom in this context can be used to choose either between targets or status effects to apply to the target,
ensure we don't try to load a statuseffect as a "child target" */
if (element.Elements().Any(static e => e.NameAsIdentifier() == "statuseffect"))
{
return;
}
//this needs to be deterministic, use RNG with a specific seed
chosenElement = element.Elements().ToList().GetRandom(rng);
}
int amount = GetAmount(chosenElement);
for (int i = 0; i < amount; i++)
{
var target = new Target(chosenElement, this, parentTarget);
targets.Add(target);
foreach (ContentXElement subElement in chosenElement.Elements())
{
LoadTarget(subElement, parentTarget: target);
}
}
}
private int GetAmount(ContentXElement targetElement)
{
int amount = targetElement.GetAttributeInt("amount", 1);
int minAmount = targetElement.GetAttributeInt("minamount", amount);
int maxAmount = targetElement.GetAttributeInt("maxamount", amount);
// if the amount is a range, pick a random value between minAmount and maxAmount
if (minAmount < maxAmount)
{
//this needs to be deterministic, use RNG with a specific seed
amount = rng.Next(minAmount, maxAmount + 1);
}
return amount;
}
protected override void StartMissionSpecific(Level level)
@@ -294,8 +420,16 @@ namespace Barotrauma
continue;
}
target.Item = new Item(target.ItemPrefab, position, null);
target.Item.body.SetTransformIgnoreContacts(target.Item.body.SimPosition, target.Item.body.Rotation);
target.Item.body.FarseerBody.BodyType = BodyType.Kinematic;
#if CLIENT
target.Item.HighlightColor = GUIStyle.Orange;
target.Item.ExternalHighlight = true;
#endif
target.Item.UpdateTransform();
if (target.Item.CurrentHull == null)
{
//prevent the body from moving if it spawned outside the hulls (we don't want it e.g. falling to the bottom of a cave or into the abyss)
target.Item.body.FarseerBody.BodyType = BodyType.Kinematic;
}
}
else if (target.RequiredRetrievalState == Target.RetrievalState.Interact)
{
@@ -344,6 +478,7 @@ namespace Barotrauma
}
if (validContainers.Any())
{
//NOTE: using unsynced random here is fine, clients don't run this logic but rely on where the server places the item
var selectedContainer = validContainers.GetRandomUnsynced();
if (selectedContainer.Combine(target.Item, user: null))
{
@@ -362,6 +497,40 @@ namespace Barotrauma
new SpawnInfo(usedExistingItem, originalInventoryID, originalItemContainerIndex, originalSlotIndex, executedEffectIndices));
#endif
}
if (!IsClient)
{
// after spawning all the items from prefabs, need to find all targets where parentTarget is defined, and set the item inside parent target container (if applicable)
foreach (var target in targets)
{
if (target.ParentTarget == null) { continue; }
if (target.Item == null)
{
DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)",
contentPackage: Prefab.ContentPackage);
continue;
}
if (target.ParentTarget.Item == null)
{
DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (parent item was null)",
contentPackage: Prefab.ContentPackage);
continue;
}
if (target.ParentTarget.Item.GetComponent<ItemContainer>() is ItemContainer container)
{
if (!container.Inventory.TryPutItem(target.Item, user: null))
{
DebugConsole.ThrowError($"Error in salvage mission {Prefab.Identifier}: failed to put the item {target.Item.Name} inside {target.ParentTarget.Item.Name}.",
contentPackage: Prefab.ContentPackage);
target.PlacingInsideParentTargetFailed = true;
}
target.OriginalContainer = target.ParentTarget.Item;
}
}
}
}
protected override void UpdateMissionSpecific(float deltaTime)
@@ -376,6 +545,7 @@ namespace Barotrauma
if (IsClient) { return; }
bool atLeastOneTargetWasRetrieved = false;
for (int i = 0; i < targets.Count; i++)
{
var target = targets[i];
@@ -388,6 +558,10 @@ namespace Barotrauma
#endif
return;
}
Entity rootInventoryOwner = target.Item.GetRootInventoryOwner();
Submarine parentSub = target.Item.CurrentHull?.Submarine ?? rootInventoryOwner?.Submarine;
bool inPlayerSub = parentSub != null && parentSub.Info.Type == SubmarineType.Player;
switch (target.State)
{
case Target.RetrievalState.None:
@@ -401,16 +575,16 @@ namespace Barotrauma
{
TrySetRetrievalState(Target.RetrievalState.PickedUp);
}
if (inPlayerSub)
{
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
}
}
break;
case Target.RetrievalState.PickedUp:
case Target.RetrievalState.RetrievedToSub:
{
Entity rootInventoryOwner = target.Item.GetRootInventoryOwner();
Submarine parentSub = target.Item.CurrentHull?.Submarine ?? rootInventoryOwner?.Submarine;
bool inPlayerSub = parentSub != null && parentSub.Info.Type == SubmarineType.Player;
bool inPlayerInventory = false;
bool playerInFriendlySub = false;
if (rootInventoryOwner is Character character && character.TeamID == CharacterTeamType.Team1)
@@ -441,33 +615,70 @@ namespace Barotrauma
if (retrievalState < target.State || target.State == retrievalState) { return; }
bool wasRetrieved = target.Retrieved;
target.State = retrievalState;
//increment the mission state if the target became retrieved
if (!wasRetrieved && target.Retrieved) { State = i + 1; }
//increment the mission state if the target became retrieved
if (!wasRetrieved && target.Retrieved)
{
State = Math.Max(i + 1, State);
atLeastOneTargetWasRetrieved = true;
}
}
}
#if CLIENT
if (atLeastOneTargetWasRetrieved)
{
TryShowRetrievedMessage();
}
#endif
if (targets.All(t => t.Retrieved))
{
State = targets.Count + 1;
}
}
}
protected override bool DetermineCompleted()
{
return targets.All(t => t.State >= t.RequiredRetrievalState);
if (requiredDeliveryAmount < 1.0f)
{
return targets.Count(t => IsTargetRetrieved(t)) / (float)targets.Count >= requiredDeliveryAmount;
}
else
{
return targets.All(IsTargetRetrieved);
}
static bool IsTargetRetrieved(Target target)
{
if (target.State < target.RequiredRetrievalState) { return false; }
if (target.RequireInsideOriginalContainer)
{
if (target.Item.ParentInventory != target.OriginalContainer?.OwnInventory) { return false; }
}
return true;
}
}
protected override void EndMissionSpecific(bool completed)
{
//consider failed (can't attempt again) if we picked up any of the items but failed to bring them out of the level
failed = !completed && targets.Any(t => t.State >= Target.RetrievalState.PickedUp);
List<Target> targetsToRemove = new List<Target>();
foreach (var target in targets)
{
if (target.RemoveItem)
if (target.RemoveItem ||
/*remove the target if it's inside another target that's set to be removed (e.g. inside the crate it spawned in)*/
targets.Any(t => t.RemoveItem && target.Item?.ParentInventory?.Owner as Item == t.Item))
{
target.Item?.Remove();
target.Reset();
targetsToRemove.Add(target);
}
}
foreach (var target in targetsToRemove)
{
if (target.Item != null && !target.Item.Removed)
{
target.Item.Remove();
}
target.Reset();
}
}
}
}