Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs
2025-09-17 13:44:21 +03:00

791 lines
36 KiB
C#

using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
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:
/// a larger or equal value than the <see href="RequiredRetrievalState">RequiredRetrievalState</see> means the item counts as retrieved
/// (if the item needs to be picked up to be considered retrieved, it's also considered retrieved if it's in the sub)
/// </summary>
public enum RetrievalState
{
None = 0,
Interact = 1,
PickedUp = 2,
RetrievedToSub = 3
}
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;
/// <summary>
/// If true, target location indicator points to the submarine where the target is inside when the target is not yet found. Not used, if target is not inside any submarine.
/// When enabled, the indicator is hidden when the player is inside the target submarine.
/// </summary>
public readonly bool PointToSub;
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>
/// Does the target need to be picked up or brought to the sub for mission to be considered successful.
/// If None, the target has no effect on the completion of the mission.
/// </summary>
public readonly RetrievalState RequiredRetrievalState;
public readonly bool HideLabelAfterRetrieved;
public readonly bool HideLabelWhenFound;
public readonly bool HideLabelWhenNotFound;
public bool Retrieved
{
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,
RetrievalState.Interact or RetrievalState.PickedUp => State >= RequiredRetrievalState,
RetrievalState.RetrievedToSub => State == RetrievalState.RetrievedToSub,
_ => throw new NotImplementedException(),
};
}
}
private RetrievalState state;
public RetrievalState State
{
get { return state; }
set
{
if (value == state) { return; }
bool wasRetrieved = Retrieved;
state = value;
#if SERVER
GameMain.Server?.UpdateMissionState(mission);
#endif
if (!wasRetrieved && Retrieved)
{
OnTargetRetrieved();
}
else if (state == RetrievalState.PickedUp)
{
OnTargetPickedUp();
}
}
}
private void OnTargetRetrieved()
{
if (Item == null) { return; }
#if CLIENT
SteamTimelineManager.OnMissionTargetRetrieved(Item, mission);
#endif
}
private void OnTargetPickedUp()
{
if (Item == null) { return; }
#if CLIENT
SteamTimelineManager.OnMissionTargetPickedUp(Item, mission);
#endif
}
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, Target parentTarget)
{
this.mission = mission;
ParentTarget = parentTarget;
ContainerTag = element.GetAttributeIdentifier("containertag", Identifier.Empty);
RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", parentTarget?.RequiredRetrievalState ?? RetrievalState.RetrievedToSub);
AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", parentTarget != null);
HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", parentTarget?.HideLabelAfterRetrieved ?? false);
HideLabelWhenFound = element.GetAttributeBool(nameof(HideLabelWhenFound), parentTarget?.HideLabelWhenFound ?? false);
HideLabelWhenNotFound = element.GetAttributeBool(nameof(HideLabelWhenNotFound), parentTarget?.HideLabelWhenNotFound ?? false);
PointToSub = element.GetAttributeBool(nameof(PointToSub), parentTarget?.PointToSub ?? false);
RequireInsideOriginalContainer = element.GetAttributeBool("requireinsideoriginalcontainer", false);
string sonarLabelTag = element.GetAttributeString("sonarlabel", "");
if (!string.IsNullOrEmpty(sonarLabelTag))
{
SonarLabel =
TextManager.Get($"MissionSonarLabel.{sonarLabelTag}")
.Fallback(TextManager.Get(sonarLabelTag))
.Fallback(element.GetAttributeString("sonarlabel", ""));
}
ExistingItemTag = element.GetAttributeIdentifier("existingitemtag", Identifier.Empty);
RemoveItem = element.GetAttributeBool("removeitem", true);
if (element.GetAttribute("itemname") != null)
{
DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item.",
contentPackage: element.ContentPackage);
string itemName = element.GetAttributeString("itemname", "");
ItemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (ItemPrefab == null && ExistingItemTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in SalvageMission: couldn't find an item prefab with the name \"{itemName}\"",
contentPackage: element.ContentPackage);
}
}
else
{
Identifier itemIdentifier = element.GetAttributeIdentifier("itemidentifier", Identifier.Empty);
if (!itemIdentifier.IsEmpty)
{
ItemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab;
}
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)
{
DebugConsole.ThrowError($"Error in SalvageMission - couldn't find an item prefab with the identifier \"{itemIdentifier}\"",
contentPackage: element.ContentPackage);
}
}
SpawnPositionType = element.GetAttributeEnum("spawntype", parentTarget?.SpawnPositionType ?? (Level.PositionType.Cave | Level.PositionType.Ruin));
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
{
var newEffect = StatusEffect.Load(subElement, parentDebugName: mission.Prefab.Name.Value);
if (newEffect == null) { continue; }
StatusEffects.Add(new List<StatusEffect> { newEffect });
break;
}
case "chooserandom":
if (subElement.Elements().Any(static e => e.NameAsIdentifier() == "statuseffect"))
{
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;
}
}
}
public void Reset()
{
state = RetrievalState.None;
Item = null;
}
}
private readonly List<Target> targets = new List<Target>();
/// <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;
private readonly ImmutableArray<Identifier> wreckTags;
private LocalizedString pickedUpMessage;
/// <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
{
get
{
foreach (var target in targets)
{
if (target.Retrieved && target.HideLabelAfterRetrieved) { continue; }
if (target.State is Target.RetrievalState.None)
{
if (target.HideLabelWhenNotFound) { continue; }
}
else if (target.HideLabelWhenFound)
{
continue;
}
if (target.Item is { Removed: false })
{
if (target.PointToSub && target.Item.Submarine is Submarine targetSub && target.State == Target.RetrievalState.None)
{
if (Character.Controlled is Character playerCharacter && playerCharacter.Submarine != targetSub)
{
// The target is not in the same sub as the player -> point to the target submarine (instead of the item position).
// When inside the target sub, don't show anything.
yield return (target.SonarLabel, targetSub.WorldPosition);
}
continue;
}
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 its 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);
}
if (!target.AllowContinueBeforeRetrieved && !target.Retrieved) { break; }
}
}
}
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));
wreckTags = prefab.ConfigElement.GetAttributeIdentifierArray("wrecktags", []).ToImmutableArray();
partiallyRetrievedMessage = GetMessage(nameof(partiallyRetrievedMessage));
allRetrievedMessage = GetMessage(nameof(allRetrievedMessage));
pickedUpMessage = GetMessage(nameof(pickedUpMessage));
foreach (ContentXElement subElement in prefab.ConfigElement.Elements())
{
if (subElement.NameAsIdentifier() == "target" ||
subElement.NameAsIdentifier() == "chooserandom")
{
LoadTarget(subElement, parentTarget: null);
}
}
if (!targets.Any())
{
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())
{
if (subElement.NameAsIdentifier() == "target" ||
subElement.NameAsIdentifier() == "chooserandom")
{
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)
{
#if SERVER
spawnInfo.Clear();
#endif
if (!IsClient)
{
// First spawn any possible characters, so that we can use their items as targets.
Target firstTarget = targets.First();
var submarine = Submarine.Loaded.Find(s => IsValidSubmarine(s, firstTarget.SpawnPositionType));
if (submarine != null)
{
InitCharacters(submarine);
}
}
foreach (var target in targets)
{
bool usedExistingItem = false;
UInt16 originalInventoryID = 0;
byte originalItemContainerIndex = 0;
int originalSlotIndex = 0;
var executedEffectIndices = new List<(int listIndex, int effectIndex)>();
target.Reset();
if (!IsClient)
{
//ruin/cave/wreck items are allowed to spawn close to the sub
float minDistance = target.SpawnPositionType switch
{
Level.PositionType.Ruin or
Level.PositionType.Cave or
Level.PositionType.Wreck or
Level.PositionType.Outpost => 0.0f,
_ => Level.Loaded.Size.X * 0.3f,
};
Vector2 position =
target.SpawnPositionType == Level.PositionType.None ?
Vector2.Zero :
Level.Loaded.GetRandomItemPos(target.SpawnPositionType, 100.0f, minDistance, 30.0f);
if (!target.ExistingItemTag.IsEmpty)
{
var suitableItems = Item.ItemList.Where(it => it.HasTag(target.ExistingItemTag));
if (GameMain.GameSession?.Missions != null)
{
//don't choose an item that was already chosen as the target for another salvage mission
suitableItems = suitableItems.Where(it =>
GameMain.GameSession.Missions.None(m => m != this && m is SalvageMission salvageMission && salvageMission.targets.Any(t => t.Item == it)));
}
switch (target.SpawnPositionType)
{
case Level.PositionType.Cave:
case Level.PositionType.MainPath:
case Level.PositionType.SidePath:
case Level.PositionType.AbyssCave:
target.Item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f);
break;
case Level.PositionType.Abyss:
target.Item = suitableItems.FirstOrDefault(it => Level.IsPositionInAbyss(it.WorldPosition));
break;
case Level.PositionType.Ruin:
case Level.PositionType.Wreck:
case Level.PositionType.Outpost:
case Level.PositionType.BeaconStation:
foreach (Item it in suitableItems)
{
if (it.Submarine is not Submarine sub) { continue; }
if (!IsValidSubmarine(sub, target.SpawnPositionType)) { continue; }
Rectangle worldBorders = sub.Borders;
worldBorders.Location += sub.WorldPosition.ToPoint();
if (Submarine.RectContains(worldBorders, it.WorldPosition))
{
target.Item = it;
break;
}
}
break;
default:
target.Item = suitableItems.FirstOrDefault();
break;
}
#if SERVER
usedExistingItem = target.Item != null;
#endif
}
if (target.Item == null)
{
if (target.ItemPrefab == null && target.ContainerTag.IsEmpty)
{
DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag}",
contentPackage: Prefab.ContentPackage);
continue;
}
target.Item = new Item(target.ItemPrefab, position, null);
#if CLIENT
target.Item.HighlightColor = GUIStyle.Orange;
target.Item.ExternalHighlight = true;
#endif
target.Item.UpdateTransform();
if (target.Item.CurrentHull == null && target.Item.body != 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;
}
}
if (target.RequiredRetrievalState == Target.RetrievalState.Interact)
{
target.Item.OnInteract += () =>
{
target.Interacted = true;
};
}
for (int i = 0; i < target.StatusEffects.Count; i++)
{
List<StatusEffect> effectList = target.StatusEffects[i];
if (effectList.Count == 0) { continue; }
int effectIndex = Rand.Int(effectList.Count);
var selectedEffect = effectList[effectIndex];
target.Item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: target.Item.Position);
#if SERVER
executedEffectIndices.Add((i, effectIndex));
#endif
}
target.Item.IsSalvageMissionItem = true;
//try to find a container and place the item inside it
if (!target.ContainerTag.IsEmpty && target.Item.ParentInventory == null)
{
List<ItemContainer> validContainers = new List<ItemContainer>();
foreach (Item it in Item.ItemList)
{
if (!it.HasTag(target.ContainerTag)) { continue; }
if (!it.IsPlayerTeamInteractable) { continue; }
if (!IsValidSubmarine(it.Submarine, target.SpawnPositionType)) { continue; }
var itemContainer = it.GetComponent<ItemContainer>();
if (itemContainer != null && itemContainer.Inventory.CanBePut(target.Item)) { validContainers.Add(itemContainer); }
}
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))
{
#if SERVER
originalInventoryID = selectedContainer.Item.ID;
originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer);
originalSlotIndex = target.Item.ParentInventory?.FindIndex(target.Item) ?? -1;
#endif
} // Placement successful
}
}
}
#if SERVER
spawnInfo.Add(
target,
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;
}
target.Item.DontCleanUp = true;
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;
}
}
}
}
private static bool IsValidSubmarine(Submarine sub, Level.PositionType spawnPosType)
{
if (sub == null)
{
return spawnPosType switch
{
Level.PositionType.Ruin or Level.PositionType.Wreck or Level.PositionType.BeaconStation or Level.PositionType.Outpost => false,
_ => true
};
}
return spawnPosType switch
{
Level.PositionType.Ruin => sub.Info.IsRuin,
Level.PositionType.Wreck => sub.Info.IsWreck,
Level.PositionType.BeaconStation => sub.Info.IsBeacon,
Level.PositionType.Outpost => sub.Info.IsOutpost,
_ => false
};
}
protected override void UpdateMissionSpecific(float deltaTime)
{
//make body dynamic when picked up
foreach (var target in targets)
{
var root = target.Item?.RootContainer ?? target.Item;
if (root == null) { continue; }
if (target.Item.ParentInventory != null && target.Item.body != null) { target.Item.body.FarseerBody.BodyType = BodyType.Dynamic; }
}
if (IsClient) { return; }
bool atLeastOneTargetWasRetrieved = false;
for (int i = 0; i < targets.Count; i++)
{
var target = targets[i];
if (i > 0 && !targets[i - 1].AllowContinueBeforeRetrieved && !targets[i - 1].Retrieved) { break; }
if (target.Item == null)
{
#if DEBUG
DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)",
contentPackage: Prefab.ContentPackage);
#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:
if (target.Interacted)
{
TrySetRetrievalState(Target.RetrievalState.Interact);
}
var root = target.Item?.RootContainer ?? target.Item;
if (root.ParentInventory?.Owner is Character { TeamID: CharacterTeamType.Team1 })
{
TrySetRetrievalState(Target.RetrievalState.PickedUp);
#if CLIENT
TryShowPickedUpMessage();
#endif
}
if (inPlayerSub)
{
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
}
break;
case Target.RetrievalState.PickedUp:
case Target.RetrievalState.RetrievedToSub:
bool inPlayerInventory = false;
bool playerInFriendlySub = false;
if (rootInventoryOwner is Character { TeamID: CharacterTeamType.Team1 } character)
{
inPlayerInventory = true;
if (character.Submarine != null)
{
playerInFriendlySub =
character.IsInFriendlySub ||
(character.Submarine == Level.Loaded?.StartOutpost && Level.IsLoadedFriendlyOutpost && GameMain.GameSession?.Campaign.CurrentLocation is not { IsFactionHostile: true });
}
}
if (inPlayerSub || (inPlayerInventory && playerInFriendlySub))
{
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
}
else
{
target.State = Target.RetrievalState.PickedUp;
}
break;
}
void TrySetRetrievalState(Target.RetrievalState retrievalState)
{
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 = 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()
{
if (requiredDeliveryAmount < 1.0f)
{
return targets.Count(IsTargetRetrieved) / (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 ||
/*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))
{
targetsToRemove.Add(target);
}
}
foreach (var target in targetsToRemove)
{
if (target.Item is { Removed: false })
{
target.Item.Remove();
}
target.Reset();
}
}
public override void AdjustLevelData(LevelData levelData)
{
if (wreckTags.Length > 0)
{
var selectedWreck = GetRandomWreckByTags(wreckTags, levelData);
if (selectedWreck != null)
{
levelData.ForceWreck = selectedWreck;
}
else
{
DebugConsole.ThrowError($"Salvage mission \"{Prefab.Identifier}\" could not find a suitable wreck with wrecktags \"{string.Join(", ", wreckTags)}\" for level difficulty {levelData.Difficulty:F1}.",
contentPackage: Prefab.ContentPackage);
}
}
}
private static SubmarineInfo GetRandomWreckByTags(ImmutableArray<Identifier> tags, LevelData levelData)
{
return GetRandomSubmarineByTagsAndDifficulty(
tags,
levelData,
s => s.IsWreck,
"wreck");
}
}
}