Merge pull request #9 from Regalis11/master

0.14.9.0
This commit is contained in:
Evil Factory
2021-08-25 13:18:02 -03:00
committed by GitHub
140 changed files with 1448 additions and 621 deletions
@@ -300,9 +300,9 @@ namespace Barotrauma
if (containedItem == null) { continue; }
if (predicate == null || predicate(containedItem))
{
if (character.Submarine != Submarine.MainSub && avoidDroppingInSea)
if (avoidDroppingInSea && !character.IsInFriendlySub)
{
// If we are outside of main sub, try to put the item in the inventory instead dropping it in the sea.
// If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
{
continue;
@@ -2527,9 +2527,8 @@ namespace Barotrauma
// Don't target items that we own.
// This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive)
if (owner == character) { continue; }
if (owner != null && IsFriendly(Character, owner))
if (owner != null && (IsFriendly(Character, owner) || owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)))
{
// If the item is held by a friendly character, ignore it.
continue;
}
}
@@ -2595,7 +2594,7 @@ namespace Barotrauma
{
if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub)
{
if (!(SelectedAiTarget.Entity is Structure wall))
if (!(SelectedAiTarget?.Entity is Structure wall))
{
wall = wallTarget?.Structure;
}
@@ -475,22 +475,23 @@ namespace Barotrauma
bool needsGear = NeedsDivingGear(Character.CurrentHull, out _);
if (!needsGear || oxygenLow)
{
bool shouldKeepTheGearOn =
bool isCurrentObjectiveFindSafety = ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>();
bool shouldKeepTheGearOn =
isCurrentObjectiveFindSafety ||
Character.AnimController.InWater ||
Character.AnimController.HeadInWater ||
Character.CurrentHull == null ||
(Character.Submarine?.TeamID != Character.TeamID && !Character.IsEscorted) || // these instances should maybe be combined to a method
ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>() ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
if (oxygenLow && Character.CurrentHull.Oxygen > 0)
Character.Submarine == null ||
(Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
if (oxygenLow && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
{
shouldKeepTheGearOn = false;
// Remove the suit before we pass out
removeDivingSuit = true;
}
else if (Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10)
{
shouldKeepTheGearOn = true;
}
bool removeDivingSuit = !shouldKeepTheGearOn && Character.Submarine?.TeamID == Character.TeamID && (!(ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo) || goTo.Target != Character);
bool takeMaskOff = !shouldKeepTheGearOn;
if (!shouldKeepTheGearOn && !oxygenLow)
{
@@ -1348,13 +1349,15 @@ namespace Barotrauma
/// <summary>
/// Check whether the character has a diving suit in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true,
predicate: (Item item) => { return character.HasEquippedItem(item, InvSlotType.OuterClothes); });
public static bool HasDivingSuit(Character character, float conditionPercentage = 0)
=> HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true,
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes));
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
public static bool HasDivingMask(Character character, float conditionPercentage = 0)
=> HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
private static List<Item> matchingItems = new List<Item>();
@@ -2048,7 +2051,7 @@ namespace Barotrauma
{
var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
if (repairItemsObjective == null) { continue; }
if (repairItemsObjective.SubObjectives.None(o => o is AIObjectiveRepairItem repairObjective && repairObjective.Item == target))
if (!(repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is AIObjectiveRepairItem activeObjective) || activeObjective.Item != target)
{
// Not targeting the same item.
continue;
@@ -133,7 +133,7 @@ namespace Barotrauma
}
else
{
if (ItemToContain.ParentInventory == character.Inventory && character.Submarine == Submarine.MainSub)
if (ItemToContain.ParentInventory == character.Inventory && character.IsInFriendlySub)
{
ItemToContain.Drop(character);
}
@@ -53,7 +53,7 @@ namespace Barotrauma
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
(needsSuit ?
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN) :
!HumanAIController.HasDivingMask(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN)))
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN)))
{
Priority = 100;
}
@@ -108,6 +108,7 @@ namespace Barotrauma
void ReportWeldingFuelTankCount()
{
if (character.Submarine != Submarine.MainSub) { return; }
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("weldingfuel") && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
@@ -131,7 +132,7 @@ namespace Barotrauma
Abandon = true;
return;
}
Vector2 toLeak = Leak.WorldPosition - character.WorldPosition;
Vector2 toLeak = Leak.WorldPosition - character.AnimController.AimSourceWorldPos;
// TODO: use the collider size/reach?
if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150)
{
@@ -157,6 +158,7 @@ namespace Barotrauma
{
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(Leak, character, objectiveManager)
{
UseDistanceRelativeToAimSourcePos = true,
CloseEnough = reach,
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null,
TargetName = Leak.FlowTargetHull?.DisplayName,
@@ -165,7 +167,7 @@ namespace Barotrauma
onAbandon: () =>
{
if (CheckObjectiveSpecific()) { IsCompleted = true; }
else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > MathUtils.Pow(reach * 2, 2))
else if ((Leak.WorldPosition - character.AnimController.AimSourceWorldPos).LengthSquared() > MathUtils.Pow(reach * 2, 2))
{
// Too far
Abandon = true;
@@ -66,6 +66,11 @@ namespace Barotrauma
public bool AlwaysUseEuclideanDistance { get; set; } = true;
/// <summary>
/// If true, the distance to the destination is calculated from the character's AimSourcePos (= shoulder) instead of the collider's position
/// </summary>
public bool UseDistanceRelativeToAimSourcePos { get; set; } = false;
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public override bool AllowOutsideSubmarine => AllowGoingOutside;
@@ -237,7 +242,7 @@ namespace Barotrauma
Character followTarget = Target as Character;
bool needsDivingSuit = targetIsOutside;
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
if (!needsDivingGear && mimic)
if (mimic)
{
if (HumanAIController.HasDivingSuit(followTarget))
{
@@ -589,7 +594,9 @@ namespace Barotrauma
float xDiff = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X);
return xDiff <= CloseEnough;
}
return Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
Vector2 sourcePos = UseDistanceRelativeToAimSourcePos ? character.AnimController.AimSourceWorldPos : character.WorldPosition;
return Vector2.DistanceSquared(Target.WorldPosition, sourcePos) < CloseEnough * CloseEnough;
}
}
@@ -215,7 +215,9 @@ namespace Barotrauma
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
{
// Don't stop in ladders, because we can't interact with other items while holding the ladders.
endNodeFilter = node => node.Waypoint.Ladders == null
endNodeFilter = node => node.Waypoint.Ladders == null,
// Allow repairing hatches and airlock doors.
AllowGoingOutside = HumanAIController.ObjectiveManager.IsCurrentOrder<AIObjectiveRepairItems>() && Item.GetComponent<Door>() != null
};
if (repairTool != null)
{
@@ -82,6 +82,7 @@ namespace Barotrauma
public static bool ViableForRepair(Item item, Character character, HumanAIController humanAIController)
{
if (!IsValidTarget(item, character)) { return false; }
if (item.CurrentHull == null) { return true; }
if (item.CurrentHull.FireSources.Count > 0) { return false; }
// Don't repair items in rooms that have enemies inside.
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !humanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
@@ -150,7 +151,6 @@ namespace Barotrauma
if (item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (item.IsFullCondition) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null || character.Submarine == null) { return false; }
//player crew ignores items in outposts
if (character.IsOnPlayerTeam && item.Submarine.Info.IsOutpost) { return false; }
@@ -234,8 +234,6 @@ namespace Barotrauma
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundwoundedtarget" + targetCharacter.Name, 60.0f);
}
character.SelectCharacter(targetCharacter);
}
GiveTreatment(deltaTime);
}
@@ -268,6 +266,8 @@ namespace Barotrauma
}
treatmentTimer = TreatmentDelay;
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
//find which treatments are the most suitable to treat the character's current condition
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false);
@@ -282,6 +282,7 @@ namespace Barotrauma
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
if (matchingItem == null) { continue; }
if (targetCharacter != character) { character.SelectCharacter(targetCharacter); }
ApplyTreatment(affliction, matchingItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
@@ -292,7 +293,6 @@ namespace Barotrauma
// Find treatments outside of own inventory only if inside the own sub.
if (character.Submarine != null && character.Submarine.TeamID == character.TeamID)
{
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
//didn't have any suitable treatments available, try to find some medical items
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
{
@@ -312,7 +312,7 @@ namespace Barotrauma
}
}
}
if (itemNameList.Count > 0)
if (itemNameList.Any())
{
string itemListStr = "";
if (itemNameList.Count == 1)
@@ -329,7 +329,6 @@ namespace Barotrauma
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
}
character.DeselectCharacter();
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
@@ -343,11 +342,31 @@ namespace Barotrauma
}
});
}
else if (cprSuitability <= 0)
{
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f);
Abandon = true;
}
}
}
else if (!targetCharacter.IsUnconscious)
{
//no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR)
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f);
Abandon = true;
return;
}
if (character != targetCharacter)
{
character.AnimController.Anim = AnimController.Animation.CPR;
if (cprSuitability > 0.0f)
{
character.SelectCharacter(targetCharacter);
character.AnimController.Anim = AnimController.Animation.CPR;
}
else
{
character.DeselectCharacter();
}
}
}
@@ -63,7 +63,7 @@ namespace Barotrauma
allItems = Wreck.GetItems(false);
thalamusItems = allItems.FindAll(i => IsThalamus(i.prefab));
hulls.AddRange(Wreck.GetHulls(false));
var potentialBrainHulls = new Dictionary<Hull, float>();
var potentialBrainHulls = new List<(Hull hull, float weight)>();
brain = new Item(brainPrefab, Vector2.Zero, Wreck);
thalamusItems.Add(brain);
Point minSize = brain.Rect.Size.Multiply(brain.Scale);
@@ -100,10 +100,10 @@ namespace Barotrauma
}
if (weight > 0)
{
potentialBrainHulls.TryAdd(hull, weight);
potentialBrainHulls.Add((hull, weight));
}
}
Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Keys.ToList(), potentialBrainHulls.Values.ToList(), Rand.RandSync.Server);
Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Select(pbh => pbh.hull).ToList(), potentialBrainHulls.Select(pbh => pbh.weight).ToList(), Rand.RandSync.Server);
var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(p => IsThalamus(p));
if (brainHull == null)
{
@@ -187,8 +187,11 @@ namespace Barotrauma
if (!spawnOrgans.Contains(item))
{
spawnOrgans.Add(item);
// Try to flood the hull so that the spawner won't die.
item.CurrentHull.WaterVolume = item.CurrentHull.Volume;
if (item.CurrentHull != null)
{
// Try to flood the hull so that the spawner won't die.
item.CurrentHull.WaterVolume = item.CurrentHull.Volume;
}
}
}
}
@@ -108,6 +108,16 @@ namespace Barotrauma
public enum Animation { None, Climbing, UsingConstruction, Struggle, CPR };
public Animation Anim;
public Vector2 AimSourceWorldPos
{
get
{
Vector2 sourcePos = character.AnimController.AimSourcePos;
if (character.Submarine != null) { sourcePos += character.Submarine.Position; }
return sourcePos;
}
}
public Vector2 AimSourcePos => ConvertUnits.ToDisplayUnits(AimSourceSimPos);
public virtual Vector2 AimSourceSimPos => Collider.SimPosition;
@@ -1463,7 +1463,7 @@ namespace Barotrauma
target.CharacterHealth.CalculateVitality();
if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f)
{
character.Info.IncreaseSkillLevel("medical", SkillSettings.Current.SkillIncreasePerCprRevive, character.Position + Vector2.UnitY * 150.0f);
character.Info?.IncreaseSkillLevel("medical", SkillSettings.Current.SkillIncreasePerCprRevive, character.Position + Vector2.UnitY * 150.0f);
SteamAchievementManager.OnCharacterRevived(target, character);
lastReviveTime = (float)Timing.TotalTime;
#if SERVER
@@ -576,7 +576,7 @@ namespace Barotrauma
get { return pressureProtection; }
set
{
pressureProtection = Math.Max(value, 0.0f);
pressureProtection = Math.Max(value, pressureProtection);
pressureProtectionLastSet = Timing.TotalTime;
}
}
@@ -638,6 +638,8 @@ namespace Barotrauma
public CharacterHealth CharacterHealth { get; private set; }
public bool DisableHealthWindow;
public float Vitality
{
get { return CharacterHealth.Vitality; }
@@ -882,6 +884,8 @@ namespace Barotrauma
}
}
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath);
public OnDeathHandler OnDeath;
@@ -2065,10 +2069,9 @@ namespace Barotrauma
if (!CanInteract || inventory.Locked) { return false; }
//the inventory belongs to some other character
if (inventory.Owner is Character && inventory.Owner != this)
if (inventory.Owner is Character character && inventory.Owner != this)
{
var owner = (Character)inventory.Owner;
var owner = character;
//can only be accessed if the character is incapacitated and has been selected
return SelectedCharacter == owner && owner.CanInventoryBeAccessed;
}
@@ -2319,14 +2322,13 @@ namespace Barotrauma
public void SelectCharacter(Character character)
{
if (character == null) return;
if (character == null || character == this) { return; }
SelectedCharacter = character;
}
public void DeselectCharacter()
{
if (SelectedCharacter == null) return;
if (SelectedCharacter == null) { return; }
SelectedCharacter.AnimController?.ResetPullJoints();
SelectedCharacter = null;
}
@@ -2361,7 +2363,7 @@ namespace Barotrauma
#if CLIENT
if (isLocalPlayer)
{
if (!IsMouseOnUI)
if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this))
{
if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen)
{
@@ -2386,6 +2388,7 @@ namespace Barotrauma
}
else
{
FocusedCharacter = null;
focusedItem = null;
}
findFocusedTimer -= deltaTime;
@@ -2707,17 +2710,13 @@ namespace Barotrauma
UpdateAIChatMessages(deltaTime);
//Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us!
bool allowRagdoll = GameMain.NetworkMember != null ? GameMain.NetworkMember.ServerSettings.AllowRagdollButton : true;
bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 1f;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
tooFastToUnragdoll = false;
}
bool allowRagdoll = GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true;
bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 5.0f * 5.0f;
if (IsForceRagdolled)
{
IsRagdolled = IsForceRagdolled;
}
else if (IsRemotePlayer)
else if (this != Controlled)
{
IsRagdolled = IsKeyDown(InputType.Ragdoll);
}
@@ -2726,6 +2725,7 @@ namespace Barotrauma
{
if (ragdollingLockTimer > 0.0f)
{
SetInput(InputType.Ragdoll, false, true);
ragdollingLockTimer -= deltaTime;
}
else
@@ -2832,7 +2832,7 @@ namespace Barotrauma
{
if (Timing.TotalTime > pressureProtectionLastSet + 0.1)
{
PressureProtection = 0.0f;
pressureProtection = 0.0f;
}
}
if (NeedsWater)
@@ -2907,7 +2907,7 @@ namespace Barotrauma
}
private float despawnTimer;
private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false)
private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false, bool createNetworkEvents = true)
{
if (!EnableDespawn) { return; }
@@ -2978,10 +2978,10 @@ namespace Barotrauma
if (itemContainer == null) { return; }
foreach (Item inventoryItem in Inventory.AllItemsMod)
{
if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null))
if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null, createNetworkEvent: createNetworkEvents))
{
//if the item couldn't be put inside the despawn container, just drop it
inventoryItem.Drop(dropper: this);
inventoryItem.Drop(dropper: this, createNetworkEvent: createNetworkEvents);
}
}
}
@@ -2993,11 +2993,8 @@ namespace Barotrauma
public void DespawnNow(bool createNetworkEvents = true)
{
despawnTimer = GameMain.Config.CorpseDespawnDelay;
UpdateDespawn(1.0f, ignoreThresholds: true);
if (createNetworkEvents)
{
Spawner.Update();
}
UpdateDespawn(1.0f, ignoreThresholds: true, createNetworkEvents: createNetworkEvents);
Spawner.Update(createNetworkEvents);
}
public static void RemoveByPrefab(CharacterPrefab prefab)
@@ -3526,6 +3523,7 @@ namespace Barotrauma
}
bool wasDead = IsDead;
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
float prevVitality = CharacterHealth.Vitality;
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration);
CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking);
if (attacker != this)
@@ -3534,7 +3532,7 @@ namespace Barotrauma
OnAttackedProjSpecific(attacker, attackResult, stun);
if (!wasDead)
{
TryAdjustAttackerSkill(attacker, -attackResult.Damage);
TryAdjustAttackerSkill(attacker, CharacterHealth.Vitality - prevVitality);
if (IsDead)
{
attacker?.RecordKill(this);
@@ -3996,7 +3994,7 @@ namespace Barotrauma
//now there's just one, try to put the extra items where they fit (= stack them)
for (int i = 0; i < inventory.Capacity; i++)
{
if (inventory.CanBePut(newItem, i))
if (inventory.CanBePutInSlot(newItem, i))
{
slotIndices[0] = i;
canBePutInOriginalInventory = true;
@@ -4006,7 +4004,7 @@ namespace Barotrauma
}
else
{
canBePutInOriginalInventory = inventory.CanBePut(newItem, slotIndices[0]);
canBePutInOriginalInventory = inventory.CanBePutInSlot(newItem, slotIndices[0], ignoreCondition: true);
}
if (canBePutInOriginalInventory)
@@ -97,7 +97,7 @@ namespace Barotrauma
State = InfectionState.Final;
ActivateHusk();
ApplyDamage(deltaTime, applyForce: true);
character.SetStun(1);
character.SetStun(5);
}
}
@@ -18,10 +18,10 @@
OnFire, InWater, NotInWater,
OnImpact,
OnEating,
OnDeath = OnBroken,
OnDamaged,
OnSevered,
OnProduceSpawned,
OnOpen, OnClose,
OnDeath = OnBroken,
}
}
@@ -10,8 +10,8 @@ namespace Barotrauma
[Serialize("", true)]
public string NPCTag { get; set; }
[Serialize(0, true)]
public int TeamTag { get; set; }
[Serialize(CharacterTeamType.None, true)]
public CharacterTeamType TeamTag { get; set; }
[Serialize(false, true)]
public bool AddToCrew { get; set; }
@@ -29,11 +29,10 @@ namespace Barotrauma
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
foreach (var npc in affectedNpcs)
{
CharacterTeamType newTeam = (CharacterTeamType)TeamTag;
// characters will still remain on friendlyNPC team for rest of the tick
npc.SetOriginalTeam(newTeam);
npc.SetOriginalTeam(TeamTag);
if (AddToCrew && (newTeam == CharacterTeamType.Team1 || newTeam == CharacterTeamType.Team2))
if (AddToCrew && (TeamTag == CharacterTeamType.Team1 || TeamTag == CharacterTeamType.Team2))
{
npc.Info.StartItemsGiven = true;
@@ -44,11 +43,11 @@ namespace Barotrauma
var wifiComponent = item.GetComponent<Items.Components.WifiComponent>();
if (wifiComponent != null)
{
wifiComponent.TeamID = newTeam;
wifiComponent.TeamID = TeamTag;
}
}
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AddToCrew, newTeam, npc.Inventory.AllItems.Select(it => it.ID).ToArray() });
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AddToCrew, TeamTag, npc.Inventory.AllItems.Select(it => it.ID).ToArray() });
#endif
}
}
@@ -142,7 +142,7 @@ namespace Barotrauma
npcOrItem = item;
item.CampaignInteractionType = CampaignMode.InteractionType.Examine;
if (player.SelectedConstruction == item ||
player.Inventory.Contains(item) ||
player.Inventory != null && player.Inventory.Contains(item) ||
(player.FocusedItem == item && player.IsKeyHit(InputType.Use)))
{
Trigger(e1, e2);
@@ -124,13 +124,24 @@ namespace Barotrauma
}
MTRandom rand = new MTRandom(seed);
var initialEventSet = SelectRandomEvents(EventSet.List, rand);
EventSet initialEventSet = SelectRandomEvents(EventSet.List, rand);
EventSet additiveSet = null;
if (initialEventSet != null && initialEventSet.Additive)
{
additiveSet = initialEventSet;
initialEventSet = SelectRandomEvents(EventSet.List.FindAll(e => !e.Additive), rand);
}
if (initialEventSet != null)
{
pendingEventSets.Add(initialEventSet);
CreateEvents(initialEventSet, rand);
}
if (additiveSet != null)
{
pendingEventSets.Add(additiveSet);
CreateEvents(additiveSet, rand);
}
if (level?.LevelData?.Type == LevelData.LevelType.Outpost)
{
//if the outpost is connected to a locked connection, create an event to unlock it
@@ -94,6 +94,8 @@ namespace Barotrauma
public readonly bool TriggerEventCooldown;
public readonly bool Additive;
public readonly Dictionary<string, float> Commonness;
public readonly List<(EventPrefab prefab, float commonness, float probability)> EventPrefabs;
@@ -117,6 +119,8 @@ namespace Barotrauma
MinLevelDifficulty = element.GetAttributeFloat("minleveldifficulty", 0);
MaxLevelDifficulty = Math.Max(element.GetAttributeFloat("maxleveldifficulty", 100), MinLevelDifficulty);
Additive = element.GetAttributeBool("additive", false);
string levelTypeStr = element.GetAttributeString("leveltype", "LocationConnection");
if (!Enum.TryParse(levelTypeStr, true, out LevelType))
{
@@ -24,6 +24,8 @@ namespace Barotrauma
private Submarine sub;
private readonly List<CargoMission> previouslySelectedMissions = new List<CargoMission>();
public override string Description
{
get
@@ -42,7 +44,13 @@ namespace Barotrauma
{
this.sub = sub;
itemConfig = prefab.ConfigElement.Element("Items");
requiredDeliveryAmount = Math.Min(prefab.ConfigElement.GetAttributeFloat("requireddeliveryamount", 0.98f), 1.0f);
requiredDeliveryAmount = Math.Min(prefab.ConfigElement.GetAttributeFloat("requireddeliveryamount", 0.98f), 1.0f);
//this can get called between rounds when the client receives a campaign save
//don't attempt to determine cargo if the sub hasn't been fully loaded
if (sub == null || sub.Loading || sub.Removed || Submarine.Unloading || !Submarine.Loaded.Contains(sub))
{
return;
}
DetermineCargo();
}
@@ -58,6 +66,30 @@ namespace Barotrauma
List<(ItemContainer container, int freeSlots)> containers = sub.GetCargoContainers();
containers.Sort((c1, c2) => { return c2.container.Capacity.CompareTo(c1.container.Capacity); });
previouslySelectedMissions.Clear();
if (GameMain.GameSession?.StartLocation?.SelectedMissions != null)
{
bool isPriorMission = true;
foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions)
{
if (!(mission is CargoMission otherMission)) { continue; }
if (mission == this) { isPriorMission = false; }
previouslySelectedMissions.Add(otherMission);
if (!isPriorMission) { continue; }
foreach (var (element, container) in otherMission.itemsToSpawn)
{
for (int i = 0; i < containers.Count; i++)
{
if (containers[i].container == container)
{
containers[i] = (containers[i].container, containers[i].freeSlots - 1);
break;
}
}
}
}
}
maxItemCount = 0;
foreach (XElement subElement in itemConfig.Elements())
{
@@ -87,9 +119,9 @@ namespace Barotrauma
}
calculatedReward = 0;
foreach (var itemToSpawn in itemsToSpawn)
foreach (var (element, container) in itemsToSpawn)
{
int price = itemToSpawn.element.GetAttributeInt("reward", Prefab.Reward / itemsToSpawn.Count);
int price = element.GetAttributeInt("reward", Prefab.Reward / itemsToSpawn.Count);
if (rewardPerCrate.HasValue)
{
if (price != rewardPerCrate.Value) { rewardPerCrate = -1; }
@@ -108,7 +140,28 @@ namespace Barotrauma
public override int GetReward(Submarine sub)
{
if (sub != this.sub)
bool missionsChanged = false;
if (GameMain.GameSession?.StartLocation?.SelectedMissions != null)
{
List<Mission> currentMissions = GameMain.GameSession.StartLocation.SelectedMissions.Where(m => m is CargoMission).ToList();
if (currentMissions.Count != previouslySelectedMissions.Count)
{
missionsChanged = true;
}
else
{
for (int i = 0; i < previouslySelectedMissions.Count; i++)
{
if (previouslySelectedMissions[i] != currentMissions[i])
{
missionsChanged = true;
break;
}
}
}
}
if (sub != this.sub || missionsChanged)
{
this.sub = sub;
DetermineCargo();
@@ -183,7 +183,7 @@ namespace Barotrauma
}
}
public virtual void SetDifficulty(float difficulty) { }
public virtual void SetLevel(LevelData level) { }
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
{
@@ -423,13 +423,16 @@ namespace Barotrauma
protected Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.Server, bool giveTags = true)
{
if (positionToStayIn == null)
{
positionToStayIn = WayPoint.GetRandom(SpawnType.Human, null, submarine);
}
var characterInfo = humanPrefab.GetCharacterInfo(Rand.RandSync.Server) ?? new CharacterInfo(CharacterPrefab.HumanSpeciesName, npcIdentifier: humanPrefab.Identifier, jobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync);
characterInfo.TeamID = teamType;
if (positionToStayIn == null)
{
positionToStayIn =
WayPoint.GetRandom(SpawnType.Human, characterInfo.Job?.Prefab, submarine) ??
WayPoint.GetRandom(SpawnType.Human, null, submarine);
}
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
spawnedCharacter.Prefab = humanPrefab;
humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn);
@@ -18,7 +18,7 @@ namespace Barotrauma
//string = filename, point = min,max
private readonly HashSet<Tuple<CharacterPrefab, Point>> monsterPrefabs = new HashSet<Tuple<CharacterPrefab, Point>>();
private readonly float itemSpawnRadius = 800.0f;
private float itemSpawnRadius = 800.0f;
private readonly float approachItemsRadius = 1000.0f;
private readonly float nestObjectRadius = 1000.0f;
private readonly float monsterSpawnRadius = 3000.0f;
@@ -107,6 +107,7 @@ namespace Barotrauma
//ruin/cave/wreck items are allowed to spawn close to the sub
float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Cave || spawnPositionType == Level.PositionType.Wreck ?
0.0f : Level.Loaded.Size.X * 0.3f;
nestPosition = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f);
List<GraphEdge> spawnEdges = new List<GraphEdge>();
if (spawnPositionType == Level.PositionType.Cave)
@@ -149,20 +150,21 @@ namespace Barotrauma
if (!spawnEdges.Any())
{
GraphEdge closestEdge = null;
float closestDist = float.PositiveInfinity;
float closestDistSqr = float.PositiveInfinity;
foreach (var edge in nearbyCells.SelectMany(c => c.Edges))
{
if (!edge.NextToCave || !edge.IsSolid) { continue; }
float dist = Vector2.DistanceSquared(edge.Center, nestPosition);
if (dist < closestDist)
if (dist < closestDistSqr)
{
closestEdge = edge;
closestDist = dist;
closestDistSqr = dist;
}
}
if (closestEdge != null)
{
spawnEdges.Add(closestEdge);
itemSpawnRadius = Math.Max(itemSpawnRadius, (float)Math.Sqrt(closestDistSqr) * 1.5f);
}
}
}
@@ -28,6 +28,8 @@ namespace Barotrauma
private float pirateSightingUpdateTimer;
private Vector2? lastSighting;
private LevelData levelData;
public override int TeamCount => 2;
private bool outsideOfSonarRange;
@@ -83,21 +85,24 @@ namespace Barotrauma
characterTypeConfig = prefab.ConfigElement.Element("CharacterTypes");
addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0);
// for campaign missions, set difficulty at construction
// for campaign missions, set level 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);
if (levelData != null)
{
SetLevel(levelData);
}
}
public override void SetDifficulty(float difficulty)
public override void SetLevel(LevelData level)
{
if (missionDifficulty > 0f)
if (levelData != null)
{
// difficulty already set
//level already set
return;
}
missionDifficulty = difficulty;
levelData = level;
missionDifficulty = level?.Difficulty ?? 0;
XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
@@ -123,23 +128,24 @@ namespace Barotrauma
submarineInfo = new SubmarineInfo(contentFile.Path);
}
private float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier)
private float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand)
{
return Math.Abs(levelDifficulty - preferredDifficulty + (Rand.Range(-randomnessModifier, randomnessModifier, Rand.RandSync.Server)));
return Math.Abs(levelDifficulty - preferredDifficulty + MathHelper.Lerp(-randomnessModifier, randomnessModifier, (float)rand.NextDouble()));
}
private int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty)
private int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty, Random rand)
{
return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * ((levelDifficulty + Rand.Range(-RandomnessModifier, RandomnessModifier, Rand.RandSync.Server)) / MaxDifficulty)), minAmount);
return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * (levelDifficulty + MathHelper.Lerp(-RandomnessModifier, RandomnessModifier, (float)rand.NextDouble())) / MaxDifficulty), minAmount);
}
private XElement GetRandomDifficultyModifiedElement(XElement parentElement, float levelDifficulty, float randomnessModifier)
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
// 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);
float applicabilityValue = GetDifficultyModifiedValue(element.GetAttributeFloat(0f, "preferreddifficulty"), levelDifficulty, randomnessModifier, rand);
if (applicabilityValue < bestValue)
{
bestElement = element;
@@ -154,11 +160,11 @@ namespace Barotrauma
Vector2 patrolPos = enemySub.WorldPosition;
Point subSize = enemySub.GetDockedBorders().Size;
if (!Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out preferredSpawnPos))
if (!Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out preferredSpawnPos))
{
DebugConsole.ThrowError("Could not spawn pirate submarine in an interesting location! " + this);
}
if (!Level.Loaded.TryGetInterestingPositionAwayFromPoint(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out patrolPos, preferredSpawnPos, minDistFromPoint: 10000f))
if (!Level.Loaded.TryGetInterestingPositionAwayFromPoint(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out patrolPos, preferredSpawnPos, minDistFromPoint: 10000f))
{
DebugConsole.ThrowError("Could not give pirate submarine an interesting location to patrol to! " + this);
}
@@ -182,7 +188,7 @@ namespace Barotrauma
}
}
private void InitPirateShip(Vector2 spawnPos)
private void InitPirateShip()
{
enemySub.NeutralizeBallast();
if (enemySub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
@@ -193,6 +199,7 @@ namespace Barotrauma
enemySub.TeamID = CharacterTeamType.None;
//make the enemy sub withstand atleast the same depth as the player sub
enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth);
enemySub.ImmuneToBallastFlora = true;
}
private void InitPirates()
@@ -214,12 +221,14 @@ namespace Barotrauma
float enemyCreationDifficulty = missionDifficulty + playerCount * addedMissionDifficultyPerPlayer;
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
bool commanderAssigned = false;
foreach (XElement element in characterConfig.Elements())
{
// 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);
int amountCreated = GetDifficultyModifiedAmount(element.GetAttributeInt("minamount", 0), element.GetAttributeInt("maxamount", 0), enemyCreationDifficulty, rand);
for (int i = 0; i < amountCreated; i++)
{
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == element.GetAttributeString("typeidentifier", string.Empty)).FirstOrDefault();
@@ -307,7 +316,7 @@ namespace Barotrauma
#endif
if (!IsClient)
{
InitPirateShip(spawnPos);
InitPirateShip();
}
enemySub.SetPosition(spawnPos);
@@ -651,6 +651,7 @@ namespace Barotrauma
location.ClearMissions();
location.Discovered = false;
location.LevelData?.EventHistory?.Clear();
location.UnlockInitialMissions();
}
Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation));
Map.SelectLocation(-1);
@@ -700,7 +701,7 @@ namespace Barotrauma
if (npc == null || interactor == null) { yield return CoroutineStatus.Failure; }
HumanAIController humanAI = npc.AIController as HumanAIController;
if (humanAI == null) { yield return CoroutineStatus.Failure; }
if (humanAI == null) { yield return CoroutineStatus.Success; }
var waitOrder = Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase));
humanAI.SetForcedOrder(waitOrder, string.Empty, null);
@@ -719,8 +720,10 @@ namespace Barotrauma
#if CLIENT
ShowCampaignUI = false;
#endif
humanAI.ClearForcedOrder();
if (!npc.Removed)
{
humanAI.ClearForcedOrder();
}
yield return CoroutineStatus.Success;
}
@@ -729,13 +732,16 @@ namespace Barotrauma
public void AssignNPCMenuInteraction(Character character, InteractionType interactionType)
{
character.CampaignInteractionType = interactionType;
if (interactionType == InteractionType.None)
character.DisableHealthWindow =
interactionType != InteractionType.None &&
interactionType != InteractionType.Examine &&
interactionType != InteractionType.Talk;
if (interactionType == InteractionType.None)
{
character.SetCustomInteract(null, null);
return;
return;
}
character.CharacterHealth.UseHealthWindow = false;
//character.CanInventoryBeAccessed = false;
character.SetCustomInteract(
NPCInteract,
#if CLIENT
@@ -1,4 +1,5 @@
using Barotrauma.Networking;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma
@@ -29,20 +30,30 @@ namespace Barotrauma
public XElement OrderData { get; private set; }
partial void InitProjSpecific(Client client);
public CharacterCampaignData(Client client)
public CharacterCampaignData(Client client, bool giveRespawnPenaltyAffliction = false)
{
Name = client.Name;
InitProjSpecific(client);
healthData = new XElement("health");
client.Character.CharacterHealth.Save(healthData);
if (client.Character.Inventory != null)
client.Character?.CharacterHealth?.Save(healthData);
if (giveRespawnPenaltyAffliction)
{
var respawnPenaltyAffliction = RespawnManager.GetRespawnPenaltyAffliction();
healthData.Add(new XElement("Affliction",
new XAttribute("identifier", respawnPenaltyAffliction.Identifier),
new XAttribute("strength", respawnPenaltyAffliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
}
if (client.Character?.Inventory != null)
{
itemData = new XElement("inventory");
Character.SaveInventory(client.Character.Inventory, itemData);
}
OrderData = new XElement("orders");
CharacterInfo.SaveOrderData(client.Character.Info, OrderData);
if (client.Character != null)
{
CharacterInfo.SaveOrderData(client.Character.Info, OrderData);
}
}
public CharacterCampaignData(XElement element)
@@ -368,8 +368,8 @@ namespace Barotrauma
foreach (Mission mission in GameMode.Missions)
{
// setting difficulty for missions that may involve difficulty-related submarine creation
mission.SetDifficulty(levelData?.Difficulty ?? 0f);
// setting level for missions that may involve difficulty-related submarine creation
mission.SetLevel(levelData);
}
if (Submarine.MainSubs[1] == null)
@@ -296,10 +296,12 @@ namespace Barotrauma
return;
}
var linkedItems = GetLinkedItemsToSwap(itemToRemove);
int price = 0;
if (!itemToRemove.AvailableSwaps.Contains(itemToInstall))
{
price = itemToInstall.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation);
price = itemToInstall.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count;
}
if (force)
@@ -309,7 +311,7 @@ namespace Barotrauma
if (Campaign.Money >= price)
{
PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToRemove);
PurchasedItemSwaps.RemoveAll(p => linkedItems.Contains(p.ItemToRemove));
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
// only make the NPC speak if more than 5 minutes have passed since the last purchased service
@@ -322,23 +324,27 @@ namespace Barotrauma
Campaign.Money -= price;
itemToRemove.AvailableSwaps.Add(itemToRemove.Prefab);
if (itemToInstall != null && !itemToRemove.AvailableSwaps.Contains(itemToInstall))
foreach (Item itemToSwap in linkedItems)
{
itemToRemove.PurchasedNewSwap = true;
itemToRemove.AvailableSwaps.Add(itemToInstall);
itemToSwap.AvailableSwaps.Add(itemToSwap.Prefab);
if (itemToInstall != null && !itemToSwap.AvailableSwaps.Contains(itemToInstall))
{
itemToSwap.PurchasedNewSwap = true;
itemToSwap.AvailableSwaps.Add(itemToInstall);
}
if (itemToSwap.Prefab != itemToInstall && itemToInstall != null)
{
itemToSwap.PendingItemSwap = itemToInstall;
PurchasedItemSwaps.Add(new PurchasedItemSwap(itemToSwap, itemToInstall));
DebugLog($"CLIENT: Swapped item \"{itemToSwap.Name}\" with \"{itemToInstall.Name}\".", Color.Orange);
}
else
{
DebugLog($"CLIENT: Cancelled swapping the item \"{itemToSwap.Name}\" with \"{(itemToSwap.PendingItemSwap?.Name ?? null)}\".", Color.Orange);
}
}
if (itemToRemove.Prefab != itemToInstall && itemToInstall != null)
{
itemToRemove.PendingItemSwap = itemToInstall;
PurchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall));
DebugLog($"CLIENT: Swapped item \"{itemToRemove.Name}\" with \"{itemToInstall.Name}\".", Color.Orange);
}
else
{
DebugLog($"CLIENT: Cancelled swapping the item \"{itemToRemove.Name}\" with \"{(itemToRemove.PendingItemSwap?.Name ?? null)}\".", Color.Orange);
}
OnUpgradesChanged?.Invoke();
}
else
@@ -382,30 +388,55 @@ namespace Barotrauma
}
}
if (itemToRemove.PendingItemSwap == null)
var linkedItems = GetLinkedItemsToSwap(itemToRemove);
foreach (Item itemToCancel in linkedItems)
{
var replacement = MapEntityPrefab.Find("", swappableItem.ReplacementOnUninstall) as ItemPrefab;
if (replacement == null)
if (itemToCancel.PendingItemSwap == null)
{
DebugConsole.ThrowError($"Failed to uninstall item \"{itemToRemove.Name}\". Could not find the replacement item \"{swappableItem.ReplacementOnUninstall}\".");
return;
var replacement = MapEntityPrefab.Find("", swappableItem.ReplacementOnUninstall) as ItemPrefab;
if (replacement == null)
{
DebugConsole.ThrowError($"Failed to uninstall item \"{itemToCancel.Name}\". Could not find the replacement item \"{swappableItem.ReplacementOnUninstall}\".");
return;
}
PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToCancel);
PurchasedItemSwaps.Add(new PurchasedItemSwap(itemToCancel, replacement));
DebugLog($"Uninstalled item item \"{itemToCancel.Name}\".", Color.Orange);
itemToCancel.PendingItemSwap = replacement;
}
else
{
PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToCancel);
DebugLog($"Cancelled swapping the item \"{itemToCancel.Name}\" with \"{itemToCancel.PendingItemSwap.Name}\".", Color.Orange);
itemToCancel.PendingItemSwap = null;
}
PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToRemove);
PurchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, replacement));
DebugLog($"Uninstalled item item \"{itemToRemove.Name}\".", Color.Orange);
itemToRemove.PendingItemSwap = replacement;
}
else
{
PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToRemove);
DebugLog($"Cancelled swapping the item \"{itemToRemove.Name}\" with \"{itemToRemove.PendingItemSwap.Name}\".", Color.Orange);
itemToRemove.PendingItemSwap = null;
}
#if CLIENT
OnUpgradesChanged?.Invoke();
#endif
}
public List<Item> GetLinkedItemsToSwap(Item item)
{
List<Item> linkedItems = new List<Item>() { item };
foreach (MapEntity linkedEntity in item.linkedTo)
{
foreach (MapEntity secondLinkedEntity in linkedEntity.linkedTo)
{
if (!(secondLinkedEntity is Item linkedItem) || linkedItem == item) { continue; }
if (linkedItem.AllowSwapping &&
linkedItem.Prefab.SwappableItem != null && (linkedItem.Prefab.SwappableItem.CanBeBought || item.Prefab.SwappableItem.ReplacementOnUninstall == linkedItem.prefab.Identifier) &&
linkedItem.Prefab.SwappableItem.SwapIdentifier.Equals(item.Prefab.SwappableItem.SwapIdentifier, StringComparison.OrdinalIgnoreCase))
{
linkedItems.Add(linkedItem);
}
}
}
return linkedItems;
}
/// <summary>
/// Applies all our pending upgrades to the submarine.
/// </summary>
@@ -565,7 +596,7 @@ namespace Barotrauma
/// <param name="submarine"></param>
/// <param name="level"></param>
/// <returns>New level that was applied, -1 if no upgrades were applied.</returns>
private static int BuyUpgrade(UpgradePrefab prefab, UpgradeCategory category, Submarine submarine, int level = 1)
private static int BuyUpgrade(UpgradePrefab prefab, UpgradeCategory category, Submarine submarine, int level = 1, Submarine parentSub = null)
{
int? newLevel = null;
if (category.IsWallUpgrade)
@@ -604,6 +635,7 @@ namespace Barotrauma
foreach (Submarine loadedSub in Submarine.Loaded.Where(sub => sub != submarine))
{
if (loadedSub == parentSub) { continue; }
XElement? root = loadedSub.Info?.SubmarineElement;
if (root == null) { continue; }
@@ -615,7 +647,7 @@ namespace Barotrauma
ushort dockingPortID = (ushort) root.GetAttributeInt("originallinkedto", 0);
if (dockingPortID > 0 && submarine.GetItems(true).Any(item => item.ID == dockingPortID))
{
BuyUpgrade(prefab, category, loadedSub, level);
BuyUpgrade(prefab, category, loadedSub, level, submarine);
}
}
}
@@ -132,17 +132,17 @@ namespace Barotrauma
return false;
}
public override bool CanBePut(Item item, int i)
public override bool CanBePutInSlot(Item item, int i, bool ignoreCondition = false)
{
return
base.CanBePut(item, i) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) &&
base.CanBePutInSlot(item, i, ignoreCondition) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) &&
(SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1);
}
public override bool CanBePut(ItemPrefab itemPrefab, int i)
public override bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition)
{
return
base.CanBePut(itemPrefab, i) &&
base.CanBePutInSlot(itemPrefab, i, condition) &&
(SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1);
}
@@ -214,7 +214,7 @@ namespace Barotrauma
}
}
if (allowedSlots != null && !allowedSlots.Contains(InvSlotType.Any))
if (allowedSlots != null && allowedSlots.Any() && !allowedSlots.Contains(InvSlotType.Any))
{
bool allSlotsTaken = true;
foreach (var allowedSlot in allowedSlots)
@@ -261,7 +261,7 @@ namespace Barotrauma
/// <summary>
/// If there is room, puts the item in the inventory and returns true, otherwise returns false
/// </summary>
public override bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
public override bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false)
{
if (allowedSlots == null || !allowedSlots.Any()) { return false; }
if (item == null)
@@ -326,7 +326,7 @@ namespace Barotrauma
#if CLIENT
if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; }
#endif
if (!slots[i].First().AllowedSlots.Contains(InvSlotType.Any) || !TryPutItem(slots[i].FirstOrDefault(), character, new List<InvSlotType> { InvSlotType.Any }, true))
if (!slots[i].First().AllowedSlots.Contains(InvSlotType.Any) || !TryPutItem(slots[i].FirstOrDefault(), character, new List<InvSlotType> { InvSlotType.Any }, true, ignoreCondition))
{
free = false;
#if CLIENT
@@ -371,7 +371,7 @@ namespace Barotrauma
for (int i = 0; i < capacity; i++)
{
if (SlotTypes[i] != InvSlotType.Any) { continue; }
if (!slots[i].Empty() && CanBePut(item, i))
if (!slots[i].Empty() && CanBePutInSlot(item, i))
{
return i;
}
@@ -387,7 +387,7 @@ namespace Barotrauma
for (int i = 0; i < capacity; i++)
{
if (SlotTypes[i] != InvSlotType.Any) { continue; }
if (CanBePut(item, i))
if (CanBePutInSlot(item, i))
{
return i;
}
@@ -402,14 +402,14 @@ namespace Barotrauma
}
else
{
if (!CanBePut(item, i)) { continue; }
if (!CanBePutInSlot(item, i)) { continue; }
}
return i;
}
return -1;
}
public override bool TryPutItem(Item item, int index, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true)
public override bool TryPutItem(Item item, int index, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false)
{
if (index < 0 || index >= slots.Length)
{
@@ -424,7 +424,7 @@ namespace Barotrauma
if (slots[index].Any())
{
if (slots[index].Contains(item)) { return false; }
return base.TryPutItem(item, index, allowSwapping, allowCombine, user, createNetworkEvent);
return base.TryPutItem(item, index, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition);
}
if (SlotTypes[index] == InvSlotType.Any)
@@ -460,7 +460,7 @@ namespace Barotrauma
if (!slotsFree) { return false; }
return TryPutItem(item, user, new List<InvSlotType>() { placeToSlots }, createNetworkEvent);
return TryPutItem(item, user, new List<InvSlotType>() { placeToSlots }, createNetworkEvent, ignoreCondition);
}
}
}
@@ -167,10 +167,9 @@ namespace Barotrauma.Items.Components
{
foreach (DockingPort port in list)
{
if (port == this || port.item.Submarine == item.Submarine) continue;
if (Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X) > DistanceTolerance.X) continue;
if (Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y) > DistanceTolerance.Y) continue;
if (port == this || port.item.Submarine == item.Submarine || port.IsHorizontal != IsHorizontal) { continue; }
if (Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X) > DistanceTolerance.X) { continue; }
if (Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y) > DistanceTolerance.Y) { continue; }
return port;
}
@@ -82,13 +82,13 @@ namespace Barotrauma.Items.Components
get { return nodes; }
}
private readonly List<Pair<Character,Node>> charactersInRange = new List<Pair<Character, Node>>();
private readonly List<(Character character, Node node)> charactersInRange = new List<(Character character, Node node)>();
private bool charging;
private float timer;
private Attack attack;
private readonly Attack attack;
public ElectricalDischarger(Item item, XElement element) :
base(item, element)
@@ -114,8 +114,8 @@ namespace Barotrauma.Items.Components
{
//already active, do nothing
if (IsActive) { return false; }
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
if (character != null && !CharacterUsable) { return false; }
CurrPowerConsumption = powerConsumption;
charging = true;
@@ -182,9 +182,9 @@ namespace Barotrauma.Items.Components
FindNodes(item.WorldPosition, Range);
if (attack != null)
{
foreach (Pair<Character, Node> characterInRange in charactersInRange)
foreach ((Character character, Node node) in charactersInRange)
{
characterInRange.First.ApplyAttack(null, characterInRange.Second.WorldPosition, attack, 1.0f);
character.ApplyAttack(null, node.WorldPosition, attack, 1.0f);
}
}
DischargeProjSpecific();
@@ -315,7 +315,6 @@ namespace Barotrauma.Items.Components
if (closestIndex == -1 || closestDist > currentRange)
{
int originalParentNodeIndex = parentNodeIndex;
//nothing in range, create some arcs to random directions
for (int i = 0; i < Rand.Int(4); i++)
{
@@ -455,7 +454,7 @@ namespace Barotrauma.Items.Components
AddNodesBetweenPoints(currPos, targetPos, 0.25f, ref parentNodeIndex);
nodes.Add(new Node(targetPos, parentNodeIndex));
entitiesInRange.RemoveAt(closestIndex);
charactersInRange.Add(new Pair<Character, Node>(character, nodes[parentNodeIndex]));
charactersInRange.Add((character, nodes[parentNodeIndex]));
FindNodes(entitiesInRange, targetPos, nodes.Count - 1, currentRange);
}
}
@@ -342,7 +342,7 @@ namespace Barotrauma.Items.Components
allowInsideFixture: true);
hitBodies.Clear();
hitBodies.AddRange(bodies);
hitBodies.AddRange(bodies.Distinct());
lastPickedFraction = Submarine.LastPickedFraction;
Type lastHitType = null;
@@ -678,7 +678,7 @@ namespace Barotrauma.Items.Components
Reset();
previousGap = leak;
}
Vector2 fromCharacterToLeak = leak.WorldPosition - character.WorldPosition;
Vector2 fromCharacterToLeak = leak.WorldPosition - character.AnimController.AimSourceWorldPos;
float dist = fromCharacterToLeak.Length();
float reach = AIObjectiveFixLeak.CalculateReach(this, character);
@@ -692,10 +692,10 @@ namespace Barotrauma.Items.Components
if (!character.AnimController.InWater)
{
// TODO: use the collider size?
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController humanAnim &&
Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f)
{
((HumanoidAnimController)character.AnimController).Crouching = true;
humanAnim.Crouching = true;
}
}
if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.inWater))
@@ -12,6 +12,9 @@ namespace Barotrauma.Items.Components
private bool midAir;
//continuous collision detection is used while the item is moving faster than this
const float ContinuousCollisionThreshold = 5.0f;
public Character CurrentThrower
{
get;
@@ -61,6 +64,13 @@ namespace Barotrauma.Items.Components
if (!item.body.Enabled) { return; }
if (midAir)
{
if (item.body.FarseerBody.IsBullet)
{
if (item.body.LinearVelocity.LengthSquared() < ContinuousCollisionThreshold * ContinuousCollisionThreshold)
{
item.body.FarseerBody.IsBullet = false;
}
}
if (item.body.LinearVelocity.LengthSquared() < 0.01f)
{
CurrentThrower = null;
@@ -156,6 +166,7 @@ namespace Barotrauma.Items.Components
//disable platform collisions until the item comes back to rest again
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
item.body.FarseerBody.IsBullet = true;
midAir = true;
ac.GetLimb(LimbType.Head).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
@@ -165,6 +176,7 @@ namespace Barotrauma.Items.Components
item.body.AngularVelocity = rightHand.body.AngularVelocity;
throwPos = 0;
throwDone = true;
IsActive = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
@@ -44,7 +44,7 @@ namespace Barotrauma.Items.Components
protected bool canBeCombined;
protected bool removeOnCombined;
public bool WasUsed;
public bool WasUsed, WasSecondaryUsed;
public readonly Dictionary<ActionType, List<StatusEffect>> statusEffectLists;
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using FarseerPhysics;
namespace Barotrauma.Items.Components
{
@@ -60,7 +61,21 @@ namespace Barotrauma.Items.Components
Drawable = !hideItems;
}
}
#if DEBUG
[Editable]
#endif
[Serialize("0.0,0.0", false, description: "The position where the contained items get drawn at (offset from the upper left corner of the sprite in pixels).")]
public Vector2 ItemPos { get; set; }
#if DEBUG
[Editable]
#endif
[Serialize("0.0,0.0", false, description: "The interval at which the contained items are spaced apart from each other (in pixels).")]
public Vector2 ItemInterval { get; set; }
[Serialize(100, false, description: "How many items are placed in a row before starting a new row.")]
public int ItemsPerRow { get; set; }
[Serialize(true, false, description: "Should the inventory of this item be visible when the item is selected.")]
public bool DrawInventory
{
@@ -118,6 +133,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, false, description: "Should the items configured using SpawnWithId spawn if this item is broken.")]
public bool SpawnWithIdWhenBroken
{
get;
set;
}
[Serialize(false, false)]
public bool RemoveContainedItemsOnDeconstruct { get; set; }
@@ -335,20 +357,66 @@ namespace Barotrauma.Items.Components
public void SetContainedItemPositions()
{
Vector2 simPos = item.SimPosition;
Vector2 displayPos = item.Position;
Vector2 transformedItemPos = ItemPos * item.Scale;
Vector2 transformedItemInterval = ItemInterval * item.Scale;
Vector2 transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f);
Vector2 transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y);
if (item.body == null)
{
if (item.FlippedX)
{
transformedItemPos.X = -transformedItemPos.X;
transformedItemPos.X += item.Rect.Width;
transformedItemInterval.X = -transformedItemInterval.X;
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
}
if (item.FlippedY)
{
transformedItemPos.Y = -transformedItemPos.Y;
transformedItemPos.Y -= item.Rect.Height;
transformedItemInterval.Y = -transformedItemInterval.Y;
transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y;
}
transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y);
if (Math.Abs(item.Rotation) > 0.01f)
{
Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation));
transformedItemPos = Vector2.Transform(transformedItemPos, transform);
transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform);
}
}
else
{
Matrix transform = Matrix.CreateRotationZ(item.body.Rotation);
if (item.body.Dir == -1.0f)
{
transformedItemPos.X = -transformedItemPos.X;
transformedItemInterval.X = -transformedItemInterval.X;
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
}
transformedItemPos = Vector2.Transform(transformedItemPos, transform);
transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
transformedItemPos += item.Position;
}
float currentRotation = itemRotation;
if (item.body != null)
{
currentRotation += item.body.Rotation;
}
int i = 0;
Vector2 currentItemPos = transformedItemPos;
foreach (Item contained in Inventory.AllItems)
{
if (contained.body != null)
{
try
{
Vector2 simPos = ConvertUnits.ToSimUnits(currentItemPos);
contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, currentRotation);
contained.body.SetPrevTransform(contained.body.SimPosition, contained.body.Rotation);
contained.body.UpdateDrawPosition();
@@ -365,14 +433,29 @@ namespace Barotrauma.Items.Components
contained.Rect =
new Rectangle(
(int)(displayPos.X - contained.Rect.Width / 2.0f),
(int)(displayPos.Y + contained.Rect.Height / 2.0f),
(int)(currentItemPos.X - contained.Rect.Width / 2.0f),
(int)(currentItemPos.Y + contained.Rect.Height / 2.0f),
contained.Rect.Width, contained.Rect.Height);
contained.Submarine = item.Submarine;
contained.CurrentHull = item.CurrentHull;
contained.SetContainedItemPositions();
i++;
if (Math.Abs(ItemInterval.X) > 0.001f && Math.Abs(ItemInterval.Y) > 0.001f)
{
//interval set on both axes -> use a grid layout
currentItemPos += transformedItemIntervalHorizontal;
if (i % ItemsPerRow == 0)
{
currentItemPos = transformedItemPos;
currentItemPos += transformedItemIntervalVertical * (i / ItemsPerRow);
}
}
else
{
currentItemPos += transformedItemInterval;
}
}
}
@@ -400,7 +483,7 @@ namespace Barotrauma.Items.Components
foreach (ushort id in itemIds[i])
{
if (!(Entity.FindEntityByID(id) is Item item)) { continue; }
Inventory.TryPutItem(item, i, false, false, null, false);
Inventory.TryPutItem(item, i, false, false, null, createNetworkEvent: false, ignoreCondition: true);
}
}
itemIds = null;
@@ -410,7 +493,7 @@ namespace Barotrauma.Items.Components
private void SpawnAlwaysContainedItems()
{
if (SpawnWithId.Length > 0)
if (SpawnWithId.Length > 0 && (item.Condition > 0.0f || SpawnWithIdWhenBroken))
{
string[] splitIds = SpawnWithId.Split(',');
foreach (string id in splitIds)
@@ -102,6 +102,12 @@ namespace Barotrauma.Items.Components
get { return limbPositions.Count > 0; }
}
public bool UserInCorrectPosition
{
get;
private set;
}
public bool AllowAiming
{
get;
@@ -149,6 +155,7 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
this.cam = cam;
UserInCorrectPosition = false;
if (IsToggle)
{
@@ -189,6 +196,7 @@ namespace Barotrauma.Items.Components
else
{
user.AnimController.TargetMovement = Vector2.Zero;
UserInCorrectPosition = true;
}
}
else
@@ -197,7 +205,7 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && user != Character.Controlled)
{
if (Math.Abs(diff.X) > 20.0f)
{
{
//wait for the character to walk to the correct position
return;
}
@@ -218,7 +226,8 @@ namespace Barotrauma.Items.Components
return;
}
}
user.AnimController.TargetMovement = Vector2.Zero;
user.AnimController.TargetMovement = Vector2.Zero;
UserInCorrectPosition = true;
}
}
@@ -365,7 +374,7 @@ namespace Barotrauma.Items.Components
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
{
if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f) { continue; }
if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f || item.LastSentSignalRecipients[i].IsPower) { continue; }
if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected)
{
return item.LastSentSignalRecipients[i].Item;
@@ -211,7 +211,7 @@ namespace Barotrauma.Items.Components
{
for (int i = inputContainer.Inventory.Capacity - 2; i >= 0; i--)
{
while (inputContainer.Inventory.GetItemAt(i) is Item item1 && inputContainer.Inventory.CanBePut(item1, i + 1))
while (inputContainer.Inventory.GetItemAt(i) is Item item1 && inputContainer.Inventory.CanBePutInSlot(item1, i + 1))
{
if (!inputContainer.Inventory.TryPutItem(item1, i + 1, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: true))
{
@@ -170,7 +170,7 @@ namespace Barotrauma.Items.Components
private void StartFabricating(FabricationRecipe selectedItem, Character user, bool addToServerLog = true)
{
if (selectedItem == null) { return; }
if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem)) { return; }
if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; }
#if CLIENT
itemList.Enabled = false;
@@ -308,7 +308,7 @@ namespace Barotrauma.Items.Components
}
Character tempUser = user;
int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem);
int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health);
for (int i = 0; i < fabricatedItem.Amount; i++)
{
if (i < amountFittingContainer)
@@ -334,7 +334,7 @@ namespace Barotrauma.Items.Components
}
}
if (user != null && !user.Removed)
if (user?.Info != null && !user.Removed)
{
foreach (Skill skill in fabricatedItem.RequiredSkills)
{
@@ -50,7 +50,7 @@ namespace Barotrauma.Items.Components
set { maxFlow = value; }
}
[Editable, Serialize(true, false, alwaysUseInstanceValues: true)]
[Editable, Serialize(true, true, alwaysUseInstanceValues: true)]
public bool IsOn
{
get { return IsActive; }
@@ -234,7 +234,10 @@ namespace Barotrauma.Items.Components
//use a smoothed "correct output" instead of the actual correct output based on the load
//so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily
correctTurbineOutput += MathHelper.Clamp((load / MaxPowerOutput * 100.0f) - correctTurbineOutput, -10.0f, 10.0f) * deltaTime;
if (!MathUtils.NearlyEqual(MaxPowerOutput, 0.0f))
{
correctTurbineOutput += MathHelper.Clamp((load / MaxPowerOutput * 100.0f) - correctTurbineOutput, -10.0f, 10.0f) * deltaTime;
}
//calculate tolerances of the meters based on the skills of the user
//more skilled characters have larger "sweet spots", making it easier to keep the power output at a suitable level
@@ -320,7 +323,7 @@ namespace Barotrauma.Items.Components
//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;
correctTurbineOutput = MathUtils.NearlyEqual(MaxPowerOutput, 0.0f) ? 0.0f : 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);
@@ -795,6 +795,7 @@ namespace Barotrauma.Items.Components
steeringInput = XMLExtensions.ParseVector2(signal.value, errorMessages: false);
steeringInput.X = MathHelper.Clamp(steeringInput.X, -100.0f, 100.0f);
steeringInput.Y = MathHelper.Clamp(-steeringInput.Y, -100.0f, 100.0f);
TargetVelocity = steeringInput;
}
else
{
@@ -74,8 +74,6 @@ namespace Barotrauma.Items.Components
[Serialize(100f, true, "How much fertilizer can the planter hold.")]
public float FertilizerCapacity { get; set; }
public string LastAction { get; set; } = "";
public Growable?[] GrowableSeeds = new Growable?[0];
private readonly List<RelatedItem> SuitableFertilizer = new List<RelatedItem>();
@@ -119,7 +117,7 @@ namespace Barotrauma.Items.Components
GrowableSeeds = new Growable[container.Capacity];
}
public override bool HasRequiredItems(Character character, bool addMessage, string msg = null)
public override bool HasRequiredItems(Character character, bool addMessage, string? msg = null)
{
if (container?.Inventory == null) { return false; }
@@ -137,9 +135,16 @@ namespace Barotrauma.Items.Components
return true;
}
Msg = MsgHarvest;
if (GrowableSeeds.Any(s => s != null))
{
Msg = MsgHarvest;
ParseMsg();
return true;
}
Msg = string.Empty;
ParseMsg();
return true;
return false;
}
public override bool Pick(Character character)
@@ -163,9 +168,16 @@ namespace Barotrauma.Items.Components
switch (plantItem.Type)
{
case PlantItemType.Seed:
LastAction = "PlantSeed";
ApplyStatusEffects(ActionType.OnPicked, 1.0f, character);
return container.Inventory.TryPutItem(plantItem.Item, character, new List<InvSlotType> { InvSlotType.Any });
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
return container.Inventory.TryPutItem(plantItem.Item, character);
}
else
{
//let the server handle moving the item
return false;
}
case PlantItemType.Fertilizer when plantItem.Item != null:
float canAdd = FertilizerCapacity - Fertilizer;
float maxAvailable = plantItem.Item.Condition;
@@ -175,7 +187,6 @@ namespace Barotrauma.Items.Components
#if CLIENT
character.UpdateHUDProgressBar(this, Item.DrawPosition, Fertilizer / FertilizerCapacity, Color.SaddleBrown, Color.SaddleBrown, "entityname.fertilizer");
#endif
LastAction = "ApplyFertilizer";
ApplyStatusEffects(ActionType.OnPicked, 1.0f, character);
return false;
}
@@ -203,7 +214,6 @@ namespace Barotrauma.Items.Components
container?.Inventory.RemoveItem(seed.Item);
Entity.Spawner?.AddToRemoveQueue(seed.Item);
GrowableSeeds[i] = null;
LastAction = "Harvest";
ApplyStatusEffects(ActionType.OnPicked, 1.0f, character);
return true;
}
@@ -133,6 +133,12 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (item.Connections == null)
{
IsActive = false;
return;
}
isRunning = true;
float chargeRatio = charge / capacity;
float gridPower = 0.0f;
@@ -173,7 +179,13 @@ namespace Barotrauma.Items.Components
}
else
{
currPowerConsumption = MathHelper.Lerp(currPowerConsumption, rechargeSpeed, 0.05f);
float missingCharge = capacity - charge;
float targetRechargeSpeed = rechargeSpeed;
if (missingCharge < 1.0f)
{
targetRechargeSpeed *= missingCharge;
}
currPowerConsumption = MathHelper.Lerp(currPowerConsumption, targetRechargeSpeed, 0.05f);
Charge += currPowerConsumption * Math.Min(Voltage, 1.0f) / 3600.0f;
}
@@ -408,21 +408,25 @@ namespace Barotrauma.Items.Components
}
}
bool hitSomething = false;
int hitCount = 0;
Vector2 lastHitPos = item.WorldPosition;
hits = hits.OrderBy(h => h.Fraction).ToList();
foreach (HitscanResult h in hits)
for (int i = 0; i < hits.Count; i++)
{
var h = hits[i];
item.SetTransform(h.Point, rotation);
if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero))
{
LaunchProjSpecific(rayStartWorld, item.WorldPosition);
hitSomething = true;
break;
hitCount++;
if (hitCount >= MaxTargetsToHit || i == hits.Count - 1)
{
LaunchProjSpecific(rayStartWorld, item.WorldPosition);
break;
}
}
}
//the raycast didn't hit anything -> the projectile flew somewhere outside the level and is permanently lost
if (!hitSomething)
//the raycast didn't hit anything (or didn't hit enough targets to stop the projectile) -> the projectile flew somewhere outside the level and is permanently lost
if (hitCount < MaxTargetsToHit)
{
item.body.SetTransformIgnoreContacts(item.body.SimPosition, rotation);
LaunchProjSpecific(rayStartWorld, rayEndWorld);
@@ -467,7 +471,7 @@ namespace Barotrauma.Items.Components
}
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; }
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; }
//if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
if (submarine != null)
@@ -505,7 +509,7 @@ namespace Barotrauma.Items.Components
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; }
if (fixture.Body?.UserData as string == "ruinroom") { return -1; }
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; }
//ignore everything else than characters, sub walls and level walls
if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) &&
@@ -640,7 +644,7 @@ namespace Barotrauma.Items.Components
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir,
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir,
collisionCategory: Physics.CollisionWall);
if (wallBody?.FixtureList?.First() != null && wallBody.UserData is Structure &&
if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) &&
//ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction
Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0)
{
@@ -738,7 +742,15 @@ namespace Barotrauma.Items.Components
}
else if (target.Body.UserData is IDamageable damageable)
{
if (Attack != null) { attackResult = Attack.DoDamage(User ?? Attacker, damageable, item.WorldPosition, 1.0f); }
if (Attack != null)
{
Vector2 pos = item.WorldPosition;
if (item.Submarine == null && damageable is Structure structure && structure.Submarine != null && Vector2.DistanceSquared(item.WorldPosition, structure.WorldPosition) > 10000.0f * 10000.0f)
{
item.Submarine = structure.Submarine;
}
attackResult = Attack.DoDamage(User ?? Attacker, damageable, pos, 1.0f);
}
}
else if (target.Body.UserData is VoronoiCell voronoiCell && voronoiCell.IsDestructible && Attack != null && Math.Abs(Attack.LevelWallDamage) > 0.0f)
{
@@ -139,7 +139,7 @@ namespace Barotrauma.Items.Components
//backwards compatibility
var repairThresholdAttribute =
element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("showrepairuithreshold", StringComparison.OrdinalIgnoreCase)) ??
element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("airepairth44reshold", StringComparison.OrdinalIgnoreCase));
element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("airepairthreshold", StringComparison.OrdinalIgnoreCase));
if (repairThresholdAttribute != null)
{
if (float.TryParse(repairThresholdAttribute.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out float repairThreshold))
@@ -349,7 +349,7 @@ namespace Barotrauma.Items.Components
foreach (Skill skill in requiredSkills)
{
float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier);
CurrentFixer.Info.IncreaseSkillLevel(skill.Identifier,
CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier,
SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f),
CurrentFixer.Position + Vector2.UnitY * 100.0f);
}
@@ -379,7 +379,7 @@ namespace Barotrauma.Items.Components
foreach (Skill skill in requiredSkills)
{
float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier);
CurrentFixer.Info.IncreaseSkillLevel(skill.Identifier,
CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier,
SkillSettings.Current.SkillIncreasePerSabotage / Math.Max(characterSkillLevel, 1.0f),
CurrentFixer.Position + Vector2.UnitY * 100.0f);
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -23,7 +24,7 @@ namespace Barotrauma.Items.Components
private int signalQueueSize;
private int delayTicks;
private Queue<DelayedSignal> signalQueue;
private readonly Queue<DelayedSignal> signalQueue;
private DelayedSignal prevQueuedSignal;
@@ -37,7 +38,7 @@ namespace Barotrauma.Items.Components
if (value == delay) { return; }
delay = value;
delayTicks = (int)(delay / Timing.Step);
signalQueueSize = delayTicks * 2;
signalQueueSize = Math.Max(delayTicks, 1) * 2;
}
}
@@ -74,7 +75,14 @@ namespace Barotrauma.Items.Components
var signalOut = signalQueue.Peek();
signalOut.SendDuration -= 1;
item.SendSignal(new Signal(signalOut.Signal.value, strength: signalOut.Signal.strength), "signal_out");
if (signalOut.SendDuration <= 0) { signalQueue.Dequeue(); } else { break; }
if (signalOut.SendDuration <= 0)
{
signalQueue.Dequeue();
}
else
{
break;
}
}
}
@@ -27,7 +27,7 @@ namespace Barotrauma.Items.Components
return retVal;
}
public static bool operator==(Signal a, Signal b) =>
public static bool operator ==(Signal a, Signal b) =>
a.value == b.value &&
a.stepsTaken == b.stepsTaken &&
a.sender == b.sender &&
@@ -35,6 +35,6 @@ namespace Barotrauma.Items.Components
MathUtils.NearlyEqual(a.power, b.power) &&
MathUtils.NearlyEqual(a.strength, b.strength);
public static bool operator!=(Signal a, Signal b) => !(a == b);
public static bool operator !=(Signal a, Signal b) => !(a == b);
}
}
@@ -77,11 +77,10 @@ namespace Barotrauma.Items.Components
//item in water -> we definitely want to send the True output
isInWater = true;
}
else if (item.CurrentHull != null)
else if (item.CurrentHull != null && item.CurrentHull.WaterPercentage > 0.0f)
{
//item in not water -> check if there's water anywhere within the rect of the item
if (item.CurrentHull.Surface > item.CurrentHull.Rect.Y - item.CurrentHull.Rect.Height + 1 &&
item.CurrentHull.Surface > item.Rect.Y - item.Rect.Height)
//(center of the) item in not water -> check if the water surface is below the bottom of the item's rect
if (item.CurrentHull.Surface > item.Rect.Y - item.Rect.Height)
{
isInWater = true;
}
@@ -844,10 +844,11 @@ namespace Barotrauma.Items.Components
ClearConnections();
base.RemoveComponentSpecific();
#if CLIENT
if (DraggingWire == this) { draggingWire = null; }
overrideSprite?.Remove();
overrideSprite = null;
wireSprite = null;
#endif
}
}
}
}
@@ -562,6 +562,7 @@ namespace Barotrauma.Items.Components
//use linked projectile containers in case they have to react to the turret being launched somehow
//(play a sound, spawn more projectiles)
if (!(e is Item linkedItem)) { continue; }
if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; }
if (linkedItem.Condition <= 0.0f)
{
loaderBroken = true;
@@ -971,6 +972,7 @@ namespace Barotrauma.Items.Components
foreach (MapEntity e in item.linkedTo)
{
if (!item.IsInteractable(character)) { continue; }
if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; }
if (e is Item projectileContainer)
{
var container = projectileContainer.GetComponent<ItemContainer>();
@@ -1323,6 +1325,7 @@ namespace Barotrauma.Items.Components
CheckProjectileContainer(item, projectiles, out bool _);
foreach (MapEntity e in item.linkedTo)
{
if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; }
if (e is Item projectileContainer)
{
CheckProjectileContainer(projectileContainer, projectiles, out bool stopSearching);
@@ -28,22 +28,25 @@ namespace Barotrauma
get { return items.Count; }
}
public bool CanBePut(Item item)
public bool CanBePut(Item item, bool ignoreCondition = false)
{
if (item == null) { return false; }
if (items.Count > 0)
{
if (item.IsFullCondition)
if (!ignoreCondition)
{
if (items.Any(it => !it.IsFullCondition)) { return false; }
}
else if (MathUtils.NearlyEqual(item.Condition, 0.0f))
{
if (items.Any(it => !MathUtils.NearlyEqual(it.Condition, 0.0f))) { return false; }
}
else
{
return false;
if (item.IsFullCondition)
{
if (items.Any(it => !it.IsFullCondition)) { return false; }
}
else if (MathUtils.NearlyEqual(item.Condition, 0.0f))
{
if (items.Any(it => !MathUtils.NearlyEqual(it.Condition, 0.0f))) { return false; }
}
else
{
return false;
}
}
if (items[0].Prefab.Identifier != item.Prefab.Identifier ||
items.Count + 1 > item.Prefab.MaxStackSize)
@@ -54,12 +57,31 @@ namespace Barotrauma
return true;
}
public bool CanBePut(ItemPrefab itemPrefab)
public bool CanBePut(ItemPrefab itemPrefab, float? condition = null)
{
if (itemPrefab == null) { return false; }
if (items.Count > 0)
{
if (items.Any(it => !it.IsFullCondition)) { return false; }
if (condition.HasValue)
{
if (MathUtils.NearlyEqual(condition.Value, 0.0f))
{
if (items.Any(it => it.Condition > 0.0f)) { return false; }
}
else if (MathUtils.NearlyEqual(condition.Value, itemPrefab.Health))
{
if (items.Any(it => !it.IsFullCondition)) { return false; }
}
else
{
return false;
}
}
else
{
if (items.Any(it => !it.IsFullCondition)) { return false; }
}
if (items[0].Prefab.Identifier != itemPrefab.Identifier ||
items.Count + 1 > itemPrefab.MaxStackSize)
{
@@ -70,13 +92,31 @@ namespace Barotrauma
}
/// <param name="maxStackSize">Defaults to <see cref="ItemPrefab.MaxStackSize"/> if null</param>
public int HowManyCanBePut(ItemPrefab itemPrefab, int? maxStackSize = null)
public int HowManyCanBePut(ItemPrefab itemPrefab, int? maxStackSize = null, float? condition = null)
{
if (itemPrefab == null) { return 0; }
maxStackSize ??= itemPrefab.MaxStackSize;
if (items.Count > 0)
{
if (items.Any(it => !it.IsFullCondition)) { return 0; }
if (condition.HasValue)
{
if (MathUtils.NearlyEqual(condition.Value, 0.0f))
{
if (items.Any(it => it.Condition > 0.0f)) { return 0; }
}
else if (MathUtils.NearlyEqual(condition.Value, itemPrefab.Health))
{
if (items.Any(it => !it.IsFullCondition)) { return 0; }
}
else
{
return 0;
}
}
else
{
if (items.Any(it => !it.IsFullCondition)) { return 0; }
}
if (items[0].Prefab.Identifier != itemPrefab.Identifier) { return 0; }
return maxStackSize.Value - items.Count;
}
@@ -340,7 +380,7 @@ namespace Barotrauma
return ownerItem.ParentInventory.ItemOwnsSelf(item);
}
public virtual int FindAllowedSlot(Item item)
public virtual int FindAllowedSlot(Item item, bool ignoreCondition = false)
{
if (ItemOwnsSelf(item)) { return -1; }
@@ -352,7 +392,7 @@ namespace Barotrauma
for (int i = 0; i < capacity; i++)
{
if (slots[i].CanBePut(item)) { return i; }
if (slots[i].CanBePut(item, ignoreCondition)) { return i; }
}
return -1;
@@ -365,7 +405,7 @@ namespace Barotrauma
{
for (int i = 0; i < capacity; i++)
{
if (CanBePut(item, i)) { return true; }
if (CanBePutInSlot(item, i)) { return true; }
}
return false;
}
@@ -373,57 +413,57 @@ namespace Barotrauma
/// <summary>
/// Can the item be put in the specified slot.
/// </summary>
public virtual bool CanBePut(Item item, int i)
public virtual bool CanBePutInSlot(Item item, int i, bool ignoreCondition = false)
{
if (ItemOwnsSelf(item)) { return false; }
if (i < 0 || i >= slots.Length) { return false; }
return slots[i].CanBePut(item);
return slots[i].CanBePut(item, ignoreCondition);
}
public bool CanBePut(ItemPrefab itemPrefab)
public bool CanBePut(ItemPrefab itemPrefab, float? condition = null)
{
for (int i = 0; i < capacity; i++)
{
if (CanBePut(itemPrefab, i)) { return true; }
if (CanBePutInSlot(itemPrefab, i, condition)) { return true; }
}
return false;
}
public virtual bool CanBePut(ItemPrefab itemPrefab, int i)
public virtual bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition = null)
{
if (i < 0 || i >= slots.Length) { return false; }
return slots[i].CanBePut(itemPrefab);
return slots[i].CanBePut(itemPrefab, condition);
}
public int HowManyCanBePut(ItemPrefab itemPrefab)
public int HowManyCanBePut(ItemPrefab itemPrefab, float? condition = null)
{
int count = 0;
for (int i = 0; i < capacity; i++)
{
count += HowManyCanBePut(itemPrefab, i);
count += HowManyCanBePut(itemPrefab, i, condition);
}
return count;
}
public virtual int HowManyCanBePut(ItemPrefab itemPrefab, int i)
public virtual int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition)
{
if (i < 0 || i >= slots.Length) { return 0; }
return slots[i].HowManyCanBePut(itemPrefab);
return slots[i].HowManyCanBePut(itemPrefab, condition: condition);
}
/// <summary>
/// If there is room, puts the item in the inventory and returns true, otherwise returns false
/// </summary>
public virtual bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
public virtual bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false)
{
int slot = FindAllowedSlot(item);
int slot = FindAllowedSlot(item, ignoreCondition);
if (slot < 0) { return false; }
PutItem(item, slot, user, true, createNetworkEvent);
return true;
}
public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true)
public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false)
{
if (i < 0 || i >= slots.Length)
{
@@ -441,12 +481,12 @@ namespace Barotrauma
//item in the slot removed as a result of combining -> put this item in the now free slot
if (!slots[i].Any())
{
return TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent);
return TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition);
}
return true;
}
}
if (CanBePut(item, i))
if (CanBePutInSlot(item, i, ignoreCondition))
{
PutItem(item, i, user, true, createNetworkEvent);
return true;
@@ -150,8 +150,10 @@ namespace Barotrauma
set
{
parentInventory = value;
if (parentInventory != null) Container = parentInventory.Owner as Item;
if (parentInventory != null) { Container = parentInventory.Owner as Item; }
#if SERVER
PreviousParentInventory = value;
#endif
}
}
@@ -241,7 +243,7 @@ namespace Barotrauma
private float rotationRad;
[Editable(0.0f, 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, true)]
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, MinValueFloat = 0.0f, MaxValueFloat = 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, true)]
public float Rotation
{
get
@@ -723,11 +725,7 @@ namespace Barotrauma
public override string ToString()
{
#if CLIENT
return (GameMain.DebugDraw) ? Name + " (ID: " + ID + ")" : Name;
#elif SERVER
return Name + " (ID: " + ID + ")";
#endif
}
private readonly List<ISerializableEntity> allPropertyObjects = new List<ISerializableEntity>();
@@ -1593,14 +1591,12 @@ namespace Barotrauma
{
ic.PlaySound(ActionType.Always);
ic.UpdateSounds();
if (!ic.WasUsed)
{
ic.StopSounds(ActionType.OnUse);
ic.StopSounds(ActionType.OnSecondaryUse);
}
if (!ic.WasUsed) { ic.StopSounds(ActionType.OnUse); }
if (!ic.WasSecondaryUsed) { ic.StopSounds(ActionType.OnSecondaryUse); }
}
#endif
ic.WasUsed = false;
ic.WasSecondaryUsed = false;
if (ic.IsActive)
{
@@ -2031,7 +2027,7 @@ namespace Barotrauma
//if there's an equal signal waiting to be sent
//to the same connection, don't add a new one
signal.stepsTaken = 0;
if (!delayedSignals.Contains((signal, connection)))
if (!delayedSignals.Any(s => s.Connection == connection && s.Signal.source == signal.source && s.Signal.value == signal.value && s.Signal.sender == signal.sender))
{
delayedSignals.Add((signal, connection));
CoroutineManager.StartCoroutine(DelaySignal(signal, connection));
@@ -2053,8 +2049,11 @@ namespace Barotrauma
private IEnumerable<object> DelaySignal(Signal signal, Connection connection)
{
//wait one frame
yield return CoroutineStatus.Running;
do
{
//wait at least one frame
yield return CoroutineStatus.Running;
} while (CoroutineManager.DeltaTime <= 0.0f);
delayedSignals.Remove((signal, connection));
@@ -2288,7 +2287,7 @@ namespace Barotrauma
if (!ic.HasRequiredContainedItems(character, isControlled)) { continue; }
if (ic.SecondaryUse(deltaTime, character))
{
ic.WasUsed = true;
ic.WasSecondaryUsed = true;
#if CLIENT
ic.PlaySound(ActionType.OnSecondaryUse, character);
@@ -2829,7 +2828,7 @@ namespace Barotrauma
int level = subElement.GetAttributeInt("level", 1);
if (upgradePrefab != null)
{
item.AddUpgrade(new Upgrade(item, upgradePrefab, level, subElement));
item.AddUpgrade(new Upgrade(item, upgradePrefab, level, appliedSwap != null ? null : subElement));
}
else
{
@@ -21,7 +21,7 @@ namespace Barotrauma
this.container = container;
}
public override int FindAllowedSlot(Item item)
public override int FindAllowedSlot(Item item, bool ignoreCondition = false)
{
if (ItemOwnsSelf(item)) { return -1; }
@@ -32,38 +32,38 @@ namespace Barotrauma
//try to stack first
for (int i = 0; i < capacity; i++)
{
if (slots[i].Any() && CanBePut(item, i)) { return i; }
if (slots[i].Any() && CanBePutInSlot(item, i, ignoreCondition)) { return i; }
}
for (int i = 0; i < capacity; i++)
{
if (CanBePut(item, i)) { return i; }
if (CanBePutInSlot(item, i, ignoreCondition)) { return i; }
}
return -1;
}
public override bool CanBePut(Item item, int i)
public override bool CanBePutInSlot(Item item, int i, bool ignoreCondition = false)
{
if (ItemOwnsSelf(item)) { return false; }
if (i < 0 || i >= slots.Length) { return false; }
if (!container.CanBeContained(item)) { return false; }
return item != null && slots[i].CanBePut(item) && slots[i].ItemCount < container.MaxStackSize;
return item != null && slots[i].CanBePut(item, ignoreCondition) && slots[i].ItemCount < container.MaxStackSize;
}
public override bool CanBePut(ItemPrefab itemPrefab, int i)
public override bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition)
{
if (i < 0 || i >= slots.Length) { return false; }
if (!container.CanBeContained(itemPrefab)) { return false; }
return itemPrefab != null && slots[i].CanBePut(itemPrefab) && slots[i].ItemCount < container.MaxStackSize;
return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition) && slots[i].ItemCount < container.MaxStackSize;
}
public override int HowManyCanBePut(ItemPrefab itemPrefab, int i)
public override int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition)
{
if (itemPrefab == null) { return 0; }
if (i < 0 || i >= slots.Length) { return 0; }
if (!container.CanBeContained(itemPrefab)) { return 0; }
return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.MaxStackSize, container.MaxStackSize));
return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.MaxStackSize, container.MaxStackSize), condition);
}
public override bool IsFull(bool takeStacksIntoAccount = false)
@@ -88,9 +88,9 @@ namespace Barotrauma
return true;
}
public override bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
public override bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false)
{
bool wasPut = base.TryPutItem(item, user, allowedSlots, createNetworkEvent);
bool wasPut = base.TryPutItem(item, user, allowedSlots, createNetworkEvent, ignoreCondition);
if (wasPut)
{
@@ -108,9 +108,9 @@ namespace Barotrauma
return wasPut;
}
public override bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true)
public override bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false)
{
bool wasPut = base.TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent);
bool wasPut = base.TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition);
if (wasPut && item.ParentInventory == this)
{
foreach (Character c in Character.CharacterList)
@@ -942,6 +942,7 @@ namespace Barotrauma.MapCreatures.Behavior
Anger += 0.01f;
bool wasRemoved = branch.Removed;
Branches.Remove(branch);
branch.Removed = true;
@@ -986,9 +987,11 @@ namespace Barotrauma.MapCreatures.Behavior
Kill();
return;
}
#if SERVER
SendNetworkMessage(this, NetworkHeader.BranchRemove, branch);
if (!wasRemoved)
{
SendNetworkMessage(this, NetworkHeader.BranchRemove, branch);
}
#endif
}
@@ -42,7 +42,7 @@ namespace Barotrauma
public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f, float ballastFloraStrength = 0.0f)
{
Attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, range)
Attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, Math.Min(range, 1000000))
{
SeverLimbsProbability = 1.0f
};
@@ -57,6 +57,7 @@ namespace Barotrauma
get { return open; }
set
{
if (float.IsNaN(value)) { return; }
if (value > open) { openedTimer = 1.0f; }
open = MathHelper.Clamp(value, 0.0f, 1.0f);
}
@@ -525,7 +525,13 @@ namespace Barotrauma
public override MapEntity Clone()
{
return new Hull(MapEntityPrefab.Find(null, "hull"), rect, Submarine);
var clone = new Hull(MapEntityPrefab.Find(null, "hull"), rect, Submarine);
foreach (KeyValuePair<string, SerializableProperty> property in SerializableProperties)
{
if (!property.Value.Attributes.OfType<Editable>().Any()) { continue; }
clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
}
return clone;
}
public static EntityGrid GenerateEntityGrid(Rectangle worldRect)
@@ -1837,7 +1837,7 @@ namespace Barotrauma
foreach (RuinShape ruinShape in ruin.RuinShapes)
{
var tooClose = GetTooCloseCells(ruinShape.Rect.Center.ToVector2(), Math.Max(ruinShape.Rect.Width, ruinShape.Rect.Height));
var tooClose = GetTooCloseCells(ruinShape.Rect.Center.ToVector2(), Math.Max(ruinShape.Rect.Width, ruinShape.Rect.Height) * 4);
foreach (VoronoiCell cell in tooClose)
{
@@ -1984,7 +1984,7 @@ namespace Barotrauma
private DestructibleLevelWall CreateIceSpire(List<GraphEdge> usedSpireEdges)
{
const float maxLength = 15000.0f;
float minEdgeLength = 100.0f;
var mainPathPos = PositionsOfInterest.Where(pos => pos.PositionType == PositionType.MainPath).GetRandom(Rand.RandSync.Server);
double closestDistSqr = double.PositiveInfinity;
GraphEdge closestEdge = null;
@@ -1999,8 +1999,9 @@ namespace Barotrauma
if (edge.Center.Y > Size.Y / 2 && (edge.Center.X < Size.X * 0.3f || edge.Center.X > Size.X * 0.7f)) { continue; }
if (Vector2.DistanceSquared(edge.Center, StartPosition) < maxLength * maxLength) { continue; }
if (Vector2.DistanceSquared(edge.Center, EndPosition) < maxLength * maxLength) { continue; }
//don't spawn on very long edges
if (Vector2.DistanceSquared(edge.Point1, edge.Point2) > 1000.0f * 1000.0f) { continue; }
//don't spawn on very long or very short edges
float edgeLengthSqr = Vector2.DistanceSquared(edge.Point1, edge.Point2);
if (edgeLengthSqr > 1000.0f * 1000.0f || edgeLengthSqr < minEdgeLength * minEdgeLength) { continue; }
//don't spawn on edges facing away from the main path
if (Vector2.Dot(Vector2.Normalize(mainPathPos.Position.ToVector2()) - edge.Center, edge.GetNormal(cell)) < 0.5f) { continue; }
double distSqr = MathUtils.DistanceSquared(edge.Center.X, edge.Center.Y, mainPathPos.Position.X, mainPathPos.Position.Y);
@@ -2723,7 +2724,7 @@ namespace Barotrauma
if (tries == 10)
{
position = EndPosition - Vector2.UnitY * 300.0f;
position = startPos;
}
} while (tries < 10);
@@ -3048,8 +3049,10 @@ namespace Barotrauma
var waypoints = WayPoint.WayPointList.Where(wp =>
wp.Submarine == null &&
wp.SpawnType == SpawnType.Path &&
wp.WorldPosition.X < EndExitPosition.X &&
!IsCloseToStart(wp.WorldPosition, minDistance) &&
!IsCloseToEnd(wp.WorldPosition, minDistance)).ToList();
!IsCloseToEnd(wp.WorldPosition, minDistance)
).ToList();
var subDoc = SubmarineInfo.OpenFile(contentFile.Path);
Rectangle subBorders = Submarine.GetBorders(subDoc.Root);
@@ -3134,7 +3137,7 @@ namespace Barotrauma
}
tempSW.Stop();
Debug.WriteLine($"Sub {sub.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)");
sub.SetPosition(spawnPoint);
sub.SetPosition(spawnPoint, forceUndockFromStaticSubmarines: false);
wreckPositions.Add(sub, positions);
blockedRects.Add(sub, rects);
return sub;
@@ -75,6 +75,10 @@ namespace Barotrauma
}
Cells = new List<VoronoiCell>() { wallCell };
Body = CaveGenerator.GeneratePolygons(Cells, level, out triangles);
if (triangles.Count == 0)
{
throw new ArgumentException("Failed to generate a wall (not enough triangles). Original vertices: " + string.Join(", ", originalVertices.Select(v => v.ToString())));
}
#if CLIENT
GenerateVertices();
#endif
@@ -192,6 +192,7 @@ namespace Barotrauma
if (!SelectedMissions.Contains(mission) && mission != null)
{
selectedMissions.Add(mission);
selectedMissions.Sort((m1, m2) => availableMissions.IndexOf(m1).CompareTo(availableMissions.IndexOf(m2)));
}
}
@@ -461,6 +462,18 @@ namespace Barotrauma
CreateStore(force: true);
}
public void UnlockInitialMissions()
{
if (Type.MissionIdentifiers.Any())
{
UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(Rand.RandSync.Server));
}
if (Type.MissionTags.Any())
{
UnlockMissionByTag(Type.MissionTags.GetRandom(Rand.RandSync.Server));
}
}
public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection)
{
if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
@@ -484,14 +484,7 @@ namespace Barotrauma
{
Difficulty = MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f)
};
if (location.Type.MissionIdentifiers.Any())
{
location.UnlockMissionByIdentifier(location.Type.MissionIdentifiers.GetRandom());
}
if (location.Type.MissionTags.Any())
{
location.UnlockMissionByTag(location.Type.MissionTags.GetRandom());
}
location.UnlockInitialMissions();
}
foreach (LocationConnection connection in Connections)
{
@@ -1078,7 +1078,7 @@ namespace Barotrauma
#endif
}
float gapOpen = (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold));
float gapOpen = MaxHealth <= 0.0f ? 0.0f : (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold));
Sections[sectionIndex].gap.Open = gapOpen;
}
@@ -1095,7 +1095,7 @@ namespace Barotrauma
{
if (damageDiff < 0.0f)
{
attacker.Info.IncreaseSkillLevel("mechanical",
attacker.Info?.IncreaseSkillLevel("mechanical",
-damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f),
SectionPosition(sectionIndex));
}
@@ -282,9 +282,11 @@ namespace Barotrauma
}
private float ballastFloraTimer;
public bool ImmuneToBallastFlora { get; set; }
public void AttemptBallastFloraInfection(string identifier, float deltaTime, float probability)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (ImmuneToBallastFlora) { return; }
if (ballastFloraTimer < 1f)
{
@@ -1264,7 +1266,7 @@ namespace Barotrauma
{
List<(ItemContainer container, int freeSlots)> containers = new List<(ItemContainer container, int freeSlots)>();
var connectedSubs = GetConnectedSubs();
foreach (Item item in Item.ItemList)
foreach (Item item in Item.ItemList.ToList())
{
if (!connectedSubs.Contains(item.Submarine)) { continue; }
if (!item.HasTag("cargocontainer")) { continue; }
@@ -671,17 +671,20 @@ namespace Barotrauma
Body.LinearVelocity -= velChange;
float damageAmount = contactDot * Body.Mass / limb.character.Mass;
limb.character.LastDamageSource = submarine;
limb.character.DamageLimb(ConvertUnits.ToDisplayUnits(collision.ImpactPos), limb,
AfflictionPrefab.ImpactDamage.Instantiate(damageAmount).ToEnumerable(), 0.0f, true, 0.0f);
if (limb.character.IsDead)
if (contactDot > 0.1f)
{
foreach (LimbJoint limbJoint in limb.character.AnimController.LimbJoints)
float damageAmount = contactDot * Body.Mass / limb.character.Mass;
limb.character.LastDamageSource = submarine;
limb.character.DamageLimb(ConvertUnits.ToDisplayUnits(collision.ImpactPos), limb,
AfflictionPrefab.ImpactDamage.Instantiate(damageAmount).ToEnumerable(), 0.0f, true, 0.0f);
if (limb.character.IsDead)
{
if (limbJoint.IsSevered || (limbJoint.LimbA != limb && limbJoint.LimbB != limb)) continue;
limb.character.AnimController.SeverLimbJoint(limbJoint);
foreach (LimbJoint limbJoint in limb.character.AnimController.LimbJoints)
{
if (limbJoint.IsSevered || (limbJoint.LimbA != limb && limbJoint.LimbB != limb)) continue;
limb.character.AnimController.SeverLimbJoint(limbJoint);
}
}
}
}
@@ -404,6 +404,8 @@ namespace Barotrauma
{
var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine)
.Concat(vanilla.GetFilesOfType(ContentType.Wreck))
.Concat(vanilla.GetFilesOfType(ContentType.BeaconStation))
.Concat(vanilla.GetFilesOfType(ContentType.EnemySubmarine))
.Concat(vanilla.GetFilesOfType(ContentType.Outpost))
.Concat(vanilla.GetFilesOfType(ContentType.OutpostModule));
string pathToCompare = FilePath.Replace(@"\", @"/").ToLowerInvariant();
@@ -779,6 +779,7 @@ namespace Barotrauma
public static WayPoint[] SelectCrewSpawnPoints(List<CharacterInfo> crew, Submarine submarine)
{
List<WayPoint> subWayPoints = WayPointList.FindAll(wp => wp.Submarine == submarine);
subWayPoints.Shuffle();
List<WayPoint> unassignedWayPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human);
@@ -789,7 +790,7 @@ namespace Barotrauma
//try to give the crew member a spawnpoint that hasn't been assigned to anyone and matches their job
for (int n = 0; n < unassignedWayPoints.Count; n++)
{
if (crew[i].Job.Prefab != unassignedWayPoints[n].AssignedJob) continue;
if (crew[i].Job.Prefab != unassignedWayPoints[n].AssignedJob) { continue; }
assignedWayPoints[i] = unassignedWayPoints[n];
unassignedWayPoints.RemoveAt(n);
@@ -800,17 +801,17 @@ namespace Barotrauma
//go through the crewmembers that don't have a spawnpoint yet (if any)
for (int i = 0; i < crew.Count; i++)
{
if (assignedWayPoints[i] != null) continue;
if (assignedWayPoints[i] != null) { continue; }
//try to assign a spawnpoint that matches the job, even if the spawnpoint is already assigned to someone else
foreach (WayPoint wp in subWayPoints)
{
if (wp.spawnType != SpawnType.Human || wp.AssignedJob != crew[i].Job.Prefab) continue;
if (wp.spawnType != SpawnType.Human || wp.AssignedJob != crew[i].Job.Prefab) { continue; }
assignedWayPoints[i] = wp;
break;
}
if (assignedWayPoints[i] != null) continue;
if (assignedWayPoints[i] != null) { continue; }
//try to assign a spawnpoint that isn't meant for any specific job
var nonJobSpecificPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human && wp.AssignedJob == null);
@@ -819,7 +820,7 @@ namespace Barotrauma
assignedWayPoints[i] = nonJobSpecificPoints[Rand.Int(nonJobSpecificPoints.Count, Rand.RandSync.Server)];
}
if (assignedWayPoints[i] != null) continue;
if (assignedWayPoints[i] != null) { continue; }
//everything else failed -> just give a random spawnpoint inside the sub
assignedWayPoints[i] = GetRandom(SpawnType.Human, null, submarine, useSyncedRand: true);
@@ -264,9 +264,17 @@ namespace Barotrauma.Networking
{
radio = null;
if (sender?.Inventory == null || sender.Removed) { return false; }
radio = sender.Inventory.AllItems.FirstOrDefault(i => i.GetComponent<WifiComponent>() != null)?.GetComponent<WifiComponent>();
if (radio?.Item == null) { return false; }
return sender.HasEquippedItem(radio.Item) && radio.CanTransmit();
foreach (Item item in sender.Inventory.AllItems)
{
var wifiComponent = item.GetComponent<WifiComponent>();
if (wifiComponent == null || !wifiComponent.CanTransmit() || !sender.HasEquippedItem(item)) { continue; }
if (radio == null || wifiComponent.Range > radio.Range)
{
radio = wifiComponent;
}
}
return radio?.Item != null;
}
}
}
@@ -377,7 +377,7 @@ namespace Barotrauma
return removeQueue.Contains(entity);
}
public void Update()
public void Update(bool createNetworkEvents = true)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
while (spawnQueue.Count > 0)
@@ -387,10 +387,13 @@ namespace Barotrauma
var spawnedEntity = entitySpawnInfo.Spawn();
if (spawnedEntity != null)
{
CreateNetworkEventProjSpecific(spawnedEntity, false);
if (spawnedEntity is Item)
if (createNetworkEvents)
{
CreateNetworkEventProjSpecific(spawnedEntity, false);
}
if (spawnedEntity is Item item)
{
((Item)spawnedEntity).Condition = ((ItemSpawnInfo)entitySpawnInfo).Condition;
item.Condition = ((ItemSpawnInfo)entitySpawnInfo).Condition;
}
entitySpawnInfo.OnSpawned(spawnedEntity);
}
@@ -403,7 +406,10 @@ namespace Barotrauma
{
item.SendPendingNetworkUpdates();
}
CreateNetworkEventProjSpecific(removedEntity, true);
if (createNetworkEvents)
{
CreateNetworkEventProjSpecific(removedEntity, true);
}
removedEntity.Remove();
}
}
@@ -318,7 +318,22 @@ namespace Barotrauma.Networking
{
RespawnCharactersProjSpecific(shuttlePos);
}
public static Affliction GetRespawnPenaltyAffliction()
{
var respawnPenaltyAffliction = AfflictionPrefab.List.FirstOrDefault(a => a.AfflictionType.Equals("respawnpenalty", StringComparison.OrdinalIgnoreCase));
return respawnPenaltyAffliction?.Instantiate(10.0f);
}
public static void GiveRespawnPenaltyAffliction(Character character)
{
var respawnPenaltyAffliction = GetRespawnPenaltyAffliction();
if (respawnPenaltyAffliction != null)
{
character.CharacterHealth.ApplyAffliction(targetLimb: null, respawnPenaltyAffliction);
}
}
public Vector2 FindSpawnPos()
{
if (Level.Loaded == null || Submarine.MainSub == null) { return Vector2.Zero; }
@@ -76,7 +76,8 @@ namespace Barotrauma
//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,
IsSwappableItem
IsSwappableItem,
AllowRotating
}
public bool IsEditable(ISerializableEntity entity)
@@ -86,7 +87,13 @@ namespace Barotrauma
case ConditionType.AllowLinkingWifiToChat:
return GameMain.NetworkMember?.ServerSettings?.AllowLinkingWifiToChat ?? true;
case ConditionType.IsSwappableItem:
return entity is Item item && item.Prefab.SwappableItem != null;
{
return entity is Item item && item.Prefab.SwappableItem != null;
}
case ConditionType.AllowRotating:
{
return entity is Item item && item.Prefab.AllowRotatingInEditor && Screen.Selected == GameMain.SubEditorScreen;
}
}
return false;
}
@@ -685,7 +685,7 @@ namespace Barotrauma
if (HasTargetType(TargetType.NearbyItems))
{
//optimization for powered components that can be easily fetched from Powered.PoweredList
if (targetIdentifiers.Count == 1 &&
if (targetIdentifiers?.Count == 1 &&
(targetIdentifiers.Contains("powered") || targetIdentifiers.Contains("junctionbox") || targetIdentifiers.Contains("relaycomponent")))
{
foreach (Powered powered in Powered.PoweredList)
@@ -971,6 +971,13 @@ namespace Barotrauma
{
position = targetLimb.WorldPosition;
}
else if (HasTargetType(TargetType.Contained))
{
if (targets.FirstOrDefault(t => t is Item) is Item targetItem)
{
position = targetItem.WorldPosition;
}
}
}
}
position += Offset;
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.
+118
View File
@@ -1,3 +1,121 @@
---------------------------------------------------------------------------------------------------------
v0.14.9.0
---------------------------------------------------------------------------------------------------------
Changes:
- If multiple turrets have been linked to the same loader, the turrets can't be swapped individually (both need to be swapped at the same time to prevent turrets from getting linked to an incorrect type of loader).
- Emergency lights don't cast shadows by default (they have such a short range that disabling the shadows usually doesn't make a difference visually, but improves the performance of the subs).
- Allow closing the submarine previews with esc.
- Made ballast pumps in Kastrull's drone indestructible (because they're impossible to access without breaking the walls/floors).
- Made exploding coilgun ammo more expensive and increased the capacity of the ammo boxes a bit.
- Made pirate subs immune to ballast flora.
- Security's stun guns spawn fully loaded.
- Reduced escort missions' base commonness. Doesn't have an effect in the campaign because the commonness is defined in the mission events, but fixes escort missions being much more common than other mission types in mission mode.
- Destroying a reactor with explosives depletes the fuel rods.
- Hide the equipment slots and character portait in wiring mode.
- Added platforms behind Remora Drone's hatches.
Fixes:
- Fixed enemies sometimes not spawning at all during a mission. Happened when the game selected the "stowaway" event to occur during the mission.
- Fixed pirate and escort missions not being available from late-game outposts.
- Fixed occasional "unauthorized multithreaded access to RandSync.Server" errors when starting a pirate mission in multiplayer.
- Fixed crashing with a "E_INVALIDARG" SharpDXException if an ice spire happens to generate on a very short level wall edge.
- Fixed crashing when trying to spawn inventory items for a character who left before respawning during the previous round.
- Fixed batteries always recharging at "full speed" when not full, regardless of how much power is being drawn from the battery. E.g. a battery that's only connected to a lamp drawing 5 kW of power would always recharge at the full 500 kWmin.
- Fixed traitor items sometimes spawning in non-interactable or hidden containers.
- Fixed turret rotation limit widgets working unreliably in the sub editor.
- Fixed barrels being misaligned on scaled turrets in the sub preview.
- Stores don't offer/request sealed supply crates as daily specials.
- Fixed campaign settings (radiation enabled, max missions) resetting when reopening the campaign settings menu.
- Fixed hitscan projectiles not hitting structures outside hulls when the turret itself is inside a hull.
- Fixed inability to swap items between the character's inventory and a container in some situations in multiplayer (when the container was right at the edge of the character's interaction range).
- Fixed bots being unable to repair the pump in Typhon's bottom airlock.
- Fixed crashing when repairing or fabricating something as a husk (or some other character with no CharacterInfo).
- Fixed crashing when reviving someone using CPR as a character with no CharacterInfo (e.g. husk).
- Fixed upgrades using the original value from a previous item when swapping to a new one (e.g. swapping a railgun to a coilgun sets the power consumption to the railgun's power consumption). More specifically, this happened when the campaign was saved and reloaded after purchasing the upgrade, and the item was swapped after that.
- Fixed wrecks sometimes being positioned right side of the exit position.
- Fixed characters not getting knocked down at the last stage of husk infection if they have the Vigor buff.
- Fixed discharge coil triggering when trying to rewire it.
- Fixed some ActionTypes having the same value (e.g. OnFire == OnDamaged). Caused wrong type of status effects triggering in some cases, for example OnDamage effects when an indestructible thalamus organ is on fire.
- Fixed items that are set to be hidden in-game being visible in the sub preview.
- Fixed bots being unable to weld leaks that are too high above them, even if they're actually in reach. Happened because the distance to the leak was calculated relative to the character's position, even though the range of the welding tool is relative to the character's shoulder.
- Fixed cargo mission rewards being displayed incorrectly when there's multiple cargo missions selected and not enough capacity for all the cargo.
- Fixed caret getting misplaced when clicking on a textbox with padding.
- Fixed headset only being able to send signals to each wifi component once.
- Fixed bots sometimes ignoring broken devices. Happened when another bot had selected the device as their repair target, even if the bot was currently repairing something else.
- Fixed nav terminal labels sometimes being draw under linked status monitor HUDs.
- Fixed bots sometimes not fixing airlock doors/hatches. More specifically, doors whose center point was outside a hull.
- Fixed medic bots grabbing the target and never letting go when there's no suitable treatments available anywhere in the sub, or when they're in an outpost and not carrying any suitable treatments.
- Fixed bots suffocating when they run out of oxygen tanks if ordered to wait wearing a suit.
- Fixed ability to select other items when operating periscopes. Didn't cause problems in vanilla subs, but in custom subs where the turret was placed close to the periscope and other interactable items, it was possible to accidentally select something else when trying to fire.
- Made nav terminal's "velocity_in" input change the target velocity, not just the steering input, making it possible to adjust the velocity with signals when using autopilot.
- Fixed text fields in a component's editing menu not refreshing until you've interacted with another component.
- Fixed ruins sometimes overlapping with level walls (when they happen to spawn next to a very large cell).
- Fixed ability to "partially open" the health interface in the sub editor, causing the inventory layout to get messed up when clicking on the character portrait even though the health interface isn't drawn.
- Fixed ability to keep dragging an item from a stunned/ragdolled character's inventory after they get back up.
- Fixed nuclear weapons not damaging ballast flora.
- Fixed normal uniforms deflecting projectiles even though they shouldn't.
- Fixed clients who've opted to spawn with reaper's tax getting a new character if the round ends before the client respawns.
- Fixed occasional crash with the error message "collection was modified; enumeration operation may not execute" when loading mods on startup.
- Fixed ability to keep dragging the previously equipped wire in the sub editor after you equip another wire with the wire hotkeys.
- Fixed characters' sprite depth changing during the "autowalk" towards a bed or chair.
- Require the sub to move a bit faster to crush characters between the sub and a wall. Previously essentially any non-zero velocity was enough to crush a character, even if the sub didn't appear to move.
- Fixed nests sometimes getting placed outside caves. Happened when the game tried to position the nest above an opening at the bottom of the cave.
- Fixed inability to use the health interface on the escorted characters.
- Fixed 1st client (usually the host) always spawning at the same spawnpoint in multiplayer even if there's multiple suitable ones available.
- Fixed stacked partially used items (e.g. stacked explosives that have been damaged) dropping from character inventories at the start of a round.
- Fixed occasional freezing when replacing lost shuttles after purchasing submarine upgrades.
- Adjusted Endworm's colliders so that you can't hit the tail between the armor segments before first breaking the armor (making it less easy to cut the worm in half).
- Fixed hull properties not carrying over when copying hulls in the sub editor.
- Fixed occasional "collection was modified" exception in CargoMission.DetermineCargo. Happened if the client received an updated campaign save while trying to load the sub between rounds.
- Fixed a broken waypoint in Berilia's cargo bay.
- Fixed seeds sometimes vanishing when trying to plant them in MP.
- Fixed planter boxes displaying the "uproot" message when empty.
- Fixed depth charges going through doors and hatches.
- Fixed ability to dock docking hatches to ports and vice versa.
- Fixed camera being able to focus on a turret when a periscope's "position_out" connection is wired to a turret's "power_in" connection.
- Fixed water detectors not detecting very small (water depth < 1px) amounts of water in hulls.
- Fixed duffel bags not spawning at the end of a round if the character hasn't despawned/respawned yet (i.e. if the character's corpse is still present in the sub).
- Fixed wrecked Dugong's distress signal being impossible to receive due to "allow cross-team communication" being set to false on the wifi component sending the signal.
- Fixed fabricator being unable to stack empty items in the output slots, preventing empty tanks from being fabricated when the output slots are occupied even if additional tanks could be stacked on them.
- Fixed messed up "Shuttle Shell A Glass A" sprite.
- Fixed delay components not working if the delay is set to 0.
- Fixed loaders getting slightly misaligned when swapping an empty hardpoint with some weapon.
- Fixed pumps placed in the sub editor being off by default.
- Fixed weapon skill increases not being capped according to the max vitality of the target. Resulted in enormous weapon skill gains when doing massive amounts of damage (more than the target's max vitality), e.g. by shooting a hammerhead matriarch in the egg sack with a nuke.
- Fixed ability to delete vanilla beacon stations and pirate subs in the sub editor.
- Fixed pirate subs sometimes spawning in side paths that are too narrow for the sub to pass through.
- Fixed an exploit that allowed creating game-crashing infinite signal loops using wifi components.
- Fixed a typo in the abyss diving suit description (100,000 m -> 10,000 m).
- Fixed several vanilla sub docking hatches requiring a welding tool instead of a wrench to repair them.
- Fixed "Operate Weapons" order being available when interacting with a turret directly, leading to a crash when trying to do so.
- Fixed missions disappearing from abandoned outposts after finishing the campaign.
- Fixed canister shells refilling automatically between rounds.
- Fixed turrets working with incorrect loader types (e.g. coilgun being able to fire laser bolts when linked to a pulse laser loader).
- Fixed "exterior pressure exceeds diving suit capabilities" hint popping up when carrying a diving suit.
- Fixed biome noise loops still playing after the round ends.
- Fixed detonator rapidly draining condition from flash powder and incendium instead of exploding them immediately (potentially resulting in partially used items).
- Fixed exploit in plastiseal crafting recipe (plastiseal deconstructing to 1 bandage even though 1 bandage can be used to craft 2 plastiseal).
- Fixed RepairTools applying their effects multiple times to items whose bodies consist of multiple fixtures. Caused minerals to get deattached 3 times faster than they should.
- Fixed AFK kicks when staying in any of the campaign UIs (store, etc) or other input-blocking menus for too long.
- Fixed monsters sometimes targeting owners of targeted items although the owner is ignored.
- Fixed some items held in the left hand rendering in front of the character's thigh.
- Fixed characters being focusable through UI elements (e.g. item interfaces), causing accidental interactions when the NPC interaction key is bind to LMB.
Modding:
- Added support for "additive" event sets which get added on top of another normal event set, allowing mods to spawn additional types of monsters without having to touch the vanilla event sets. See the "transitevents" event set in OutpostEvents.xml for an usage example.
- Fixed subs appearing to disappear after being published in the Workshop, because the content package the sub gets moved to wasn't automatically selected.
- Fixed MaxTargetsToHit not working on hitscan projectiles.
- Fixed offsets not being taken into account when positioning contained items, causing status effects to happen at the position of the container instead of the position of the contained item. Didn't have a noticeable effect in the vanilla game because most contained items are positioned close to the container's origin.
- Fixed spawnpoint's job restrictions being ignored when spawning pirates and escortees.
- It's now possible to use multiple equipped WifiComponents at the same time: instead of finding the first equipped item and seeing if it can receive/transmit, the game now goes through the items until it finds one that can.
- Fixed crashing when trying to pick an item that can't be put in any type of inventory slot from a container. Doesn't happen in the vanilla game because there are no such items in any container.
- Fixed crashing during wreck generation if there's any thalamus organs outside hulls.
- Fixed crashing if a wall whose max health is set to 0 in the sub editor takes damage.
- Fixed crashing if a reactor's maximum output is set to 0 and it's set to be on by default.
- Fixed a crashing due to a null reference exception if a status effect uses "target" instead of "targets" or "targettypes" in the definition.
- Fixed shuttles docked to a wreck undocking during level generation.
---------------------------------------------------------------------------------------------------------
v0.14.6.0
---------------------------------------------------------------------------------------------------------