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
@@ -257,5 +257,7 @@
<OutpostModule file="Content/Map/Outposts/MineModule_04.sub" />
<OutpostModule file="Content/Map/Outposts/HallModuleHorizontal_Abandoned.sub" />
<OutpostModule file="Content/Map/Outposts/HallModuleVertical_Abandoned.sub" />
<EnemySubmarine file="Content/Map/EnemySubmarines/DugongPirate.sub"/>
<EnemySubmarine file="Content/Map/EnemySubmarines/HumpbackPirate.sub"/>
<EnemySubmarine file="Content/Map/EnemySubmarines/Typhon2Pirate.sub"/>
</contentpackage>
@@ -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(); });
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+75
View File
@@ -1,3 +1,78 @@
------------------------------------------------------------------------------------------------------
v0.1400.1.0 (unstable)
---------------------------------------------------------------------------------------------------------
Changes:
- Improvements/balancing to the pulse laser and chaingun.
- Option to choose multiple missions per level.
- Added some hardpoints to the vanilla subs.
- Replaced the old railgun loaders with the new smaller versions.
- Improvements to the weapon customization menu.
- Docking ports can't be rewired in outpost levels because it can be abused to force to submarine to depart from the outpost.
- The ice shards and exploding mushrooms in caves can be destroyed with weapons and explosives.
- The pirate missions can no longer be completed by exploding the reactor: all the pirates need to be killed for the mission to be successful.
- Changed the way diving suits are picked up: now LMB equips the suit like it did before, and the suits can be picked up with E.
- Taking items that contain stolen items counts as stealing, so you can't for example put a toolbox inside an outpost cabinet, load it full of items and then take it.
- Added a button for resetting in-game hints to the settings.
- Mention diving suits' depth limits in their descriptions.
- Modified the locked path tooltips to mention the officer in the outpost.
- Reset reactor fission rate, turbine output and temperature to optimal levels at the start of a round. Prevents the reactor from catching fire at the start of a round if it was being operated at a high fission rate at the end of the previous round.
- Increased canister shell pellet count from 7 to 10, increased pellet damage from 22 to 25, reduced spread from 25 to 20.
- Option to disable swapping specific weapons in the sub editor.
- Items that are set to be hidden in-game can't be swapped.
- Removed ability to drag and drop seeds into planters.
- Added ability to load .sub, .xml, .png and .jpeg files in sub editor by dragging and dropping them onto the game window.
- Added new biome-specific background noise tracks.
- Made upgrades affect all submarines. Old purchased submarines will have their upgrades overridden by the currently loaded submarine's upgrades.
- Improved the command interface minimap view by adding connector lines between icons and item positions to better visualize which item each icon is linked to.
- Projectiles go through severed limbs.
Fixes:
- Fixed rotation of mirrored items getting messed up when saving and reloading a sub.
- Fixed inability to detach items attached outside the sub.
- Removed colliders from decorative pirate submarine pieces.
- Fixed pirate bandana sprite.
- Fixed depth decoy sprite.
- Fixed items that are inside a hull being difficult to target from outside the sub (docking ports/hatches in particular).
- Fixed crashing when trying to load a campaign save that contains pets that can't be found (e.g. if you've saved while using a mod that adds custom pets and try to load the save in the vanilla game).
- Fixed crashing when trying to change a location's type to a type that can't be found (e.g. if a mod includes custom location types which are configured incorrectly).
- Fixed "are you sure you want to depart without a mission" prompt popping up even if there's no missions available.
- Fixed pirate missions not ending automatically when docking with the destination outpost in mission mode.
- Fixed client-side "index out of range" errors when a pirate gets assigned the "navigatetactical" order.
- Fixed health bars showing up through walls.
- Fixed main sub's docking ports not showing up on the respawn shuttle's sonar.
- Fixed abandoned outposts' docking ports not showing up on sonar.
- Fixed handheld sonar showing the enemy sub in PvP mode when used outside the main sub.
- Fixed how weapons are displayed on the outpost display shelves.
- Fixed location portrait overlapping with the texts in the tab menu's mission tab.
- Fixed horizontal docking ports sometimes docking from the wrong side in mirrored subs.
- Fixed mouse wheel zooming the nav terminal view when the server log (or any other UI element) is blocking it.
- Fixed chainguns not damaging items (e.g. eggs, piezo crystals).
- Fixed Watcher always spawning at the start when the difficulty is between 10 and 20.
- Fixed wrecks never spawning in certain biomes, even if there's a wreck mission selected.
- Fixed legacy railgun loaders exploding the shells.
- Fixed campaign's initial text popup getting stuck if the sub automatically undocks at the start of a round.
- Swapped weapons inherit the scale, rotation, sprite depth and sprite color from the previously installed item.
- Non-empty items (ammo boxes, fuel rods, etc) can't be recycled.
- Fixed afflictions applying their status effects multiple times, causing afflictions that apply other afflictions way faster than they should (e.g. opiate withdrawal caused by opiate addiction, burns caused by radiation sickness).
- Fixed all sonar markers of a given mission displaying the same distance reading.
- Fixed command interface minimap tooltips not being displayed.
- Fixed bot orders not being saved in multiplayer.
- Disabled the tab menu missions tab in the test game modes (e.g. sub editor) to fix a related crash.
- Made airlock door assembly behave a bit more reliably. The circuit in the assembly simply toggles the state of both doors, meaning that one of the doors needs to always be closed and the other open for the logic to work correctly. If using the assembly in a respawn shuttle, it'd break when the shuttle leaves and it's doors are forced to close.
- Fixed inability to go through docking ports when the door/hatch at the other side is broken.
- Fixed other entities' sprites disappearing when reloading a sprite in the sub editor.
- Fixed Remora drone's docking hatch being repairable with a welding tool instead of a wrench.
- Fixed some of Remora drone's walls being transparent.
Modding:
- Made it possible to use StatusHUD components in non-equippable items like turrets.
- Fixed scripted event's StatusEffectAction not being able to target ItemComponents.
- Added support for making triggering scripted events.
- Option to configure the color of an explosion's flash.
- Option to configure the number of gates between biomes (see MapGenerationParameters.xml).
- Fixed "publishing [mod] in the steam workshop failed" error when trying to publish a mod that contains enemy subs.
------------------------------------------------------------------------------------------------------
v0.1400.0.0 (unstable)
---------------------------------------------------------------------------------------------------------