Unstable 0.1400.1.0

This commit is contained in:
Markus Isberg
2021-05-20 16:12:54 +03:00
parent 92f0264af2
commit 5bc850cddb
181 changed files with 2475 additions and 1588 deletions
@@ -66,7 +66,7 @@ namespace Barotrauma
private IEnumerable<object> Update(ISpatialEntity targetEntity, Camera cam)
{
if (targetEntity == null) { yield return CoroutineStatus.Success; }
if (targetEntity == null || (targetEntity is Entity e && e.Removed)) { yield return CoroutineStatus.Success; }
prevControlled = Character.Controlled;
if (RemoveControlFromCharacter)
@@ -1475,7 +1475,9 @@ namespace Barotrauma
Character thief = character;
bool someoneSpoke = false;
if (item.SpawnedInOutpost && !item.AllowStealing && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
if ((item.SpawnedInOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
{
foreach (Character otherCharacter in Character.CharacterList)
{
@@ -56,8 +56,23 @@ namespace Barotrauma
public float BasePriority { get; set; }
public float PriorityModifier { get; private set; } = 1;
// For forcing the highest priority temporarily. Will reset after each priority calculation, so it will need to be kept alive by something.
public bool ForceHighestPriority { get; set; }
private float resetPriorityTimer;
private readonly float resetPriorityTime = 1;
private bool _forceHighestPriority;
// For forcing the highest priority temporarily. Will reset automatically after one second, unless kept alive by something.
public bool ForceHighestPriority
{
get { return _forceHighestPriority; }
set
{
if (_forceHighestPriority == value) { return; }
_forceHighestPriority = value;
if (_forceHighestPriority)
{
resetPriorityTimer = resetPriorityTime;
}
}
}
// For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something.
// The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority.
@@ -171,7 +186,6 @@ namespace Barotrauma
Act(deltaTime);
}
// TODO: check turret aioperate
public void AddSubObjective(AIObjective objective, bool addFirst = false)
{
var type = objective.GetType();
@@ -294,6 +308,14 @@ namespace Barotrauma
public virtual void Update(float deltaTime)
{
if (resetPriorityTimer > 0)
{
resetPriorityTimer -= deltaTime;
}
else
{
ForceHighestPriority = false;
}
if (!objectiveManager.IsOrder(this) && objectiveManager.WaitTimer <= 0)
{
UpdateDevotion(deltaTime);
@@ -91,7 +91,7 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI()))
if (container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI())
{
Abandon = true;
return;
@@ -146,7 +146,10 @@ namespace Barotrauma
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = container.Item.Name,
AbortCondition = obj => !ItemToContain.IsOwnedBy(character),
AbortCondition = obj =>
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI() ||
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>()
},
onAbandon: () => Abandon = true,
@@ -614,7 +614,11 @@ namespace Barotrauma
/// <summary>
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
/// </summary>
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective
{
if (CurrentObjective == null) { return Enumerable.Empty<T>(); }
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
}
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
@@ -46,7 +46,7 @@ namespace Barotrauma
Abandon = !isOrder;
return Priority;
}
if (component.Item.ConditionPercentage <= 0)
if (!isOrder && component.Item.ConditionPercentage <= 0)
{
Priority = 0;
}
@@ -1,6 +1,7 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
@@ -43,7 +44,6 @@ namespace Barotrauma
}
return Priority;
}
// TODO: priority list?
// Ignore items that are being repaired by someone else.
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character))
{
@@ -66,7 +66,20 @@ namespace Barotrauma
float devotion = (CumulatedDevotion + selectedBonus) / 100;
float reduction = isPriority ? 1 : isSelected ? 2 : 3;
float max = AIObjectiveManager.LowestOrderPriority - reduction;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
float highestWeight = -1;
foreach (string tag in Item.Prefab.Tags)
{
if (JobPrefab.ItemRepairPriorities.TryGetValue(tag, out float weight) && weight > highestWeight)
{
highestWeight = weight;
}
}
if (highestWeight == -1)
{
// Predefined weight not found.
highestWeight = 1;
}
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * highestWeight * PriorityModifier), 0, 1));
}
return Priority;
}
@@ -150,6 +150,8 @@ namespace Barotrauma
//legacy support
public readonly string[] AppropriateJobs;
public readonly string[] Options;
public readonly string[] HiddenOptions;
public readonly string[] AllOptions;
private readonly Dictionary<string, string> OptionNames;
public readonly Dictionary<string, Sprite> OptionSprites;
@@ -310,6 +312,8 @@ namespace Barotrauma
TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false);
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
Options = orderElement.GetAttributeStringArray("options", new string[0]);
HiddenOptions = orderElement.GetAttributeStringArray("hiddenoptions", new string[0]);
AllOptions = Options.Concat(HiddenOptions).ToArray();
var category = orderElement.GetAttributeString("category", null);
if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
@@ -400,7 +400,7 @@ namespace Barotrauma
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
}
var pet = Character.Create(speciesName, spawnPos, seed);
var petBehavior = (pet.AIController as EnemyAIController)?.PetBehavior;
var petBehavior = (pet?.AIController as EnemyAIController)?.PetBehavior;
if (petBehavior != null)
{
petBehavior.Owner = owner;
@@ -113,7 +113,7 @@ namespace Barotrauma
if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority()?.Order != CurrentOrder)
{
#if DEBUG
DebugConsole.NewMessage($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}");
ShipCommandManager.ShipCommandLog($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}");
#endif
return false;
}
@@ -121,7 +121,7 @@ namespace Barotrauma
if (!shipCommandManager.AbleToTakeOrder(OrderedCharacter))
{
#if DEBUG
DebugConsole.NewMessage(OrderedCharacter + " was unable to perform assigned order in " + this);
ShipCommandManager.ShipCommandLog(OrderedCharacter + " was unable to perform assigned order in " + this);
#endif
return false;
}
@@ -93,7 +93,7 @@ namespace Barotrauma
}
}
static void ShipCommandLog(string text)
public static void ShipCommandLog(string text)
{
if (GameSettings.VerboseLogging)
{
@@ -74,7 +74,7 @@ namespace Barotrauma
}
}
public bool HasMultipleLimbsOfSameType => Limbs.Length > limbDictionary.Count;
public bool HasMultipleLimbsOfSameType => limbs == null ? false : Limbs.Length > limbDictionary.Count;
private bool frozen;
public bool Frozen
@@ -416,10 +416,7 @@ namespace Barotrauma
protected void CreateColliders()
{
if (collider != null)
{
collider.ForEach(c => c.Remove());
}
collider?.ForEach(c => c.Remove());
DebugConsole.Log($"Creating colliders from {RagdollParams.Name}.");
collider = new List<PhysicsBody>();
foreach (var cParams in RagdollParams.Colliders)
@@ -479,10 +476,7 @@ namespace Barotrauma
protected void CreateLimbs()
{
if (limbs != null)
{
limbs.ForEach(l => l.Remove());
}
limbs?.ForEach(l => l.Remove());
DebugConsole.Log($"Creating limbs from {RagdollParams.Name}.");
limbDictionary = new Dictionary<LimbType, Limb>();
limbs = new Limb[RagdollParams.Limbs.Count];
@@ -395,14 +395,12 @@ namespace Barotrauma
Affliction affliction;
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab != null)
if (afflictionPrefab == null)
{
affliction = afflictionPrefab.Instantiate(0.0f);
}
else
{
affliction = new Affliction(null, 0);
DebugConsole.ThrowError($"Couldn't find the affliction with the identifier {afflictionIdentifier} referenced in {element.Document.ParseContentPathFromUri()}");
continue;
}
affliction = afflictionPrefab.Instantiate(0.0f);
affliction.Deserialize(subElement);
//backwards compatibility
if (subElement.Attribute("amount") != null && subElement.Attribute("strength") == null)
@@ -1523,6 +1523,16 @@ namespace Barotrauma
greatestNegativeHealthMultiplier = 1f;
}
/// <summary>
/// Can be used to modify a character's health for runtime session. Change with AddHealthMultiplier
/// </summary>
public float StaticHealthMultiplier { get; private set; } = 1;
public void AddStaticHealthMultiplier(float newMultiplier)
{
StaticHealthMultiplier *= newMultiplier;
}
/// <summary>
/// Speed reduction from the current limb specific damage. Min 0, max 1.
/// </summary>
@@ -1916,19 +1926,19 @@ namespace Barotrauma
return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb;
}
public bool CanSeeTarget(ISpatialEntity target, Limb seeingLimb = null)
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null)
{
seeingLimb ??= GetSeeingLimb();
if (seeingLimb == null) { return false; }
ISpatialEntity seeingEntity = AnimController.SimplePhysicsEnabled ? this : seeingLimb as ISpatialEntity;
seeingEntity ??= AnimController.SimplePhysicsEnabled ? this as ISpatialEntity : GetSeeingLimb() as ISpatialEntity;
if (seeingEntity == null) { return false; }
ISpatialEntity sourceEntity = seeingEntity ;
// TODO: Could we just use the method below? If not, let's refactor it so that we can.
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition);
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - sourceEntity.WorldPosition);
Body closestBody;
//both inside the same sub (or both outside)
//OR the we're inside, the other character outside
if (target.Submarine == Submarine || target.Submarine == null)
{
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
closestBody = Submarine.CheckVisibility(sourceEntity.SimPosition, sourceEntity.SimPosition + diff);
}
//we're outside, the other character inside
else if (Submarine == null)
@@ -1938,7 +1948,7 @@ namespace Barotrauma
//both inside different subs
else
{
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
closestBody = Submarine.CheckVisibility(sourceEntity.SimPosition, sourceEntity.SimPosition + diff);
if (!IsBlocking(closestBody))
{
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
@@ -1966,29 +1976,6 @@ namespace Barotrauma
}
}
/// <summary>
/// TODO: ensure that works. CheckVisibility takes positions in sim space, but this method uses world positions
/// </summary>
public bool CanSeeCharacter(Character target, Vector2 sourceWorldPos)
{
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - sourceWorldPos);
Body closestBody;
if (target.Submarine == null)
{
closestBody = Submarine.CheckVisibility(sourceWorldPos, sourceWorldPos + diff);
if (closestBody == null) { return true; }
}
else
{
closestBody = Submarine.CheckVisibility(target.WorldPosition, target.WorldPosition - diff);
if (closestBody == null) { return true; }
}
Structure wall = closestBody.UserData as Structure;
Item item = closestBody.UserData as Item;
Door door = item?.GetComponent<Door>();
return (wall == null || !wall.CastShadow) && (door == null || door.CanBeTraversed);
}
/// <summary>
/// A simple check if the character Dir is towards the target or not. Uses the world coordinates.
/// </summary>
@@ -2233,7 +2220,10 @@ namespace Barotrauma
Rectangle itemDisplayRect = new Rectangle(item.InteractionRect.X, item.InteractionRect.Y - item.InteractionRect.Height, item.InteractionRect.Width, item.InteractionRect.Height);
// Get the point along the line between lowerBodyPosition and upperBodyPosition which is closest to the center of itemDisplayRect
Vector2 playerDistanceCheckPosition = Vector2.Clamp(itemDisplayRect.Center.ToVector2(), lowerBodyPosition, upperBodyPosition);
Vector2 playerDistanceCheckPosition =
lowerBodyPosition.Y < upperBodyPosition.Y ?
Vector2.Clamp(itemDisplayRect.Center.ToVector2(), lowerBodyPosition, upperBodyPosition) :
Vector2.Clamp(itemDisplayRect.Center.ToVector2(), upperBodyPosition, lowerBodyPosition);
// If playerDistanceCheckPosition is inside the itemDisplayRect then we consider the character to within 0 distance of the item
if (itemDisplayRect.Contains(playerDistanceCheckPosition))
@@ -2271,7 +2261,7 @@ namespace Barotrauma
itemPosition -= Submarine.SimPosition;
}
var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true);
if (body != null && body.UserData as Item != item) { return false; }
if (body != null && body.UserData as Item != item && Submarine.LastPickedFixture?.UserData as Item != item) { return false; }
}
return true;
@@ -3657,7 +3647,11 @@ namespace Barotrauma
// OnDamaged is called only for the limb that is hit.
AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime));
}
CharacterHealth.ApplyAfflictionStatusEffects(actionType);
//OnActive effects are handled by the afflictions themselves
if (actionType != ActionType.OnActive)
{
CharacterHealth.ApplyAfflictionStatusEffects(actionType);
}
}
private void Implode(bool isNetworkMessage = false)
@@ -3801,10 +3795,7 @@ namespace Barotrauma
return;
}
if (aiTarget != null)
{
aiTarget.Remove();
}
aiTarget?.Remove();
aiTarget = new AITarget(this);
CharacterHealth.RemoveAllAfflictions();
@@ -456,7 +456,7 @@ namespace Barotrauma
public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1;
// Used for creating the data
public CharacterInfo(string speciesName, string name = "", string originalName = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced)
public CharacterInfo(string speciesName, string name = "", string originalName = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced, string npcIdentifier = "")
{
if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
@@ -485,8 +485,12 @@ namespace Barotrauma
{
Name = name;
}
else
else if (!string.IsNullOrEmpty(npcIdentifier) && TextManager.Get("npctitle." + npcIdentifier, true) is string npcTitle)
{
Name = npcTitle;
}
else
{
name = "";
if (CharacterConfigElement.Element("name") != null)
{
@@ -150,12 +150,9 @@ namespace Barotrauma
{
max += Character.Info.Job.Prefab.VitalityModifier;
}
max *= Character.StaticHealthMultiplier;
return max * Character.HealthMultiplier;
}
set
{
maxVitality = Math.Max(0, value);
}
}
public float MinVitality
@@ -121,11 +121,12 @@ namespace Barotrauma
public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null)
{
npc.CharacterHealth.MaxVitality *= HealthMultiplier;
npc.AddStaticHealthMultiplier(HealthMultiplier);
if (GameMain.NetworkMember != null)
{
npc.CharacterHealth.MaxVitality *= HealthMultiplierInMultiplayer;
npc.AddStaticHealthMultiplier(HealthMultiplierInMultiplayer);
}
var humanAI = npc.AIController as HumanAIController;
if (humanAI != null)
{
@@ -227,10 +228,7 @@ namespace Barotrauma
}
IdCard idCardComponent = item.GetComponent<IdCard>();
if (idCardComponent != null)
{
idCardComponent.Initialize(character.Info);
}
idCardComponent?.Initialize(character.Info);
var idCardTags = itemElement.GetAttributeStringArray("tags", new string[0]);
foreach (string tag in idCardTags)
@@ -243,10 +241,7 @@ namespace Barotrauma
{
wifiComponent.TeamID = character.TeamID;
}
if (parentItem != null)
{
parentItem.Combine(item, user: null);
}
parentItem?.Combine(item, user: null);
foreach (XElement childItemElement in itemElement.Elements())
{
InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents);
@@ -204,10 +204,7 @@ namespace Barotrauma
item.AddTag("job:" + Name);
IdCard idCardComponent = item.GetComponent<IdCard>();
if (idCardComponent != null)
{
idCardComponent.Initialize(character.Info);
}
idCardComponent?.Initialize(character.Info);
}
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
@@ -1,9 +1,9 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -43,6 +43,12 @@ namespace Barotrauma
Prefabs.Remove(this);
}
private static readonly Dictionary<string, float> _itemRepairPriorities = new Dictionary<string, float>();
/// <summary>
/// Tag -> priority.
/// </summary>
public static IReadOnlyDictionary<string, float> ItemRepairPriorities => _itemRepairPriorities;
public static XElement NoJobElement;
public static JobPrefab Get(string identifier)
{
@@ -294,7 +300,7 @@ namespace Barotrauma
}
foreach (XElement element in mainElement.Elements())
{
if (element.Name.ToString().Equals("nojob", StringComparison.OrdinalIgnoreCase)) { continue; }
if (!element.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase)) { continue; }
if (element.IsOverride())
{
var job = new JobPrefab(element.FirstElement(), file.Path)
@@ -312,8 +318,31 @@ namespace Barotrauma
Prefabs.Add(job, false);
}
}
NoJobElement = NoJobElement ?? mainElement.Element("NoJob");
NoJobElement = NoJobElement ?? mainElement.Element("nojob");
NoJobElement ??= mainElement.GetChildElement("nojob");
var itemRepairPrioritiesElement = mainElement.GetChildElement("ItemRepairPriorities");
if (itemRepairPrioritiesElement != null)
{
foreach (var subElement in itemRepairPrioritiesElement.Elements())
{
string tag = subElement.GetAttributeString("tag", null);
if (tag != null)
{
float priority = subElement.GetAttributeFloat("priority", -1f);
if (priority >= 0)
{
_itemRepairPriorities.TryAdd(tag, priority);
}
else
{
DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {subElement} of {file.Path}.");
}
}
else
{
DebugConsole.AddWarning($"The 'tag' attribute is missing from the the item repair priorities definition in {subElement} of {file.Path}.");
}
}
}
}
public static void RemoveByFile(string filePath)
@@ -837,10 +837,7 @@ namespace Barotrauma
}
}
if (attack != null)
{
attack.UpdateCoolDown(deltaTime);
}
attack?.UpdateCoolDown(deltaTime);
}
private float reEnableTimer = -1;
@@ -194,13 +194,13 @@ namespace Barotrauma
isCorePackage = value;
if (isCorePackage && regularPackages.Contains(this))
{
corePackages.Add(this);
regularPackages.Remove(this);
corePackages.AddOnMainThread(this);
regularPackages.RemoveOnMainThread(this);
}
else if (!isCorePackage && corePackages.Contains(this))
{
regularPackages.Add(this);
corePackages.Remove(this);
regularPackages.AddOnMainThread(this);
corePackages.RemoveOnMainThread(this);
}
}
}
@@ -411,6 +411,7 @@ namespace Barotrauma
case ContentType.Submarine:
case ContentType.Wreck:
case ContentType.BeaconStation:
case ContentType.EnemySubmarine:
break;
default:
try
@@ -528,7 +529,7 @@ namespace Barotrauma
{
refreshFiles = true;
}
corePackages.Remove(p);
corePackages.RemoveOnMainThread(p);
}
else
{
@@ -536,16 +537,16 @@ namespace Barotrauma
{
refreshFiles = true;
}
regularPackages.Remove(p);
regularPackages.RemoveOnMainThread(p);
}
}
if (IsCorePackage)
{
corePackages.Add(this);
corePackages.AddOnMainThread(this);
}
else
{
regularPackages.Add(this);
regularPackages.AddOnMainThread(this);
}
if (refreshFiles)
@@ -743,18 +744,18 @@ namespace Barotrauma
}
if (newPackage.IsCorePackage)
{
corePackages.Add(newPackage);
corePackages.AddOnMainThread(newPackage);
}
else
{
regularPackages.Add(newPackage);
regularPackages.AddOnMainThread(newPackage);
}
}
public static void RemovePackage(ContentPackage package)
{
if (package.IsCorePackage) { corePackages.Remove(package); }
else { regularPackages.Remove(package); }
if (package.IsCorePackage) { corePackages.RemoveOnMainThread(package); }
else { regularPackages.RemoveOnMainThread(package); }
}
public static void LoadAll()
@@ -775,9 +776,9 @@ namespace Barotrauma
IEnumerable<string> files = Directory.GetFiles(folder, "*.xml");
corePackages.Clear();
corePackages.ClearOnMainThread();
var prevRegularPackages = regularPackages.Select(p => p.Name.ToLowerInvariant()).ToList();
regularPackages.Clear();
regularPackages.ClearOnMainThread();
foreach (string filePath in files)
{
@@ -815,7 +816,7 @@ namespace Barotrauma
.OrderBy(p => order(p))
.ThenBy(p => regularPackages.IndexOf(p))
.ToList();
regularPackages.Clear(); regularPackages.AddRange(ordered);
regularPackages.ClearOnMainThread(); regularPackages.AddRangeOnMainThread(ordered);
(config ?? GameMain.Config)?.SortContentPackages(refreshAll);
}
@@ -825,12 +826,12 @@ namespace Barotrauma
{
if (IsCorePackage)
{
corePackages.Remove(this);
corePackages.RemoveOnMainThread(this);
if (GameMain.Config.CurrentCorePackage == this) { GameMain.Config.AutoSelectCorePackage(null); }
}
else
{
regularPackages.Remove(this);
regularPackages.RemoveOnMainThread(this);
if (GameMain.Config.EnabledRegularPackages.Contains(this)) { GameMain.Config.DisableRegularPackage(this); }
}
GameMain.Config.SaveNewPlayerConfig();
@@ -303,9 +303,15 @@ namespace Barotrauma
private bool IsValidTarget(Entity e)
{
return
e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
bool isValid = e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
(e == Character.Controlled || character.IsRemotePlayer);
#if SERVER
UpdateIgnoredClients();
isValid &= !ignoredClients.Keys.Any(c => c.Character == e);
#elif CLIENT
isValid &= (e != Character.Controlled || !GUI.InputBlockingMenuOpen);
#endif
return isValid;
}
private void TryStartConversation(Character speaker, Character targetCharacter = null)
@@ -348,7 +354,7 @@ namespace Barotrauma
{
ParentEvent.AddTarget(InvokerTag, targetCharacter);
}
ShowDialog(speaker, targetCharacter);
dialogOpened = true;
@@ -14,6 +14,18 @@ namespace Barotrauma
[Serialize("", true)]
public string MissionTag { get; set; }
[Serialize("", true, description: "The type of the location the mission will be unlocked in (if empty, any location can be selected).")]
public string LocationType { get; set; }
[Serialize(0, true, description: "Minimum distance to the location the mission is unlocked in (1 = one path between locations).")]
public int MinLocationDistance { get; set; }
[Serialize(true, true, description: "If true, the mission has to be unlocked in a location further on the campaign map.")]
public bool UnlockFurtherOnMap { get; set; }
[Serialize(false, true, description: "If true, a suitable location is forced on the map if one isn't found.")]
public bool CreateLocationIfNotFound { get; set; }
private bool isFinished;
public MissionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
@@ -44,33 +56,82 @@ namespace Barotrauma
if (GameMain.GameSession.GameMode is CampaignMode campaign)
{
MissionPrefab prefab = null;
if (!string.IsNullOrEmpty(MissionIdentifier))
var unlockLocation = FindUnlockLocation();
if (unlockLocation == null && CreateLocationIfNotFound)
{
prefab = campaign.Map.CurrentLocation.UnlockMissionByIdentifier(MissionIdentifier);
}
else if (!string.IsNullOrEmpty(MissionTag))
{
prefab = campaign.Map.CurrentLocation.UnlockMissionByTag(MissionTag);
}
if (campaign is MultiPlayerCampaign mpCampaign)
{
mpCampaign.LastUpdateID++;
//find an empty location at least 3 steps away, further on the map
var emptyLocation = FindUnlockLocationRecursive(campaign.Map.CurrentLocation, Math.Max(MinLocationDistance, 3), "none", true, new HashSet<Location>());
if (emptyLocation != null)
{
emptyLocation.ChangeType(Barotrauma.LocationType.List.Find(lt => lt.Identifier.Equals(LocationType, StringComparison.OrdinalIgnoreCase)));
unlockLocation = emptyLocation;
}
}
if (prefab != null)
if (unlockLocation != null)
{
#if CLIENT
new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", prefab.Name),
new string[0], type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128))
if (!string.IsNullOrEmpty(MissionIdentifier))
{
IconColor = prefab.IconColor
};
#else
NotifyMissionUnlock(prefab);
#endif
prefab = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier);
}
else if (!string.IsNullOrEmpty(MissionTag))
{
prefab = unlockLocation.UnlockMissionByTag(MissionTag);
}
if (campaign is MultiPlayerCampaign mpCampaign)
{
mpCampaign.LastUpdateID++;
}
if (prefab != null)
{
DebugConsole.NewMessage($"Unlocked mission \"{prefab.Name}\" in the location \"{unlockLocation.Name}\".");
#if CLIENT
new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", prefab.Name),
new string[0], type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128))
{
IconColor = prefab.IconColor
};
#else
NotifyMissionUnlock(prefab);
#endif
}
}
else
{
DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationType}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})");
}
}
isFinished = true;
isFinished = true;
}
private Location FindUnlockLocation()
{
var campaign = GameMain.GameSession.GameMode as CampaignMode;
if (string.IsNullOrEmpty(LocationType) && MinLocationDistance <= 1)
{
return campaign.Map.CurrentLocation;
}
return FindUnlockLocationRecursive(campaign.Map.CurrentLocation, 0, LocationType, UnlockFurtherOnMap, new HashSet<Location>());
}
private Location FindUnlockLocationRecursive(Location currLocation, int currDistance, string locationType, bool unlockFurtherOnMap, HashSet<Location> checkedLocations)
{
var campaign = GameMain.GameSession.GameMode as CampaignMode;
if (currLocation.Type.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase) && currDistance >= MinLocationDistance &&
(!unlockFurtherOnMap || currLocation.MapPosition.X > campaign.Map.CurrentLocation.MapPosition.X))
{
return currLocation;
}
checkedLocations.Add(currLocation);
foreach (LocationConnection connection in currLocation.Connections)
{
var otherLocation = connection.OtherLocation(currLocation);
if (checkedLocations.Contains(otherLocation)) { continue; }
var unlockLocation = FindUnlockLocationRecursive(otherLocation, ++currDistance, locationType, unlockFurtherOnMap, checkedLocations);
if (unlockLocation != null) { return unlockLocation; }
}
return null;
}
public override string ToDebugString()
@@ -84,8 +145,8 @@ namespace Barotrauma
foreach (Client client in GameMain.Server.ConnectedClients)
{
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.Write((byte) ServerPacketHeader.EVENTACTION);
outmsg.Write((byte) EventManager.NetworkEventType.MISSION);
outmsg.Write((byte)ServerPacketHeader.EVENTACTION);
outmsg.Write((byte)EventManager.NetworkEventType.MISSION);
outmsg.Write(prefab.Identifier);
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
}
@@ -41,9 +41,14 @@ namespace Barotrauma
foreach (Item item in npc.Inventory.AllItems)
{
item.AllowStealing = true;
var wifiComponent = item.GetComponent<Items.Components.WifiComponent>();
if (wifiComponent != null)
{
wifiComponent.TeamID = newTeam;
}
}
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AddToCrew });
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AddToCrew, newTeam, npc.Inventory.AllItems.Select(it => it.ID).ToArray() });
#endif
}
}
@@ -15,7 +15,8 @@ namespace Barotrauma
Outpost,
MainPath,
Ruin,
Wreck
Wreck,
BeaconStation
}
[Serialize("", true, description: "Species name of the character to spawn.")]
@@ -225,6 +226,7 @@ namespace Barotrauma
SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsOutpost),
SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsWreck),
SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.ParentRuin != null),
SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsBeacon),
_ => throw new NotImplementedException()
};
@@ -250,6 +252,7 @@ namespace Barotrauma
SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost),
SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.Submarine.Info.IsWreck),
SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.ParentRuin != null),
SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.Submarine.Info.IsBeacon),
_ => throw new NotImplementedException()
};
@@ -51,7 +51,14 @@ namespace Barotrauma
{
foreach (var target in targets)
{
effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity);
if (target is Item targetItem)
{
effect.Apply(effect.type, deltaTime, target, targetItem.AllPropertyObjects);
}
else
{
effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity);
}
}
}
#if SERVER
@@ -124,7 +124,7 @@ namespace Barotrauma
npc.CampaignInteractionType = CampaignMode.InteractionType.Examine;
#if CLIENT
npc.SetCustomInteract(
Trigger,
(speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } },
TextManager.GetWithVariable("CampaignInteraction.Examine", "[key]", GameMain.Config.KeyBindText(InputType.Use)));
#else
npc.SetCustomInteract(
@@ -107,7 +107,7 @@ namespace Barotrauma
totalPathLength = 0.0f;
if (level != null)
{
var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(Level.Loaded.StartPosition), ConvertUnits.ToSimUnits(Level.Loaded.EndPosition));
var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(level.StartPosition), ConvertUnits.ToSimUnits(level.EndPosition));
totalPathLength = steeringPath.TotalLength;
}
@@ -124,7 +124,7 @@ namespace Barotrauma
}
MTRandom rand = new MTRandom(seed);
var initialEventSet = SelectRandomEvents(EventSet.List);
var initialEventSet = SelectRandomEvents(EventSet.List, rand);
if (initialEventSet != null)
{
pendingEventSets.Add(initialEventSet);
@@ -386,21 +386,25 @@ namespace Barotrauma
{
if (level == null) { return; }
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
#if DEBUG
DebugConsole.NewMessage($"Loading event set {eventSet.DebugIdentifier}", Color.LightBlue);
#else
DebugConsole.Log($"Loading event set {eventSet.DebugIdentifier}");
#endif
int applyCount = 1;
List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>();
if (eventSet.PerRuin)
{
applyCount = Level.Loaded.Ruins.Count();
foreach (var ruin in Level.Loaded.Ruins)
applyCount = level.Ruins.Count();
foreach (var ruin in level.Ruins)
{
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Ruin == ruin; });
}
}
else if (eventSet.PerCave)
{
applyCount = Level.Loaded.Caves.Count();
foreach (var cave in Level.Loaded.Caves)
applyCount = level.Caves.Count();
foreach (var cave in level.Caves)
{
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Cave == cave; });
}
@@ -417,7 +421,8 @@ namespace Barotrauma
var suitablePrefabs = eventSet.EventPrefabs.FindAll(e =>
string.IsNullOrEmpty(e.First.BiomeIdentifier) ||
e.First.BiomeIdentifier.Equals(Level.Loaded.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase));
e.First.BiomeIdentifier.Equals(level.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase));
for (int i = 0; i < applyCount; i++)
{
if (eventSet.ChooseRandom)
@@ -435,7 +440,11 @@ namespace Barotrauma
if (newEvent == null) { continue; }
newEvent.Init(true);
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
DebugConsole.Log("Initialized event " + newEvent.ToString());
#if DEBUG
DebugConsole.NewMessage($"Initialized event {newEvent}");
#else
DebugConsole.Log($"Initialized event {newEvent}");
#endif
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
@@ -447,8 +456,11 @@ namespace Barotrauma
}
if (eventSet.ChildSets.Count > 0)
{
var newEventSet = SelectRandomEvents(eventSet.ChildSets);
if (newEventSet != null) { CreateEvents(newEventSet, rand); }
var newEventSet = SelectRandomEvents(eventSet.ChildSets, rand);
if (newEventSet != null)
{
CreateEvents(newEventSet, rand);
}
}
}
else
@@ -458,7 +470,11 @@ namespace Barotrauma
var newEvent = eventPrefab.First.CreateInstance();
if (newEvent == null) { continue; }
newEvent.Init(true);
DebugConsole.Log("Initialized event " + newEvent.ToString());
#if DEBUG
DebugConsole.NewMessage($"Initialized event {newEvent}");
#else
DebugConsole.Log($"Initialized event {newEvent}");
#endif
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
@@ -474,10 +490,10 @@ namespace Barotrauma
}
}
private EventSet SelectRandomEvents(List<EventSet> eventSets)
private EventSet SelectRandomEvents(List<EventSet> eventSets, Random random = null)
{
if (level == null) { return null; }
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed));
var allowedEventSets =
eventSets.Where(es =>
@@ -496,7 +512,8 @@ namespace Barotrauma
}
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
float randomNumber = (float)rand.NextDouble() * totalCommonness;
float randomNumber = (float)rand.NextDouble();
randomNumber *= totalCommonness;
foreach (EventSet eventSet in allowedEventSets)
{
float commonness = eventSet.GetCommonness(level);
@@ -835,7 +852,7 @@ namespace Barotrauma
{
if (level == null) { return 0.0f; }
var refEntity = GetRefEntity();
Vector2 target = ConvertUnits.ToSimUnits(Level.Loaded.EndPosition);
Vector2 target = ConvertUnits.ToSimUnits(level.EndPosition);
var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(refEntity.WorldPosition), target);
if (steeringPath.Unreachable || float.IsPositiveInfinity(totalPathLength))
{
@@ -953,15 +970,15 @@ namespace Barotrauma
const int maxDist = 1000;
if (Level.Loaded != null)
if (level != null)
{
foreach (var ruin in Level.Loaded.Ruins)
foreach (var ruin in level.Ruins)
{
Rectangle area = ruin.Area;
area.Inflate(maxDist, maxDist);
if (area.Contains(character.WorldPosition)) { return true; }
}
foreach (var cave in Level.Loaded.Caves)
foreach (var cave in level.Caves)
{
Rectangle area = cave.Area;
area.Inflate(maxDist, maxDist);
@@ -1,4 +1,5 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -15,6 +16,10 @@ namespace Barotrauma
protected readonly HashSet<Character> requireKill = new HashSet<Character>();
protected readonly HashSet<Character> requireRescue = new HashSet<Character>();
private readonly string itemTag;
private readonly XElement itemConfig;
private readonly List<Item> items = new List<Item>();
protected const int HostagesKilledState = 5;
private readonly string hostagesKilledMessage;
@@ -33,6 +38,43 @@ namespace Barotrauma
}
}
public override IEnumerable<Vector2> SonarPositions
{
get
{
if (State > 0)
{
return Enumerable.Empty<Vector2>();
}
else
{
return Targets.Select(t => t.WorldPosition);
}
}
}
private IEnumerable<Entity> Targets
{
get
{
if (State > 0)
{
return Enumerable.Empty<Entity>();
}
else
{
if (items.Any())
{
return items.Where(it => !it.Removed && it.Condition > 0.0f).Cast<Entity>().Concat(requireKill.Where(c => !c.Removed && !c.IsDead)).Concat(requireRescue);
}
else
{
return requireKill.Concat(requireRescue);
}
}
}
}
protected bool wasDocked;
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations, Submarine sub) :
@@ -42,6 +84,9 @@ namespace Barotrauma
string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", "");
hostagesKilledMessage = TextManager.Get(msgTag, returnNull: true) ?? msgTag;
itemConfig = prefab.ConfigElement.Element("Items");
itemTag = prefab.ConfigElement.GetAttributeString("targetitem", "");
}
protected override void StartMissionSpecific(Level level)
@@ -52,8 +97,13 @@ namespace Barotrauma
characterItems.Clear();
requireKill.Clear();
requireRescue.Clear();
items.Clear();
#if SERVER
spawnedItems.Clear();
#endif
var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub;
InitItems(submarine);
if (!IsClient)
{
InitCharacters(submarine);
@@ -62,49 +112,101 @@ namespace Barotrauma
wasDocked = Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost);
}
private void InitItems(Submarine submarine)
{
if (!string.IsNullOrEmpty(itemTag))
{
var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag));
if (!itemsToDestroy.Any())
{
DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".");
}
else
{
items.AddRange(itemsToDestroy);
}
}
if (itemConfig != null && !IsClient)
{
foreach (XElement element in itemConfig.Elements())
{
string itemIdentifier = element.GetAttributeString("identifier", "");
if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab))
{
DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found");
continue;
}
string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null);
string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null);
ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy,
moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
if (spawnPoint == null)
{
spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
}
Vector2 spawnPos = spawnPoint.WorldPosition;
if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100)
{
spawnPos = new Vector2(
MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X + 50, wp.CurrentHull.WorldRect.Right - 50),
wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 16.0f);
}
var item = new Item(itemPrefab, spawnPos, null);
items.Add(item);
#if SERVER
spawnedItems.Add(item);
#endif
}
}
}
private void InitCharacters(Submarine submarine)
{
characters.Clear();
characterItems.Clear();
if (characterConfig == null) { return; }
foreach (XElement element in characterConfig.Elements())
{
if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }
int defaultCount = element.GetAttributeInt("count", -1);
if (defaultCount < 0)
if (characterConfig != null)
{
foreach (XElement element in characterConfig.Elements())
{
defaultCount = element.GetAttributeInt("amount", 1);
}
int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255);
int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255);
int count = Rand.Range(min, max + 1);
if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
{
HumanPrefab humanPrefab = CreateHumanPrefabFromElement(element);
for (int i = 0; i < count; i++)
int defaultCount = element.GetAttributeInt("count", -1);
if (defaultCount < 0)
{
LoadHuman(humanPrefab, element, submarine);
defaultCount = element.GetAttributeInt("amount", 1);
}
int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255);
int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255);
int count = Rand.Range(min, max + 1);
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
{
HumanPrefab humanPrefab = CreateHumanPrefabFromElement(element);
for (int i = 0; i < count; i++)
{
LoadHuman(humanPrefab, element, submarine);
}
}
else
{
string speciesName = element.GetAttributeString("character", element.GetAttributeString("identifier", ""));
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found");
continue;
}
for (int i = 0; i < count; i++)
{
LoadMonster(characterPrefab, element, submarine);
}
}
}
else
{
string speciesName = element.GetAttributeString("character", element.GetAttributeString("identifier", ""));
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found");
continue;
}
for (int i = 0; i < count; i++)
{
LoadMonster(characterPrefab, element, submarine);
}
}
}
}
}
private void LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
@@ -175,7 +277,7 @@ namespace Barotrauma
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
if (State != HostagesKilledState)
{
@@ -203,7 +305,8 @@ namespace Barotrauma
{
case 0:
if (requireKill.All(c => c.Removed || c.IsDead) &&
if (items.All(it => it.Removed || it.Condition <= 0.0f) &&
requireKill.All(c => c.Removed || c.IsDead) &&
requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player))
{
State = 1;
@@ -53,7 +53,7 @@ namespace Barotrauma
}
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient) { return; }
if (!swarmSpawned && level.CheckBeaconActive())
@@ -15,7 +15,7 @@ namespace Barotrauma
private readonly Dictionary<Item, UInt16> parentInventoryIDs = new Dictionary<Item, UInt16>();
private readonly Dictionary<Item, byte> parentItemContainerIndices = new Dictionary<Item, byte>();
private int requiredDeliveryAmount;
private float requiredDeliveryAmount;
private readonly List<(XElement element, ItemContainer container)> itemsToSpawn = new List<(XElement element, ItemContainer container)>();
private int? rewardPerCrate;
@@ -29,7 +29,7 @@ namespace Barotrauma
{
this.sub = sub;
itemConfig = prefab.ConfigElement.Element("Items");
requiredDeliveryAmount = prefab.ConfigElement.GetAttributeInt("requireddeliveryamount", 0);
requiredDeliveryAmount = Math.Min(prefab.ConfigElement.GetAttributeFloat("requireddeliveryamount", 0.9f), 1.0f);
DetermineCargo();
}
@@ -123,12 +123,7 @@ namespace Barotrauma
LoadItemAsChild(element, container?.Item);
}
if (requiredDeliveryAmount == 0) { requiredDeliveryAmount = items.Count; }
if (requiredDeliveryAmount > items.Count)
{
DebugConsole.AddWarning($"Error in mission \"{Prefab.Identifier}\". Required delivery amount is {requiredDeliveryAmount} but there's only {items.Count} items to deliver.");
requiredDeliveryAmount = items.Count;
}
if (requiredDeliveryAmount <= 0.0f) { requiredDeliveryAmount = 1.0f; }
}
private ItemPrefab FindItemPrefab(XElement element)
@@ -220,7 +215,7 @@ namespace Barotrauma
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
{
int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f);
if (deliveredItemCount >= requiredDeliveryAmount)
if (deliveredItemCount / (float)items.Count >= requiredDeliveryAmount)
{
GiveReward();
completed = true;
@@ -39,13 +39,10 @@ namespace Barotrauma
itemConfig = prefab.ConfigElement.Element("TerroristItems");
}
public override int Reward
public override int GetReward(Submarine sub)
{
get
{
int multiplier = CalculateScalingEscortedCharacterCount();
return Prefab.Reward * multiplier;
}
int multiplier = CalculateScalingEscortedCharacterCount();
return Prefab.Reward * multiplier;
}
int CalculateScalingEscortedCharacterCount(bool inMission = false)
@@ -58,7 +55,7 @@ namespace Barotrauma
}
return 1;
}
return (int)Math.Round(baseEscortedCharacters + scalingEscortedCharacters * Submarine.MainSub.Info.RecommendedCrewSizeMin);
return (int)Math.Round(baseEscortedCharacters + scalingEscortedCharacters * (Submarine.MainSub.Info.RecommendedCrewSizeMin + Submarine.MainSub.Info.RecommendedCrewSizeMax) / 2);
}
private void InitEscort()
@@ -88,6 +85,25 @@ namespace Barotrauma
}
}
}
if (terroristChance > 0f)
{
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));
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)
{
DebugConsole.AddWarning(character.Name + " is a terrorist.");
}
#endif
}
}
private void InitCharacters()
@@ -120,26 +136,6 @@ namespace Barotrauma
}
i++;
}
if (!IsClient && terroristChance > 0f)
{
int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count);
terroristCount = Math.Clamp(terroristCount, 1, characters.Count);
terroristCharacters.Clear();
characters.Shuffle();
characters.GetRange(0, terroristCount).ForEach(c => terroristCharacters.Add(c));
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)
{
DebugConsole.AddWarning(character.Name + " is a terrorist.");
}
#endif
}
}
protected override void StartMissionSpecific(Level level)
@@ -207,10 +203,10 @@ namespace Barotrauma
bool NonTerroristsStillAlive(IEnumerable<Character> characterList)
{
return characterList.Any(c => !terroristCharacters.Contains(c) && IsAlive(c));
return characterList.All(c => terroristCharacters.Contains(c) || IsAlive(c));
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
if (!IsClient)
{
@@ -261,9 +257,10 @@ namespace Barotrauma
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
{
bool terroristsSurvived = terroristCharacters.Any(c => Survived(c) && !IsCaptured(c));
bool friendliesSurvived = characters.Except(terroristCharacters).Any(c => Survived(c));
bool friendliesSurvived = characters.Except(terroristCharacters).All(c => Survived(c));
bool vipDied = false;
// this logic is currently irrelevant, as the mission is failed regardless of who dies
if (vipCharacter != null)
{
vipDied = !Survived(vipCharacter);
@@ -0,0 +1,28 @@
using Barotrauma.Networking;
namespace Barotrauma
{
partial class GoToMission : Mission
{
public GoToMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
}
protected override void UpdateMissionSpecific(float deltaTime)
{
State = 1;
}
#if CLIENT
public override void ClientReadInitial(IReadMessage msg)
{
}
#elif SERVER
public override void ServerWriteInitial(IWriteMessage msg, Client c)
{
}
#endif
}
}
@@ -115,7 +115,7 @@ namespace Barotrauma
FindRelevantLevelResources();
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient) { return; }
switch (State)
@@ -23,6 +23,7 @@ namespace Barotrauma
if (state != value)
{
state = value;
TryTriggerEvents(state);
#if SERVER
GameMain.Server?.UpdateMissionState(this, state);
#endif
@@ -128,6 +129,20 @@ namespace Barotrauma
{
get { return Prefab.Difficulty; }
}
private class DelayedTriggerEvent
{
public readonly MissionPrefab.TriggerEvent TriggerEvent;
public float Delay;
public DelayedTriggerEvent(MissionPrefab.TriggerEvent triggerEvent, float delay)
{
TriggerEvent = triggerEvent;
Delay = delay;
}
}
private List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
public Mission(MissionPrefab prefab, Location[] locations, Submarine sub)
{
@@ -167,6 +182,9 @@ namespace Barotrauma
Messages[m] = Messages[m].Replace("[reward]", rewardText);
}
}
public virtual void SetDifficulty(float difficulty) { }
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
{
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer);
@@ -216,9 +234,11 @@ namespace Barotrauma
public void Start(Level level)
{
state = 0;
#if CLIENT
shownMessages.Clear();
#endif
delayedTriggerEvents.Clear();
foreach (string categoryToShow in Prefab.UnhideEntitySubCategories)
{
foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.prefab?.HasSubCategory(categoryToShow) ?? false))
@@ -227,12 +247,27 @@ namespace Barotrauma
}
}
this.level = level;
TryTriggerEvents(0);
StartMissionSpecific(level);
}
protected virtual void StartMissionSpecific(Level level) { }
public virtual void Update(float deltaTime) { }
public void Update(float deltaTime)
{
for (int i = delayedTriggerEvents.Count - 1; i>=0;i--)
{
delayedTriggerEvents[i].Delay -= deltaTime;
if (delayedTriggerEvents[i].Delay <= 0.0f)
{
TriggerEvent(delayedTriggerEvents[i].TriggerEvent);
delayedTriggerEvents.RemoveAt(i);
}
}
UpdateMissionSpecific(deltaTime);
}
protected virtual void UpdateMissionSpecific(float deltaTime) { }
protected void ShowMessage(int missionState)
{
@@ -241,6 +276,55 @@ namespace Barotrauma
partial void ShowMessageProjSpecific(int missionState);
private void TryTriggerEvents(int state)
{
foreach (var triggerEvent in Prefab.TriggerEvents)
{
if (triggerEvent.State == state)
{
TryTriggerEvent(triggerEvent);
}
}
}
/// <summary>
/// Triggers the event or adds it to the delayedTriggerEvents it if it has a delay
/// </summary>
private void TryTriggerEvent(MissionPrefab.TriggerEvent trigger)
{
if (trigger.Delay > 0)
{
if (!delayedTriggerEvents.Any(t => t.TriggerEvent == trigger))
{
delayedTriggerEvents.Add(new DelayedTriggerEvent(trigger, trigger.Delay));
}
}
else
{
TriggerEvent(trigger);
}
}
/// <summary>
/// Triggers the event immediately, ignoring any delays
/// </summary>
private void TriggerEvent(MissionPrefab.TriggerEvent trigger)
{
var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier.Equals(trigger.EventIdentifier, StringComparison.OrdinalIgnoreCase));
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\").");
return;
}
if (GameMain.GameSession?.EventManager != null)
{
var newEvent = eventPrefab.CreateInstance();
GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent);
newEvent.Init(true);
}
}
/// <summary>
/// End the mission and give a reward if it was completed successfully
/// </summary>
@@ -344,7 +428,7 @@ namespace Barotrauma
{
positionToStayIn = WayPoint.GetRandom(SpawnType.Human, null, submarine);
}
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync);
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, npcIdentifier: humanPrefab.Identifier, jobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync);
characterInfo.TeamID = teamType;
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn);
@@ -18,11 +18,11 @@ namespace Barotrauma
Nest = 0x10,
Mineral = 0x20,
Combat = 0x40,
OutpostDestroy = 0x80,
OutpostRescue = 0x100,
Escort = 0x200,
Pirate = 0x400,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | OutpostDestroy | OutpostRescue | Escort | Pirate
AbandonedOutpost = 0x80,
Escort = 0x100,
Pirate = 0x200,
GoTo = 0x400,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo
}
partial class MissionPrefab
@@ -37,15 +37,17 @@ namespace Barotrauma
{ MissionType.Beacon, typeof(BeaconMission) },
{ MissionType.Nest, typeof(NestMission) },
{ MissionType.Mineral, typeof(MineralMission) },
{ MissionType.OutpostDestroy, typeof(OutpostDestroyMission) },
{ MissionType.OutpostRescue, typeof(AbandonedOutpostMission) },
{ MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) },
{ MissionType.Escort, typeof(EscortMission) },
{ MissionType.Pirate, typeof(PirateMission) }
{ MissionType.Pirate, typeof(PirateMission) },
{ MissionType.GoTo, typeof(GoToMission) }
};
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
{
{ MissionType.Combat, typeof(CombatMission) }
};
public static readonly HashSet<MissionType> HiddenMissionClasses = new HashSet<MissionType>() { MissionType.GoTo };
private readonly ConstructorInfo constructor;
@@ -87,6 +89,8 @@ namespace Barotrauma
public readonly bool IsSideObjective;
public readonly bool RequireWreck;
/// <summary>
/// The mission can only be received when travelling from Pair.First to Pair.Second
/// </summary>
@@ -102,6 +106,25 @@ namespace Barotrauma
/// </summary>
public readonly List<string> UnhideEntitySubCategories = new List<string>();
public class TriggerEvent
{
[Serialize("", true)]
public string EventIdentifier { get; private set; }
[Serialize(0, true)]
public int State { get; private set; }
[Serialize(0.0f, true)]
public float Delay { get; private set; }
public TriggerEvent(XElement element)
{
SerializableProperty.DeserializeProperties(this, element);
}
}
public readonly List<TriggerEvent> TriggerEvents = new List<TriggerEvent>();
public LocationTypeChange LocationTypeChangeOnCompleted;
public readonly XElement ConfigElement;
@@ -160,6 +183,7 @@ namespace Barotrauma
Reward = element.GetAttributeInt("reward", 1);
AllowRetry = element.GetAttributeBool("allowretry", false);
IsSideObjective = element.GetAttributeBool("sideobjective", false);
RequireWreck = element.GetAttributeBool("requirewreck", false);
Commonness = element.GetAttributeInt("commonness", 1);
if (element.GetAttribute("difficulty") != null)
{
@@ -272,10 +296,19 @@ namespace Barotrauma
DataRewards.Add(Tuple.Create(identifier, value, operation));
}
break;
case "triggerevent":
TriggerEvents.Add(new TriggerEvent(subElement));
break;
}
}
string missionTypeName = element.GetAttributeString("type", "");
//backwards compatibility
if (missionTypeName.Equals("outpostdestroy", StringComparison.OrdinalIgnoreCase) || missionTypeName.Equals("outpostrescue", StringComparison.OrdinalIgnoreCase))
{
missionTypeName = "AbandonedOutpost";
}
if (!Enum.TryParse(missionTypeName, out Type))
{
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
@@ -160,7 +160,7 @@ namespace Barotrauma
}
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
switch (State)
{
@@ -216,7 +216,7 @@ namespace Barotrauma
level.LevelObjectManager.PlaceNestObjects(level, cave, nestPosition, nestObjectRadius, nestObjectAmount);
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient)
{
@@ -1,165 +0,0 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class OutpostDestroyMission : AbandonedOutpostMission
{
private readonly string itemTag;
private readonly XElement itemConfig;
private readonly List<Item> items = new List<Item>();
public override IEnumerable<Vector2> SonarPositions
{
get
{
if (State > 0)
{
return Enumerable.Empty<Vector2>();
}
else
{
return Targets.Select(t => t.WorldPosition);
}
}
}
private IEnumerable<Entity> Targets
{
get
{
if (State > 0)
{
return Enumerable.Empty<Entity>();
}
else
{
if (items.Any())
{
return items.Where(it => !it.Removed && it.Condition > 0.0f).Cast<Entity>().Concat(requireKill.Where(c => !c.Removed && !c.IsDead)).Concat(requireRescue);
}
else
{
return requireKill.Concat(requireRescue);
}
}
}
}
public OutpostDestroyMission(MissionPrefab prefab, Location[] locations, Submarine sub) :
base(prefab, locations, sub)
{
itemConfig = prefab.ConfigElement.Element("Items");
itemTag = prefab.ConfigElement.GetAttributeString("targetitem", "");
}
protected override void StartMissionSpecific(Level level)
{
items.Clear();
#if SERVER
spawnedItems.Clear();
#endif
if (!string.IsNullOrEmpty(itemTag))
{
var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag));
if (!itemsToDestroy.Any())
{
DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".");
}
else
{
items.AddRange(itemsToDestroy);
}
}
if (itemConfig != null && !IsClient)
{
foreach (XElement element in itemConfig.Elements())
{
string itemIdentifier = element.GetAttributeString("identifier", "");
if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab))
{
DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found");
continue;
}
string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null);
string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null);
ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy,
moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
if (spawnPoint == null)
{
var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub;
spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
}
Vector2 spawnPos = spawnPoint.WorldPosition;
if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100)
{
spawnPos = new Vector2(
MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X + 50, wp.CurrentHull.WorldRect.Right - 50),
wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 16.0f);
}
var item = new Item(itemPrefab, spawnPos, null);
items.Add(item);
#if SERVER
spawnedItems.Add(item);
#endif
}
}
base.StartMissionSpecific(level);
}
public override void Update(float deltaTime)
{
if (requireRescue.Any(r => r.Removed || r.IsDead))
{
#if SERVER
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
{
GameMain.Server.EndGame();
}
#endif
return;
}
switch (state)
{
case 0:
if (items.Any())
{
if (items.All(it => it.Removed || it.Condition <= 0.0f) &&
requireKill.All(c => c.Removed || c.IsDead) &&
requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player))
{
State = 1;
}
}
else
{
if (requireKill.All(c => c.Removed || c.IsDead) &&
requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player))
{
State = 1;
}
}
break;
#if SERVER
case 1:
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
{
if (!Submarine.MainSub.AtStartExit || (wasDocked && !Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost)))
{
GameMain.Server.EndGame();
State = 2;
}
}
break;
#endif
}
}
}
}
@@ -11,11 +11,15 @@ namespace Barotrauma
{
partial class PirateMission : Mission
{
private readonly XElement submarineTypeConfig;
private readonly XElement characterConfig;
private readonly XElement submarineConfig;
private readonly XElement characterTypeConfig;
private readonly float addedMissionDifficultyPerPlayer;
private float missionDifficulty;
private int alternateReward;
private Submarine enemySub;
private Item reactorItem;
private readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterDictionary = new Dictionary<Character, List<Item>>();
@@ -51,17 +55,55 @@ namespace Barotrauma
}
}
public override int GetReward(Submarine sub)
{
return alternateReward;
}
private SubmarineInfo submarineInfo;
public override SubmarineInfo EnemySubmarineInfo => submarineInfo;
public override SubmarineInfo EnemySubmarineInfo
{
get
{
return submarineInfo;
}
}
// these values could also be defined within the mission XML
private const float RandomnessModifier = 25;
private const float ShipRandomnessModifier = 15;
private const float MaxDifficulty = 100;
public PirateMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
submarineConfig = prefab.ConfigElement.Element("Submarine");
submarineTypeConfig = prefab.ConfigElement.Element("SubmarineTypes");
characterConfig = prefab.ConfigElement.Element("Characters");
characterTypeConfig = prefab.ConfigElement.Element("CharacterTypes");
addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0);
// for campaign missions, set difficulty at construction
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
SetDifficulty(levelData?.Difficulty ?? Level.Loaded?.Difficulty ?? 0f);
}
public override void SetDifficulty(float difficulty)
{
if (missionDifficulty > 0f)
{
// difficulty already set
return;
}
missionDifficulty = difficulty;
XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward);
string submarineIdentifier = submarineConfig.GetAttributeString("identifier", string.Empty);
if (submarineIdentifier == string.Empty)
{
DebugConsole.ThrowError("No identifier used for submarine for pirate mission!");
@@ -74,9 +116,36 @@ namespace Barotrauma
DebugConsole.ThrowError("No submarine file found with the identifier!");
return;
}
submarineInfo = new SubmarineInfo(contentFile.Path);
}
private float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier)
{
return Math.Abs(levelDifficulty - preferredDifficulty + (Rand.Range(-randomnessModifier, randomnessModifier, Rand.RandSync.Server)));
}
private int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty)
{
return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * ((levelDifficulty + Rand.Range(-RandomnessModifier, RandomnessModifier, Rand.RandSync.Server)) / MaxDifficulty)), minAmount);
}
private XElement GetRandomDifficultyModifiedElement(XElement parentElement, float levelDifficulty, float randomnessModifier)
{
// look for the element that is closest to our difficulty, with some randomness
XElement bestElement = null;
float bestValue = float.MaxValue;
foreach (XElement element in parentElement.Elements())
{
float applicabilityValue = GetDifficultyModifiedValue(element.GetAttributeFloat(0f, "preferreddifficulty"), levelDifficulty, randomnessModifier);
if (applicabilityValue < bestValue)
{
bestElement = element;
bestValue = applicabilityValue;
}
}
return bestElement;
}
private void CreateMissionPositions(out Vector2 preferredSpawnPos)
{
Vector2 patrolPos = enemySub.WorldPosition;
@@ -116,7 +185,6 @@ namespace Barotrauma
if (enemySub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
reactor.PowerUpImmediately();
reactorItem = reactor.Item;
}
enemySub.EnableMaintainPosition();
enemySub.SetPosition(spawnPos);
@@ -134,21 +202,45 @@ namespace Barotrauma
return;
}
int playerCount = 1;
#if SERVER
playerCount = GameMain.Server.ConnectedClients.Where(c => !c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating).Count();
#endif
float enemyCreationDifficulty = missionDifficulty + playerCount * addedMissionDifficultyPerPlayer;
bool commanderAssigned = false;
foreach (XElement element in characterConfig.Elements())
{
Character spawnedCharacter = CreateHuman(CreateHumanPrefabFromElement(element), characters, characterDictionary, enemySub, CharacterTeamType.None, null);
if (!commanderAssigned)
// it is possible to get more than the "max" amount of characters if the modified difficulty is high enough; this is intentional
// if necessary, another "hard max" value could be used to clamp the value for performance/gameplay concerns
int amountCreated = GetDifficultyModifiedAmount(element.GetAttributeInt("minamount", 0), element.GetAttributeInt("maxamount", 0), enemyCreationDifficulty);
for (int i = 0; i < amountCreated; i++)
{
bool isCommander = element.GetAttributeBool("iscommander", false);
if (isCommander && spawnedCharacter.AIController is HumanAIController humanAIController)
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == element.GetAttributeString("typeidentifier", string.Empty)).FirstOrDefault();
if (characterType == null)
{
humanAIController.InitShipCommandManager();
foreach (var patrolPos in patrolPositions)
DebugConsole.ThrowError("No character types defined in CharacterTypes for a declared type identifier in mission file " + this);
return;
}
XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier);
Character spawnedCharacter = CreateHuman(CreateHumanPrefabFromElement(variantElement), characters, characterDictionary, enemySub, CharacterTeamType.None, null);
if (!commanderAssigned)
{
bool isCommander = variantElement.GetAttributeBool("iscommander", false);
if (isCommander && spawnedCharacter.AIController is HumanAIController humanAIController)
{
humanAIController.ShipCommandManager.patrolPositions.Add(patrolPos);
humanAIController.InitShipCommandManager();
foreach (var patrolPos in patrolPositions)
{
humanAIController.ShipCommandManager.patrolPositions.Add(patrolPos);
}
commanderAssigned = true;
}
commanderAssigned = true;
}
}
}
@@ -217,7 +309,7 @@ namespace Barotrauma
}
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
int newState = State;
float sqrSonarRange = MathUtils.Pow2(Sonar.DefaultSonarRange);
@@ -268,7 +360,7 @@ namespace Barotrauma
State = newState;
}
private bool CheckWinState() => !IsClient && (characters.All(m => !Survived(m)) || reactorItem.Condition <= 0f);
private bool CheckWinState() => !IsClient && (characters.All(m => !Survived(m)));
private bool Survived(Character character)
{
@@ -205,7 +205,7 @@ namespace Barotrauma
}
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
if (item == null)
{
@@ -442,7 +442,7 @@ namespace Barotrauma
}
else if (!spawnPosType.HasFlag(Level.PositionType.MainPath))
{
scatterAmount = 100;
scatterAmount = 0;
}
for (int i = 0; i < amount; i++)
{
@@ -455,7 +455,7 @@ namespace Barotrauma
System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events.");
Vector2 pos = spawnPos.Value + Rand.Vector(scatterAmount);
if (scatterAmount > 100)
if (scatterAmount > 0)
{
if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(pos)))
{
@@ -49,7 +49,7 @@ namespace Barotrauma
//duration of the camera transition at the end of a round
protected const float EndTransitionDuration = 5.0f;
//there can be no events before this time has passed during the 1st campaign round
const float FirstRoundEventDelay = 30.0f;
const float FirstRoundEventDelay = 0.0f;
public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub }
@@ -113,12 +113,15 @@ namespace Barotrauma
{
get
{
if (Map.CurrentLocation?.SelectedMission != null)
if (Map.CurrentLocation != null)
{
if (Map.CurrentLocation.SelectedMission.Locations[0] == Map.CurrentLocation.SelectedMission.Locations[1] ||
Map.CurrentLocation.SelectedMission.Locations.Contains(Map.SelectedLocation))
foreach (Mission mission in map.CurrentLocation.SelectedMissions)
{
yield return Map.CurrentLocation.SelectedMission;
if (mission.Locations[0] == mission.Locations[1] ||
mission.Locations.Contains(Map.SelectedLocation))
{
yield return mission;
}
}
}
foreach (Mission mission in extraMissions)
@@ -240,23 +243,24 @@ namespace Barotrauma
if (levelData.Type == LevelData.LevelType.Outpost)
{
//if there's an available mission that takes place in the outpost, select it
var availableMissionsInLocation = currentLocation.AvailableMissions.Where(m => m.Locations[0] == currentLocation && m.Locations[1] == currentLocation);
if (availableMissionsInLocation.Any())
foreach (var availableMission in currentLocation.AvailableMissions)
{
currentLocation.SelectedMission = availableMissionsInLocation.FirstOrDefault();
}
else
{
currentLocation.SelectedMission = null;
if (availableMission.Locations[0] == currentLocation && availableMission.Locations[1] == currentLocation)
{
currentLocation.SelectMission(availableMission);
}
}
}
else
{
//if we had selected a mission that takes place in the outpost, deselect it when leaving the outpost
if (currentLocation.SelectedMission?.Locations[0] == currentLocation &&
currentLocation.SelectedMission?.Locations[1] == currentLocation)
foreach (Mission mission in currentLocation.SelectedMissions.ToList())
{
currentLocation.SelectedMission = null;
//if we had selected a mission that takes place in the outpost, deselect it when leaving the outpost
if (mission.Locations[0] == currentLocation &&
mission.Locations[1] == currentLocation)
{
currentLocation.DeselectMission(mission);
}
}
if (levelData.HasBeaconStation && !levelData.IsBeaconActive)
@@ -851,11 +855,14 @@ namespace Barotrauma
DebugConsole.NewMessage(" " + i + ". " + destination.Name, Color.White);
}
}
if (map.CurrentLocation?.SelectedMission != null)
if (map.CurrentLocation != null)
{
DebugConsole.NewMessage(" Selected mission: " + map.CurrentLocation.SelectedMission.Name, Color.White);
DebugConsole.NewMessage("\n" + map.CurrentLocation.SelectedMission.Description, Color.White);
foreach (Mission mission in map.CurrentLocation.SelectedMissions)
{
DebugConsole.NewMessage(" Selected mission: " + mission.Name, Color.White);
DebugConsole.NewMessage("\n" + mission.Description, Color.White);
}
}
}
@@ -162,6 +162,18 @@ namespace Barotrauma
private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable<MissionPrefab> missionPrefabs = null, MissionType missionType = MissionType.None)
{
if (gameModePreset.GameModeType == typeof(CoOpMode) || gameModePreset.GameModeType == typeof(PvPMode))
{
//don't allow hidden mission types (e.g. GoTo) in single mission modes
var missionTypes = (MissionType[])Enum.GetValues(typeof(MissionType));
for (int i = 0; i < missionTypes.Length; i++)
{
if (MissionPrefab.HiddenMissionClasses.Contains(missionTypes[i]))
{
missionType &= ~missionTypes[i];
}
}
}
if (gameModePreset.GameModeType == typeof(CoOpMode))
{
return missionPrefabs != null ?
@@ -354,6 +366,12 @@ namespace Barotrauma
}
}
foreach (Mission mission in GameMode.Missions)
{
// setting difficulty for missions that may involve difficulty-related submarine creation
mission.SetDifficulty(levelData?.Difficulty ?? 0f);
}
if (Submarine.MainSubs[1] == null)
{
var enemySubmarineInfo = GameMode is PvPMode ? SubmarineInfo : GameMode.Missions.FirstOrDefault(m => m.EnemySubmarineInfo != null)?.EnemySubmarineInfo;
@@ -91,7 +91,6 @@ namespace Barotrauma
private CampaignMetadata Metadata => Campaign.CampaignMetadata;
private readonly CampaignMode Campaign;
private int spentMoney;
public event Action? OnUpgradesChanged;
@@ -125,7 +124,7 @@ namespace Barotrauma
}
}
public int DetermineItemSwapCost(Item item, ItemPrefab replacement)
public int DetermineItemSwapCost(Item item, ItemPrefab? replacement)
{
if (replacement == null)
{
@@ -226,7 +225,6 @@ namespace Barotrauma
}
Campaign.Money -= price;
spentMoney += price;
PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
@@ -270,17 +268,22 @@ namespace Barotrauma
DebugConsole.ThrowError($"Cannot swap null item!");
return;
}
if (itemToRemove.HiddenInGame)
{
DebugConsole.ThrowError($"Cannot swap item \"{itemToRemove.Name}\" because it's set to be hidden in-game.");
return;
}
if (!itemToRemove.AllowSwapping)
{
DebugConsole.ThrowError($"Cannot swap item \"{itemToRemove.Name}\" because it's configured to be non-swappable.");
return;
}
if (!UpgradeCategory.Categories.Any(c => c.ItemTags.Any(t => itemToRemove.HasTag(t)) && c.ItemTags.Any(t => itemToInstall.Tags.Contains(t))))
{
DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" with \"{itemToInstall.Name}\" (not in the same upgrade category).");
return;
}
/*if (itemToRemove.PendingItemSwap != null)
{
CancelItemSwap(itemToRemove);
}
else */
if (itemToRemove.prefab == itemToInstall)
{
DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" (trying to swap with the same item!).");
@@ -318,7 +321,6 @@ namespace Barotrauma
}
Campaign.Money -= price;
spentMoney += price;
itemToRemove.AvailableSwaps.Add(itemToRemove.Prefab);
if (itemToInstall != null) { itemToRemove.AvailableSwaps.Add(itemToInstall); }
@@ -436,36 +438,12 @@ namespace Barotrauma
{
int newLevel = BuyUpgrade(prefab, category, Submarine.MainSub, level);
DebugConsole.Log($" - {category.Identifier}.{prefab.Identifier} lvl. {level}, new: ({newLevel})");
if (newLevel > 0)
{
SetUpgradeLevel(prefab, category, Math.Clamp(newLevel, 0, prefab.MaxLevel));
}
SetUpgradeLevel(prefab, category, Math.Clamp(level, 0, prefab.MaxLevel));
}
PendingUpgrades.Clear();
loadedUpgrades?.Clear();
loadedUpgrades = null;
spentMoney = 0;
}
/// <summary>
/// Cancels the pending upgrades and refunds the money spent
/// </summary>
private void RefundUpgrades()
{
DebugConsole.Log($"Refunded {spentMoney} marks in pending upgrades.");
if (spentMoney > 0)
{
#if CLIENT
GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("UpgradeRefundTitle"), TextManager.Get("UpgradeRefundBody"), new[] { TextManager.Get("Ok") });
msgBox.Buttons[0].OnClicked += msgBox.Close;
#endif
}
Campaign.Money += spentMoney;
spentMoney = 0;
PendingUpgrades.Clear();
PurchasedUpgrades.Clear();
}
public void CreateUpgradeErrorMessage(string text, bool isSinglePlayer, Character character)
@@ -523,7 +501,7 @@ namespace Barotrauma
if (upgrade == null || upgrade.Level != level || isOverMax)
{
DebugConsole.AddWarning($"{wall.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing...");
DebugLog($"{wall.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing...");
FixUpgradeOnItem(wall, prefab, level);
}
}
@@ -552,7 +530,7 @@ namespace Barotrauma
if (upgrade == null || upgrade.Level != level || isOverMax)
{
DebugConsole.AddWarning($"{item.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}{(isOverMax ? " (Over max level!)" : string.Empty)}. Fixing...");
DebugLog($"{item.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}{(isOverMax ? " (Over max level!)" : string.Empty)}. Fixing...");
FixUpgradeOnItem(item, prefab, level);
}
}
@@ -695,112 +673,6 @@ namespace Barotrauma
return Campaign.PendingSubmarineSwitch == null;
}
public void RefundResetAndReload(SubmarineInfo newSubmarine, bool notifyClients = false)
{
RefundUpgrades();
ResetUpgrades();
Dictionary<string, int> newUpgrades = ReloadUpgradeValues(newSubmarine);
#if SERVER
if (notifyClients)
{
SendUpgradeResetMessage(newUpgrades);
}
#endif
}
/// <summary>
/// Parses a SubmarineInfo and sets the store values accordingly.
/// Used when reloading a previously saved submarine.
/// </summary>
/// <param name="info"></param>
private Dictionary<string, int> ReloadUpgradeValues(SubmarineInfo info)
{
Dictionary<string, int> newValues = new Dictionary<string, int>();
IEnumerable<XElement> linkedSubElements = info.SubmarineElement.Elements().Where(element => element.Name.ToString().Equals("LinkedSubmarine", StringComparison.OrdinalIgnoreCase)).SelectMany(element => element.Elements());
IEnumerable<XElement> mainSubElements = info.SubmarineElement.Elements().Where(Predicate);
List<XElement> elements = mainSubElements.Concat(linkedSubElements.Where(Predicate)).ToList();
foreach (UpgradeCategory category in UpgradeCategory.Categories)
{
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
{
if (!prefab.UpgradeCategories.Contains(category)) { continue; }
List<int> levels = GetUpgradeFromXML(elements, category, prefab);
if (levels.Any())
{
int level = (int) levels.Average(i => i);
newValues.Add(FormatIdentifier(prefab, category), level);
}
}
}
foreach (var (dataIdentifier, level) in newValues)
{
Campaign.CampaignMetadata.SetValue(dataIdentifier, level);
}
return newValues;
static List<int> GetUpgradeFromXML(List<XElement> elements, UpgradeCategory category, UpgradePrefab prefab)
{
List<int> levels = new List<int>();
foreach (XElement subElement in elements)
{
if (!category.CanBeApplied(subElement, prefab)) { continue; }
foreach (XElement component in subElement.Elements())
{
if (string.Equals(component.Name.ToString(), "upgrade", StringComparison.OrdinalIgnoreCase))
{
string identifier = component.GetAttributeString("identifier", string.Empty);
int level = component.GetAttributeInt("level", -1);
if (string.IsNullOrWhiteSpace(identifier) || level <= 0) { continue; }
UpgradePrefab? matchingPrefab = UpgradePrefab.Find(identifier);
if (matchingPrefab == null || matchingPrefab != prefab) { continue; }
if (matchingPrefab.UpgradeCategories.Contains(category)) { levels.Add(level); }
}
}
}
return levels;
}
static bool Predicate(XElement element) => element.HasElements && element.Elements().Any(e => e.Name.ToString().Equals("upgrade", StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Resets our upgrade progress and prices.
/// This does not actually remove the upgrades from the submarine but resets the store interface.
/// </summary>
/// <remarks>
/// This method works by iterating thru all upgrade categories and prefabs and checking if they have a
/// valid key stored in the metadata, if they do set it to 0, upgrades without a key stored are always
/// assumed to be 0 so they don't need to be reset.
///
/// Should initially be called server side as we can't trust clients with such a simple notification.
/// </remarks>
private void ResetUpgrades()
{
foreach (UpgradeCategory category in UpgradeCategory.Categories)
{
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
{
if (!prefab.UpgradeCategories.Contains(category)) { continue; }
string dataIdentifier = FormatIdentifier(prefab, category);
if (Metadata.HasKey(dataIdentifier))
{
Metadata.SetValue(dataIdentifier, 0);
}
}
}
OnUpgradesChanged?.Invoke();
}
public void Save(XElement? parent)
{
if (parent == null) { return; }
@@ -877,34 +749,10 @@ namespace Barotrauma
DebugConsole.ThrowError(error.TrimEnd('\n'), e);
}
public static Dictionary<string, int> GetMetadataLevels(CampaignMetadata? metadata)
{
Dictionary<string, int> values = new Dictionary<string, int>();
if (metadata == null) { return values; }
foreach (UpgradeCategory category in UpgradeCategory.Categories)
{
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
{
string identifier = FormatIdentifier(prefab, category);
if (metadata.HasKey(identifier) && !values.ContainsKey(identifier))
{
values.Add(identifier, metadata.GetInt(identifier));
}
}
}
return values;
}
/// <summary>
/// Used to sync the pending upgrades list in multiplayer.
/// </summary>
/// <param name="upgrades"></param>
/// <remarks>
/// In singleplayer this is not used and should not be.
/// </remarks>
public void SetPendingUpgrades(List<PurchasedUpgrade> upgrades)
{
PendingUpgrades.Clear();
@@ -887,10 +887,7 @@ namespace Barotrauma.Items.Components
}
var wire = item.GetComponent<Wire>();
if (wire != null)
{
wire.Drop(null);
}
wire?.Drop(null);
if (joint != null)
{
@@ -991,7 +988,7 @@ namespace Barotrauma.Items.Components
{
if (DockingTarget.Door != null && doorBody != null)
{
doorBody.Enabled = DockingTarget.Door.Body.Enabled;
doorBody.Enabled = DockingTarget.Door.Body.Enabled && !(DockingTarget.Door.Body.FarseerBody.FixtureList.FirstOrDefault()?.IsSensor ?? false);
}
dockingState = MathHelper.Lerp(dockingState, 1.0f, deltaTime * 10.0f);
}
@@ -259,6 +259,10 @@ namespace Barotrauma.Items.Components
Body.SetTransformIgnoreContacts(
ConvertUnits.ToSimUnits(new Vector2(doorRect.Center.X, doorRect.Y - doorRect.Height / 2)),
0.0f);
if (isBroken)
{
DisableBody();
}
}
public override void Move(Vector2 amount)
@@ -440,7 +440,7 @@ namespace Barotrauma.Items.Components
if (item.CurrentHull == null)
{
return attachTargetCell != null && Structure.GetAttachTarget(item.WorldPosition) != null;
return attachTargetCell != null || Structure.GetAttachTarget(item.WorldPosition) != null;
}
else
{
@@ -548,6 +548,11 @@ namespace Barotrauma.Items.Components
}
return true;
}
else if (targetBody.UserData is LevelObject levelObject && levelObject.Prefab.TakeLevelWallDamage)
{
levelObject.AddDamage(-LevelWallFixAmount, deltaTime, item);
return true;
}
else if (targetBody.UserData is Character targetCharacter)
{
if (targetCharacter.Removed) { return false; }
@@ -290,10 +290,12 @@ namespace Barotrauma.Items.Components
{
for (int i = 0; i < ingredient.Amount; i++)
{
var availableItem = availableIngredients.FirstOrDefault(it => it != null && ingredient.ItemPrefabs.Contains(it.Prefab) && it.ConditionPercentage >= ingredient.MinCondition * 100.0f);
var availableItem = availableIngredients.FirstOrDefault(it =>
it != null && ingredient.ItemPrefabs.Contains(it.Prefab) &&
it.ConditionPercentage >= ingredient.MinCondition * 100.0f &&
it.ConditionPercentage <= ingredient.MaxCondition * 100.0f);
if (availableItem == null) { continue; }
//Item4 = use condition bool
if (ingredient.UseCondition && availableItem.ConditionPercentage - ingredient.MinCondition * 100 > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0
{
availableItem.Condition -= availableItem.Prefab.Health * ingredient.MinCondition;
@@ -493,7 +495,8 @@ namespace Barotrauma.Items.Components
return
item != null &&
requiredItem.ItemPrefabs.Contains(item.prefab) &&
item.Condition / item.Prefab.Health >= requiredItem.MinCondition;
item.Condition / item.Prefab.Health >= requiredItem.MinCondition &&
item.Condition / item.Prefab.Health <= requiredItem.MaxCondition;
}
public override XElement Save(XElement parentElement)
@@ -242,7 +242,7 @@ namespace Barotrauma.Items.Components
optimalTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance);
tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess);
allowedTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance);
optimalTemperature = Vector2.Lerp(new Vector2(40.0f, 60.0f), new Vector2(30.0f, 70.0f), degreeOfSuccess);
allowedTemperature = Vector2.Lerp(new Vector2(30.0f, 70.0f), new Vector2(10.0f, 90.0f), degreeOfSuccess);
@@ -314,6 +314,33 @@ namespace Barotrauma.Items.Components
}
}
if (!loadQueue.Any() && PowerOn)
{
//loadQueue is empty, round must've just started
//reset the fission rate, turbine output and
//temperature to optimal levels to prevent fires
//at the start of the round
correctTurbineOutput = currentLoad / MaxPowerOutput * 100.0f;
tolerance = MathHelper.Lerp(2.5f, 10.0f, degreeOfSuccess);
optimalTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance);
tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess);
allowedTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance);
float desiredTurbineOutput = MathHelper.Clamp(correctTurbineOutput, 0.0f, 100.0f);
DebugConsole.Log($"Turbine output reset: {targetTurbineOutput}, {turbineOutput} -> {desiredTurbineOutput}");
targetTurbineOutput = desiredTurbineOutput;
turbineOutput = desiredTurbineOutput;
float desiredFissionRate = (optimalFissionRate.X + optimalFissionRate.Y) / 2.0f;
DebugConsole.Log($"Fission rate reset: {targetFissionRate}, {fissionRate} -> {desiredFissionRate}");
targetFissionRate = desiredFissionRate;
fissionRate = desiredFissionRate;
float desiredTemperature = (optimalTemperature.X + optimalTemperature.Y) / 2.0f;
DebugConsole.Log($"Temperature reset: {temperature} -> {desiredTemperature}");
temperature = desiredTemperature;
}
loadQueue.Enqueue(currentLoad);
while (loadQueue.Count() > 60.0f)
{
@@ -619,6 +646,18 @@ namespace Barotrauma.Items.Components
}
else
{
if (Item.ConditionPercentage <= 0 && AIObjectiveRepairItems.IsValidTarget(Item, character))
{
if (Item.Repairables.Average(r => r.DegreeOfSuccess(character)) > 0.4f)
{
objective.AddSubObjective(new AIObjectiveRepairItem(character, Item, objective.objectiveManager, isPriority: true));
return false;
}
else
{
character.Speak(TextManager.Get("DialogReactorIsBroken"), identifier: "reactorisbroken", minDurationBetweenSimilar: 30.0f);
}
}
if (TooMuchFuel())
{
DropFuel(minCondition: 0.1f, maxCondition: 100);
@@ -705,6 +705,20 @@ namespace Barotrauma.Items.Components
}
}
user = character;
if (Item.ConditionPercentage <= 0 && AIObjectiveRepairItems.IsValidTarget(Item, character))
{
if (Item.Repairables.Average(r => r.DegreeOfSuccess(character)) > 0.4f)
{
objective.AddSubObjective(new AIObjectiveRepairItem(character, Item, objective.objectiveManager, isPriority: true));
return false;
}
else
{
character.Speak(TextManager.Get("DialogNavTerminalIsBroken"), identifier: "navterminalisbroken", minDurationBetweenSimilar: 30.0f);
}
}
if (!AutoPilot)
{
unsentChanges = true;
@@ -171,6 +171,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, false, description: "Override random spread with static spread; hitscan are launched with an equal amount of angle between them. Only applies when firing multiple hitscan.")]
public bool StaticSpread
{
get;
set;
}
public Body StickTarget
{
get;
@@ -267,9 +274,24 @@ namespace Barotrauma.Items.Components
{
if (character != null && !characterUsable) { return false; }
for (int i = 0; i < HitScanCount; i++)
{
float launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * Rand.Range(-0.5f, 0.5f));
float launchAngle = 0f;
if (StaticSpread)
{
float staticSpread = Spread / (HitScanCount - 1);
// because the position of the item changes as hitscan are fired, we will set an
// initial offset on the first hitscan and then increase the item's angle by a set amount as hitscan are fired
float offset = i == 0 ? -staticSpread * (HitScanCount -1) : 0f;
launchAngle = item.body.Rotation + MathHelper.ToRadians(staticSpread + offset);
}
else
{
launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * Rand.Range(-0.5f, 0.5f));
}
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
if (Hitscan)
{
@@ -430,8 +452,15 @@ namespace Barotrauma.Items.Components
var aabb = new FarseerPhysics.Collision.AABB(rayStart - Vector2.One * 0.001f, rayStart + Vector2.One * 0.001f);
GameMain.World.QueryAABB((fixture) =>
{
//ignore sensors and items
if (fixture?.Body == null || fixture.IsSensor) { return true; }
if (fixture?.Body.UserData is LevelObject levelObj)
{
if (!levelObj.Prefab.TakeLevelWallDamage) { return true; }
}
else if (fixture?.Body == null || fixture.IsSensor)
{
//ignore sensors and items
return true;
}
if (fixture.Body.UserData is VineTile) { return true; }
if (fixture.Body.UserData is Item item && (item.GetComponent<Door>() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return true; }
if (fixture.Body.UserData as string == "ruinroom") { return true; }
@@ -439,6 +468,7 @@ namespace Barotrauma.Items.Components
//if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
if (submarine != null)
{
if (fixture.Body.UserData is VoronoiCell) { return true; }
if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return true; }
}
@@ -459,7 +489,15 @@ namespace Barotrauma.Items.Components
GameMain.World.RayCast((fixture, point, normal, fraction) =>
{
//ignore sensors and items
if (fixture?.Body == null || fixture.IsSensor) { return -1; }
if (fixture?.Body.UserData is LevelObject levelObj)
{
if (!levelObj.Prefab.TakeLevelWallDamage) { return -1; }
}
else if (fixture?.Body == null || fixture.IsSensor)
{
//ignore sensors and items
return -1;
}
if (fixture.Body.UserData is VineTile) { return -1; }
if (fixture.Body.UserData is Item item && (item.GetComponent<Door>() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return -1; }
@@ -473,21 +511,40 @@ namespace Barotrauma.Items.Components
//if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
if (submarine != null)
{
if (fixture.Body.UserData is VoronoiCell) { return -1; }
if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return -1; }
}
//ignore level cells if the item and the point of impact are inside a sub
if (fixture.Body.UserData is VoronoiCell && this.item.Submarine != null)
if (fixture.Body.UserData is VoronoiCell)
{
if (Hull.FindHull(ConvertUnits.ToDisplayUnits(point), this.item.CurrentHull) != null)
if (Hull.FindHull(ConvertUnits.ToDisplayUnits(point), this.item.CurrentHull) != null && this.item.Submarine != null)
{
return -1;
}
}
if (hits.Count > 50)
{
float furthestHit = 0.0f;
int furthestHitIndex = -1;
for (int i = 0; i < hits.Count; i++)
{
if (hits[i].Fraction > furthestHit)
{
furthestHitIndex = i;
furthestHit = hits[i].Fraction;
}
}
if (furthestHitIndex > -1)
{
hits.RemoveAt(furthestHitIndex);
}
}
hits.Add(new HitscanResult(fixture, point, normal, fraction));
return hits.Count < 25 ? 1 : 0;
return 1;
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel);
return hits;
@@ -590,11 +647,11 @@ namespace Barotrauma.Items.Components
}
else if (target.Body.UserData is Limb limb)
{
//severed limbs don't deactivate the projectile (but may still slow it down enough to make it inactive)
if (limb.IsSevered)
{
target.Body.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass);
return true;
//push the severed limb around a bit, but let the projectile pass through it
limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition);
return false;
}
}
else if (target.Body.UserData is Item item)
@@ -654,9 +711,7 @@ namespace Barotrauma.Items.Components
projectileNewSpeed = 1f;
projectileDeflectedNewSpeed = 0.8f;
}
//severed limbs don't deactivate the projectile (but may still slow it down enough to make it inactive)
if (limb.IsSevered) { return true; }
if (limb.character == null || limb.character.Removed) { return false; }
if (limb.IsSevered || limb.character == null || limb.character.Removed) { return false; }
limb.character.LastDamageSource = item;
if (Attack != null) { attackResult = Attack.DoDamageToLimb(User, limb, item.WorldPosition, 1.0f); }
@@ -674,7 +729,7 @@ namespace Barotrauma.Items.Components
{
if (Attack != null) { attackResult = Attack.DoDamage(User, damageable, item.WorldPosition, 1.0f); }
}
else if (target.Body.UserData is VoronoiCell voronoiCell && voronoiCell.IsDestructible && Attack != null && Math.Abs(Attack.StructureDamage) > 0.0f)
else if (target.Body.UserData is VoronoiCell voronoiCell && voronoiCell.IsDestructible && Attack != null && Math.Abs(Attack.LevelWallDamage) > 0.0f)
{
if (Level.Loaded?.ExtraWalls.Find(w => w.Body == target.Body) is DestructibleLevelWall destructibleWall)
{
@@ -197,19 +197,13 @@ namespace Barotrauma.Items.Components
if (Math.Abs(SourcePullForce) > 0.001f)
{
var sourceBody = GetBodyToPull(source);
if (sourceBody != null)
{
sourceBody.ApplyForce(forceDir * SourcePullForce);
}
sourceBody?.ApplyForce(forceDir * SourcePullForce);
}
if (Math.Abs(TargetPullForce) > 0.001f)
{
var targetBody = GetBodyToPull(target);
if (targetBody != null)
{
targetBody.ApplyForce(-forceDir * TargetPullForce);
}
targetBody?.ApplyForce(-forceDir * TargetPullForce);
}
}
@@ -34,6 +34,11 @@ namespace Barotrauma.Items.Components
set;
}
public bool TemporarilyLocked
{
get { return Level.IsLoadedOutpost && item.GetComponent<DockingPort>() != null; }
}
//connection panels can't be deactivated externally (by signals or status effects)
public override bool IsActive
{
@@ -37,7 +37,7 @@ namespace Barotrauma.Items.Components
range = MathHelper.Clamp(value, 0.0f, 4096.0f);
#if CLIENT
item.ResetCachedVisibleSize();
if (light != null) { light.Range = range; }
if (Light != null) { Light.Range = range; }
#endif
}
}
@@ -53,7 +53,7 @@ namespace Barotrauma.Items.Components
{
castShadows = value;
#if CLIENT
if (light != null) light.CastShadows = value;
if (Light != null) Light.CastShadows = value;
#endif
}
}
@@ -67,7 +67,7 @@ namespace Barotrauma.Items.Components
{
drawBehindSubs = value;
#if CLIENT
if (light != null) light.IsBackground = drawBehindSubs;
if (Light != null) Light.IsBackground = drawBehindSubs;
#endif
}
}
@@ -93,7 +93,7 @@ namespace Barotrauma.Items.Components
{
flicker = MathHelper.Clamp(value, 0.0f, 1.0f);
#if CLIENT
if (light != null) { light.LightSourceParams.Flicker = flicker; }
if (Light != null) { Light.LightSourceParams.Flicker = flicker; }
#endif
}
}
@@ -106,7 +106,7 @@ namespace Barotrauma.Items.Components
{
flickerSpeed = value;
#if CLIENT
if (light != null) { light.LightSourceParams.FlickerSpeed = flickerSpeed; }
if (Light != null) { Light.LightSourceParams.FlickerSpeed = flickerSpeed; }
#endif
}
}
@@ -119,7 +119,7 @@ namespace Barotrauma.Items.Components
{
pulseFrequency = MathHelper.Clamp(value, 0.0f, 60.0f);
#if CLIENT
if (light != null) { light.LightSourceParams.PulseFrequency = pulseFrequency; }
if (Light != null) { Light.LightSourceParams.PulseFrequency = pulseFrequency; }
#endif
}
}
@@ -132,7 +132,7 @@ namespace Barotrauma.Items.Components
{
pulseAmount = MathHelper.Clamp(value, 0.0f, 1.0f);
#if CLIENT
if (light != null) { light.LightSourceParams.PulseAmount = pulseAmount; }
if (Light != null) { Light.LightSourceParams.PulseAmount = pulseAmount; }
#endif
}
}
@@ -145,7 +145,7 @@ namespace Barotrauma.Items.Components
{
blinkFrequency = MathHelper.Clamp(value, 0.0f, 60.0f);
#if CLIENT
if (light != null) { light.LightSourceParams.BlinkFrequency = blinkFrequency; }
if (Light != null) { Light.LightSourceParams.BlinkFrequency = blinkFrequency; }
#endif
}
}
@@ -158,7 +158,7 @@ namespace Barotrauma.Items.Components
{
lightColor = value;
#if CLIENT
if (light != null) light.Color = IsActive ? lightColor : Color.Transparent;
if (Light != null) Light.Color = IsActive ? lightColor : Color.Transparent;
#endif
}
}
@@ -173,7 +173,7 @@ namespace Barotrauma.Items.Components
public override void Move(Vector2 amount)
{
#if CLIENT
light.Position += amount;
Light.Position += amount;
#endif
}
@@ -197,7 +197,7 @@ namespace Barotrauma.Items.Components
: base(item, element)
{
#if CLIENT
light = new LightSource(element)
Light = new LightSource(element)
{
ParentSub = item.CurrentHull?.Submarine,
Position = item.Position,
@@ -206,11 +206,11 @@ namespace Barotrauma.Items.Components
SpriteScale = Vector2.One * item.Scale,
Range = range
};
light.LightSourceParams.Flicker = flicker;
light.LightSourceParams.FlickerSpeed = FlickerSpeed;
light.LightSourceParams.PulseAmount = pulseAmount;
light.LightSourceParams.PulseFrequency = pulseFrequency;
light.LightSourceParams.BlinkFrequency = blinkFrequency;
Light.LightSourceParams.Flicker = flicker;
Light.LightSourceParams.FlickerSpeed = FlickerSpeed;
Light.LightSourceParams.PulseAmount = pulseAmount;
Light.LightSourceParams.PulseFrequency = pulseFrequency;
Light.LightSourceParams.BlinkFrequency = blinkFrequency;
#endif
IsActive = IsOn;
@@ -233,7 +233,7 @@ namespace Barotrauma.Items.Components
UpdateOnActiveEffects(deltaTime);
#if CLIENT
light.ParentSub = item.Submarine;
Light.ParentSub = item.Submarine;
#endif
if (item.Container != null)
{
@@ -243,23 +243,23 @@ namespace Barotrauma.Items.Components
#if CLIENT
if (ParentBody != null)
{
light.Position = ParentBody.Position;
Light.Position = ParentBody.Position;
}
else if (turret != null)
{
light.Position = new Vector2(item.Rect.X + turret.TransformedBarrelPos.X, item.Rect.Y - turret.TransformedBarrelPos.Y);
Light.Position = new Vector2(item.Rect.X + turret.TransformedBarrelPos.X, item.Rect.Y - turret.TransformedBarrelPos.Y);
}
else
{
light.Position = item.Position;
Light.Position = item.Position;
}
#endif
PhysicsBody body = ParentBody ?? item.body;
if (body != null)
{
#if CLIENT
light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi;
light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically;
Light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi;
Light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically;
#endif
if (!body.Enabled)
{
@@ -270,8 +270,8 @@ namespace Barotrauma.Items.Components
else
{
#if CLIENT
light.Rotation = -Rotation - MathHelper.ToRadians(item.Rotation);
light.LightSpriteEffect = item.SpriteEffects;
Light.Rotation = -Rotation - MathHelper.ToRadians(item.Rotation);
Light.LightSpriteEffect = item.SpriteEffects;
#endif
}
@@ -71,7 +71,7 @@ namespace Barotrauma.Items.Components
get
{
if (GameMain.NetworkMember?.ServerSettings != null && !GameMain.NetworkMember.ServerSettings.AllowRewiring) { return false; }
return locked || connections.Any(c => c != null && c.ConnectionPanel.Locked);
return locked || connections.Any(c => c != null && (c.ConnectionPanel.Locked || c.ConnectionPanel.TemporarilyLocked));
}
set { locked = value; }
}
@@ -920,14 +920,14 @@ namespace Barotrauma.Items.Components
if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget &&
previousTarget.IsDead)
{
character.Speak(TextManager.Get("DialogTurretTargetDead"), null, 0.0f, "killedtarget" + previousTarget.ID, 10.0f);
character.Speak(TextManager.Get("DialogTurretTargetDead"), identifier: "killedtarget" + previousTarget.ID, minDurationBetweenSimilar: 10.0f);
character.AIController.SelectTarget(null);
}
bool canShoot = true;
if (!HasPowerToShoot())
{
var batteries = item.GetConnectedComponents<PowerContainer>();
float lowestCharge = 0.0f;
PowerContainer batteryToLoad = null;
foreach (PowerContainer battery in batteries)
@@ -938,15 +938,31 @@ namespace Barotrauma.Items.Components
batteryToLoad = battery;
lowestCharge = battery.Charge;
}
if (battery.Item.ConditionPercentage <= 0 && AIObjectiveRepairItems.IsValidTarget(battery.Item, character))
{
if (battery.Item.Repairables.Average(r => r.DegreeOfSuccess(character)) > 0.4f)
{
objective.AddSubObjective(new AIObjectiveRepairItem(character, battery.Item, objective.objectiveManager, isPriority: true));
return false;
}
else
{
character.Speak(TextManager.Get("DialogSupercapacitorIsBroken"), identifier: "supercapacitorisbroken", minDurationBetweenSimilar: 30.0f);
canShoot = false;
}
}
}
if (batteryToLoad == null) return true;
if (batteryToLoad == null) { return true; }
if (batteryToLoad.RechargeSpeed < batteryToLoad.MaxRechargeSpeed * 0.4f)
{
objective.AddSubObjective(new AIObjectiveOperateItem(batteryToLoad, character, objective.objectiveManager, option: "", requireEquip: false));
return false;
}
if (lowestCharge <= 0 && batteryToLoad.Item.ConditionPercentage > 0)
{
character.Speak(TextManager.Get("DialogTurretHasNoPower"), identifier: "turrethasnopower", minDurationBetweenSimilar: 30.0f);
canShoot = false;
}
}
int usableProjectileCount = 0;
@@ -983,7 +999,7 @@ namespace Barotrauma.Items.Components
{
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f);
character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, formatCapitals: true), identifier: "cannotloadturret", minDurationBetweenSimilar: 30.0f);
}
return true;
}
@@ -993,7 +1009,7 @@ namespace Barotrauma.Items.Components
loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier };
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "loadturret", 30.0f);
character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, formatCapitals: true), identifier: "loadturret", minDurationBetweenSimilar: 30.0f);
}
loadItemsObjective.Abandoned += CheckRemainingAmmo;
loadItemsObjective.Completed += CheckRemainingAmmo;
@@ -1007,11 +1023,11 @@ namespace Barotrauma.Items.Components
int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1);
if (remainingAmmo == 0)
{
character.Speak(TextManager.Get($"DialogOutOf{ammoType}", fallBackTag: "DialogOutOfTurretAmmo"), null, 0.0f, "outofammo", 30.0f);
character.Speak(TextManager.Get($"DialogOutOf{ammoType}", fallBackTag: "DialogOutOfTurretAmmo"), identifier: "outofammo", minDurationBetweenSimilar: 30.0f);
}
else if (remainingAmmo < 3)
{
character.Speak(TextManager.Get($"DialogLowOn{ammoType}"), null, 0.0f, "outofammo", 30.0f);
character.Speak(TextManager.Get($"DialogLowOn{ammoType}"), identifier: "outofammo", minDurationBetweenSimilar: 30.0f);
}
}
}
@@ -1253,12 +1269,15 @@ namespace Barotrauma.Items.Components
return false;
}
}
if (character.IsOnPlayerTeam)
if (canShoot)
{
character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f);
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f);
}
character.SetInput(InputType.Shoot, true, true);
}
aiTargetingGraceTimer = 5f;
character.SetInput(InputType.Shoot, true, true);
return false;
}
@@ -164,10 +164,7 @@ namespace Barotrauma
if (IsInitialized) { return; }
_gender = UnassignedSpritePath.Contains("[GENDER]") ? gender : Gender.None;
ParsePath(false);
if (Sprite != null)
{
Sprite.Remove();
}
Sprite?.Remove();
Sprite = new Sprite(SourceElement, file: SpritePath);
Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true);
HideLimb = SourceElement.GetAttributeBool("hidelimb", false);
@@ -125,8 +125,7 @@ namespace Barotrauma
{
if (!component.AllowInGameEditing) { continue; }
if (component.SerializableProperties.Values.Any(p => p.Attributes.OfType<InGameEditable>().Any())
|| component.SerializableProperties.Values.Any(p => p.Attributes.OfType<ConditionallyEditable>().Any(a => a.IsEditable()))
)
|| component.SerializableProperties.Values.Any(p => p.Attributes.OfType<ConditionallyEditable>().Any(a => a.IsEditable(this))))
{
hasInGameEditableProperties = true;
break;
@@ -198,6 +197,13 @@ namespace Barotrauma
set;
}
[ConditionallyEditable(ConditionallyEditable.ConditionType.IsSwappableItem), Serialize(true, true, alwaysUseInstanceValues: true)]
public bool AllowSwapping
{
get;
set;
}
/// <summary>
/// Checks both <see cref="NonInteractable"/> and <see cref="NonPlayerTeamInteractable"/>
/// </summary>
@@ -1084,10 +1090,7 @@ namespace Barotrauma
public void RemoveContained(Item contained)
{
if (ownInventory != null)
{
ownInventory.RemoveItem(contained);
}
ownInventory?.RemoveItem(contained);
contained.Container = null;
}
@@ -2043,6 +2046,8 @@ namespace Barotrauma
public bool TryInteract(Character picker, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceActionKey = false)
{
if (CampaignInteractionType != CampaignMode.InteractionType.None) { return false; }
bool picked = false, selected = false;
#if CLIENT
bool hasRequiredSkills = true;
@@ -2467,7 +2472,7 @@ namespace Barotrauma
private List<Pair<object, SerializableProperty>> GetInGameEditableProperties()
{
return GetProperties<ConditionallyEditable>()
.Where(ce => ce.Second.GetAttribute<ConditionallyEditable>().IsEditable())
.Where(ce => ce.Second.GetAttribute<ConditionallyEditable>().IsEditable(this))
.Union(GetProperties<InGameEditable>()).ToList();
}
@@ -2746,7 +2751,7 @@ namespace Barotrauma
}
}
}
if (usePrefabValues)
if (usePrefabValues && appliedSwap == null)
{
//use prefab scale when overriding a non-overridden item or vice versa
item.Scale = prefab.ConfigElement.GetAttributeFloat(item.scale, "scale", "Scale");
@@ -2764,22 +2769,34 @@ namespace Barotrauma
}
}
float prevRotation = item.Rotation;
if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); }
if (element.GetAttributeBool("flippedy", false)) { item.FlipY(false); }
item.Rotation = prevRotation;
if (appliedSwap != null && oldPrefab.SwappableItem != null && prefab.SwappableItem != null)
if (appliedSwap != null)
{
Vector2 oldRelativeOrigin = (oldPrefab.SwappableItem.SwapOrigin - oldPrefab.Size / 2) * element.GetAttributeFloat(item.scale, "scale", "Scale");
oldRelativeOrigin.Y = -oldRelativeOrigin.Y;
oldRelativeOrigin = MathUtils.RotatePoint(oldRelativeOrigin, item.rotationRad);
Vector2 oldOrigin = centerPos + oldRelativeOrigin;
item.SpriteDepth = element.GetAttributeFloat("spritedepth", item.SpriteDepth);
item.SpriteColor = element.GetAttributeColor("spritecolor", item.SpriteColor);
item.Rotation = element.GetAttributeFloat("rotation", item.Rotation);
Vector2 relativeOrigin = (prefab.SwappableItem.SwapOrigin - prefab.Size / 2) * item.Scale;
relativeOrigin.Y = -relativeOrigin.Y;
relativeOrigin = MathUtils.RotatePoint(relativeOrigin, item.rotationRad);
Vector2 origin = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + relativeOrigin;
float scaleRelativeToPrefab = element.GetAttributeFloat(item.scale, "scale", "Scale") / oldPrefab.Scale;
item.Scale *= scaleRelativeToPrefab;
item.rect.Location -= (origin - oldOrigin).ToPoint();
if (oldPrefab.SwappableItem != null && prefab.SwappableItem != null)
{
Vector2 oldRelativeOrigin = (oldPrefab.SwappableItem.SwapOrigin - oldPrefab.Size / 2) * element.GetAttributeFloat(item.scale, "scale", "Scale");
oldRelativeOrigin.Y = -oldRelativeOrigin.Y;
oldRelativeOrigin = MathUtils.RotatePoint(oldRelativeOrigin, item.rotationRad);
Vector2 oldOrigin = centerPos + oldRelativeOrigin;
Vector2 relativeOrigin = (prefab.SwappableItem.SwapOrigin - prefab.Size / 2) * item.Scale;
relativeOrigin.Y = -relativeOrigin.Y;
relativeOrigin = MathUtils.RotatePoint(relativeOrigin, item.rotationRad);
Vector2 origin = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + relativeOrigin;
item.rect.Location -= (origin - oldOrigin).ToPoint();
}
}
float condition = element.GetAttributeFloat("condition", item.MaxCondition);
@@ -3,6 +3,7 @@ using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
@@ -39,21 +40,24 @@ namespace Barotrauma
public readonly List<ItemPrefab> ItemPrefabs;
public int Amount;
public readonly float MinCondition;
public readonly float MaxCondition;
public readonly bool UseCondition;
public RequiredItem(ItemPrefab itemPrefab, int amount, float minCondition, bool useCondition)
public RequiredItem(ItemPrefab itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition)
{
ItemPrefabs = new List<ItemPrefab>() { itemPrefab };
Amount = amount;
MinCondition = minCondition;
MaxCondition = maxCondition;
UseCondition = useCondition;
}
public RequiredItem(IEnumerable<ItemPrefab> itemPrefabs, int amount, float minCondition, bool useCondition)
public RequiredItem(IEnumerable<ItemPrefab> itemPrefabs, int amount, float minCondition, float maxCondition, bool useCondition)
{
ItemPrefabs = new List<ItemPrefab>(itemPrefabs);
Amount = amount;
MinCondition = minCondition;
MaxCondition = maxCondition;
UseCondition = useCondition;
}
}
@@ -71,7 +75,7 @@ namespace Barotrauma
{
TargetItem = itemPrefab;
string displayName = element.GetAttributeString("displayname", "");
DisplayName = string.IsNullOrEmpty(displayName) ? itemPrefab.Name : TextManager.Get($"DisplayName.{displayName}");
DisplayName = string.IsNullOrEmpty(displayName) ? itemPrefab.Name : TextManager.GetWithVariable($"DisplayName.{displayName}", "[itemname]", itemPrefab.Name);
SuitableFabricatorIdentifiers = element.GetAttributeStringArray("suitablefabricators", new string[0]);
@@ -107,6 +111,7 @@ namespace Barotrauma
}
float minCondition = subElement.GetAttributeFloat("mincondition", 1.0f);
float maxCondition = subElement.GetAttributeFloat("maxcondition", 1.0f);
//Substract mincondition from required item's condition or delete it regardless?
bool useCondition = subElement.GetAttributeBool("usecondition", true);
int count = subElement.GetAttributeInt("count", 1);
@@ -119,10 +124,12 @@ namespace Barotrauma
continue;
}
var existing = RequiredItems.Find(r => r.ItemPrefabs.Count == 1 && r.ItemPrefabs[0] == requiredItem && MathUtils.NearlyEqual(r.MinCondition, minCondition));
var existing = RequiredItems.Find(r =>
r.ItemPrefabs.Count == 1 && r.ItemPrefabs[0] == requiredItem &&
MathUtils.NearlyEqual(r.MinCondition, minCondition) && MathUtils.NearlyEqual(r.MaxCondition, maxCondition));
if (existing == null)
{
RequiredItems.Add(new RequiredItem(requiredItem, count, minCondition, useCondition));
RequiredItems.Add(new RequiredItem(requiredItem, count, minCondition, maxCondition, useCondition));
}
else
{
@@ -138,10 +145,13 @@ namespace Barotrauma
continue;
}
var existing = RequiredItems.Find(r => r.ItemPrefabs.SequenceEqual(matchingItems) && MathUtils.NearlyEqual(r.MinCondition, minCondition));
var existing = RequiredItems.Find(r =>
r.ItemPrefabs.SequenceEqual(matchingItems) &&
MathUtils.NearlyEqual(r.MinCondition, minCondition) &&
MathUtils.NearlyEqual(r.MaxCondition, maxCondition));
if (existing == null)
{
RequiredItems.Add(new RequiredItem(matchingItems, count, minCondition, useCondition));
RequiredItems.Add(new RequiredItem(matchingItems, count, minCondition, maxCondition, useCondition));
}
else
{
@@ -204,6 +214,8 @@ namespace Barotrauma
public List<(string requiredTag, string swapTo)> ConnectedItemsToSwap = new List<(string requiredTag, string swapTo)>();
public readonly Sprite SchematicSprite;
public int GetPrice(Location location = null)
{
int price = BasePrice;
@@ -221,6 +233,9 @@ namespace Barotrauma
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "schematicsprite":
SchematicSprite = new Sprite(subElement);
break;
case "swapconnecteditem":
ConnectedItemsToSwap.Add(
(subElement.GetAttributeString("tag", ""),
@@ -1203,9 +1218,9 @@ namespace Barotrauma
}
}
public List<Tuple<string, PriceInfo>> GetBuyPricesUnder(int maxCost = 0)
public ImmutableDictionary<string, PriceInfo> GetBuyPricesUnder(int maxCost = 0)
{
List<Tuple<string, PriceInfo>> priceLocations = new List<Tuple<string, PriceInfo>>();
Dictionary<string, PriceInfo> priceLocations = new Dictionary<string, PriceInfo>();
foreach (KeyValuePair<string, PriceInfo> locationPrice in locationPrices)
{
PriceInfo priceInfo = locationPrice.Value;
@@ -1220,19 +1235,19 @@ namespace Barotrauma
}
if (priceInfo.Price < maxCost || maxCost == 0)
{
priceLocations.Add(new Tuple<string, PriceInfo>(locationPrice.Key, priceInfo));
priceLocations.Add(locationPrice.Key, priceInfo);
}
}
return priceLocations;
return priceLocations.ToImmutableDictionary();
}
public List<Tuple<string, PriceInfo>> GetSellPricesOver(int minCost = 0, bool sellingImportant = true)
public ImmutableDictionary<string, PriceInfo> GetSellPricesOver(int minCost = 0, bool sellingImportant = true)
{
List<Tuple<string, PriceInfo>> priceLocations = new List<Tuple<string, PriceInfo>>();
Dictionary<string, PriceInfo> priceLocations = new Dictionary<string, PriceInfo>();
if (!CanBeSold && sellingImportant)
{
return priceLocations;
return priceLocations.ToImmutableDictionary();
}
foreach (KeyValuePair<string, PriceInfo> locationPrice in locationPrices)
@@ -1245,10 +1260,10 @@ namespace Barotrauma
}
if (priceInfo.Price > minCost)
{
priceLocations.Add(new Tuple<string, PriceInfo>(locationPrice.Key, priceInfo));
priceLocations.Add(locationPrice.Key, priceInfo);
}
}
return priceLocations;
return priceLocations.ToImmutableDictionary();
}
public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary)
@@ -25,12 +25,12 @@ namespace Barotrauma
private readonly float screenColorRange, screenColorDuration;
private bool sparks, shockwave, flames, smoke, flash, underwaterBubble;
private bool playTinnitus;
private bool applyFireEffects;
private string[] ignoreFireEffectsForTags;
private bool ignoreCover;
private bool onlyInside;
private bool onlyOutside;
private readonly Color flashColor;
private readonly bool playTinnitus;
private readonly bool applyFireEffects;
private readonly string[] ignoreFireEffectsForTags;
private readonly bool ignoreCover;
private readonly bool onlyInside,onlyOutside;
private readonly float flashDuration;
private readonly float? flashRange;
private readonly string decal;
@@ -81,6 +81,7 @@ namespace Barotrauma
flash = element.GetAttributeBool("flash", true);
flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
flashColor = element.GetAttributeColor("flashcolor", Color.LightYellow);
EmpStrength = element.GetAttributeFloat("empstrength", 0.0f);
BallastFloraDamage = element.GetAttributeFloat("ballastfloradamage", 0.0f);
@@ -395,7 +396,7 @@ namespace Barotrauma
for (int i = 0; i < structure.SectionCount; i++)
{
float distFactor = 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange);
if (distFactor <= 0.0f) continue;
if (distFactor <= 0.0f) { continue; }
structure.AddDamage(i, damage * distFactor, attacker);
@@ -412,6 +413,19 @@ namespace Barotrauma
if (Level.Loaded != null && !MathUtils.NearlyEqual(levelWallDamage, 0.0f))
{
if (Level.Loaded?.LevelObjectManager != null)
{
foreach (var levelObject in Level.Loaded.LevelObjectManager.GetAllObjects(worldPosition, worldRange))
{
if (levelObject.Prefab.TakeLevelWallDamage)
{
float distFactor = 1.0f - (Vector2.Distance(levelObject.WorldPosition, worldPosition) / worldRange);
if (distFactor <= 0.0f) { continue; }
levelObject.AddDamage(levelWallDamage * distFactor, 1.0f, null);
}
}
}
for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--)
{
if (!(Level.Loaded.ExtraWalls[i] is DestructibleLevelWall destructibleWall)) { continue; }
@@ -745,19 +745,22 @@ namespace Barotrauma
Oxygen -= OxygenDeteriorationSpeed * deltaTime;
if ((Character.Controlled?.CharacterHealth?.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
if (FakeFireSources.Count > 0)
{
for (int i = FakeFireSources.Count - 1; i >= 0; i--)
if ((Character.Controlled?.CharacterHealth?.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
{
if (FakeFireSources[i].CausedByPsychosis)
for (int i = FakeFireSources.Count - 1; i >= 0; i--)
{
FakeFireSources[i].Remove();
if (FakeFireSources[i].CausedByPsychosis)
{
FakeFireSources[i].Remove();
}
}
}
FireSource.UpdateAll(FakeFireSources, deltaTime);
}
FireSource.UpdateAll(FireSources, deltaTime);
FireSource.UpdateAll(FakeFireSources, deltaTime);
foreach (Decal decal in decals)
{
@@ -35,7 +35,7 @@ namespace Barotrauma
Cave = 0x4,
Ruin = 0x8,
Wreck = 0x10,
BeaconStation = 0x20,
BeaconStation = 0x20, // Not used anywhere
Abyss = 0x40,
AbyssCave = 0x80
}
@@ -389,7 +389,7 @@ namespace Barotrauma
private void Generate(bool mirror)
{
if (Loaded != null) { Loaded.Remove(); }
Loaded?.Remove();
Loaded = this;
Generating = true;
@@ -1154,7 +1154,7 @@ namespace Barotrauma
}
CreateWrecks();
CreateBeaconStation(cells);
CreateBeaconStation();
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
@@ -3007,20 +3007,30 @@ namespace Barotrauma
return originalTag + "_" + shortSeed;
}
public bool IsCloseToStart(Vector2 position, float minDist) => IsCloseToStart(position.ToPoint(), minDist);
public bool IsCloseToEnd(Vector2 position, float minDist) => IsCloseToEnd(position.ToPoint(), minDist);
public bool IsCloseToStart(Point position, float minDist)
{
return MathUtils.LineSegmentToPointDistanceSquared(StartPosition.ToPoint(), StartExitPosition.ToPoint(), position) < minDist * minDist;
}
public bool IsCloseToEnd(Point position, float minDist)
{
return MathUtils.LineSegmentToPointDistanceSquared(EndPosition.ToPoint(), EndExitPosition.ToPoint(), position) < minDist * minDist;
}
private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type)
{
var tempSW = new Stopwatch();
// Min distance between a sub and the start/end/other sub.
float minDistance = Sonar.DefaultSonarRange;
float squaredMinDistance = minDistance * minDistance;
Vector2 start = startPosition.ToVector2();
Vector2 end = endPosition.ToVector2();
var waypoints = WayPoint.WayPointList.Where(wp =>
wp.Submarine == null &&
wp.SpawnType == SpawnType.Path &&
Vector2.DistanceSquared(wp.WorldPosition, start) > squaredMinDistance &&
Vector2.DistanceSquared(wp.WorldPosition, end) > squaredMinDistance).ToList();
!IsCloseToStart(wp.WorldPosition, minDistance) &&
!IsCloseToEnd(wp.WorldPosition, minDistance)).ToList();
var subDoc = SubmarineInfo.OpenFile(contentFile.Path);
Rectangle subBorders = Submarine.GetBorders(subDoc.Root);
@@ -3163,7 +3173,7 @@ namespace Barotrauma
else
{
var sp = spawnPoint;
if (Wrecks.Any(w => Vector2.DistanceSquared(w.WorldPosition, sp) < squaredMinDistance))
if (Wrecks.Any(w => Vector2.DistanceSquared(w.WorldPosition, sp) < minDistance * minDistance))
{
Debug.WriteLine($"Invalid position {spawnPoint}. Too close to other wreck(s).");
return false;
@@ -3321,7 +3331,13 @@ namespace Barotrauma
int minWreckCount = Math.Min(Loaded.GenerationParams.MinWreckCount, wreckFiles.Count);
int maxWreckCount = Math.Min(Loaded.GenerationParams.MaxWreckCount, wreckFiles.Count);
int wreckCount = Rand.Range(minWreckCount, maxWreckCount, Rand.RandSync.Server);
int wreckCount = Rand.Range(minWreckCount, maxWreckCount + 1, Rand.RandSync.Server);
if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireWreck) ?? false)
{
wreckCount = Math.Max(wreckCount, 1);
}
Wrecks = new List<Submarine>(wreckCount);
for (int i = 0; i < wreckCount; i++)
{
@@ -3379,7 +3395,7 @@ namespace Barotrauma
for (int i = 0; i < 2; i++)
{
if (GameMain.GameSession.GameMode is PvPMode) { continue; }
if (GameMain.GameSession?.GameMode is PvPMode) { continue; }
bool isStart = (i == 0) == !Mirrored;
if (isStart)
@@ -3561,7 +3577,7 @@ namespace Barotrauma
}
}
private void CreateBeaconStation(List<VoronoiCell> mainPath)
private void CreateBeaconStation()
{
if (!LevelData.HasBeaconStation) { return; }
var beaconStationFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.BeaconStation).ToList();
@@ -8,7 +8,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
partial class LevelObject : ISpatialEntity
partial class LevelObject : ISpatialEntity, IDamageable, ISerializableEntity
{
public readonly LevelObjectPrefab Prefab;
public Vector3 Position;
@@ -21,6 +21,8 @@ namespace Barotrauma
private int spriteIndex;
protected bool tookDamage;
public LevelObjectPrefab ActivePrefab;
public PhysicsBody PhysicsBody
@@ -37,11 +39,15 @@ namespace Barotrauma
public bool NeedsNetworkSyncing
{
get { return Triggers != null && Triggers.Any(t => t.NeedsNetworkSyncing); }
get
{
return tookDamage || (Triggers != null && Triggers.Any(t => t.NeedsNetworkSyncing));
}
set
{
if (Triggers == null) { return; }
Triggers.ForEach(t => t.NeedsNetworkSyncing = false);
Triggers.ForEach(t => t.NeedsNetworkSyncing = false);
tookDamage = false;
}
}
@@ -50,6 +56,12 @@ namespace Barotrauma
get; private set;
}
public float Health
{
get;
private set;
}
public Sprite Sprite
{
get
@@ -67,6 +79,10 @@ namespace Barotrauma
public Submarine Submarine => null;
public string Name => Prefab?.Name ?? "LevelObject (null)";
public Dictionary<string, SerializableProperty> SerializableProperties { get; } = new Dictionary<string, SerializableProperty>();
public Level.Cave ParentCave;
public LevelObject(LevelObjectPrefab prefab, Vector3 position, float scale, float rotation = 0.0f)
@@ -75,6 +91,7 @@ namespace Barotrauma
Position = position;
Scale = scale;
Rotation = rotation;
Health = prefab.Health;
spriteIndex = ActivePrefab.Sprites.Any() ? Rand.Int(ActivePrefab.Sprites.Count, Rand.RandSync.Server) : -1;
@@ -89,10 +106,13 @@ namespace Barotrauma
if (PhysicsBody != null)
{
PhysicsBody.UserData = this;
PhysicsBody.SetTransformIgnoreContacts(PhysicsBody.SimPosition, -Rotation);
PhysicsBody.BodyType = BodyType.Static;
PhysicsBody.CollisionCategories = Physics.CollisionLevel;
PhysicsBody.CollidesWith = Physics.CollisionWall | Physics.CollisionCharacter;
PhysicsBody.CollidesWith = Prefab.TakeLevelWallDamage?
Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionProjectile :
Physics.CollisionWall | Physics.CollisionCharacter;
}
foreach (XElement triggerElement in prefab.LevelTriggerElements)
@@ -111,6 +131,10 @@ namespace Barotrauma
}
var newTrigger = new LevelTrigger(triggerElement, new Vector2(position.X, position.Y) + triggerPosition, -rotation, scale, prefab.Name);
if (newTrigger.PhysicsBody != null)
{
newTrigger.PhysicsBody.UserData = this;
}
int parentTriggerIndex = prefab.LevelTriggerElements.IndexOf(triggerElement.Parent);
if (parentTriggerIndex > -1) { newTrigger.ParentTrigger = Triggers[parentTriggerIndex]; }
Triggers.Add(newTrigger);
@@ -135,7 +159,54 @@ namespace Barotrauma
}
partial void InitProjSpecific();
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true)
{
if (Health <= 0.0f) { return new AttackResult(0.0f); }
float damage = 0.0f;
if (Prefab.TakeLevelWallDamage)
{
damage += attack.GetLevelWallDamage(deltaTime);
}
damage = Math.Max(Health, damage);
AddDamage(damage, deltaTime, attacker);
return new AttackResult(damage);
}
public void AddDamage(float damage, float deltaTime, Entity attacker, bool isNetworkEvent = false)
{
if (Health <= 0.0f) { return; }
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkEvent)
{
return;
}
tookDamage |= !MathUtils.NearlyEqual(damage, 0.0f);
Health -= damage;
if (Health <= 0.0f)
{
#if CLIENT
if (GameMain.GameSession?.Level?.LevelObjectManager != null)
{
GameMain.GameSession.Level.LevelObjectManager.ForceRefreshVisibleObjects = true;
}
#endif
if (PhysicsBody != null)
{
PhysicsBody.Enabled = false;
}
foreach (LevelTrigger trigger in Triggers)
{
trigger.PhysicsBody.Enabled = false;
foreach (StatusEffect effect in trigger.StatusEffects)
{
if (effect.type != ActionType.OnBroken) { continue; }
effect.Apply(effect.type, deltaTime, attacker, this, worldPosition: WorldPosition);
}
}
}
}
public Vector2 LocalToWorld(Vector2 localPosition, float swingState = 0.0f)
{
Vector2 emitterPos = localPosition * Scale;
@@ -169,6 +240,10 @@ namespace Barotrauma
public void ServerWrite(IWriteMessage msg, Client c)
{
if (Triggers == null) { return; }
if (Prefab.TakeLevelWallDamage)
{
msg.WriteRangedSingle(MathHelper.Clamp(Health, 0.0f, Prefab.Health), 0.0f, Prefab.Health, 8);
}
for (int j = 0; j < Triggers.Count; j++)
{
if (!Triggers[j].UseNetworkSyncing) { continue; }
@@ -136,27 +136,18 @@ namespace Barotrauma
if (prefab == null) { continue; }
if (!suitableSpawnPositions.ContainsKey(prefab))
{
float minDistance = level.Size.X * 0.2f;
suitableSpawnPositions.Add(prefab,
availableSpawnPositions.Where(sp =>
sp.SpawnPosTypes.Any(type => prefab.SpawnPos.HasFlag(type)) &&
sp.Length >= prefab.MinSurfaceWidth &&
(prefab.AllowAtStart || !closeToStart(sp.GraphEdge.Center)) &&
(prefab.AllowAtEnd || !closeToEnd(sp.GraphEdge.Center)) &&
(prefab.AllowAtStart || !level.IsCloseToStart(sp.GraphEdge.Center, minDistance)) &&
(prefab.AllowAtEnd || !level.IsCloseToEnd(sp.GraphEdge.Center, minDistance)) &&
(sp.Alignment == Alignment.Any || prefab.Alignment.HasFlag(sp.Alignment))).ToList());
spawnPositionWeights.Add(prefab,
suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList());
bool closeToStart(Vector2 position)
{
float minDist = level.Size.X * 0.2f;
return MathUtils.LineSegmentToPointDistanceSquared(level.StartPosition.ToPoint(), level.StartExitPosition.ToPoint(), position.ToPoint()) < minDist * minDist;
}
bool closeToEnd(Vector2 position)
{
float minDist = level.Size.X * 0.2f;
return MathUtils.LineSegmentToPointDistanceSquared(level.EndPosition.ToPoint(), level.EndExitPosition.ToPoint(), position.ToPoint()) < minDist * minDist;
}
}
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server);
@@ -442,10 +433,10 @@ namespace Barotrauma
public IEnumerable<LevelObject> GetAllObjects(Vector2 worldPosition, float radius)
{
var minIndices = GetGridIndices(worldPosition - Vector2.One * radius);
if (minIndices.X >= objectGrid.GetLength(0) || minIndices.Y >= objectGrid.GetLength(1)) return Enumerable.Empty<LevelObject>();
if (minIndices.X >= objectGrid.GetLength(0) || minIndices.Y >= objectGrid.GetLength(1)) { return Enumerable.Empty<LevelObject>(); }
var maxIndices = GetGridIndices(worldPosition + Vector2.One * radius);
if (maxIndices.X < 0 || maxIndices.Y < 0) return Enumerable.Empty<LevelObject>();
if (maxIndices.X < 0 || maxIndices.Y < 0) { return Enumerable.Empty<LevelObject>(); }
minIndices.X = Math.Max(0, minIndices.X);
minIndices.Y = Math.Max(0, minIndices.Y);
@@ -460,6 +451,7 @@ namespace Barotrauma
if (objectGrid[x, y] == null) { continue; }
foreach (LevelObject obj in objectGrid[x, y])
{
if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; }
objectsInRange.Add(obj);
}
}
@@ -522,6 +514,7 @@ namespace Barotrauma
obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval;
}
}
if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; }
if (obj.Triggers != null)
{
@@ -264,6 +264,27 @@ namespace Barotrauma
private set;
}
[Serialize(false, true, description: "Can the object take damage from weapons/attacks that damage level walls."), Editable]
public bool TakeLevelWallDamage
{
get;
private set;
}
[Serialize(false, true), Editable]
public bool HideWhenBroken
{
get;
private set;
}
[Serialize(100.0f, true), Editable]
public float Health
{
get;
private set;
}
public string Identifier
{
get;
@@ -38,6 +38,10 @@ namespace Barotrauma
/// Effects applied to entities that are inside the trigger
/// </summary>
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
public IEnumerable<StatusEffect> StatusEffects
{
get { return statusEffects; }
}
/// <summary>
/// Attacks applied to entities that are inside the trigger
@@ -205,7 +209,7 @@ namespace Barotrauma
PhysicsBody = new PhysicsBody(element, scale)
{
CollisionCategories = Physics.CollisionLevel,
CollidesWith = Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionProjectile | Physics.CollisionWall
CollidesWith = Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionProjectile | Physics.CollisionWall,
};
PhysicsBody.FarseerBody.OnCollision += PhysicsBody_OnCollision;
PhysicsBody.FarseerBody.OnSeparation += PhysicsBody_OnSeparation;
@@ -523,6 +527,7 @@ namespace Barotrauma
{
foreach (StatusEffect effect in statusEffects)
{
if (effect.type == ActionType.OnBroken) { continue; }
Vector2? position = null;
if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = WorldPosition; }
if (triggerer is Character character)
@@ -181,27 +181,50 @@ namespace Barotrauma
}
}
public Mission SelectedMission
private readonly List<Mission> selectedMissions = new List<Mission>();
public IEnumerable<Mission> SelectedMissions
{
get;
set;
get { return selectedMissions; }
}
public int SelectedMissionIndex
public void SelectMission(Mission mission)
{
get
if (!SelectedMissions.Contains(mission) && mission != null)
{
if (SelectedMission == null) { return -1; }
return availableMissions.IndexOf(SelectedMission);
selectedMissions.Add(mission);
}
set
}
public void DeselectMission(Mission mission)
{
selectedMissions.Remove(mission);
}
public List<int> GetSelectedMissionIndices()
{
List<int> selectedMissionIndices = new List<int>();
foreach (Mission mission in SelectedMissions)
{
if (value < 0 || value >= AvailableMissions.Count())
if (availableMissions.Contains(mission))
{
SelectedMission = null;
return;
selectedMissionIndices.Add(availableMissions.IndexOf(mission));
}
SelectedMission = availableMissions[value];
}
return selectedMissionIndices;
}
public void SetSelectedMissionIndices(IEnumerable<int> missionIndices)
{
selectedMissions.Clear();
foreach (int missionIndex in missionIndices)
{
if (missionIndex < 0 || missionIndex >= availableMissions.Count)
{
DebugConsole.ThrowError($"Failed to select a mission in location \"{Name}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})");
break;
}
selectedMissions.Add(availableMissions[missionIndex]);
}
}
@@ -415,6 +438,12 @@ namespace Barotrauma
{
if (newType == Type) { return; }
if (newType == null)
{
DebugConsole.ThrowError($"Failed to change the type of the location \"{Name}\" to null.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType);
Type = newType;
@@ -573,6 +602,7 @@ namespace Barotrauma
public void InstantiateLoadedMissions(Map map)
{
availableMissions.Clear();
selectedMissions.Clear();
if (loadedMissions != null && loadedMissions.Any())
{
foreach (LoadedMission loadedMission in loadedMissions)
@@ -588,7 +618,7 @@ namespace Barotrauma
}
var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub);
availableMissions.Add(mission);
if (loadedMission.SelectedMission) { SelectedMission = mission; }
if (loadedMission.SelectedMission) { selectedMissions.Add(mission); }
}
loadedMissions = null;
}
@@ -612,7 +642,7 @@ namespace Barotrauma
public void ClearMissions()
{
availableMissions.Clear();
SelectedMissionIndex = -1;
selectedMissions.Clear();
}
public bool HasOutpost()
@@ -1180,7 +1210,7 @@ namespace Barotrauma
missionsElement.Add(new XElement("mission",
new XAttribute("prefabid", mission.Prefab.Identifier),
new XAttribute("destinationindex", i),
new XAttribute("selected", mission == SelectedMission)));
new XAttribute("selected", selectedMissions.Contains(mission))));
}
locationElement.Add(missionsElement);
}
@@ -24,7 +24,7 @@ namespace Barotrauma
/// From -> To
/// </summary>
public Action<Location, Location> OnLocationChanged;
public Action<LocationConnection, Mission> OnMissionSelected;
public Action<LocationConnection, IEnumerable<Mission>> OnMissionsSelected;
public Location EndLocation { get; private set; }
@@ -44,9 +44,9 @@ namespace Barotrauma
get { return Locations.IndexOf(SelectedLocation); }
}
public int SelectedMissionIndex
public IEnumerable<int> GetSelectedMissionIndices()
{
get { return SelectedConnection == null ? -1 : CurrentLocation.SelectedMissionIndex; }
return SelectedConnection == null ? Enumerable.Empty<int>() : CurrentLocation.GetSelectedMissionIndices();
}
public LocationConnection SelectedConnection { get; private set; }
@@ -388,8 +388,14 @@ namespace Barotrauma
}
}
LocationConnection[] connectionsBetweenZones = new LocationConnection[generationParams.DifficultyZones];
foreach (var connection in Connections)
List<LocationConnection>[] connectionsBetweenZones = new List<LocationConnection>[generationParams.DifficultyZones];
for (int i = 0; i < generationParams.DifficultyZones; i++)
{
connectionsBetweenZones[i] = new List<LocationConnection>();
}
var shuffledConnections = Connections.ToList();
shuffledConnections.Shuffle(Rand.RandSync.Server);
foreach (var connection in shuffledConnections)
{
int zone1 = GetZoneIndex(connection.Locations[0].MapPosition.X);
int zone2 = GetZoneIndex(connection.Locations[1].MapPosition.X);
@@ -401,17 +407,25 @@ namespace Barotrauma
zone1 = temp;
}
if (connectionsBetweenZones[zone1] == null)
if (generationParams.GateCount[zone1] == 0) { continue; }
if (!connectionsBetweenZones[zone1].Any())
{
connectionsBetweenZones[zone1] = connection;
connectionsBetweenZones[zone1].Add(connection);
}
else
else if (generationParams.GateCount[zone1] == 1)
{
if (Math.Abs(connection.CenterPos.Y - Height / 2) < Math.Abs(connectionsBetweenZones[zone1].CenterPos.Y - Height / 2))
//if there's only one connection, place it at the center of the map
if (Math.Abs(connection.CenterPos.Y - Height / 2) < Math.Abs(connectionsBetweenZones[zone1].First().CenterPos.Y - Height / 2))
{
connectionsBetweenZones[zone1] = connection;
connectionsBetweenZones[zone1].Clear();
connectionsBetweenZones[zone1].Add(connection);
}
}
else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1])
{
connectionsBetweenZones[zone1].Add(connection);
}
}
for (int i = Connections.Count - 1; i >= 0; i--)
@@ -421,7 +435,9 @@ namespace Barotrauma
if (zone1 == zone2) { continue; }
if (zone1 == generationParams.DifficultyZones || zone2 == generationParams.DifficultyZones) { continue; }
if (!connectionsBetweenZones.Contains(Connections[i]))
if (generationParams.GateCount[Math.Min(zone1, zone2)] == 0) { continue; }
if (!connectionsBetweenZones[Math.Min(zone1, zone2)].Contains(Connections[i]))
{
Connections.RemoveAt(i);
}
@@ -756,7 +772,7 @@ namespace Barotrauma
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
}
public void SelectMission(int missionIndex)
public void SelectMission(IEnumerable<int> missionIndices)
{
if (CurrentLocation == null)
{
@@ -765,23 +781,24 @@ namespace Barotrauma
GameAnalyticsManager.AddErrorEventOnce("Map.SelectMission:CurrentLocationNotSet", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
CurrentLocation.SelectedMissionIndex = missionIndex;
if (CurrentLocation.SelectedMission == null) { return; }
CurrentLocation.SetSelectedMissionIndices(missionIndices);
if (CurrentLocation.SelectedMission.Locations[0] != CurrentLocation ||
CurrentLocation.SelectedMission.Locations[1] != CurrentLocation)
foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList())
{
if (SelectedConnection == null) { return; }
//the destination must be the same as the destination of the mission
if (CurrentLocation.SelectedMission != null &&
CurrentLocation.SelectedMission.Locations[1] != SelectedLocation)
if (selectedMission.Locations[0] != CurrentLocation ||
selectedMission.Locations[1] != CurrentLocation)
{
CurrentLocation.SelectedMissionIndex = -1;
if (SelectedConnection == null) { return; }
//the destination must be the same as the destination of the mission
if (selectedMission.Locations[1] != SelectedLocation)
{
CurrentLocation.DeselectMission(selectedMission);
}
}
}
OnMissionSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMission);
OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions);
}
public void SelectRandomLocation(bool preferUndiscovered)
@@ -977,6 +994,12 @@ namespace Barotrauma
string prevName = location.Name;
var newType = LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase));
if (newType == null)
{
DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found.");
return;
}
if (newType.OutpostTeam != location.Type.OutpostTeam ||
newType.HasOutpost != location.Type.HasOutpost)
{
@@ -67,6 +67,8 @@ namespace Barotrauma
[Serialize(0.1f, true, description: "ConnectionDisplacementMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)]
public float ConnectionIndicatorDisplacementMultiplier { get; set; }
public int[] GateCount { get; private set; }
#if CLIENT
[Serialize(0.75f, true), Editable(DecimalCount = 2)]
@@ -201,6 +203,16 @@ namespace Barotrauma
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
GateCount = element.GetAttributeIntArray("gatecount", null) ?? element.GetAttributeIntArray("GateCount", null);
if (GateCount == null)
{
GateCount = new int[DifficultyZones];
for (int i = 0; i < DifficultyZones; i++)
{
GateCount[i] = 1;
}
}
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -420,7 +420,7 @@ namespace Barotrauma
if (cloneItem == null) { continue; }
var door = cloneItem.GetComponent<Door>();
if (door != null) { door.RefreshLinkedGap(); }
door?.RefreshLinkedGap();
var cloneWire = cloneItem.GetComponent<Wire>();
if (cloneWire == null) continue;
@@ -1447,7 +1447,7 @@ namespace Barotrauma
}
else
{
npc.CharacterHealth.MaxVitality *= humanPrefab.HealthMultiplier;
npc.AddStaticHealthMultiplier(humanPrefab.HealthMultiplier);
}
humanPrefab.GiveItems(npc, outpost, Rand.RandSync.Server);
foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true))
@@ -353,10 +353,7 @@ namespace Barotrauma
}
#if CLIENT
if (convexHulls!=null)
{
convexHulls.ForEach(x => x.Move(amount));
}
convexHulls?.ForEach(x => x.Move(amount));
#endif
}
@@ -77,6 +77,7 @@ namespace Barotrauma
private static Vector2 lastPickedPosition;
private static float lastPickedFraction;
private static Fixture lastPickedFixture;
private static Vector2 lastPickedNormal;
private Vector2 prevPosition;
@@ -99,6 +100,11 @@ namespace Barotrauma
get { return lastPickedFraction; }
}
public static Fixture LastPickedFixture
{
get { return lastPickedFixture; }
}
public static Vector2 LastPickedNormal
{
get { return lastPickedNormal; }
@@ -669,6 +675,7 @@ namespace Barotrauma
float closestFraction = 1.0f;
Vector2 closestNormal = Vector2.Zero;
Fixture closestFixture = null;
Body closestBody = null;
if (allowInsideFixture)
{
@@ -682,13 +689,15 @@ namespace Barotrauma
closestFraction = 0.0f;
closestNormal = Vector2.Normalize(rayEnd - rayStart);
if (fixture.Body != null) closestBody = fixture.Body;
closestFixture = fixture;
if (fixture.Body != null) { closestBody = fixture.Body; }
return false;
}, ref aabb);
if (closestFraction <= 0.0f)
{
lastPickedPosition = rayStart;
lastPickedFraction = closestFraction;
lastPickedFixture = closestFixture;
lastPickedNormal = closestNormal;
return closestBody;
}
@@ -702,6 +711,7 @@ namespace Barotrauma
{
closestFraction = fraction;
closestNormal = normal;
closestFixture = fixture;
if (fixture.Body != null) closestBody = fixture.Body;
}
return fraction;
@@ -709,6 +719,7 @@ namespace Barotrauma
lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction;
lastPickedFraction = closestFraction;
lastPickedFixture = closestFixture;
lastPickedNormal = closestNormal;
return closestBody;
@@ -752,6 +763,7 @@ namespace Barotrauma
lastPickedPosition = rayStart + (rayEnd - rayStart) * fraction;
lastPickedFraction = fraction;
lastPickedNormal = normal;
lastPickedFixture = fixture;
}
//continue
return -1;
@@ -772,6 +784,7 @@ namespace Barotrauma
lastPickedPosition = rayStart;
lastPickedFraction = 0.0f;
lastPickedNormal = Vector2.Normalize(rayEnd - rayStart);
lastPickedFixture = fixture;
bodies.Add(fixture.Body);
bodyDist[fixture.Body] = 0.0f;
return false;
@@ -828,6 +841,7 @@ namespace Barotrauma
{
Body closestBody = null;
float closestFraction = 1.0f;
Fixture closestFixture = null;
Vector2 closestNormal = Vector2.Zero;
if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.01f)
@@ -847,6 +861,8 @@ namespace Barotrauma
if (ignoreSubs && fixture.Body.UserData is Submarine) { return -1; }
if (ignoreBranches && fixture.Body.UserData is VineTile) { return -1; }
if (fixture.Body.UserData as string == "ruinroom") { return -1; }
//the hulls have solid fixtures in the submarine's world space collider, ignore them
if (fixture.UserData is Hull) { return -1; }
if (fixture.Body.UserData is Structure structure)
{
if (structure.IsPlatform || structure.StairDirection != Direction.None) { return -1; }
@@ -861,6 +877,7 @@ namespace Barotrauma
{
closestBody = fixture.Body;
closestFraction = fraction;
closestFixture = fixture;
closestNormal = normal;
}
return closestFraction;
@@ -870,6 +887,7 @@ namespace Barotrauma
lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction;
lastPickedFraction = closestFraction;
lastPickedFixture = closestFixture;
lastPickedNormal = closestNormal;
return closestBody;
}
@@ -944,19 +962,23 @@ namespace Barotrauma
mapEntity.Move(HiddenSubPosition);
}
foreach (Item item in Item.ItemList)
for (int i = 0; i < 2; i++)
{
if (bodyItems.Contains(item))
foreach (Item item in Item.ItemList)
{
item.Submarine = this;
if (Position == Vector2.Zero) item.Move(-HiddenSubPosition);
//two passes: flip docking ports on the 2nd pass because the doors need to be correctly flipped for the port's orientation to be determined correctly
if ((item.GetComponent<DockingPort>() != null) == (i == 0)) { continue; }
if (bodyItems.Contains(item))
{
item.Submarine = this;
if (Position == Vector2.Zero) { item.Move(-HiddenSubPosition); }
}
else if (item.Submarine != this)
{
continue;
}
item.FlipX(true);
}
else if (item.Submarine != this)
{
continue;
}
item.FlipX(true);
}
Item.UpdateHulls();
@@ -79,7 +79,6 @@ namespace Barotrauma.Networking
TRAITOR_MESSAGE,
MISSION,
EVENTACTION,
RESET_UPGRADES, //inform the clients that the upgrades on the submarine have been reset
CREW, //anything related to managing bots in multiplayer
READY_CHECK //start, end and update a ready check
}
@@ -186,11 +186,7 @@ namespace Barotrauma.Networking
if (updateReturnTimer > 1.0f)
{
updateReturnTimer = 0.0f;
if (shuttleSteering != null)
{
shuttleSteering.SetDestinationLevelStart();
}
shuttleSteering?.SetDestinationLevelStart();
UpdateReturningProjSpecific(deltaTime);
}
}
@@ -69,21 +69,24 @@ namespace Barotrauma
this.conditionType = conditionType;
}
private ConditionType conditionType;
private readonly ConditionType conditionType;
public enum ConditionType
{
//These need to exist at compile time, so it is a little awkward
//I would love to see a better way to do this
AllowLinkingWifiToChat
AllowLinkingWifiToChat,
IsSwappableItem
}
public bool IsEditable()
public bool IsEditable(ISerializableEntity entity)
{
switch (conditionType)
{
case ConditionType.AllowLinkingWifiToChat:
return GameMain.NetworkMember?.ServerSettings?.AllowLinkingWifiToChat ?? true;
case ConditionType.IsSwappableItem:
return entity is Item item && item.Prefab.SwappableItem != null;
}
return false;
}
@@ -766,6 +769,10 @@ namespace Barotrauma
if (entity.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property))
{
FixValue(property, entity, attribute);
if (property.Name == nameof(ItemComponent.Msg) && entity is ItemComponent component)
{
component.ParseMsg();
}
}
else if (entity is Item item1)
{
@@ -774,6 +781,10 @@ namespace Barotrauma
if (component.SerializableProperties.TryGetValue(attributeName, out SerializableProperty componentProperty))
{
FixValue(componentProperty, component, attribute);
if (componentProperty.Name == nameof(ItemComponent.Msg))
{
((ItemComponent)component).ParseMsg();
}
}
}
}
@@ -28,7 +28,7 @@ namespace Barotrauma
return spr;
}
return null;
}).Where(s => s!=null).ToList();
}).Where(s => s != null).ToList();
}
return retVal;
}
@@ -46,6 +46,8 @@ namespace Barotrauma
{
var value = (float) OriginalValue;
if (level == 0) { return value; }
if (Multiplier[^1] != '%')
{
float multiplier = ParseValue();
@@ -0,0 +1,83 @@
using System.Collections.Generic;
using System.Threading;
namespace Barotrauma
{
public static class CrossThread
{
public delegate void TaskDelegate();
private class Task
{
public TaskDelegate Deleg;
public ManualResetEvent Mre;
public bool Done;
public Task(TaskDelegate d)
{
Deleg = d;
Mre = new ManualResetEvent(false);
Done = false;
}
public void PerformWait()
{
if (!Done) { Mre.WaitOne(); }
}
}
private static List<Task> enqueuedTasks;
static CrossThread() { enqueuedTasks = new List<Task>(); }
public static void ProcessTasks()
{
lock (enqueuedTasks)
{
foreach (var task in enqueuedTasks)
{
task.Deleg();
task.Mre.Set();
task.Done = true;
}
enqueuedTasks.Clear();
}
}
public static void RequestExecutionOnMainThread(TaskDelegate deleg)
{
if (GameMain.MainThread == null || Thread.CurrentThread == GameMain.MainThread)
{
deleg();
}
else
{
Task newTask = new Task(deleg);
lock (enqueuedTasks)
{
enqueuedTasks.Add(newTask);
}
newTask.PerformWait();
}
}
public static void AddOnMainThread<T>(this List<T> list, T element)
{
RequestExecutionOnMainThread(() => { list.Add(element); });
}
public static void AddRangeOnMainThread<T>(this List<T> list, IEnumerable<T> elements)
{
RequestExecutionOnMainThread(() => { list.AddRange(elements); });
}
public static void RemoveOnMainThread<T>(this List<T> list, T element)
{
RequestExecutionOnMainThread(() => { list.Remove(element); });
}
public static void ClearOnMainThread<T>(this List<T> list)
{
RequestExecutionOnMainThread(() => { list.Clear(); });
}
}
}