v1.4.4.1 (Blood in the Water Update)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user