Faction Test 100.13.0.0
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
RadiationEnabled="false"
|
||||
StartingBalanceAmount="High"
|
||||
StartItemSet="easy"
|
||||
MaxMissionCount="3"
|
||||
Difficulty="Easy"/>
|
||||
<CampaignSettings
|
||||
presetname="Normal"
|
||||
@@ -18,12 +19,14 @@
|
||||
RadiationEnabled="false"
|
||||
StartingBalanceAmount="Medium"
|
||||
StartItemSet="normal"
|
||||
MaxMissionCount="2"
|
||||
Difficulty="Medium"/>
|
||||
<CampaignSettings
|
||||
presetname="Hard"
|
||||
TutorialEnabled="false"
|
||||
RadiationEnabled="true"
|
||||
RadiationEnabled="false"
|
||||
StartingBalanceAmount="Low"
|
||||
StartItemSet="hard"
|
||||
MaxMissionCount="1"
|
||||
Difficulty="Hard"/>
|
||||
</CampaignSettingPresets>
|
||||
@@ -352,6 +352,21 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
|
||||
{
|
||||
if (myTeam == otherTeam) { return true; }
|
||||
return myTeam switch
|
||||
{
|
||||
// NPCs are friendly to the same team and the friendly NPCs
|
||||
CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
|
||||
// Friendly NPCs are friendly to both player teams
|
||||
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
|
||||
|
||||
public void ReequipUnequipped()
|
||||
{
|
||||
foreach (var item in unequippedItems)
|
||||
|
||||
@@ -354,6 +354,10 @@ namespace Barotrauma
|
||||
{
|
||||
targetingTag = "owner";
|
||||
}
|
||||
else if (targetCharacter.AIController is HumanAIController && !IsOnFriendlyTeam(Character, targetCharacter))
|
||||
{
|
||||
targetingTag = "hostile";
|
||||
}
|
||||
else if (AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP))
|
||||
{
|
||||
targetingTag = tP.Tag;
|
||||
@@ -364,7 +368,7 @@ namespace Barotrauma
|
||||
{
|
||||
targetingTag = "husk";
|
||||
}
|
||||
else if (!Character.IsFriendly(targetCharacter))
|
||||
else if (!Character.IsSameSpeciesOrGroup(targetCharacter))
|
||||
{
|
||||
if (enemy.CombatStrength > CombatStrength)
|
||||
{
|
||||
@@ -689,12 +693,9 @@ namespace Barotrauma
|
||||
return a.Damage >= selectedTargetingParams.Threshold;
|
||||
}
|
||||
Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character;
|
||||
//if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState
|
||||
//otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it
|
||||
//TODO: a way for pets to differentiate hostile and friendly humans?
|
||||
if (attacker?.AiTarget != null && targetCharacter.SpeciesName != GetTargetingTag(attacker.AiTarget) && !attacker.IsFriendly(targetCharacter))
|
||||
if (attacker?.AiTarget != null && !Character.IsSameSpeciesOrGroup(attacker) && !targetCharacter.IsSameSpeciesOrGroup(attacker))
|
||||
{
|
||||
// Attack the character that attacked the target we are protecting
|
||||
// Can't retaliate on characters of same species or group because that would make us hostile to all friendly characters in the same group.
|
||||
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
|
||||
SelectTarget(attacker.AiTarget);
|
||||
State = AIState.Attack;
|
||||
|
||||
@@ -1514,9 +1514,18 @@ namespace Barotrauma
|
||||
startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f);
|
||||
|
||||
//do a raycast upwards to find any walls
|
||||
float minCeilingDist = Character.AnimController.Collider.Height / 2 + Character.AnimController.Collider.Radius + 0.1f;
|
||||
if (!Character.AnimController.TryGetCollider(0, out PhysicsBody mainCollider))
|
||||
{
|
||||
mainCollider = Character.AnimController.Collider;
|
||||
}
|
||||
float margin = 0.1f;
|
||||
if (shouldCrouch)
|
||||
{
|
||||
margin *= 2;
|
||||
}
|
||||
float minCeilingDist = mainCollider.Height / 2 + mainCollider.Radius + margin;
|
||||
|
||||
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return !(fixture.Body.UserData is Submarine); }) != null;
|
||||
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) != null;
|
||||
}
|
||||
|
||||
public bool AllowCampaignInteraction()
|
||||
@@ -1589,7 +1598,27 @@ namespace Barotrauma
|
||||
(!requireEquipped || character.HasEquippedItem(i)) &&
|
||||
(predicate == null || predicate(i)), recursive, matchingItems);
|
||||
items = matchingItems;
|
||||
return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.OwnInventory == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)));
|
||||
foreach (var item in matchingItems)
|
||||
{
|
||||
if (item == null) { continue; }
|
||||
|
||||
if (containedTag.IsEmpty || item.OwnInventory == null)
|
||||
{
|
||||
//no contained items required, this item's ok
|
||||
return true;
|
||||
}
|
||||
var suitableSlot = item.GetComponent<ItemContainer>().FindSuitableSubContainerIndex(containedTag);
|
||||
if (suitableSlot == null)
|
||||
{
|
||||
//no restrictions on the suitable slot
|
||||
return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage);
|
||||
}
|
||||
else
|
||||
{
|
||||
return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage && it.ParentInventory.IsInSlot(it, suitableSlot.Value));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void StructureDamaged(Structure structure, float damageAmount, Character character)
|
||||
@@ -2016,11 +2045,9 @@ namespace Barotrauma
|
||||
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
|
||||
{
|
||||
bool sameTeam = me.TeamID == other.TeamID;
|
||||
bool friendlyTeam = IsOnFriendlyTeam(me, other);
|
||||
bool teamGood = sameTeam || friendlyTeam && !onlySameTeam;
|
||||
bool teamGood = sameTeam || !onlySameTeam && IsOnFriendlyTeam(me, other);
|
||||
if (!teamGood) { return false; }
|
||||
bool speciesGood = other.IsPet || other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group);
|
||||
if (!speciesGood) { return false; }
|
||||
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
|
||||
if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
{
|
||||
var reputation = campaign.Map?.CurrentLocation?.Reputation;
|
||||
@@ -2029,30 +2056,14 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet)
|
||||
{
|
||||
// Hostile NPCs are hostile to all pets, unless they are in the same team.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
|
||||
{
|
||||
if (myTeam == otherTeam) { return true; }
|
||||
|
||||
switch (myTeam)
|
||||
{
|
||||
case CharacterTeamType.None:
|
||||
case CharacterTeamType.Team1:
|
||||
case CharacterTeamType.Team2:
|
||||
// Only friendly to the same team and friendly NPCs
|
||||
return otherTeam == CharacterTeamType.FriendlyNPC;
|
||||
case CharacterTeamType.FriendlyNPC:
|
||||
// Friendly NPCs are friendly to both teams
|
||||
return otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
|
||||
|
||||
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
|
||||
|
||||
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
|
||||
|
||||
@@ -83,6 +83,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (GameMain.GameSession.RoundDuration < 120.0f &&
|
||||
speaker?.CurrentHull != null &&
|
||||
GameMain.GameSession.Map?.CurrentLocation?.Reputation?.Value >= 0.0f &&
|
||||
(speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) &&
|
||||
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
|
||||
{
|
||||
|
||||
+6
-2
@@ -98,7 +98,7 @@ namespace Barotrauma
|
||||
int containedItemCount = 0;
|
||||
foreach (Item it in container.Inventory.AllItems)
|
||||
{
|
||||
if (CheckItem(it))
|
||||
if (CheckItem(it) && IsInTargetSlot(it))
|
||||
{
|
||||
containedItemCount++;
|
||||
}
|
||||
@@ -118,7 +118,11 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
ItemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true);
|
||||
ItemToContain = item ?? character.Inventory.FindItem(it =>
|
||||
CheckItem(it) &&
|
||||
//ignore items already in the container, unless we're trying to place to a specific slot, and the item's not in it
|
||||
(it.Container != container.Item || (TargetSlot.HasValue && it.Container.OwnInventory.FindIndex(it) != TargetSlot)),
|
||||
recursive: true);
|
||||
if (ItemToContain != null)
|
||||
{
|
||||
if (!character.CanInteractWith(ItemToContain, checkLinked: false))
|
||||
|
||||
+37
-8
@@ -1,5 +1,5 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Barotrauma
|
||||
private AIObjectiveGetItem getDivingGear;
|
||||
private AIObjectiveContainItem getOxygen;
|
||||
private Item targetItem;
|
||||
private int? oxygenSourceSlotIndex;
|
||||
|
||||
public const float MIN_OXYGEN = 10;
|
||||
|
||||
@@ -43,12 +44,15 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
targetItem = character.Inventory.FindItemByTag(gearTag, true);
|
||||
|
||||
TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true));
|
||||
if (targetItem == null && gearTag == LIGHT_DIVING_GEAR)
|
||||
{
|
||||
targetItem = character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true);
|
||||
TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true));
|
||||
}
|
||||
if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0))
|
||||
if (targetItem == null ||
|
||||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) &&
|
||||
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
|
||||
{
|
||||
TryAddSubObjective(ref getDivingGear, () =>
|
||||
{
|
||||
@@ -84,7 +88,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
float min = GetMinOxygen(character);
|
||||
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > min))
|
||||
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => IsSuitableContainedOxygenSource(it)))
|
||||
{
|
||||
TryAddSubObjective(ref getOxygen, () =>
|
||||
{
|
||||
@@ -93,7 +97,7 @@ namespace Barotrauma
|
||||
if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min))
|
||||
{
|
||||
character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f);
|
||||
if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min).Count == 1)
|
||||
if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min, recursive: true).Count == 1)
|
||||
{
|
||||
character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f);
|
||||
}
|
||||
@@ -109,7 +113,8 @@ namespace Barotrauma
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true,
|
||||
ConditionLevel = MIN_OXYGEN,
|
||||
RemoveExistingWhenNecessary = true
|
||||
RemoveExistingWhenNecessary = true,
|
||||
TargetSlot = oxygenSourceSlotIndex
|
||||
};
|
||||
if (container.HasSubContainers)
|
||||
{
|
||||
@@ -167,12 +172,36 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsSuitableContainedOxygenSource(Item item)
|
||||
{
|
||||
return
|
||||
item != null &&
|
||||
item.HasTag(OXYGEN_SOURCE) &&
|
||||
item.Condition > 0 &&
|
||||
(oxygenSourceSlotIndex == null || item.ParentInventory.IsInSlot(item, oxygenSourceSlotIndex.Value));
|
||||
}
|
||||
|
||||
private void TrySetTargetItem(Item item)
|
||||
{
|
||||
if (targetItem == item) { return; }
|
||||
targetItem = item;
|
||||
if (targetItem != null)
|
||||
{
|
||||
oxygenSourceSlotIndex = targetItem.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(OXYGEN_SOURCE);
|
||||
}
|
||||
else
|
||||
{
|
||||
oxygenSourceSlotIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
getDivingGear = null;
|
||||
getOxygen = null;
|
||||
targetItem = null;
|
||||
oxygenSourceSlotIndex = null;
|
||||
}
|
||||
|
||||
public static float GetMinOxygen(Character character)
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ namespace Barotrauma
|
||||
Priority = 100;
|
||||
}
|
||||
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
|
||||
character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
|
||||
character.Submarine != null && !AIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
|
||||
{
|
||||
// Ordered to follow, hold position, or return back to main sub inside a hostile sub
|
||||
// -> ignore find safety unless we need to find a diving gear
|
||||
|
||||
+1
-1
@@ -413,7 +413,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void ApplyTreatment(Affliction affliction, Item item)
|
||||
{
|
||||
item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction));
|
||||
|
||||
@@ -412,7 +412,8 @@ namespace Barotrauma
|
||||
private int CalculateCellCount(int minValue, int maxValue)
|
||||
{
|
||||
if (maxValue == 0) { return 0; }
|
||||
float t = MathUtils.InverseLerp(0, 100, Level.Loaded.Difficulty * Config.AgentSpawnCountDifficultyMultiplier);
|
||||
float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
|
||||
float t = MathUtils.InverseLerp(0, 100, difficulty * Config.AgentSpawnCountDifficultyMultiplier);
|
||||
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t));
|
||||
}
|
||||
|
||||
@@ -422,7 +423,8 @@ namespace Barotrauma
|
||||
float delay = Config.AgentSpawnDelay;
|
||||
float min = delay;
|
||||
float max = delay * 6;
|
||||
float t = Level.Loaded.Difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor);
|
||||
float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
|
||||
float t = difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor);
|
||||
return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t));
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Barotrauma
|
||||
public bool IsAiming => wasAiming;
|
||||
public bool IsAimingMelee => wasAimingMelee;
|
||||
|
||||
protected bool Aiming => aiming || aimingMelee || LockFlippingUntil > Timing.TotalTime && character.IsKeyDown(InputType.Aim);
|
||||
protected bool Aiming => aiming || aimingMelee || FlipLockTime > Timing.TotalTime && character.IsKeyDown(InputType.Aim);
|
||||
|
||||
public float ArmLength => upperArmLength + forearmLength;
|
||||
|
||||
@@ -278,7 +278,11 @@ namespace Barotrauma
|
||||
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
|
||||
public bool IsAboveFloor => GetHeightFromFloor() > -0.1f;
|
||||
|
||||
public float LockFlippingUntil;
|
||||
public float FlipLockTime { get; private set; }
|
||||
public void LockFlipping(float time = 0.2f)
|
||||
{
|
||||
FlipLockTime = (float)Timing.TotalTime + time;
|
||||
}
|
||||
|
||||
public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos)
|
||||
{
|
||||
|
||||
@@ -1023,7 +1023,7 @@ namespace Barotrauma
|
||||
foreach (Limb l in Limbs)
|
||||
{
|
||||
if (l.IsSevered) { continue; }
|
||||
if (!l.DoesFlip) { continue; }
|
||||
if (!l.DoesFlip) { continue; }
|
||||
if (RagdollParams.IsSpritesheetOrientationHorizontal)
|
||||
{
|
||||
//horizontally aligned limbs need to be flipped 180 degrees
|
||||
@@ -1043,7 +1043,7 @@ namespace Barotrauma
|
||||
if (l.IsSevered) { continue; }
|
||||
|
||||
float rotation = l.body.Rotation;
|
||||
if (l.DoesFlip)
|
||||
if (l.DoesMirror)
|
||||
{
|
||||
if (RagdollParams.IsSpritesheetOrientationHorizontal)
|
||||
{
|
||||
|
||||
+10
-4
@@ -431,7 +431,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (Timing.TotalTime > LockFlippingUntil && TargetDir != dir && !IsStuck)
|
||||
if (Timing.TotalTime > FlipLockTime && TargetDir != dir && !IsStuck)
|
||||
{
|
||||
Flip();
|
||||
}
|
||||
@@ -1723,7 +1723,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (target.AnimController.Dir > 0 == WorldPosition.X > target.WorldPosition.X)
|
||||
{
|
||||
target.AnimController.LockFlippingUntil = (float)Timing.TotalTime + 0.5f;
|
||||
target.AnimController.LockFlipping(0.5f);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1822,16 +1822,22 @@ namespace Barotrauma
|
||||
|
||||
public override void Flip()
|
||||
{
|
||||
if (Character == null || Character.Removed)
|
||||
{
|
||||
LogAccessedRemovedCharacterError();
|
||||
return;
|
||||
}
|
||||
|
||||
base.Flip();
|
||||
|
||||
WalkPos = -WalkPos;
|
||||
|
||||
Limb torso = GetLimb(LimbType.Torso);
|
||||
|
||||
Vector2 difference;
|
||||
if (torso == null) { return; }
|
||||
|
||||
Matrix torsoTransform = Matrix.CreateRotationZ(torso.Rotation);
|
||||
|
||||
Vector2 difference;
|
||||
foreach (Item heldItem in character.HeldItems)
|
||||
{
|
||||
if (heldItem?.body != null && !heldItem.Removed && heldItem.GetComponent<Holdable>() != null)
|
||||
|
||||
@@ -57,17 +57,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (limbs == null)
|
||||
{
|
||||
if (!accessRemovedCharacterErrorShown)
|
||||
{
|
||||
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
|
||||
errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"Ragdoll.Limbs:AccessRemoved",
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
"Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
accessRemovedCharacterErrorShown = true;
|
||||
}
|
||||
LogAccessedRemovedCharacterError();
|
||||
return Array.Empty<Limb>();
|
||||
}
|
||||
return limbs;
|
||||
@@ -158,6 +148,20 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetCollider(int index, out PhysicsBody collider)
|
||||
{
|
||||
collider = null;
|
||||
try
|
||||
{
|
||||
collider = this.collider?[index];
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public int ColliderIndex
|
||||
{
|
||||
get
|
||||
@@ -881,7 +885,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (Limb limb in Limbs)
|
||||
{
|
||||
if (limb == null || limb.IsSevered || !limb.DoesFlip) { continue; }
|
||||
if (limb == null || limb.IsSevered || !limb.DoesMirror) { continue; }
|
||||
limb.Dir = Dir;
|
||||
limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y);
|
||||
limb.MirrorPullJoint();
|
||||
@@ -1436,6 +1440,21 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void LogAccessedRemovedCharacterError()
|
||||
{
|
||||
if (!accessRemovedCharacterErrorShown)
|
||||
{
|
||||
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
|
||||
errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"Ragdoll:AccessRemoved",
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
"Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
accessRemovedCharacterErrorShown = true;
|
||||
}
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime, Camera cam);
|
||||
|
||||
partial void Splash(Limb limb, Hull limbHull);
|
||||
|
||||
@@ -498,37 +498,52 @@ namespace Barotrauma
|
||||
DamageParticles(deltaTime, worldPosition);
|
||||
|
||||
var attackResult = target?.AddDamage(attacker, worldPosition, this, deltaTime, playSound) ?? new AttackResult();
|
||||
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
|
||||
var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
|
||||
var additionalEffectType = ActionType.OnUse;
|
||||
if (targetCharacter != null && targetCharacter.IsDead)
|
||||
{
|
||||
effectType = ActionType.OnEating;
|
||||
additionalEffectType = ActionType.OnEating;
|
||||
}
|
||||
|
||||
foreach (StatusEffect effect in statusEffects)
|
||||
{
|
||||
effect.sourceBody = sourceBody;
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
// TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied
|
||||
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity, worldPosition);
|
||||
var t = sourceLimb ?? attacker as ISerializableEntity;
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, attacker, t, worldPosition);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, attacker, t, worldPosition);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Parent))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, attacker, attacker);
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, attacker, attacker);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, attacker, attacker);
|
||||
}
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetCharacter, attackResult.HitLimb);
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
|
||||
// TODO: do we need the conversion to list here? It generates garbage.
|
||||
var targets = targetCharacter.AnimController.Limbs.Cast<ISerializableEntity>().ToList();
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetCharacter, targets);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, targetCharacter, targets);
|
||||
}
|
||||
}
|
||||
if (target is Entity targetEntity)
|
||||
@@ -538,18 +553,30 @@ namespace Barotrauma
|
||||
{
|
||||
targets.Clear();
|
||||
effect.AddNearbyTargets(worldPosition, targets);
|
||||
effect.Apply(effectType, deltaTime, targetEntity, targets);
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetEntity, targets);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, targetEntity, targets);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition);
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition);
|
||||
}
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
|
||||
{
|
||||
targets.Clear();
|
||||
targets.AddRange(attacker.Inventory.AllItems);
|
||||
effect.Apply(effectType, deltaTime, attacker, targets);
|
||||
if (additionalEffectType != ActionType.OnEating)
|
||||
{
|
||||
effect.Apply(conditionalEffectType, deltaTime, attacker, targets);
|
||||
}
|
||||
effect.Apply(additionalEffectType, deltaTime, attacker, targets);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,47 +612,52 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration);
|
||||
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
|
||||
var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
|
||||
|
||||
foreach (StatusEffect effect in statusEffects)
|
||||
{
|
||||
effect.sourceBody = sourceBody;
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
|
||||
effect.Apply(conditionalEffectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Parent))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, attacker, attacker);
|
||||
effect.Apply(conditionalEffectType, deltaTime, attacker, attacker);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, attacker, attacker);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character);
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb.character);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb.character);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb);
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
|
||||
// TODO: do we need the conversion to list here? It generates garbage.
|
||||
var targets = targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList();
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
targets.Clear();
|
||||
effect.AddNearbyTargets(worldPosition, targets);
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, targets);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition);
|
||||
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
|
||||
{
|
||||
targets.Clear();
|
||||
targets.AddRange(attacker.Inventory.AllItems);
|
||||
effect.Apply(effectType, deltaTime, attacker, targets);
|
||||
effect.Apply(conditionalEffectType, deltaTime, attacker, targets);
|
||||
effect.Apply(ActionType.OnUse, deltaTime, attacker, targets);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -365,9 +365,15 @@ namespace Barotrauma
|
||||
public readonly CharacterPrefab Prefab;
|
||||
|
||||
public readonly CharacterParams Params;
|
||||
|
||||
public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier();
|
||||
|
||||
public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group;
|
||||
|
||||
public bool IsHumanoid => Params.Humanoid;
|
||||
|
||||
public bool IsMachine => Params.IsMachine;
|
||||
|
||||
public bool IsHusk => Params.Husk;
|
||||
|
||||
public bool IsMale => info?.IsMale ?? false;
|
||||
@@ -1613,7 +1619,7 @@ namespace Barotrauma
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\".");
|
||||
}
|
||||
else if (humanPrefab.GiveItems(this, Submarine, spawnPoint))
|
||||
else if (humanPrefab.GiveItems(this, spawnPoint?.Submarine ?? Submarine, spawnPoint))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1752,7 +1758,7 @@ namespace Barotrauma
|
||||
float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed);
|
||||
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
|
||||
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
|
||||
SpeedMultiplier = greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier);
|
||||
SpeedMultiplier = Math.Max(0.0f, greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier));
|
||||
targetMovement *= SpeedMultiplier;
|
||||
// Reset, status effects will set the value before the next update
|
||||
ResetSpeedMultiplier();
|
||||
@@ -3881,8 +3887,8 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Affliction affliction in attackResult.Afflictions)
|
||||
{
|
||||
if (affliction.Strength == 0.0f) continue;
|
||||
sb.Append($" {affliction.Prefab.Name}: {affliction.Strength}");
|
||||
if (Math.Abs(affliction.Strength) <= 0.1f) { continue;}
|
||||
sb.Append($" {affliction.Prefab.Name}: {affliction.Strength.ToString("0.0")}");
|
||||
}
|
||||
}
|
||||
GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack);
|
||||
@@ -4481,7 +4487,10 @@ namespace Barotrauma
|
||||
|
||||
#if CLIENT
|
||||
//ensure we apply any pending inventory updates to drop any items that need to be dropped when the character despawns
|
||||
Inventory?.ApplyReceivedState();
|
||||
if (GameMain.Client?.ClientPeer is { IsActive: true })
|
||||
{
|
||||
Inventory?.ApplyReceivedState();
|
||||
}
|
||||
#endif
|
||||
|
||||
base.Remove();
|
||||
@@ -5197,15 +5206,13 @@ namespace Barotrauma
|
||||
|
||||
public void RemoveAbilityResistance(TalentResistanceIdentifier identifier) => abilityResistances.Remove(identifier);
|
||||
|
||||
/// <summary>
|
||||
/// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs
|
||||
/// </summary>
|
||||
public bool IsFriendly(Character other) => IsFriendly(this, other);
|
||||
|
||||
/// <summary>
|
||||
/// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs
|
||||
/// </summary>
|
||||
public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group);
|
||||
public static bool IsFriendly(Character me, Character other) => AIController.IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other);
|
||||
|
||||
public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other);
|
||||
|
||||
public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group);
|
||||
|
||||
public void StopClimbing()
|
||||
{
|
||||
|
||||
@@ -779,9 +779,10 @@ namespace Barotrauma
|
||||
FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
|
||||
SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
|
||||
|
||||
Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.White);
|
||||
Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.White);
|
||||
Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.White);
|
||||
//default to transparent color, it's invalid and will be replaced with a random one in CheckColors
|
||||
Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.Transparent);
|
||||
Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.Transparent);
|
||||
Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.Transparent);
|
||||
CheckColors();
|
||||
|
||||
TryLoadNameAndTitle(npcIdentifier);
|
||||
|
||||
@@ -146,12 +146,6 @@ namespace Barotrauma
|
||||
{
|
||||
return minVitality;
|
||||
}
|
||||
|
||||
if (Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
|
||||
{
|
||||
return Math.Max(vitality, MinVitality + 1);
|
||||
}
|
||||
|
||||
return vitality;
|
||||
|
||||
}
|
||||
@@ -587,6 +581,15 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void KillIfOutOfVitality()
|
||||
{
|
||||
if (Vitality <= MinVitality &&
|
||||
!Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
|
||||
{
|
||||
Kill();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>();
|
||||
private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>();
|
||||
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
|
||||
@@ -611,7 +614,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
CalculateVitality();
|
||||
if (Vitality <= MinVitality) { Kill(); }
|
||||
KillIfOutOfVitality();
|
||||
}
|
||||
|
||||
public float GetLimbDamage(Limb limb, string afflictionType = null)
|
||||
@@ -729,10 +732,7 @@ namespace Barotrauma
|
||||
existingAffliction.Duration = existingAffliction.Prefab.Duration;
|
||||
if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
|
||||
CalculateVitality();
|
||||
if (Vitality <= MinVitality)
|
||||
{
|
||||
Kill();
|
||||
}
|
||||
KillIfOutOfVitality();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -746,10 +746,7 @@ namespace Barotrauma
|
||||
Character.HealthUpdateInterval = 0.0f;
|
||||
|
||||
CalculateVitality();
|
||||
if (Vitality <= MinVitality)
|
||||
{
|
||||
Kill();
|
||||
}
|
||||
KillIfOutOfVitality();
|
||||
#if CLIENT
|
||||
if (OpenHealthWindow != this && limbHealth != null)
|
||||
{
|
||||
@@ -844,11 +841,7 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
CalculateVitality();
|
||||
|
||||
if (Vitality <= MinVitality)
|
||||
{
|
||||
Kill();
|
||||
}
|
||||
KillIfOutOfVitality();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -879,7 +872,11 @@ namespace Barotrauma
|
||||
|
||||
private void UpdateOxygen(float deltaTime)
|
||||
{
|
||||
if (!Character.NeedsOxygen) { return; }
|
||||
if (!Character.NeedsOxygen)
|
||||
{
|
||||
oxygenLowAffliction.Strength = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
|
||||
float prevOxygen = OxygenAmount;
|
||||
@@ -1025,17 +1022,18 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private readonly List<Affliction> allAfflictions = new List<Affliction>();
|
||||
private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions)
|
||||
private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
|
||||
{
|
||||
allAfflictions.Clear();
|
||||
if (!mergeSameAfflictions)
|
||||
{
|
||||
allAfflictions.AddRange(afflictions.Keys);
|
||||
allAfflictions.AddRange(predicate == null ? afflictions.Keys : afflictions.Keys.Where(predicate));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (Affliction affliction in afflictions.Keys)
|
||||
{
|
||||
if (predicate != null && !predicate(affliction)) { continue; }
|
||||
var existingAffliction = allAfflictions.Find(a => a.Prefab == affliction.Prefab);
|
||||
if (existingAffliction == null)
|
||||
{
|
||||
|
||||
@@ -200,7 +200,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
partial class Limb : ISerializableEntity, ISpatialEntity
|
||||
{
|
||||
//how long it takes for severed limbs to fade out
|
||||
@@ -215,7 +215,7 @@ namespace Barotrauma
|
||||
|
||||
//the physics body of the limb
|
||||
public PhysicsBody body;
|
||||
|
||||
|
||||
public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale;
|
||||
|
||||
public Hull Hull;
|
||||
@@ -249,7 +249,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private bool isSevered;
|
||||
private float severedFadeOutTimer;
|
||||
|
||||
@@ -269,7 +269,7 @@ namespace Barotrauma
|
||||
mouthPos = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public readonly Attack attack;
|
||||
public List<DamageModifier> DamageModifiers { get; private set; } = new List<DamageModifier>();
|
||||
|
||||
@@ -282,39 +282,73 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (character.AnimController.CurrentAnimationParams is GroundedMovementParams)
|
||||
if (character?.AnimController.CurrentAnimationParams is GroundedMovementParams && IsLeg)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case LimbType.LeftFoot:
|
||||
case LimbType.LeftLeg:
|
||||
case LimbType.LeftThigh:
|
||||
case LimbType.RightFoot:
|
||||
case LimbType.RightLeg:
|
||||
case LimbType.RightThigh:
|
||||
// Legs always has to flip
|
||||
return true;
|
||||
}
|
||||
// Legs always has to flip when not swimming
|
||||
return true;
|
||||
}
|
||||
return Params.Flip;
|
||||
}
|
||||
}
|
||||
|
||||
public bool DoesMirror
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsLeg)
|
||||
{
|
||||
// Legs always has to mirror
|
||||
return true;
|
||||
}
|
||||
return DoesFlip;
|
||||
}
|
||||
}
|
||||
|
||||
public float SteerForce => Params.SteerForce;
|
||||
|
||||
public Vector2 DebugTargetPos;
|
||||
public Vector2 DebugRefPos;
|
||||
|
||||
public bool IsLowerBody =>
|
||||
type == LimbType.LeftLeg ||
|
||||
type == LimbType.RightLeg ||
|
||||
type == LimbType.LeftFoot ||
|
||||
type == LimbType.RightFoot ||
|
||||
type == LimbType.Tail ||
|
||||
type == LimbType.Legs ||
|
||||
type == LimbType.RightThigh ||
|
||||
type == LimbType.LeftThigh ||
|
||||
type == LimbType.Waist;
|
||||
public bool IsLowerBody
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case LimbType.LeftLeg:
|
||||
case LimbType.RightLeg:
|
||||
case LimbType.LeftFoot:
|
||||
case LimbType.RightFoot:
|
||||
case LimbType.Tail:
|
||||
case LimbType.Legs:
|
||||
case LimbType.LeftThigh:
|
||||
case LimbType.RightThigh:
|
||||
case LimbType.Waist:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLeg
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case LimbType.LeftFoot:
|
||||
case LimbType.LeftLeg:
|
||||
case LimbType.LeftThigh:
|
||||
case LimbType.RightFoot:
|
||||
case LimbType.RightLeg:
|
||||
case LimbType.RightThigh:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSevered
|
||||
{
|
||||
|
||||
+4
-3
@@ -75,13 +75,14 @@ namespace Barotrauma.Abilities
|
||||
if (wt == WeaponType.Any || !weapontype.HasFlag(wt)) { continue; }
|
||||
switch (wt)
|
||||
{
|
||||
// it is possible that an item that has both a melee and a projectile component will return true
|
||||
// even when not used as a melee/ranged weapon respectively
|
||||
// attackdata should contain data regarding whether the attack is melee or not
|
||||
case WeaponType.Melee:
|
||||
//if the item has an active projectile component (has been fired), don't consider it a melee weapon
|
||||
if (item?.GetComponent<Projectile>() is { IsActive: true }) { continue; }
|
||||
if (item?.GetComponent<MeleeWeapon>() != null) { return true; }
|
||||
break;
|
||||
case WeaponType.Ranged:
|
||||
//if the item has a melee weapon component that's being used now, don't consider it a projectile
|
||||
if (item?.GetComponent<MeleeWeapon>() is { Hitting: true }) { continue; }
|
||||
if (item?.GetComponent<Projectile>() != null) { return true; }
|
||||
break;
|
||||
case WeaponType.HandheldRanged:
|
||||
|
||||
+8
@@ -17,6 +17,14 @@ namespace Barotrauma.Abilities
|
||||
tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
|
||||
}
|
||||
|
||||
public override void InitializeAbility(bool addingFirstTime)
|
||||
{
|
||||
if (addingFirstTime)
|
||||
{
|
||||
VerifyState(conditionsMatched: true, timeSinceLastUpdate: 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
|
||||
{
|
||||
if (conditionsMatched)
|
||||
|
||||
+5
@@ -13,6 +13,11 @@
|
||||
value = abilityElement.GetAttributeFloat("value", 0f);
|
||||
}
|
||||
|
||||
public override void InitializeAbility(bool addingFirstTime)
|
||||
{
|
||||
VerifyState(conditionsMatched: true, timeSinceLastUpdate: 0.0f);
|
||||
}
|
||||
|
||||
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
|
||||
{
|
||||
if (conditionsMatched != lastState)
|
||||
|
||||
+4
@@ -1,5 +1,6 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
@@ -17,6 +18,9 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
if (!addingFirstTime) { return; }
|
||||
|
||||
// do not run client-side in multiplayer
|
||||
if (GameMain.NetworkMember is { IsClient: true }) { return; }
|
||||
|
||||
JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet());
|
||||
if (apprentice is null)
|
||||
{
|
||||
|
||||
+10
@@ -49,6 +49,16 @@ namespace Barotrauma.Abilities
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (abilityEffectType)
|
||||
{
|
||||
case AbilityEffectType.OnDieToCharacter:
|
||||
if (characterAbilities.Any(a => a.RequiresAlive))
|
||||
{
|
||||
DebugConsole.AddWarning($"Potential error in talent {characterTalent}: an ability group has the type {AbilityEffectType.OnDieToCharacter}, but includes abilities that require the character to be alive, meaning they will never execute.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void ActivateAbilityGroup(bool addingFirstTime)
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace Barotrauma
|
||||
public const string RegularPackagesElementName = "regularpackages";
|
||||
public const string RegularPackagesSubElementName = "package";
|
||||
|
||||
public static bool ModsEnabled => GameMain.VanillaContent == null || EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent);
|
||||
|
||||
public static class EnabledPackages
|
||||
{
|
||||
public static CorePackage? Core { get; private set; } = null;
|
||||
|
||||
@@ -88,6 +88,7 @@ namespace Barotrauma
|
||||
public T GetAttributeEnum<T>(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def);
|
||||
public (T1, T2) GetAttributeTuple<T1, T2>(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def);
|
||||
public (T1, T2)[] GetAttributeTupleArray<T1, T2>(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def);
|
||||
public Range<int> GetAttributeRange(string key, in Range<int> def) => Element.GetAttributeRange(key, def);
|
||||
|
||||
public Identifier VariantOf() => Element.VariantOf();
|
||||
|
||||
|
||||
@@ -1263,6 +1263,22 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
|
||||
commands.Add(new Command("showreputation", "showreputation: List the current reputation values.", (string[] args) =>
|
||||
{
|
||||
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
{
|
||||
NewMessage("Reputation:");
|
||||
foreach (var faction in campaign.Factions)
|
||||
{
|
||||
NewMessage($" - {faction.Prefab.Name}: {faction.Reputation.Value}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ThrowError("Could not show reputation (no active campaign).");
|
||||
}
|
||||
}, null));
|
||||
|
||||
commands.Add(new Command("setlocationreputation", "setlocationreputation [value]: Set the reputation in the current location to the specified value.", (string[] args) =>
|
||||
{
|
||||
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
@@ -1315,10 +1331,10 @@ namespace Barotrauma
|
||||
}
|
||||
}, () =>
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
FactionPrefab.Prefabs.Select(f => f.Identifier.Value).ToArray(),
|
||||
GameMain.GameSession?.Campaign.Factions.Select(f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
|
||||
return new[]
|
||||
{
|
||||
FactionPrefab.Prefabs.Select(static f => f.Identifier.Value).ToArray(),
|
||||
GameMain.GameSession?.Campaign?.Factions.Select(static f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
|
||||
};
|
||||
}, true));
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ namespace Barotrauma
|
||||
PumpSpeed,
|
||||
PumpMaxFlow,
|
||||
ReactorMaxOutput,
|
||||
ReactorFuelEfficiency,
|
||||
ReactorFuelConsumption,
|
||||
DeconstructorSpeed,
|
||||
FabricationSpeed
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private int[] GetEndingOptions()
|
||||
public int[] GetEndingOptions()
|
||||
{
|
||||
List<int> endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList();
|
||||
if (!ContinueConversation) { endings.Add(-1); }
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace Barotrauma
|
||||
var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable());
|
||||
if (emptyLocation != null)
|
||||
{
|
||||
emptyLocation.ChangeType(campaign, Barotrauma.LocationType.Prefabs[LocationTypes[0]]);
|
||||
emptyLocation.ChangeType(campaign, LocationType.Prefabs[LocationTypes[0]]);
|
||||
unlockLocation = emptyLocation;
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!MissionIdentifier.IsEmpty)
|
||||
{
|
||||
unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier);
|
||||
unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier);
|
||||
}
|
||||
else if (!MissionTag.IsEmpty)
|
||||
{
|
||||
@@ -89,8 +89,9 @@ namespace Barotrauma
|
||||
}
|
||||
if (unlockedMission != null)
|
||||
{
|
||||
unlockedMission.OriginLocation = campaign.Map.CurrentLocation;
|
||||
campaign.Map.Discover(unlockLocation, checkTalents: false);
|
||||
if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] ==null)
|
||||
if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] == null)
|
||||
{
|
||||
DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.Name}\".");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace Barotrauma
|
||||
public enum NetworkEventType
|
||||
{
|
||||
CONVERSATION,
|
||||
CONVERSATION_SELECTED_OPTION,
|
||||
STATUSEFFECT,
|
||||
MISSION,
|
||||
UNLOCKPATH
|
||||
|
||||
@@ -229,9 +229,8 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
|
||||
|
||||
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
|
||||
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos, giveTags: true);
|
||||
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos);
|
||||
if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo)
|
||||
{
|
||||
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier);
|
||||
@@ -240,6 +239,7 @@ namespace Barotrauma
|
||||
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (spawnPos is WayPoint wp)
|
||||
{
|
||||
spawnedCharacter.GiveIdCardTags(wp);
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace Barotrauma
|
||||
List<Submarine> connectedSubs = level.BeaconStation.GetConnectedSubs();
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (!connectedSubs.Contains(item.Submarine)) { continue; }
|
||||
if (!connectedSubs.Contains(item.Submarine) || item.Submarine?.Info is { IsPlayer: true }) { continue; }
|
||||
if (item.GetComponent<PowerTransfer>() != null ||
|
||||
item.GetComponent<PowerContainer>() != null ||
|
||||
item.GetComponent<Reactor>() != null ||
|
||||
|
||||
@@ -125,6 +125,12 @@ namespace Barotrauma
|
||||
|
||||
public Identifier SonarIconIdentifier => Prefab.SonarIconIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Where was this mission received from? Affects which faction we give reputation for if the mission is configured to give reputation for the faction that gave the mission.
|
||||
/// Defaults to Locations[0]
|
||||
/// </summary>
|
||||
public Location OriginLocation;
|
||||
|
||||
public readonly Location[] Locations;
|
||||
|
||||
public int? Difficulty
|
||||
@@ -144,7 +150,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
|
||||
private readonly List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
|
||||
|
||||
public Action<Mission> OnMissionStateChanged;
|
||||
|
||||
@@ -160,12 +166,13 @@ namespace Barotrauma
|
||||
Headers = prefab.Headers;
|
||||
var messages = prefab.Messages.ToArray();
|
||||
|
||||
OriginLocation = locations[0];
|
||||
Locations = locations;
|
||||
|
||||
var endConditionElement = prefab.ConfigElement.GetChildElement(nameof(completeCheckDataAction));
|
||||
if (endConditionElement != null)
|
||||
{
|
||||
completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier.ToString()})");
|
||||
completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier})");
|
||||
}
|
||||
|
||||
for (int n = 0; n < 2; n++)
|
||||
@@ -407,7 +414,7 @@ namespace Barotrauma
|
||||
{
|
||||
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
|
||||
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
|
||||
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value));
|
||||
info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value));
|
||||
}
|
||||
|
||||
// apply money gains afterwards to prevent them from affecting XP gains
|
||||
@@ -436,7 +443,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (reputationReward.Key == "location")
|
||||
{
|
||||
Locations[0].Reputation?.AddReputation(reputationReward.Value);
|
||||
OriginLocation.Reputation?.AddReputation(reputationReward.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -546,17 +553,14 @@ namespace Barotrauma
|
||||
return humanPrefab;
|
||||
}
|
||||
|
||||
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.ServerAndClient, bool giveTags = true)
|
||||
protected static Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient)
|
||||
{
|
||||
var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient);
|
||||
characterInfo.TeamID = teamType;
|
||||
|
||||
if (positionToStayIn == null)
|
||||
{
|
||||
positionToStayIn =
|
||||
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.HumanPrefab = humanPrefab;
|
||||
|
||||
@@ -201,7 +201,7 @@ namespace Barotrauma
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (Level.Loaded.ExtraWalls.Any(w => w.IsPointInside(position.Position.ToVector2())))
|
||||
if (Level.Loaded.IsPositionInsideWall(position.Position.ToVector2()))
|
||||
{
|
||||
removals.Add(position);
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ namespace Barotrauma
|
||||
if (!SendUserStatistics) { return; }
|
||||
if (sentEventIdentifiers.Contains(identifier)) { return; }
|
||||
|
||||
if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent))
|
||||
if (ContentPackageManager.ModsEnabled)
|
||||
{
|
||||
message = "[MODDED] " + message;
|
||||
}
|
||||
|
||||
@@ -290,6 +290,16 @@ namespace Barotrauma
|
||||
else if (!character.Info.StartItemsGiven)
|
||||
{
|
||||
character.GiveJobItems(mainSubWaypoints[i]);
|
||||
foreach (Item item in character.Inventory.AllItems)
|
||||
{
|
||||
//if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in
|
||||
//we don't want that in this case, the crew's cards shouldn't be submarine-specific
|
||||
var idCard = item.GetComponent<Items.Components.IdCard>();
|
||||
if (idCard != null)
|
||||
{
|
||||
idCard.SubmarineSpecificID = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (character.Info.HealthData != null)
|
||||
{
|
||||
@@ -298,6 +308,7 @@ namespace Barotrauma
|
||||
|
||||
character.LoadTalents();
|
||||
|
||||
character.GiveIdCardTags(mainSubWaypoints[i]);
|
||||
character.GiveIdCardTags(spawnWaypoints[i]);
|
||||
character.Info.StartItemsGiven = true;
|
||||
if (character.Info.OrderData != null)
|
||||
|
||||
@@ -14,8 +14,9 @@ namespace Barotrauma
|
||||
{
|
||||
}
|
||||
|
||||
public CampaignMetadata(XElement element)
|
||||
public void Load(XElement element)
|
||||
{
|
||||
data.Clear();
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase))
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Barotrauma
|
||||
/// Get what kind of affiliation this faction has towards the player depending on who they chose to side with via talents
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static FactionAffiliation GetPlayerAffiliationStatus(Identifier identifier, ImmutableHashSet<Character>? characterList = null)
|
||||
public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction, ImmutableHashSet<Character>? characterList = null)
|
||||
{
|
||||
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return FactionAffiliation.Neutral; }
|
||||
|
||||
@@ -37,23 +37,20 @@ namespace Barotrauma
|
||||
{
|
||||
if (character.Info is not { } info) { continue; }
|
||||
|
||||
foreach (Faction faction in factions)
|
||||
foreach (Faction otherFaction in factions)
|
||||
{
|
||||
Identifier factionIdentifier = faction.Prefab.Identifier;
|
||||
Identifier factionIdentifier = otherFaction.Prefab.Identifier;
|
||||
if (info.GetSavedStatValue(StatTypes.Affiliation, factionIdentifier) > 0f)
|
||||
{
|
||||
return factionIdentifier == identifier
|
||||
return factionIdentifier == faction.Prefab.Identifier
|
||||
? FactionAffiliation.Positive
|
||||
: FactionAffiliation.Negative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FactionAffiliation.Neutral;
|
||||
}
|
||||
|
||||
public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction, ImmutableHashSet<Character>? characterList = null) => GetPlayerAffiliationStatus(faction.Prefab.Identifier, characterList);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()} ({Prefab?.Identifier.ToString() ?? "null"})";
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace Barotrauma
|
||||
private List<Faction> factions;
|
||||
public IReadOnlyList<Faction> Factions => factions;
|
||||
|
||||
public CampaignMetadata CampaignMetadata;
|
||||
public readonly CampaignMetadata CampaignMetadata;
|
||||
|
||||
protected XElement petsElement;
|
||||
|
||||
@@ -157,6 +157,7 @@ namespace Barotrauma
|
||||
|
||||
CargoManager = new CargoManager(this);
|
||||
MedicalClinic = new MedicalClinic(this);
|
||||
CampaignMetadata = new CampaignMetadata();
|
||||
Identifier messageIdentifier = new Identifier("money");
|
||||
|
||||
#if CLIENT
|
||||
@@ -675,9 +676,11 @@ namespace Barotrauma
|
||||
//TODO: ignore players who don't have the permission to trigger a transition between levels?
|
||||
var leavingPlayers = Character.CharacterList.Where(c => !c.IsDead && (c == Character.Controlled || c.IsRemotePlayer));
|
||||
|
||||
CharacterTeamType submarineTeam = leavingPlayers.FirstOrDefault()?.TeamID ?? CharacterTeamType.Team1;
|
||||
|
||||
//allow leaving if inside an outpost, and the submarine is either docked to it or close enough
|
||||
Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers);
|
||||
Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers);
|
||||
Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers, submarineTeam);
|
||||
Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers, submarineTeam);
|
||||
|
||||
int playersInSubAtStart = leavingSubAtStart == null || !leavingSubAtStart.AtStartExit ? 0 :
|
||||
leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost));
|
||||
@@ -691,11 +694,11 @@ namespace Barotrauma
|
||||
|
||||
return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd;
|
||||
|
||||
static Submarine GetLeavingSubAtStart(IEnumerable<Character> leavingPlayers)
|
||||
static Submarine GetLeavingSubAtStart(IEnumerable<Character> leavingPlayers, CharacterTeamType submarineTeam)
|
||||
{
|
||||
if (Level.Loaded.StartOutpost == null)
|
||||
{
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
|
||||
if (closestSub == null) { return null; }
|
||||
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
|
||||
}
|
||||
@@ -705,23 +708,23 @@ namespace Barotrauma
|
||||
if (Level.Loaded.StartOutpost.DockedTo.Any())
|
||||
{
|
||||
var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault();
|
||||
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; }
|
||||
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; }
|
||||
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
|
||||
}
|
||||
|
||||
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
|
||||
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) { return null; }
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
|
||||
if (closestSub == null || !closestSub.AtStartExit) { return null; }
|
||||
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
|
||||
}
|
||||
}
|
||||
|
||||
static Submarine GetLeavingSubAtEnd(IEnumerable<Character> leavingPlayers)
|
||||
static Submarine GetLeavingSubAtEnd(IEnumerable<Character> leavingPlayers, CharacterTeamType submarineTeam)
|
||||
{
|
||||
if (Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.ExitPoints.Any())
|
||||
{
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
|
||||
if (closestSub == null || !closestSub.AtEndExit) { return null; }
|
||||
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
|
||||
}
|
||||
@@ -733,7 +736,7 @@ namespace Barotrauma
|
||||
|
||||
if (Level.Loaded.EndOutpost == null)
|
||||
{
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
|
||||
if (closestSub == null) { return null; }
|
||||
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
|
||||
}
|
||||
@@ -743,13 +746,13 @@ namespace Barotrauma
|
||||
if (Level.Loaded.EndOutpost.DockedTo.Any())
|
||||
{
|
||||
var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault();
|
||||
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; }
|
||||
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; }
|
||||
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
|
||||
}
|
||||
|
||||
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
|
||||
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) { return null; }
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
|
||||
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
|
||||
if (closestSub == null || !closestSub.AtEndExit) { return null; }
|
||||
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
|
||||
}
|
||||
@@ -870,7 +873,7 @@ namespace Barotrauma
|
||||
}
|
||||
foreach (Location location in Map.Locations)
|
||||
{
|
||||
location.LevelData = new LevelData(location, location.Biome.AdjustedMaxDifficulty);
|
||||
location.LevelData = new LevelData(location, Map, location.Biome.AdjustedMaxDifficulty);
|
||||
location.Reset(this);
|
||||
}
|
||||
Map.ClearLocationHistory();
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public const int DefaultMaxMissionCount = 2;
|
||||
public const int MaxMissionCountLimit = 10;
|
||||
public const int MaxMissionCountLimit = 3;
|
||||
public const int MinMissionCountLimit = 1;
|
||||
|
||||
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
|
||||
|
||||
+4
-6
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.IO;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.IO;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
@@ -53,7 +54,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateFlag(NetFlags flag)
|
||||
private static bool ValidateFlag(NetFlags flag)
|
||||
{
|
||||
if (MathHelper.IsPowerOfTwo((int)flag)) { return true; }
|
||||
#if DEBUG
|
||||
@@ -105,7 +106,6 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
CampaignID = currentCampaignID;
|
||||
CampaignMetadata = new CampaignMetadata();
|
||||
UpgradeManager = new UpgradeManager(this);
|
||||
InitFactions();
|
||||
}
|
||||
@@ -194,7 +194,7 @@ namespace Barotrauma
|
||||
}
|
||||
break;
|
||||
case "metadata":
|
||||
CampaignMetadata = new CampaignMetadata(subElement);
|
||||
CampaignMetadata.Load(subElement);
|
||||
break;
|
||||
case "upgrademanager":
|
||||
case "pendingupgrades":
|
||||
@@ -237,10 +237,8 @@ namespace Barotrauma
|
||||
};
|
||||
}
|
||||
|
||||
CampaignMetadata ??= new CampaignMetadata();
|
||||
UpgradeManager ??= new UpgradeManager(this);
|
||||
|
||||
InitFactions();
|
||||
#if SERVER
|
||||
characterData.Clear();
|
||||
string characterDataPath = GetCharacterDataSavePath();
|
||||
|
||||
@@ -179,14 +179,41 @@ namespace Barotrauma
|
||||
|
||||
int price = prefab.Price.GetBuyPrice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation);
|
||||
int currentLevel = GetUpgradeLevel(prefab, category);
|
||||
int newLevel = currentLevel + 1;
|
||||
|
||||
int maxLevel = prefab.GetMaxLevelForCurrentSub();
|
||||
if (currentLevel + 1 > maxLevel)
|
||||
{
|
||||
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({currentLevel + 1} > {maxLevel}). The transaction has been cancelled.");
|
||||
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({newLevel} > {maxLevel}). The transaction has been cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool TryTakeResources(Character character)
|
||||
{
|
||||
bool result = prefab.TryTakeResources(character, newLevel);
|
||||
if (!result)
|
||||
{
|
||||
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" but the player does not have the required resources.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
switch (GameMain.NetworkMember)
|
||||
{
|
||||
case null when Character.Controlled is { } controlled: // singleplayer
|
||||
if (!TryTakeResources(controlled)) { return; }
|
||||
break;
|
||||
case { IsClient: true }:
|
||||
if (!prefab.HasResourcesToUpgrade(Character.Controlled, newLevel)) { return; }
|
||||
break;
|
||||
case { IsServer: true } when client?.Character is { } character:
|
||||
if (!TryTakeResources(character)) { return; }
|
||||
break;
|
||||
default:
|
||||
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" without a player.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (price < 0)
|
||||
{
|
||||
Location? location = Campaign.Map?.CurrentLocation;
|
||||
|
||||
@@ -188,16 +188,26 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private DockingPort FindAdjacentPort()
|
||||
{
|
||||
float closestDist = float.MaxValue;
|
||||
DockingPort closestPort = null;
|
||||
foreach (DockingPort port in list)
|
||||
{
|
||||
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; }
|
||||
float xDist = Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X);
|
||||
if (xDist > DistanceTolerance.X) { continue; }
|
||||
float yDist = Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y);
|
||||
if (yDist > DistanceTolerance.Y) { continue; }
|
||||
|
||||
return port;
|
||||
float dist = xDist + yDist;
|
||||
//disfavor non-interactable ports
|
||||
if (port.item.NonInteractable) { dist *= 2; }
|
||||
if (dist < closestDist)
|
||||
{
|
||||
closestPort = port;
|
||||
closestDist = dist;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return closestPort;
|
||||
}
|
||||
|
||||
private void AttemptDock()
|
||||
|
||||
@@ -365,7 +365,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
lastBrokenTime = Timing.TotalTime;
|
||||
//the door has to be restored to 50% health before collision detection on the body is re-enabled
|
||||
if (item.ConditionPercentage > 50.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
|
||||
if (item.ConditionPercentage / Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f &&
|
||||
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
|
||||
{
|
||||
IsBroken = false;
|
||||
}
|
||||
@@ -480,11 +481,16 @@ namespace Barotrauma.Items.Components
|
||||
ce = ce.Next;
|
||||
}
|
||||
}
|
||||
|
||||
if (OutsideSubmarineFixture != null)
|
||||
{
|
||||
OutsideSubmarineFixture.CollidesWith = Category.None;
|
||||
}
|
||||
linkedGap.Open = 1.0f;
|
||||
if (linkedGap != null)
|
||||
{
|
||||
linkedGap.Open = 1.0f;
|
||||
}
|
||||
|
||||
IsOpen = false;
|
||||
#if CLIENT
|
||||
if (convexHull != null) { convexHull.Enabled = false; }
|
||||
|
||||
@@ -366,7 +366,6 @@ namespace Barotrauma.Items.Components
|
||||
for (int i = 0; i < entitiesInRange.Count; i++)
|
||||
{
|
||||
float dist = float.MaxValue;
|
||||
|
||||
if (entitiesInRange[i] is Structure structure)
|
||||
{
|
||||
if (structure.IsHorizontal)
|
||||
@@ -388,7 +387,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else if (entitiesInRange[i] is Character character)
|
||||
{
|
||||
dist = MathUtils.LineSegmentToPointDistanceSquared(currPos, nodes[parentNodeIndex].WorldPosition, character.WorldPosition);
|
||||
dist = MathF.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(currPos, nodes[parentNodeIndex].WorldPosition, character.WorldPosition));
|
||||
}
|
||||
|
||||
if (dist < closestDist)
|
||||
|
||||
@@ -49,6 +49,13 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Disable to make the weapon ignore all hit effects when it collides with walls, doors, or other items.")]
|
||||
public bool HitOnlyCharacters
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize(true, IsPropertySaveable.No)]
|
||||
public bool Swing { get; set; }
|
||||
|
||||
@@ -112,7 +119,7 @@ namespace Barotrauma.Items.Components
|
||||
reloadTimer = reload;
|
||||
reloadTimer /= 1f + character.GetStatValue(StatTypes.MeleeAttackSpeed);
|
||||
reloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier);
|
||||
character.AnimController.LockFlippingUntil = (float)Timing.TotalTime + reloadTimer * 0.9f;
|
||||
character.AnimController.LockFlipping();
|
||||
|
||||
item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile;
|
||||
item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking;
|
||||
@@ -219,7 +226,7 @@ namespace Barotrauma.Items.Components
|
||||
ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
|
||||
if (ac.InWater)
|
||||
{
|
||||
ac.LockFlippingUntil = (float)Timing.TotalTime + Reload;
|
||||
ac.LockFlipping();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -343,33 +350,36 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
hitTargets.Add(targetCharacter);
|
||||
}
|
||||
else if ((f2.Body.UserData as Structure ?? f2.UserData as Structure) is Structure targetStructure)
|
||||
else if (!HitOnlyCharacters)
|
||||
{
|
||||
if (AllowHitMultiple)
|
||||
if ((f2.Body.UserData as Structure ?? f2.UserData as Structure) is Structure targetStructure)
|
||||
{
|
||||
if (hitTargets.Contains(targetStructure)) { return true; }
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetStructure)) { return true; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitTargets.Any(t => t is Structure)) { return true; }
|
||||
}
|
||||
hitTargets.Add(targetStructure);
|
||||
}
|
||||
else
|
||||
else if (f2.Body.UserData is Item targetItem)
|
||||
{
|
||||
if (hitTargets.Any(t => t is Structure)) { return true; }
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetItem)) { return true; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitTargets.Any(t => t is Item)) { return true; }
|
||||
}
|
||||
hitTargets.Add(targetItem);
|
||||
}
|
||||
hitTargets.Add(targetStructure);
|
||||
}
|
||||
else if (f2.Body.UserData is Item targetItem)
|
||||
{
|
||||
if (AllowHitMultiple)
|
||||
else if (f2.Body.UserData is Holdable holdable && holdable.CanPush)
|
||||
{
|
||||
if (hitTargets.Contains(targetItem)) { return true; }
|
||||
hitTargets.Add(holdable.Item);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitTargets.Any(t => t is Item)) { return true; }
|
||||
}
|
||||
hitTargets.Add(targetItem);
|
||||
}
|
||||
else if (f2.Body.UserData is Holdable holdable && holdable.CanPush)
|
||||
{
|
||||
hitTargets.Add(holdable.Item);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -381,6 +391,7 @@ namespace Barotrauma.Items.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
private System.Text.StringBuilder serverLogger;
|
||||
private void HandleImpact(Fixture targetFixture)
|
||||
{
|
||||
var target = targetFixture.Body;
|
||||
@@ -398,11 +409,13 @@ namespace Barotrauma.Items.Components
|
||||
Character user = User;
|
||||
Limb targetLimb = target.UserData as Limb;
|
||||
Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
|
||||
Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
|
||||
Item targetItem = target.UserData as Item;
|
||||
Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
|
||||
if (Attack != null)
|
||||
{
|
||||
Attack.SetUser(user);
|
||||
Attack.DamageMultiplier = damageMultiplier;
|
||||
|
||||
if (targetLimb != null)
|
||||
{
|
||||
if (targetLimb.character.Removed) { return; }
|
||||
@@ -415,12 +428,12 @@ namespace Barotrauma.Items.Components
|
||||
targetCharacter.LastDamageSource = item;
|
||||
Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if ((target.UserData as Structure ?? targetFixture.UserData as Structure) is Structure targetStructure)
|
||||
else if (targetStructure != null)
|
||||
{
|
||||
if (targetStructure.Removed) { return; }
|
||||
Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f);
|
||||
}
|
||||
else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
|
||||
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
|
||||
{
|
||||
if (targetItem.Removed) { return; }
|
||||
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
|
||||
@@ -457,27 +470,43 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
conditionalActionType = ActionType.OnFailure;
|
||||
}
|
||||
if (GameMain.NetworkMember is { IsServer: true } server && targetCharacter != null)
|
||||
if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null)
|
||||
{
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb));
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb));
|
||||
#if SERVER
|
||||
if (GameMain.Server != null) //TODO: Log structure hits
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, targetEntity));
|
||||
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, targetEntity));
|
||||
serverLogger ??= new System.Text.StringBuilder();
|
||||
serverLogger.Clear();
|
||||
serverLogger.Append($"{picker?.LogName} used {item.Name}");
|
||||
if (item.ContainedItems != null && item.ContainedItems.Any())
|
||||
{
|
||||
string logStr = picker?.LogName + " used " + item.Name;
|
||||
if (item.ContainedItems != null && item.ContainedItems.Any())
|
||||
{
|
||||
logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")";
|
||||
}
|
||||
logStr += " on " + targetCharacter.LogName + ".";
|
||||
Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack);
|
||||
serverLogger.Append($"({string.Join(", ", item.ContainedItems.Select(i => i?.Name))})");
|
||||
}
|
||||
#endif
|
||||
string targetName;
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
targetName = targetCharacter.LogName;
|
||||
}
|
||||
else if (targetItem != null)
|
||||
{
|
||||
targetName = targetItem.Name;
|
||||
}
|
||||
else if (targetStructure != null)
|
||||
{
|
||||
targetName = targetStructure.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetName = targetEntity.ToString();
|
||||
}
|
||||
serverLogger.Append($" on {targetName}.");
|
||||
#if SERVER
|
||||
Networking.GameServer.Log(serverLogger.ToString(), Networking.ServerLog.MessageType.Attack);
|
||||
#endif
|
||||
}
|
||||
if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe??
|
||||
if (targetEntity != null)
|
||||
{
|
||||
ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier);
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier);
|
||||
ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
|
||||
}
|
||||
|
||||
if (DeleteOnUse)
|
||||
|
||||
@@ -215,6 +215,8 @@ namespace Barotrauma.Items.Components
|
||||
baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure);
|
||||
}
|
||||
ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f);
|
||||
ReloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.FiringRateMultiplier);
|
||||
|
||||
currentChargeTime = 0f;
|
||||
|
||||
if (character != null)
|
||||
|
||||
@@ -4,7 +4,6 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.MapCreatures.Behavior;
|
||||
|
||||
@@ -185,24 +184,23 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
float degreeOfSuccess = character == null ? 0.5f : DegreeOfSuccess(character);
|
||||
|
||||
bool failed = false;
|
||||
if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
|
||||
return false;
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (UsableIn == UseEnvironment.None)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
|
||||
return false;
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (item.InWater)
|
||||
{
|
||||
if (UsableIn == UseEnvironment.Air)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
|
||||
return false;
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -210,9 +208,15 @@ namespace Barotrauma.Items.Components
|
||||
if (UsableIn == UseEnvironment.Water)
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
|
||||
return false;
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
if (failed)
|
||||
{
|
||||
// Always apply ActionType.OnUse. If doesn't fail, the effect is called later.
|
||||
ApplyStatusEffects(ActionType.OnUse, deltaTime, character);
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector2 rayStart;
|
||||
Vector2 rayStartWorld;
|
||||
@@ -312,9 +316,12 @@ namespace Barotrauma.Items.Components
|
||||
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair;
|
||||
|
||||
//if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them
|
||||
if (statusEffectLists != null && statusEffectLists.ContainsKey(ActionType.OnUse))
|
||||
if (statusEffectLists != null)
|
||||
{
|
||||
if (statusEffectLists[ActionType.OnUse].Any(s => s.SeverLimbsProbability > 0.0f))
|
||||
static bool CanSeverJoints(ActionType type, Dictionary<ActionType, List<StatusEffect>> effectList) =>
|
||||
effectList.TryGetValue(type, out List<StatusEffect> effects) && effects.Any(e => e.SeverLimbsProbability > 0);
|
||||
|
||||
if (CanSeverJoints(ActionType.OnUse, statusEffectLists) || CanSeverJoints(ActionType.OnSuccess, statusEffectLists))
|
||||
{
|
||||
float rangeSqr = ConvertUnits.ToSimUnits(Range);
|
||||
rangeSqr *= rangeSqr;
|
||||
@@ -537,6 +544,7 @@ namespace Barotrauma.Items.Components
|
||||
if (nonFixableEntities.Contains(targetStructure.Prefab.Identifier) || nonFixableEntities.Any(t => targetStructure.Tags.Contains(t))) { return false; }
|
||||
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, structure: targetStructure);
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, structure: targetStructure);
|
||||
FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex);
|
||||
|
||||
float structureFixAmount = StructureFixAmount;
|
||||
@@ -605,6 +613,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetCharacter, limb: closestLimb);
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetCharacter, limb: closestLimb);
|
||||
FixCharacterProjSpecific(user, deltaTime, targetCharacter);
|
||||
return true;
|
||||
}
|
||||
@@ -621,6 +630,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
targetLimb.character.LastDamageSource = item;
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetLimb.character, limb: targetLimb);
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetLimb.character, limb: targetLimb);
|
||||
FixCharacterProjSpecific(user, deltaTime, targetLimb.character);
|
||||
return true;
|
||||
}
|
||||
@@ -663,6 +673,7 @@ namespace Barotrauma.Items.Components
|
||||
targetItem.IsHighlighted = true;
|
||||
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem);
|
||||
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, targetItem);
|
||||
|
||||
if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f))
|
||||
{
|
||||
@@ -875,7 +886,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else if (effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
currentTargets.Add(character);
|
||||
currentTargets.Add(user);
|
||||
effect.Apply(actionType, deltaTime, item, currentTargets);
|
||||
}
|
||||
else if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
|
||||
@@ -210,7 +210,7 @@ namespace Barotrauma.Items.Components
|
||||
if (!(GameMain.NetworkMember is { IsClient: true }))
|
||||
{
|
||||
//Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse"
|
||||
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower);
|
||||
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, useTarget: CurrentThrower, user: CurrentThrower);
|
||||
}
|
||||
throwState = ThrowState.None;
|
||||
}
|
||||
|
||||
@@ -849,7 +849,13 @@ namespace Barotrauma.Items.Components
|
||||
if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; }
|
||||
if (user != null) { effect.SetUser(user); }
|
||||
effect.AfflictionMultiplier = afflictionMultiplier;
|
||||
item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
|
||||
var c = character;
|
||||
if (user != null && effect.HasTargetType(StatusEffect.TargetType.Character) && !effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
// A bit hacky, but fixes MeleeWeapons targeting the use target instead of the attacker. Also applies to Projectiles and Throwables, or other callers that passes the user.
|
||||
c = user;
|
||||
}
|
||||
item.ApplyStatusEffect(effect, type, deltaTime, c, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
|
||||
effect.AfflictionMultiplier = 1.0f;
|
||||
reducesCondition |= effect.ReducesItemCondition();
|
||||
}
|
||||
|
||||
@@ -762,7 +762,6 @@ namespace Barotrauma.Items.Components
|
||||
var relatedItem = FindContainableItem(contained);
|
||||
if (relatedItem != null)
|
||||
{
|
||||
if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; }
|
||||
if (relatedItem.ItemPos.HasValue)
|
||||
{
|
||||
Vector2 pos = relatedItem.ItemPos.Value;
|
||||
|
||||
@@ -431,7 +431,7 @@ namespace Barotrauma.Items.Components
|
||||
//disable flipping for 0.5 seconds, because flipping the character when it's in a weird pose (e.g. lying in bed) can mess up the ragdoll
|
||||
if (character.AnimController is HumanoidAnimController humanoidAnim)
|
||||
{
|
||||
humanoidAnim.LockFlippingUntil = (float)Timing.TotalTime + 0.5f;
|
||||
humanoidAnim.LockFlipping(0.5f);
|
||||
}
|
||||
|
||||
if (character.SelectedItem == item) { character.SelectedItem = null; }
|
||||
|
||||
@@ -874,6 +874,6 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
private float GetMaxOutput() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput);
|
||||
private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelEfficiency, fuelConsumptionRate);
|
||||
private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -781,7 +781,7 @@ namespace Barotrauma.Items.Components
|
||||
limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition);
|
||||
return false;
|
||||
}
|
||||
if (!FriendlyFire && User != null && limb.character.IsFriendly(User) && HumanAIController.IsOnFriendlyTeam(limb.character, User))
|
||||
if (!FriendlyFire && User != null && limb.character.IsFriendly(User))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -789,7 +789,13 @@ namespace Barotrauma.Items.Components
|
||||
else if (target.Body.UserData is Item item)
|
||||
{
|
||||
if (item.Condition <= 0.0f) { return false; }
|
||||
if (!item.Prefab.DamagedByProjectiles) { return false; }
|
||||
if (!item.Prefab.DamagedByProjectiles)
|
||||
{
|
||||
if (item.GetComponent<Door>() == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (target.Body.UserData is Holdable { CanPush: false })
|
||||
{
|
||||
@@ -903,7 +909,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else if (target.Body.UserData is Limb limb)
|
||||
{
|
||||
if (!FriendlyFire && User != null && limb.character.IsFriendly(User) && HumanAIController.IsOnFriendlyTeam(limb.character, User))
|
||||
if (!FriendlyFire && User != null && limb.character.IsFriendly(User))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -922,6 +928,8 @@ namespace Barotrauma.Items.Components
|
||||
else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item ?? target.UserData as Item) is Item targetItem)
|
||||
{
|
||||
if (targetItem.Removed) { return false; }
|
||||
//hit the external collider of an item (turret?) of the same sub -> ignore
|
||||
if (target.UserData is Item && targetItem.Submarine != null && targetItem.Submarine == Launcher?.Submarine) { return false; }
|
||||
if (Attack != null && (targetItem.Prefab.DamagedByProjectiles || DamageDoors && targetItem.GetComponent<Door>() != null) && targetItem.Condition > 0)
|
||||
{
|
||||
attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f);
|
||||
@@ -974,8 +982,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (target.Body.UserData is Limb targetLimb)
|
||||
{
|
||||
ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: User);
|
||||
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: User);
|
||||
ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: character, user: User);
|
||||
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, useTarget: character, user: User);
|
||||
var attack = targetLimb.attack;
|
||||
if (attack != null)
|
||||
{
|
||||
@@ -986,14 +994,22 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
||||
{
|
||||
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition);
|
||||
effect.Apply(effect.type, 1.0f, User, User);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Character) || effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
targets.Clear();
|
||||
effect.AddNearbyTargets(targetLimb.WorldPosition, targets);
|
||||
effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets);
|
||||
effect.Apply(effect.type, 1.0f, targetLimb.character, targets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,15 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (submarine?.Info == null || level == null || submarine.Info.Type == SubmarineType.Player) { return 0; }
|
||||
|
||||
float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, 1.0f);
|
||||
float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, level.LevelData.Biome.ActualMaxDifficulty / 100.0f);
|
||||
|
||||
if (level.Type == LevelData.LevelType.Outpost &&
|
||||
level.StartLocation?.Type?.OutpostTeam == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
//no high-quality spawns in friendly outposts
|
||||
difficultyFactor = 0.0f;
|
||||
}
|
||||
|
||||
return ToolBox.SelectWeightedRandom(Enumerable.Range(0, MaxQuality + 1), q => GetCommonness(q, difficultyFactor), randSync);
|
||||
|
||||
static float GetCommonness(int quality, float difficultyFactor)
|
||||
|
||||
@@ -159,9 +159,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
var user = item.GetComponent<Projectile>()?.User;
|
||||
if (source == null || target == null || target.Removed ||
|
||||
(source is Entity sourceEntity && sourceEntity.Removed) ||
|
||||
(source is Limb limb && limb.Removed))
|
||||
(source is Limb limb && limb.Removed) ||
|
||||
(user != null && user.Removed))
|
||||
{
|
||||
ResetSource();
|
||||
target = null;
|
||||
@@ -293,7 +295,6 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
targetMass = float.MaxValue;
|
||||
}
|
||||
var user = item.GetComponent<Projectile>()?.User;
|
||||
if (targetMass > TargetMinMass)
|
||||
{
|
||||
if (Math.Abs(SourcePullForce) > 0.001f)
|
||||
@@ -302,7 +303,7 @@ namespace Barotrauma.Items.Components
|
||||
if (sourceBody != null)
|
||||
{
|
||||
var targetBody = GetBodyToPull(target);
|
||||
if (targetBody != null && !(targetBody.UserData is Character))
|
||||
if (targetBody != null && targetBody.UserData is not Character)
|
||||
{
|
||||
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
|
||||
}
|
||||
|
||||
+19
-12
@@ -94,7 +94,7 @@ namespace Barotrauma.Items.Components
|
||||
if (isOn == value && IsActive == value) { return; }
|
||||
|
||||
IsActive = isOn = value;
|
||||
SetLightSourceState(value, value ? lightBrightness : 0.0f);
|
||||
SetLightSourceState(value);
|
||||
OnStateChanged();
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,7 @@ namespace Barotrauma.Items.Components
|
||||
#if CLIENT
|
||||
if (Light != null)
|
||||
{
|
||||
Light.Color = IsOn ? lightColor.Multiply(currentBrightness) : Color.Transparent;
|
||||
Light.Color = IsOn ? lightColor.Multiply(lightBrightness) : Color.Transparent;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -205,7 +205,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (base.IsActive == value) { return; }
|
||||
base.IsActive = isOn = value;
|
||||
SetLightSourceState(value, value ? lightBrightness : 0.0f);
|
||||
SetLightSourceState(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ namespace Barotrauma.Items.Components
|
||||
public override void OnItemLoaded()
|
||||
{
|
||||
base.OnItemLoaded();
|
||||
SetLightSourceState(IsActive, lightBrightness);
|
||||
SetLightSourceState(IsActive);
|
||||
turret = item.GetComponent<Turret>();
|
||||
#if CLIENT
|
||||
if (Screen.Selected.IsEditor)
|
||||
@@ -248,6 +248,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void OnMapLoaded()
|
||||
{
|
||||
#if CLIENT
|
||||
if (item.HiddenInGame)
|
||||
{
|
||||
Light.Enabled = false;
|
||||
}
|
||||
#endif
|
||||
CheckIfNeedsUpdate();
|
||||
}
|
||||
|
||||
@@ -263,8 +269,7 @@ namespace Barotrauma.Items.Components
|
||||
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
|
||||
(IsActiveConditionals == null || IsActiveConditionals.Count == 0))
|
||||
{
|
||||
lightBrightness = 1.0f;
|
||||
SetLightSourceState(true, lightBrightness);
|
||||
SetLightSourceState(true);
|
||||
SetLightSourceTransformProjSpecific();
|
||||
base.IsActive = false;
|
||||
isOn = true;
|
||||
@@ -285,13 +290,15 @@ namespace Barotrauma.Items.Components
|
||||
UpdateAITarget(item.AiTarget);
|
||||
}
|
||||
UpdateOnActiveEffects(deltaTime);
|
||||
//something in UpdateOnActiveEffects may deactivate the light -> return so we don't turn it back on
|
||||
if (!IsActive) { return; }
|
||||
|
||||
#if CLIENT
|
||||
Light.ParentSub = item.Submarine;
|
||||
#endif
|
||||
if (item.Container != null && !(item.GetRootInventoryOwner() is Character))
|
||||
if (item.Container != null && item.GetRootInventoryOwner() is not Character)
|
||||
{
|
||||
SetLightSourceState(false, 0.0f);
|
||||
SetLightSourceState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -300,7 +307,7 @@ namespace Barotrauma.Items.Components
|
||||
PhysicsBody body = ParentBody ?? item.body;
|
||||
if (body != null && !body.Enabled)
|
||||
{
|
||||
SetLightSourceState(false, 0.0f);
|
||||
SetLightSourceState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -325,7 +332,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void UpdateBroken(float deltaTime, Camera cam)
|
||||
{
|
||||
SetLightSourceState(false, 0.0f);
|
||||
SetLightSourceState(false);
|
||||
}
|
||||
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
@@ -357,7 +364,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
LightColor = XMLExtensions.ParseColor(signal.value, false);
|
||||
#if CLIENT
|
||||
SetLightSourceState(Light.Enabled, currentBrightness);
|
||||
SetLightSourceState(Light.Enabled);
|
||||
#endif
|
||||
prevColorSignal = signal.value;
|
||||
}
|
||||
@@ -375,7 +382,7 @@ namespace Barotrauma.Items.Components
|
||||
target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness);
|
||||
}
|
||||
|
||||
partial void SetLightSourceState(bool enabled, float brightness);
|
||||
partial void SetLightSourceState(bool enabled, float? brightness = null);
|
||||
|
||||
public void SetLightSourceTransform()
|
||||
{
|
||||
|
||||
@@ -478,7 +478,9 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
// single charged shot guns will decharge after firing
|
||||
// for cosmetic reasons, this is done by lerping in half the reload time
|
||||
currentChargeTime = Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f));
|
||||
currentChargeTime = reloadTime > 0.0f ?
|
||||
Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f)) :
|
||||
0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -268,6 +268,8 @@ namespace Barotrauma
|
||||
get { return capacity; }
|
||||
}
|
||||
|
||||
public int EmptySlotCount => slots.Count(i => !i.Empty());
|
||||
|
||||
public bool AllowSwappingContainedItems = true;
|
||||
|
||||
public Inventory(Entity owner, int capacity, int slotsPerRow = 5)
|
||||
@@ -887,10 +889,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (recursive)
|
||||
{
|
||||
if (item.OwnInventory != null)
|
||||
{
|
||||
item.OwnInventory.FindAllItems(predicate, recursive: true, list);
|
||||
}
|
||||
item.OwnInventory?.FindAllItems(predicate, recursive: true, list);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
|
||||
@@ -111,6 +111,7 @@ namespace Barotrauma
|
||||
private float sendConditionUpdateTimer;
|
||||
private bool conditionUpdatePending;
|
||||
|
||||
private float prevCondition;
|
||||
private float condition;
|
||||
|
||||
private bool inWater;
|
||||
@@ -1584,7 +1585,7 @@ namespace Barotrauma
|
||||
tags.Add(newTag);
|
||||
}
|
||||
|
||||
public IEnumerable<Identifier> GetTags()
|
||||
public IReadOnlyCollection<Identifier> GetTags()
|
||||
{
|
||||
return tags;
|
||||
}
|
||||
@@ -1753,15 +1754,14 @@ namespace Barotrauma
|
||||
if (Indestructible) { return; }
|
||||
if (InvulnerableToDamage && value <= condition) { return; }
|
||||
|
||||
float prev = condition;
|
||||
bool wasInFullCondition = IsFullCondition;
|
||||
|
||||
condition = MathHelper.Clamp(value, 0.0f, MaxCondition);
|
||||
if (MathUtils.NearlyEqual(prev, condition, epsilon: 0.000001f)) { return; }
|
||||
if (MathUtils.NearlyEqual(prevCondition, condition, epsilon: 0.000001f)) { return; }
|
||||
|
||||
RecalculateConditionValues();
|
||||
|
||||
if (condition == 0.0f && prev > 0.0f)
|
||||
if (condition == 0.0f && prevCondition > 0.0f)
|
||||
{
|
||||
//Flag connections to be updated as device is broken
|
||||
flagChangedConnections(connections);
|
||||
@@ -1775,7 +1775,7 @@ namespace Barotrauma
|
||||
#endif
|
||||
ApplyStatusEffects(ActionType.OnBroken, 1.0f, null);
|
||||
}
|
||||
else if (condition > 0.0f && prev <= 0.0f)
|
||||
else if (condition > 0.0f && prevCondition <= 0.0f)
|
||||
{
|
||||
//Flag connections to be updated as device is now working again
|
||||
flagChangedConnections(connections);
|
||||
@@ -1803,8 +1803,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
LastConditionChange = condition - prev;
|
||||
LastConditionChange = condition - prevCondition;
|
||||
ConditionLastUpdated = Timing.TotalTime;
|
||||
prevCondition = condition;
|
||||
|
||||
static void flagChangedConnections(Dictionary<string, Connection> connections)
|
||||
{
|
||||
@@ -2172,8 +2173,9 @@ namespace Barotrauma
|
||||
var projectile = GetComponent<Projectile>();
|
||||
if (projectile != null)
|
||||
{
|
||||
//ignore character colliders (a projectile only hits limbs)
|
||||
if (f2.CollisionCategories == Physics.CollisionCharacter && f2.Body.UserData is Character) { return false; }
|
||||
// Ignore characters so that the impact sound only plays when the item hits a a wall or a door.
|
||||
// Projectile collisions are handled in Projectile.OnProjectileCollision(), so it should be safe to do this.
|
||||
if (f2.CollisionCategories == Physics.CollisionCharacter) { return false; }
|
||||
if (projectile.IgnoredBodies != null && projectile.IgnoredBodies.Contains(f2.Body)) { return false; }
|
||||
if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; }
|
||||
}
|
||||
@@ -2726,7 +2728,7 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
ic.PlaySound(ActionType.OnUse, character);
|
||||
#endif
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb);
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: targetLimb?.character, user: character);
|
||||
|
||||
if (ic.DeleteOnUse) { remove = true; }
|
||||
}
|
||||
@@ -2757,7 +2759,7 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
ic.PlaySound(ActionType.OnSecondaryUse, character);
|
||||
#endif
|
||||
ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character);
|
||||
ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character);
|
||||
|
||||
if (ic.DeleteOnUse) { remove = true; }
|
||||
}
|
||||
@@ -2796,8 +2798,8 @@ namespace Barotrauma
|
||||
#endif
|
||||
ic.WasUsed = true;
|
||||
|
||||
ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: user);
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: user);
|
||||
ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: targetLimb?.character, user: user);
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, useTarget: targetLimb?.character, user: user);
|
||||
|
||||
if (GameMain.NetworkMember is { IsServer: true })
|
||||
{
|
||||
@@ -3463,7 +3465,7 @@ namespace Barotrauma
|
||||
item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition);
|
||||
}
|
||||
}
|
||||
item.lastSentCondition = item.condition;
|
||||
item.lastSentCondition = item.prevCondition = item.condition;
|
||||
item.RecalculateConditionValues();
|
||||
item.SetActiveSprite();
|
||||
|
||||
@@ -3537,15 +3539,10 @@ namespace Barotrauma
|
||||
upgrade.Save(element);
|
||||
}
|
||||
|
||||
if (condition < MaxCondition)
|
||||
{
|
||||
element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var conditionAttribute = element.GetAttribute("condition");
|
||||
if (conditionAttribute != null) { conditionAttribute.Remove(); }
|
||||
}
|
||||
element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture)));
|
||||
|
||||
var conditionAttribute = element.GetAttribute("condition");
|
||||
if (conditionAttribute != null) { conditionAttribute.Remove(); }
|
||||
|
||||
parentElement.Add(element);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace Barotrauma
|
||||
public EventType EventType { get; }
|
||||
}
|
||||
|
||||
public struct ComponentStateEventData : IEventData
|
||||
public readonly struct ComponentStateEventData : IEventData
|
||||
{
|
||||
public EventType EventType => EventType.ComponentState;
|
||||
public readonly ItemComponent Component;
|
||||
|
||||
@@ -867,6 +867,7 @@ namespace Barotrauma
|
||||
(endPath != null && GetDistToTunnel(cell.Center, endPath) < minMainPathWidth) ||
|
||||
(endHole != null && GetDistToTunnel(cell.Center, endHole) < minMainPathWidth)) { continue; }
|
||||
if (cell.Edges.Any(e => e.AdjacentCell(cell)?.CellType != CellType.Path || e.NextToCave)) { continue; }
|
||||
if (PositionsOfInterest.Any(p => cell.IsPointInside(p.Position.ToVector2()))) { continue; }
|
||||
potentialIslands.Add(cell);
|
||||
}
|
||||
for (int i = 0; i < GenerationParams.IslandCount; i++)
|
||||
@@ -1112,6 +1113,7 @@ namespace Barotrauma
|
||||
caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells));
|
||||
foreach (var caveCell in caveCells)
|
||||
{
|
||||
if (PositionsOfInterest.Any(p => caveCell.IsPointInside(p.Position.ToVector2()))) { continue; }
|
||||
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio)
|
||||
{
|
||||
var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f);
|
||||
@@ -3189,7 +3191,7 @@ namespace Barotrauma
|
||||
TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos, filter);
|
||||
|
||||
Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.ServerAndClient), Rand.RandSync.ServerAndClient);
|
||||
if (!cells.Any(c => c.IsPointInside(startPos + offset)))
|
||||
if (!IsPositionInsideWall(startPos + offset))
|
||||
{
|
||||
startPos += offset;
|
||||
}
|
||||
@@ -3245,10 +3247,9 @@ namespace Barotrauma
|
||||
{
|
||||
suitablePositions.RemoveAll(p => !filter(p));
|
||||
}
|
||||
//avoid floating ice chunks on the main path
|
||||
if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath))
|
||||
{
|
||||
suitablePositions.RemoveAll(p => ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(p.Position.ToVector2()))));
|
||||
suitablePositions.RemoveAll(p => IsPositionInsideWall(p.Position.ToVector2()));
|
||||
}
|
||||
if (!suitablePositions.Any())
|
||||
{
|
||||
@@ -3301,10 +3302,16 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position;
|
||||
position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced)].Position;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsPositionInsideWall(Vector2 worldPosition)
|
||||
{
|
||||
var closestCell = GetClosestCell(worldPosition);
|
||||
return closestCell != null && closestCell.IsPointInside(worldPosition);
|
||||
}
|
||||
|
||||
public void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
LevelObjectManager.Update(deltaTime);
|
||||
@@ -3347,14 +3354,13 @@ namespace Barotrauma
|
||||
|
||||
public Vector2 GetBottomPosition(float xPosition)
|
||||
{
|
||||
int index = (int)Math.Floor(xPosition / Size.X * (bottomPositions.Count - 1));
|
||||
float interval = Size.X / (bottomPositions.Count - 1);
|
||||
|
||||
int index = (int)Math.Floor(xPosition / interval);
|
||||
if (index < 0 || index >= bottomPositions.Count - 1) { return new Vector2(xPosition, BottomPos); }
|
||||
|
||||
float t = (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X);
|
||||
//t can go slightly outside the 0-1 due to rounding, safe to ignore
|
||||
Debug.Assert(t <= 1.001f && t >= -0.001f);
|
||||
float t = (xPosition - bottomPositions[index].X) / interval;
|
||||
t = MathHelper.Clamp(t, 0.0f, 1.0f);
|
||||
|
||||
float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t);
|
||||
|
||||
return new Vector2(xPosition, yPos);
|
||||
|
||||
@@ -161,7 +161,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public LevelData(LocationConnection locationConnection)
|
||||
{
|
||||
Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName;
|
||||
Seed = locationConnection.Locations[0].LevelData.Seed + locationConnection.Locations[1].LevelData.Seed;
|
||||
Biome = locationConnection.Biome;
|
||||
Type = LevelType.LocationConnection;
|
||||
Difficulty = locationConnection.Difficulty;
|
||||
@@ -194,9 +194,9 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Instantiates level data using the properties of the location
|
||||
/// </summary>
|
||||
public LevelData(Location location, float difficulty)
|
||||
public LevelData(Location location, Map map, float difficulty)
|
||||
{
|
||||
Seed = location.BaseName;
|
||||
Seed = location.BaseName + map.Locations.IndexOf(location);
|
||||
Biome = location.Biome;
|
||||
Type = LevelType.Outpost;
|
||||
Difficulty = difficulty;
|
||||
|
||||
@@ -646,7 +646,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biome = default)
|
||||
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biomeId = default)
|
||||
{
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
|
||||
|
||||
@@ -661,14 +661,29 @@ namespace Barotrauma
|
||||
lp.Type == type &&
|
||||
(lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) &&
|
||||
!lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier()));
|
||||
matchingLevelParams = biome.IsEmpty
|
||||
? matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome))
|
||||
: matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biome));
|
||||
if (biomeId.IsEmpty)
|
||||
{
|
||||
//we don't want end levels when generating a completely random level (e.g. in mission mode)
|
||||
matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome));
|
||||
}
|
||||
else
|
||||
{
|
||||
bool isEndBiome = Biome.Prefabs.TryGet(biomeId, out Biome biome) && biome.IsEndBiome;
|
||||
if (isEndBiome && matchingLevelParams.Any(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId)))
|
||||
{
|
||||
//in the end biome, we must choose level parameters meant specifically for the end levels
|
||||
matchingLevelParams = matchingLevelParams.Where(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId));
|
||||
}
|
||||
else
|
||||
{
|
||||
matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biomeId));
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchingLevelParams.Any())
|
||||
{
|
||||
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")");
|
||||
if (!biome.IsEmpty)
|
||||
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")");
|
||||
if (!biomeId.IsEmpty)
|
||||
{
|
||||
//try to find params that at least have a suitable type
|
||||
matchingLevelParams = levelParamsOrdered.Where(lp => lp.Type == type);
|
||||
@@ -682,7 +697,7 @@ namespace Barotrauma
|
||||
|
||||
if (!matchingLevelParams.Any(lp => difficulty >= lp.MinLevelDifficulty && difficulty <= lp.MaxLevelDifficulty))
|
||||
{
|
||||
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})");
|
||||
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -457,12 +457,14 @@ namespace Barotrauma
|
||||
private struct LoadedMission
|
||||
{
|
||||
public MissionPrefab MissionPrefab { get; }
|
||||
public int OriginLocationIndex { get; }
|
||||
public int DestinationIndex { get; }
|
||||
public bool SelectedMission { get; }
|
||||
|
||||
public LoadedMission(MissionPrefab prefab, int destinationIndex, bool selectedMission)
|
||||
public LoadedMission(MissionPrefab prefab, int originLocationIndex, int destinationIndex, bool selectedMission)
|
||||
{
|
||||
MissionPrefab = prefab;
|
||||
OriginLocationIndex = originLocationIndex;
|
||||
DestinationIndex = destinationIndex;
|
||||
SelectedMission = selectedMission;
|
||||
}
|
||||
@@ -663,9 +665,10 @@ namespace Barotrauma
|
||||
if (string.IsNullOrWhiteSpace(id)) { continue; }
|
||||
var prefab = MissionPrefab.Prefabs.Find(p => p.Identifier == id);
|
||||
if (prefab == null) { continue; }
|
||||
var origin = childElement.GetAttributeInt("origin", -1);
|
||||
var destination = childElement.GetAttributeInt("destinationindex", -1);
|
||||
var selected = childElement.GetAttributeBool("selected", false);
|
||||
loadedMissions.Add(new LoadedMission(prefab, destination, selected));
|
||||
loadedMissions.Add(new LoadedMission(prefab, origin, destination, selected));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -926,6 +929,10 @@ namespace Barotrauma
|
||||
destination = Connections.First().OtherLocation(this);
|
||||
}
|
||||
var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub);
|
||||
if (loadedMission.OriginLocationIndex >= 0 && loadedMission.OriginLocationIndex < map.Locations.Count)
|
||||
{
|
||||
mission.OriginLocation = map.Locations[loadedMission.OriginLocationIndex];
|
||||
}
|
||||
availableMissions.Add(mission);
|
||||
if (loadedMission.SelectedMission) { selectedMissions.Add(mission); }
|
||||
}
|
||||
@@ -1520,10 +1527,12 @@ namespace Barotrauma
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this);
|
||||
var i = map.Locations.IndexOf(location);
|
||||
var destinationIndex = map.Locations.IndexOf(location);
|
||||
var originIndex = map.Locations.IndexOf(mission.OriginLocation);
|
||||
missionsElement.Add(new XElement("mission",
|
||||
new XAttribute("prefabid", mission.Prefab.Identifier),
|
||||
new XAttribute("destinationindex", i),
|
||||
new XAttribute("destinationindex", destinationIndex),
|
||||
new XAttribute("origin", originIndex),
|
||||
new XAttribute("selected", selectedMissions.Contains(mission))));
|
||||
}
|
||||
locationElement.Add(missionsElement);
|
||||
|
||||
@@ -167,7 +167,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
int startLocationindex = element.GetAttributeInt("startlocation", -1);
|
||||
if (startLocationindex > 0 && startLocationindex < Locations.Count)
|
||||
if (startLocationindex >= 0 && startLocationindex < Locations.Count)
|
||||
{
|
||||
StartLocation = Locations[startLocationindex];
|
||||
}
|
||||
@@ -188,10 +188,9 @@ namespace Barotrauma
|
||||
{
|
||||
//backwards compatibility
|
||||
int endLocationIndex = element.GetAttributeInt("endlocation", -1);
|
||||
if (endLocationIndex > 0 && endLocationIndex < Locations.Count)
|
||||
if (endLocationIndex >= 0 && endLocationIndex < Locations.Count)
|
||||
{
|
||||
endLocations.Add(Locations[endLocationIndex]);
|
||||
Locations[endLocationIndex].LevelData.ReassignGenerationParams(Seed);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -203,7 +202,7 @@ namespace Barotrauma
|
||||
int[] endLocationindices = element.GetAttributeIntArray("endlocations", Array.Empty<int>());
|
||||
foreach (int endLocationIndex in endLocationindices)
|
||||
{
|
||||
if (endLocationIndex > 0 && endLocationIndex < Locations.Count)
|
||||
if (endLocationIndex >= 0 && endLocationIndex < Locations.Count)
|
||||
{
|
||||
endLocations.Add(Locations[endLocationIndex]);
|
||||
}
|
||||
@@ -245,7 +244,7 @@ namespace Barotrauma
|
||||
{
|
||||
Biome = endLocations.First().Biome
|
||||
};
|
||||
newEndLocation.LevelData = new LevelData(newEndLocation, difficulty: 100.0f);
|
||||
newEndLocation.LevelData = new LevelData(newEndLocation, this, difficulty: 100.0f);
|
||||
Locations.Add(newEndLocation);
|
||||
endLocations.Add(newEndLocation);
|
||||
}
|
||||
@@ -339,7 +338,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (StartLocation != null)
|
||||
{
|
||||
StartLocation.LevelData = new LevelData(StartLocation, 0);
|
||||
StartLocation.LevelData = new LevelData(StartLocation, this, 0);
|
||||
}
|
||||
|
||||
//ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy
|
||||
@@ -701,7 +700,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome));
|
||||
location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome));
|
||||
if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient);
|
||||
@@ -902,6 +901,7 @@ namespace Barotrauma
|
||||
{
|
||||
for (int i = 0; i < endLocations.Count; i++)
|
||||
{
|
||||
endLocations[i].LevelData.ReassignGenerationParams(Seed);
|
||||
var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i);
|
||||
if (outpostParams != null)
|
||||
{
|
||||
|
||||
@@ -124,9 +124,18 @@ namespace Barotrauma
|
||||
int eventCount = GameMain.Server.EntityEventManager.Events.Count();
|
||||
int uniqueEventCount = GameMain.Server.EntityEventManager.UniqueEvents.Count();
|
||||
#endif
|
||||
List<MapEntity> entities = MapEntity.mapEntityList.FindAll(e => e.Submarine == sub);
|
||||
HashSet<Submarine> connectedSubs = new HashSet<Submarine>() { sub };
|
||||
foreach (Submarine otherSub in Submarine.Loaded)
|
||||
{
|
||||
//remove linked subs too
|
||||
if (otherSub.Submarine == sub) { connectedSubs.Add(otherSub); }
|
||||
}
|
||||
List<MapEntity> entities = MapEntity.mapEntityList.FindAll(e => connectedSubs.Contains(e.Submarine));
|
||||
entities.ForEach(e => e.Remove());
|
||||
sub.Remove();
|
||||
foreach (Submarine otherSub in connectedSubs)
|
||||
{
|
||||
otherSub.Remove();
|
||||
}
|
||||
#if SERVER
|
||||
//remove any events created during the removal of the entities
|
||||
GameMain.Server.EntityEventManager.Events.RemoveRange(eventCount, GameMain.Server.EntityEventManager.Events.Count - eventCount);
|
||||
@@ -543,12 +552,8 @@ namespace Barotrauma
|
||||
foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient))
|
||||
{
|
||||
if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; }
|
||||
if (!allowExtendBelowInitialModule)
|
||||
{
|
||||
//don't continue downwards if it'd extend below the airlock
|
||||
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; }
|
||||
}
|
||||
|
||||
if (DisallowBelowAirlock(allowExtendBelowInitialModule, gapPosition, currentModule)) { continue; }
|
||||
|
||||
PlacedModule newModule = null;
|
||||
//try appending to the current module if possible
|
||||
if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition))
|
||||
@@ -569,6 +574,7 @@ namespace Barotrauma
|
||||
foreach (OutpostModuleInfo.GapPosition otherGapPosition in
|
||||
GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g)))
|
||||
{
|
||||
if (DisallowBelowAirlock(allowExtendBelowInitialModule, otherGapPosition, otherModule)) { continue; }
|
||||
newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType);
|
||||
if (newModule != null)
|
||||
{
|
||||
@@ -617,6 +623,16 @@ namespace Barotrauma
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule)));
|
||||
}
|
||||
|
||||
static bool DisallowBelowAirlock(bool allowExtendBelowInitialModule, OutpostModuleInfo.GapPosition gapPosition, PlacedModule currentModule)
|
||||
{
|
||||
if (!allowExtendBelowInitialModule)
|
||||
{
|
||||
//don't continue downwards if it'd extend below the airlock
|
||||
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1367,7 +1367,7 @@ namespace Barotrauma
|
||||
return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W));
|
||||
}
|
||||
|
||||
public Submarine(SubmarineInfo info, bool showWarningMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID)
|
||||
public Submarine(SubmarineInfo info, bool showErrorMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID)
|
||||
{
|
||||
upgradeEventIdentifier = new Identifier($"Submarine{ID}");
|
||||
Loading = true;
|
||||
@@ -1438,64 +1438,65 @@ namespace Barotrauma
|
||||
center.Y -= center.Y % GridSize.Y;
|
||||
|
||||
RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this));
|
||||
}
|
||||
|
||||
subBody = new SubmarineBody(this, showWarningMessages);
|
||||
Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition);
|
||||
subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f);
|
||||
subBody = new SubmarineBody(this, showErrorMessages);
|
||||
Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition);
|
||||
subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f);
|
||||
|
||||
if (info.IsOutpost)
|
||||
if (info.IsOutpost)
|
||||
{
|
||||
ShowSonarMarker = false;
|
||||
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
|
||||
TeamID = CharacterTeamType.FriendlyNPC;
|
||||
|
||||
bool indestructible =
|
||||
GameMain.NetworkMember != null &&
|
||||
!GameMain.NetworkMember.ServerSettings.DestructibleOutposts &&
|
||||
!(info.OutpostGenerationParams?.AlwaysDestructible ?? false);
|
||||
|
||||
foreach (MapEntity me in MapEntity.mapEntityList)
|
||||
{
|
||||
ShowSonarMarker = false;
|
||||
TeamID = CharacterTeamType.FriendlyNPC;
|
||||
|
||||
bool indestructible =
|
||||
GameMain.NetworkMember != null &&
|
||||
!GameMain.NetworkMember.ServerSettings.DestructibleOutposts &&
|
||||
!(info.OutpostGenerationParams?.AlwaysDestructible ?? false);
|
||||
|
||||
foreach (MapEntity me in MapEntity.mapEntityList)
|
||||
if (me.Submarine != this) { continue; }
|
||||
if (me is Item item)
|
||||
{
|
||||
if (me.Submarine != this) { continue; }
|
||||
if (me is Item item)
|
||||
item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null;
|
||||
item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true;
|
||||
if (item.GetComponent<Repairable>() != null && indestructible)
|
||||
{
|
||||
item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null;
|
||||
item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true;
|
||||
if (item.GetComponent<Repairable>() != null && indestructible)
|
||||
item.Indestructible = true;
|
||||
}
|
||||
foreach (ItemComponent ic in item.Components)
|
||||
{
|
||||
if (ic is ConnectionPanel connectionPanel)
|
||||
{
|
||||
item.Indestructible = true;
|
||||
}
|
||||
foreach (ItemComponent ic in item.Components)
|
||||
{
|
||||
if (ic is ConnectionPanel connectionPanel)
|
||||
//prevent rewiring
|
||||
if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable)
|
||||
{
|
||||
//prevent rewiring
|
||||
if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable)
|
||||
{
|
||||
connectionPanel.Locked = true;
|
||||
}
|
||||
connectionPanel.Locked = true;
|
||||
}
|
||||
else if (ic is Holdable holdable && holdable.Attached && item.GetComponent<LevelResource>() == null)
|
||||
{
|
||||
//prevent deattaching items from walls
|
||||
}
|
||||
else if (ic is Holdable holdable && holdable.Attached && item.GetComponent<LevelResource>() == null)
|
||||
{
|
||||
//prevent deattaching items from walls
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession?.GameMode is TutorialMode) { continue; }
|
||||
#endif
|
||||
holdable.CanBePicked = false;
|
||||
holdable.CanBeSelected = false;
|
||||
}
|
||||
holdable.CanBePicked = false;
|
||||
holdable.CanBeSelected = false;
|
||||
}
|
||||
}
|
||||
else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible)
|
||||
{
|
||||
structure.Indestructible = true;
|
||||
}
|
||||
}
|
||||
else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible)
|
||||
{
|
||||
structure.Indestructible = true;
|
||||
}
|
||||
}
|
||||
else if (info.IsRuin)
|
||||
{
|
||||
ShowSonarMarker = false;
|
||||
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
|
||||
}
|
||||
}
|
||||
else if (info.IsRuin)
|
||||
{
|
||||
ShowSonarMarker = false;
|
||||
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
|
||||
}
|
||||
|
||||
if (entityGrid != null)
|
||||
@@ -1542,7 +1543,7 @@ namespace Barotrauma
|
||||
#endif
|
||||
//if the sub was made using an older version,
|
||||
//halve the brightness of the lights to make them look (almost) right on the new lighting formula
|
||||
if (showWarningMessages &&
|
||||
if (showErrorMessages &&
|
||||
!string.IsNullOrEmpty(Info.FilePath) &&
|
||||
Screen.Selected != GameMain.SubEditorScreen &&
|
||||
(Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0")))
|
||||
|
||||
@@ -116,7 +116,7 @@ namespace Barotrauma
|
||||
get { return submarine; }
|
||||
}
|
||||
|
||||
public SubmarineBody(Submarine sub, bool showWarningMessages = true)
|
||||
public SubmarineBody(Submarine sub, bool showErrorMessages = true)
|
||||
{
|
||||
this.submarine = sub;
|
||||
|
||||
@@ -126,9 +126,9 @@ namespace Barotrauma
|
||||
if (!Hull.HullList.Any(h => h.Submarine == sub))
|
||||
{
|
||||
farseerBody = GameMain.World.CreateRectangle(1.0f, 1.0f, 1.0f);
|
||||
if (showWarningMessages)
|
||||
if (showErrorMessages)
|
||||
{
|
||||
DebugConsole.ThrowError("WARNING: no hulls found, generating a physics body for the submarine failed.");
|
||||
DebugConsole.ThrowError($"No hulls found in the submarine \"{sub.Info.Name}\". Generating a physics body for the submarine failed.");
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
interface IServerPositionSync : IServerSerializable
|
||||
{
|
||||
#if SERVER
|
||||
void ServerWritePosition(IWriteMessage msg, Client c);
|
||||
void ServerWritePosition(ReadWriteMessage tempBuffer, Client c);
|
||||
#endif
|
||||
#if CLIENT
|
||||
void ClientReadPosition(IReadMessage msg, float sendingTime);
|
||||
|
||||
@@ -99,6 +99,19 @@ namespace Barotrauma.Networking
|
||||
EntityEventInitial
|
||||
}
|
||||
|
||||
[NetworkSerialize]
|
||||
readonly record struct EntityPositionHeader(
|
||||
bool IsItem,
|
||||
UInt32 PrefabUintIdentifier,
|
||||
UInt16 EntityId) : INetSerializableStruct
|
||||
{
|
||||
public static EntityPositionHeader FromEntity(Entity entity)
|
||||
=> new (
|
||||
IsItem: entity is Item,
|
||||
PrefabUintIdentifier: entity is MapEntity me ? me.Prefab.UintIdentifier : 0,
|
||||
EntityId: entity.ID);
|
||||
}
|
||||
|
||||
enum TraitorMessageType
|
||||
{
|
||||
Server,
|
||||
|
||||
@@ -171,6 +171,8 @@ namespace Barotrauma.Networking
|
||||
public bool ShouldCreateAnalyticsEvent
|
||||
=> DisconnectReason is not (
|
||||
DisconnectReason.Disconnected
|
||||
or DisconnectReason.ServerShutdown
|
||||
or DisconnectReason.ServerFull
|
||||
or DisconnectReason.Banned
|
||||
or DisconnectReason.Kicked
|
||||
or DisconnectReason.TooManyFailedLogins
|
||||
|
||||
@@ -621,6 +621,15 @@ namespace Barotrauma
|
||||
return stringValue.Split(';').Select(s => ParseTuple<T1, T2>(s, default)).ToArray();
|
||||
}
|
||||
|
||||
public static Range<int> GetAttributeRange(this XElement element, string name, Range<int> defaultValue)
|
||||
{
|
||||
var attribute = element?.GetAttribute(name);
|
||||
if (attribute is null) { return defaultValue; }
|
||||
|
||||
string stringValue = attribute.Value;
|
||||
return string.IsNullOrEmpty(stringValue) ? defaultValue : ParseRange(stringValue);
|
||||
}
|
||||
|
||||
public static string ElementInnerText(this XElement el)
|
||||
{
|
||||
StringBuilder str = new StringBuilder();
|
||||
@@ -903,6 +912,37 @@ namespace Barotrauma
|
||||
return floatArray;
|
||||
}
|
||||
|
||||
// parse a range string, e.g "1-3" or "3"
|
||||
public static Range<int> ParseRange(string rangeString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rangeString)) { return GetDefault(rangeString); }
|
||||
|
||||
string[] split = rangeString.Split('-');
|
||||
return split.Length switch
|
||||
{
|
||||
1 when TryParseInt(split[0], out int value) => new Range<int>(value, value),
|
||||
2 when TryParseInt(split[0], out int min) && TryParseInt(split[1], out int max) && min < max => new Range<int>(min, max),
|
||||
_ => GetDefault(rangeString)
|
||||
};
|
||||
|
||||
static bool TryParseInt(string value, out int result)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result);
|
||||
}
|
||||
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
static Range<int> GetDefault(string rangeString)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error parsing range: \"{rangeString}\" (using default value 0-99)");
|
||||
return new Range<int>(0, 99);
|
||||
}
|
||||
}
|
||||
|
||||
public static Identifier VariantOf(this XElement element) =>
|
||||
element.GetAttributeIdentifier("inherit", element.GetAttributeIdentifier("variantof", ""));
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ namespace Barotrauma
|
||||
EnableMouseLook = true,
|
||||
ChatOpen = true,
|
||||
CrewMenuOpen = true,
|
||||
EditorDisclaimerShown = false,
|
||||
ShowOffensiveServerPrompt = true,
|
||||
TutorialSkipWarning = true,
|
||||
CorpseDespawnDelay = 600,
|
||||
@@ -132,7 +131,6 @@ namespace Barotrauma
|
||||
public EnemyHealthBarMode ShowEnemyHealthBars;
|
||||
public bool ChatOpen;
|
||||
public bool CrewMenuOpen;
|
||||
public bool EditorDisclaimerShown;
|
||||
public bool ShowOffensiveServerPrompt;
|
||||
public bool TutorialSkipWarning;
|
||||
public int CorpseDespawnDelay;
|
||||
|
||||
@@ -75,6 +75,7 @@ namespace Barotrauma
|
||||
case "targetgrandparent":
|
||||
case "targetcontaineditem":
|
||||
case "skillrequirement":
|
||||
case "targetslot":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
|
||||
@@ -1773,12 +1773,17 @@ namespace Barotrauma
|
||||
|
||||
void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo)
|
||||
{
|
||||
Item parentItem = entity as Item;
|
||||
if (user == null && parentItem != null)
|
||||
{
|
||||
// Set the user for projectiles spawned from status effects (e.g. flak shrapnels)
|
||||
SetUser(parentItem.GetComponent<Projectile>()?.User);
|
||||
}
|
||||
switch (chosenItemSpawnInfo.SpawnPosition)
|
||||
{
|
||||
case ItemSpawnInfo.SpawnPositionType.This:
|
||||
Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem =>
|
||||
{
|
||||
Item parentItem = entity as Item;
|
||||
Projectile projectile = newItem.GetComponent<Projectile>();
|
||||
if (entity != null)
|
||||
{
|
||||
|
||||
@@ -85,15 +85,6 @@ namespace Barotrauma.Steam
|
||||
return Steamworks.SteamUGC.NumSubscribedItems;
|
||||
}
|
||||
|
||||
public static PublishedFileId[] GetSubscribedItems()
|
||||
{
|
||||
if (!IsInitialized || !Steamworks.SteamClient.IsValid)
|
||||
{
|
||||
return Array.Empty<PublishedFileId>();
|
||||
}
|
||||
return Steamworks.SteamUGC.GetSubscribedItems();
|
||||
}
|
||||
|
||||
public static bool UnlockAchievement(string achievementIdentifier) =>
|
||||
UnlockAchievement(achievementIdentifier.ToIdentifier());
|
||||
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
#nullable enable
|
||||
using Barotrauma.IO;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Steamworks.Data;
|
||||
using WorkshopItemSet = System.Collections.Generic.ISet<Steamworks.Ugc.Item>;
|
||||
|
||||
namespace Barotrauma.Steam
|
||||
{
|
||||
static partial class SteamManager
|
||||
{
|
||||
public const string WorkshopItemPreviewImageFolder = "Workshop";
|
||||
public const string PreviewImageName = "PreviewImage.png";
|
||||
public const string DefaultPreviewImagePath = "Content/DefaultWorkshopPreviewImage.png";
|
||||
|
||||
public static bool TryExtractSteamWorkshopId(this ContentPackage contentPackage, [NotNullWhen(true)]out SteamWorkshopId? workshopId)
|
||||
{
|
||||
workshopId = null;
|
||||
@@ -47,16 +41,22 @@ namespace Barotrauma.Steam
|
||||
private static async Task<WorkshopItemSet> GetWorkshopItems(Steamworks.Ugc.Query query, int? maxPages = null)
|
||||
{
|
||||
if (!IsInitialized) { return new HashSet<Steamworks.Ugc.Item>(); }
|
||||
|
||||
|
||||
await Task.Yield();
|
||||
query = query.WithKeyValueTags(true).WithLongDescription(true);
|
||||
var set = new HashSet<Steamworks.Ugc.Item>(ItemEqualityComparer.Instance);
|
||||
int prevSize = 0;
|
||||
for (int i = 1; maxPages is null || i <= maxPages; i++)
|
||||
for (int i = 1; i <= (maxPages ?? int.MaxValue); i++)
|
||||
{
|
||||
Steamworks.Ugc.ResultPage? page = await query.GetPageAsync(i);
|
||||
if (page is null || !page.Value.Entries.Any()) { break; }
|
||||
set.UnionWith(page.Value.Entries);
|
||||
using Steamworks.Ugc.ResultPage? page = await query.GetPageAsync(i);
|
||||
if (page is not { Entries: var entries }) { break; }
|
||||
|
||||
// This queries the results on the i-th page and stores them,
|
||||
// using page.Entries directly would result in two GetQueryUGCResult calls
|
||||
entries = entries.ToArray();
|
||||
|
||||
if (entries.None()) { break; }
|
||||
|
||||
set.UnionWith(entries);
|
||||
|
||||
if (set.Count == prevSize) { break; }
|
||||
prevSize = set.Count;
|
||||
@@ -66,10 +66,17 @@ namespace Barotrauma.Steam
|
||||
// which can happen on items that are not visible to the currently
|
||||
// logged in player (i.e. private & friends-only items)
|
||||
set.RemoveWhere(it => it.ConsumerApp != AppID);
|
||||
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
public static ImmutableHashSet<Steamworks.Data.PublishedFileId> GetSubscribedItemIds()
|
||||
{
|
||||
return IsInitialized
|
||||
? Steamworks.SteamUGC.GetSubscribedItems().ToImmutableHashSet()
|
||||
: ImmutableHashSet<Steamworks.Data.PublishedFileId>.Empty;
|
||||
}
|
||||
|
||||
public static async Task<WorkshopItemSet> GetAllSubscribedItems()
|
||||
{
|
||||
if (!IsInitialized) { return new HashSet<Steamworks.Ugc.Item>(); }
|
||||
@@ -98,14 +105,86 @@ namespace Barotrauma.Steam
|
||||
.WhereUserPublished());
|
||||
}
|
||||
|
||||
public static async Task<Steamworks.Ugc.Item?> GetItem(UInt64 itemId)
|
||||
private static class SingleItemRequestPool
|
||||
{
|
||||
private static readonly object mutex = new();
|
||||
private static readonly TimeSpan delayAfterNewRequest = TimeSpan.FromSeconds(0.5);
|
||||
private static readonly HashSet<UInt64> ids = new();
|
||||
|
||||
private static Task<WorkshopItemSet>? currentBatch = null;
|
||||
|
||||
private static async Task<WorkshopItemSet> PrepareNewBatch()
|
||||
{
|
||||
// Wait for a bunch of requests to be made
|
||||
await Task.Delay(delayAfterNewRequest);
|
||||
|
||||
Task<WorkshopItemSet> queryTask;
|
||||
lock (mutex)
|
||||
{
|
||||
DebugConsole.Log(
|
||||
$"{nameof(SteamManager)}.{nameof(Workshop)}.{nameof(SingleItemRequestPool)}: " +
|
||||
$"Running batch of {ids.Count} requests");
|
||||
|
||||
queryTask = GetWorkshopItems(
|
||||
Steamworks.Ugc.Query.All
|
||||
.WithFileId(
|
||||
ids
|
||||
.Select(id => (Steamworks.Data.PublishedFileId)id)
|
||||
.ToArray()));
|
||||
ids.Clear();
|
||||
|
||||
// Immediately clear the current batch so the next request starts a new one
|
||||
currentBatch = null;
|
||||
}
|
||||
|
||||
return await queryTask;
|
||||
}
|
||||
|
||||
public static async Task<Steamworks.Ugc.Item?> MakeRequest(UInt64 id)
|
||||
{
|
||||
Task<WorkshopItemSet> ourTask;
|
||||
lock (mutex)
|
||||
{
|
||||
ids.Add(id);
|
||||
if (currentBatch is not { IsCompleted: false })
|
||||
{
|
||||
// There is no currently pending batch, start a new one
|
||||
currentBatch = Task.Run(PrepareNewBatch);
|
||||
}
|
||||
ourTask = currentBatch;
|
||||
}
|
||||
|
||||
var items = await ourTask;
|
||||
var result = items.FirstOrNull(it => it.Id == id);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a Workshop item's metadata. This is batched to minimize Steamworks API calls.
|
||||
/// The description of the returned item is truncated to save bandwidth.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Workshop Item ID</param>
|
||||
public static Task<Steamworks.Ugc.Item?> GetItem(UInt64 itemId)
|
||||
=> SingleItemRequestPool.MakeRequest(itemId);
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a Workshop item's metadata in its own API call instead of batching.
|
||||
/// This minimizes delay but needs to be used with caution to prevent rate limiting.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Workshop Item ID</param>
|
||||
/// <param name="withLongDescription">
|
||||
/// If true, ask for the item's entire description, otherwise it'll be truncated.
|
||||
/// </param>
|
||||
public static async Task<Steamworks.Ugc.Item?> GetItemAsap(UInt64 itemId, bool withLongDescription = false)
|
||||
{
|
||||
if (!IsInitialized) { return null; }
|
||||
|
||||
var items = await GetWorkshopItems(
|
||||
Steamworks.Ugc.Query.All
|
||||
.WithFileId(itemId));
|
||||
return items.Any() ? items.First() : (Steamworks.Ugc.Item?)null;
|
||||
.WithFileId(itemId)
|
||||
.WithLongDescription(withLongDescription));
|
||||
return items.Any() ? items.First() : null;
|
||||
}
|
||||
|
||||
public static async Task ForceRedownload(UInt64 itemId)
|
||||
|
||||
@@ -57,13 +57,12 @@ namespace Barotrauma
|
||||
|
||||
if (characterList.Any())
|
||||
{
|
||||
if (location?.Reputation is { } reputation && Faction.GetPlayerAffiliationStatus(reputation.Identifier, characterList) is FactionAffiliation.Positive)
|
||||
if (location?.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction, characterList) is FactionAffiliation.Positive)
|
||||
{
|
||||
price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated));
|
||||
}
|
||||
price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier));
|
||||
}
|
||||
|
||||
return (int)price;
|
||||
}
|
||||
}
|
||||
@@ -270,10 +269,59 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class UpgradePrefab : UpgradeContentPrefab
|
||||
internal readonly struct UpgradeResourceCost
|
||||
{
|
||||
public readonly int Amount;
|
||||
private readonly ImmutableArray<Identifier> targetTags;
|
||||
public readonly Range<int> TargetLevels;
|
||||
|
||||
public UpgradeResourceCost(ContentXElement element)
|
||||
{
|
||||
Amount = element.GetAttributeInt("amount", 0);
|
||||
targetTags = element.GetAttributeIdentifierArray("item", Array.Empty<Identifier>())!.ToImmutableArray();
|
||||
TargetLevels = element.GetAttributeRange("levels", new Range<int>(0, 99));
|
||||
}
|
||||
|
||||
public bool AppliesForLevel(int currentLevel) => TargetLevels.Contains(currentLevel);
|
||||
|
||||
public bool AppliesForLevel(Range<int> newLevels) => newLevels.Start <= TargetLevels.End && newLevels.End >= TargetLevels.Start;
|
||||
|
||||
public bool MatchesItem(Item item) => MatchesItem(item.Prefab);
|
||||
|
||||
public bool MatchesItem(ItemPrefab item)
|
||||
{
|
||||
foreach (Identifier tag in targetTags)
|
||||
{
|
||||
if (tag.Equals(item.Identifier) || item.Tags.Contains(tag)) { return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct ApplicableResourceCollection
|
||||
{
|
||||
public readonly ImmutableArray<ItemPrefab> MatchingItems;
|
||||
public readonly UpgradeResourceCost Cost;
|
||||
public readonly int Count;
|
||||
|
||||
public ApplicableResourceCollection(IEnumerable<ItemPrefab> matchingItems, int count, UpgradeResourceCost cost)
|
||||
{
|
||||
MatchingItems = matchingItems.ToImmutableArray();
|
||||
Count = count;
|
||||
Cost = cost;
|
||||
}
|
||||
|
||||
public static ApplicableResourceCollection CreateFor(UpgradeResourceCost cost)
|
||||
{
|
||||
return new ApplicableResourceCollection(ItemPrefab.Prefabs.Where(cost.MatchesItem), cost.Amount, cost);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class UpgradePrefab : UpgradeContentPrefab
|
||||
{
|
||||
public static readonly PrefabCollection<UpgradePrefab> Prefabs = new PrefabCollection<UpgradePrefab>(
|
||||
onAdd: (prefab, isOverride) =>
|
||||
onAdd: static (prefab, isOverride) =>
|
||||
{
|
||||
if (!prefab.SuppressWarnings && !isOverride)
|
||||
{
|
||||
@@ -342,6 +390,7 @@ namespace Barotrauma
|
||||
|
||||
private Dictionary<string, string[]> targetProperties { get; }
|
||||
private readonly ImmutableArray<UpgradeMaxLevelMod> MaxLevelsMods;
|
||||
public readonly ImmutableHashSet<UpgradeResourceCost> ResourceCosts;
|
||||
|
||||
public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file)
|
||||
{
|
||||
@@ -354,6 +403,7 @@ namespace Barotrauma
|
||||
|
||||
var targetProperties = new Dictionary<string, string[]>();
|
||||
var maxLevels = new List<UpgradeMaxLevelMod>();
|
||||
var resourceCosts = new HashSet<UpgradeResourceCost>();
|
||||
|
||||
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", "");
|
||||
if (!nameIdentifier.IsEmpty)
|
||||
@@ -396,6 +446,11 @@ namespace Barotrauma
|
||||
maxLevels.Add(new UpgradeMaxLevelMod(subElement));
|
||||
break;
|
||||
}
|
||||
case "resourcecost":
|
||||
{
|
||||
resourceCosts.Add(new UpgradeResourceCost(subElement));
|
||||
break;
|
||||
}
|
||||
#if CLIENT
|
||||
case "decorativesprite":
|
||||
{
|
||||
@@ -427,6 +482,7 @@ namespace Barotrauma
|
||||
|
||||
this.targetProperties = targetProperties;
|
||||
MaxLevelsMods = maxLevels.ToImmutableArray();
|
||||
ResourceCosts = resourceCosts.ToImmutableHashSet();
|
||||
|
||||
upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray("categories", Array.Empty<Identifier>())?
|
||||
.ToImmutableHashSet() ?? ImmutableHashSet<Identifier>.Empty;
|
||||
@@ -469,6 +525,58 @@ namespace Barotrauma
|
||||
return GetMaxLevel(info) > 0;
|
||||
}
|
||||
|
||||
public bool HasResourcesToUpgrade(Character? character, int currentLevel)
|
||||
{
|
||||
if (character is null) { return false; }
|
||||
if (!ResourceCosts.Any()) { return true; }
|
||||
|
||||
List<Item> allItems = character.Inventory.FindAllItems(recursive: true);
|
||||
return ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)).All(cost => cost.Amount <= allItems.Count(cost.MatchesItem));
|
||||
}
|
||||
|
||||
public bool TryTakeResources(Character character, int currentLevel)
|
||||
{
|
||||
IEnumerable<UpgradeResourceCost> costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel));
|
||||
|
||||
if (!costs.Any()) { return true; }
|
||||
|
||||
List<Item> allItems = character.Inventory.FindAllItems(recursive: true);
|
||||
HashSet<Item> itemsToRemove = new HashSet<Item>();
|
||||
|
||||
foreach (UpgradeResourceCost cost in costs)
|
||||
{
|
||||
int amountNeeded = cost.Amount;
|
||||
foreach (Item item in allItems.Where(cost.MatchesItem))
|
||||
{
|
||||
itemsToRemove.Add(item);
|
||||
amountNeeded--;
|
||||
if (amountNeeded <= 0) { break; }
|
||||
}
|
||||
|
||||
if (amountNeeded > 0) { return false; }
|
||||
}
|
||||
|
||||
foreach (Item item in itemsToRemove)
|
||||
{
|
||||
item.Remove();
|
||||
}
|
||||
|
||||
if (GameMain.IsMultiplayer) { character.Inventory.CreateNetworkEvent(); }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public ImmutableArray<ApplicableResourceCollection> GetApplicableResources(int level)
|
||||
{
|
||||
var applicableCosts = ResourceCosts.Where(cost => cost.AppliesForLevel(level)).ToImmutableHashSet();
|
||||
|
||||
var costs = applicableCosts.Any()
|
||||
? applicableCosts.Select(ApplicableResourceCollection.CreateFor).ToImmutableArray()
|
||||
: ImmutableArray<ApplicableResourceCollection>.Empty;
|
||||
|
||||
return costs;
|
||||
}
|
||||
|
||||
public bool IsDisallowed(MapEntity item)
|
||||
{
|
||||
return item.DisallowedUpgradeSet.Contains(Identifier)
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool Contains(in T v)
|
||||
public readonly bool Contains(in T v)
|
||||
=> start.CompareTo(v) <= 0 && end.CompareTo(v) >= 0;
|
||||
|
||||
private void VerifyStartLessThanEnd()
|
||||
|
||||
@@ -11,10 +11,10 @@ namespace Barotrauma
|
||||
public abstract bool IsSuccess { get; }
|
||||
public bool IsFailure => !IsSuccess;
|
||||
|
||||
public static Success<T, TError> Success(T value)
|
||||
public static Result<T, TError> Success(T value)
|
||||
=> new Success<T, TError>(value);
|
||||
|
||||
public static Failure<T, TError> Failure(TError error)
|
||||
public static Result<T, TError> Failure(TError error)
|
||||
=> new Failure<T, TError>(error);
|
||||
|
||||
public abstract bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value);
|
||||
|
||||
@@ -1,3 +1,40 @@
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v100.13.0.0
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
|
||||
- Fixes included in versions up to v0.21.0.0.
|
||||
- Added translations for the endgame and faction content.
|
||||
- Fixed reputation resetting between rounds.
|
||||
- Added a round light component variant.
|
||||
- Fixed z-fighting in HuskDistrict_Module_02.
|
||||
- Fixed missing gaps in the Winterhalter wreck in the end level.
|
||||
- Fixed end levels still sometimes including normal level geometry in saves started in the stable version.
|
||||
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v100.12.0.0
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
|
||||
- Fixes included in versions up to v0.20.15.0.
|
||||
- Fixed faction-specific bots not getting the correct ID card tags if they spawn for the first time in an outpost (= if you save and quit and reload in an outpost).
|
||||
- The "x reputation needed to unlock some biome" text is hidden in the tab menu if some other faction has already unlocked the path.
|
||||
- Fixed NPC following the player indefinitely in the "way to ascension" event in which you need to deliver husk eggs.
|
||||
- Fixed "Campaigning" talent not giving a discount on upgrades.
|
||||
- Fixed items appearing stolen if you enter another outpost that happens to have the same name as the one you stole the items from.
|
||||
- The generic outpost NPC greeting lines ("nice to see a friendly face", etc) aren't used when you have negative reputation.
|
||||
- Fixed ladders being drawn in front of the docking hatch in DockingModule_01..
|
||||
- Fixed Subra assassination events working even if you've started the main husk cult chain (or even after you've hired Subra)
|
||||
- Fixed submarine name being displayed as "Unknown" in the ending montage.
|
||||
- Improvements/fixes to dialogs that are shown to multiple clients: disable the option buttons when another client chooses an option, and highlight the option that was chosen.
|
||||
- Fixed outpost modules sometimes being placed in a way that makes them overlap with the sub.
|
||||
- Adjusted the layout of the research beacon station to prevent the entrance from ending up in an impossible-to-reach spot, made beacon station docking ports visible on sonar.
|
||||
- Fixed missing gaps in the end level's wrecks.
|
||||
- Fixed ancient weapons not damaging doors anymore.
|
||||
- Reputation rewards are displayed in the round summary.
|
||||
- Fixed missions that are configured to give location reputation (= reputation for the faction controlling the location you got the mission from) sometimes not giving rep. Happened because the game assumed the correct location is the one at the start of the connection the mission takes place in, which may not be the case (the location may have been unlocked further on the map, or it may be at the end of the connection).
|
||||
- Fixed mini nuke explosion sound.
|
||||
- Fixed "could not determine reputation change" console warnings when checking reputation in the tab menu or round summary during the 1st round in MP.
|
||||
- Fixed campaign map sometimes displaying incorrect reputation values in multiplayer.
|
||||
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v100.11.0.0
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
@@ -120,6 +157,74 @@ Test version of the faction overhaul:
|
||||
- There's now always two paths from biome to another, one controlled by the Coalition and one by Separatists.
|
||||
- Improvements to the campaign map.
|
||||
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v0.21.0.0 (unstable)
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
|
||||
Changes:
|
||||
- The "max missions" campaign setting is restricted to a maximum of 3.
|
||||
- Revisited the skill requirements and the OnFailure conditions on welding tool, plasma cutter, flamer, and steam gun. Flamer and steam gun now apply burns on the user only when they don't have enough skills to use the item. All these items are now less efficient when used with low skills.
|
||||
|
||||
Bugfixes:
|
||||
- Fixed high-quality items spawning earlier in the campaign when playing with a higher campaign difficulty setting.
|
||||
- Fixed attacking with a melee weapon making you unable to turn (flip) for a while.
|
||||
- Hull fixes to vanilla subs and wrecks.
|
||||
- Fixed alien flares practically never spawning in ruins.
|
||||
- Fixed status effects defined inside an attack definition still using the old OnUse/OnFailure logic instead of OnSuccess/OnFailure.
|
||||
|
||||
Modding:
|
||||
- Addressed various inconsistencies, issues and limitations in how status effects are used in certain cases:
|
||||
- Status effects defined for attacks with the type "UseTarget" now correctly target the use target, instead of the attacker like they used to.
|
||||
- Changed the status effects of type "Character" to "UseTarget" for MeleeWeapon, Projectile, and Throwable components. The motivation behind this change is that previously we couldn't target the attacker at all within these item components, which might be desirable for some melee weapons.
|
||||
- MODDERS, PLEASE NOTE: Effects that target "Character" in the previously mentioned components now affect the user - if that's not the intention, the target should be changed to "UseTarget".
|
||||
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v0.20.16.1
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
|
||||
- Fixed console errors when firing a Flak Cannon using spreader ammo in multiplayer.
|
||||
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v0.20.16.0
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
|
||||
- Fixed non-hitscan projectiles going through doors.
|
||||
- Fixed electrical discharge coils hitting characters very unreliably, unless the character happens to be right next to a wall.
|
||||
- Fixed makeshift shelves being containable in crates and cabinets, allowing for infinite recursive storage space.
|
||||
- Fixed the effects of the "Grid Maintainer" and "Egghead" talents.
|
||||
- Fixed incorrect "I Am That Guy" description (it gives a flat 20 skill bonus, not 20%).
|
||||
- Fixed "Cruisin'" talent increasing fuel consumption by 10% instead of decreasing it by 20%.
|
||||
- Optimized Steam Workshop queries done by the game (less bandwidth usage and stress on Steam's servers).
|
||||
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v0.20.15.0
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
|
||||
- Fixed Revenge Squad talent doing nothing.
|
||||
- Fixed Quickdraw talent's 8 second cooldown not working.
|
||||
- Fixed flamer being fabricable without the appropriate talent.
|
||||
- Fixed incorrect Aggressive Engineering talent description.
|
||||
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v0.20.14.0
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
|
||||
- Fixed missing Rifle fabrication recipe.
|
||||
- Fixed command devices (nav terminals, status monitors) not being in the electrical category, preventing "Better Than New" talent from having an effect on them.
|
||||
- Fixed Graduation Ceremony still unlocking double the talents in multiplayer.
|
||||
- Spawn husk eggs instead of husk syringes as creature loot.
|
||||
- Cultist events require husk eggs instead of syringes.
|
||||
- Adjusted shotgun fabrication recipe.
|
||||
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v0.20.13.0
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
|
||||
- Fixed incorrect admin module sometimes being used in colonies, making it impossible to get to.
|
||||
- Fixed a performance issue (character vitality logic being unnecessarily heavy) caused by some of the recent talent changes.
|
||||
- Fixed inability to fabricate Rifle Bullets.
|
||||
- Fixed contained items that have been configured to be hidden not being positioned correctly (meaning any positional effects done by the contained item would not work correctly).
|
||||
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v0.20.12.0
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
@@ -133,20 +238,19 @@ Talent overhaul:
|
||||
|
||||
Balance:
|
||||
- Slightly adjusted values of handheld weapons.
|
||||
- Power levels match cost better.
|
||||
- Damage to structures has been revised (f.e. knives shouldn't be so efficient at cutting through walls).
|
||||
- Some tools are now slightly more damaging and viable as a last resort weapon (don't atually try to fight mudraptors with a wrench though).
|
||||
- Improved ammo availability for basic weapons.
|
||||
- Made some weapons available later in game, to increase feeling of progression.
|
||||
- Damage values of weapons have been adjusted to be more in line with their cost
|
||||
- Damage to structures has been revised (f.e. knives shouldn't be so efficient at cutting through walls).
|
||||
- Improved ammo availability for basic weapons.
|
||||
- Usage of a minimum difficulty level to have some weapons appear in stores only later in the game. Even some previously talent-only items can appear in stores now in very late biomes.
|
||||
- Made some weapons available later in the game to increase the feeling of progression.
|
||||
- Slightly adjusted values of apparel (armor, clothing, diving suits) to better highlight strengths and weaknesses.
|
||||
- Combat Diving Suit is now actually better for combat than the regular diving suit, due to higher damage resistances.
|
||||
- PUCS no longer gives a bonus to speed when using Underwater Scooter, as it has plenty of other strengths.
|
||||
- Mechanic's apparel now has higher laceration protection than Engineer's apparel, as that's typically the damage they'd get from failing to repair.
|
||||
- All starter clothing gives less protection now, while some shop/npc clothing now gives some benefit.
|
||||
- Usage of a minimum difficulty level to have some weapons appear in stores only later in the game. Even some previously talent-only items can appear in stores now in very late biomes.
|
||||
- Combat Diving Suit is now actually better for combat than the regular diving suit, due to higher damage resistances.
|
||||
- PUCS no longer gives a bonus to speed when using Underwater Scooter, as it has plenty of other strengths.
|
||||
- Mechanic's apparel now has higher laceration protection than Engineer's apparel, as that's typically the damage they'd get from failing to repair.
|
||||
- All starter clothing gives less protection now, while some shop/npc clothing now gives some benefit.
|
||||
- Chance of finding good/excellent/masterwork quality items in higher-difficulty levels.
|
||||
- Plasma cutter is now much better at cutting.
|
||||
- Rebalanced damage dealt by tools. Damage should be a bit higher overall.
|
||||
- Rebalanced damage dealt by tools. Damage should be a bit higher overall. (don't actually try to fight mudraptors with a wrench though)
|
||||
|
||||
Tutorial improvements:
|
||||
- A new campaign-integrated tutorial that teaches the basics of the campaign mode in the first outpost.
|
||||
@@ -157,27 +261,27 @@ Tutorial improvements:
|
||||
|
||||
Changes and additions:
|
||||
- New weapons: Rifle, Heavy Machine Gun, Machine Pistol, Harpoon Coil-Rifle.
|
||||
- Flashlight can now be attached on all the ranged weapons held with two hands.
|
||||
- Flashlight can now be attached to all ranged weapons held with two hands.
|
||||
- Limit which submarines are available in each outpost: high-tier subs become available as you get further in the campaign, and the submarine class selection depends on the type of the outpost.
|
||||
- Two new music tracks.
|
||||
- Added a slider to the fabricator that can be used to select how many items to fabricate.
|
||||
- Added an option to hide enemy health bars.
|
||||
- Items' damage modifiers are shown in store tooltips.
|
||||
- Added a button for treating all characters in one go to the medical clinic.
|
||||
- Added a button for treating all characters in one go in the medical clinic.
|
||||
- Breaches through the submarine's outer hull throws shrapnels that can cause minor damage to nearby characters, making monsters that can't get inside more of a threat to the crew (as opposed to just the submarine itself).
|
||||
- Added a new honking scary random event to beacon stations.
|
||||
- Added some particle, sound and light effects to water-sensitive materials and made them explode when they've been in water for 3 seconds, not immediately.
|
||||
- Water-sensitive materials now explode when they've been in water for 3 seconds, not immediately. Added particle, sound and light effects.
|
||||
- Affliction descriptions change depending on the strength of the affliction, and whether you're treating someone else or yourself.
|
||||
- Added a button for opening the Steam Workshop to all tabs of the workshop menu.
|
||||
- Added tooltips that explain how the bot spawn modes work to the server lobby.
|
||||
- Added various new loot items to different creatures.
|
||||
- Large monsters (Abyss monsters, Moloch, Watcher) drop items upon death.
|
||||
- Husk eggs now come in two forms: Husk eggs with actual egg-like appearance and the syringe version.
|
||||
- Large monsters (Abyss monsters, Moloch, Watcher) now also drop items upon death.
|
||||
- Husk eggs now come in two forms: Husk eggs with actual egg-like appearance and the syringe version. Typically syringes are crafted, so finding a syringe on creatures felt a bit out of place. The eggs look as yucky as you’d expect.
|
||||
- Made saline significantly less effective as a treatment for bloodloss to make blood packs more useful.
|
||||
- Nerfed flak cannon's explosive ammo.
|
||||
- Emp damage now stuns and damages electrical characters (Fractalguardian and Defensebot). Modders: implemented as an affliction, so it's not tied to the "empstrength" attribute defined for explosions.
|
||||
- Emp damage now stuns and damages electrical characters (Fractalguardian and Defensebot). Modders note: it’s implemented as an affliction, so it's not tied to the "empstrength" attribute defined for explosions.
|
||||
- Allow putting medium items (e.g. storage container) in medical and toxic cabinets.
|
||||
- Some changes to wrecked item sprites (replacing the old low-res pictures with modified versions of the normal items' sprites).
|
||||
- SMG can now be crafted.
|
||||
- Optimized the server lobby: there was an issue in the logic that updates the microphone icon that caused the game to check available audio devices every frame.
|
||||
- Optimized status monitors: previously some parts of their UI were always updated regardless if anyone is viewing the UI.
|
||||
|
||||
@@ -189,7 +293,7 @@ Multiplayer:
|
||||
- Fixed inability to connect to IPv4 servers when IPv6 is disabled.
|
||||
- Fixed occasional crashes when shutting down a server (for example with the error messages "pipe is broken" or "ChildServerRelay readTask did not run to completion").
|
||||
- Fixed "no core packages in the list of mods the server has enabled" error when trying to join a server that's using a different version of the core package you have enabled.
|
||||
- Fixed "Input contains duplicate packages" error still occuring if you try to join a server that has empty content packages when you don't have those packages yourself.
|
||||
- Fixed "Input contains duplicate packages" error still occurring if you try to join a server that has empty content packages when you don't have those packages yourself.
|
||||
- Fixed networking errors when the connection to the server is momentarily lost and then re-established.
|
||||
- Added a cooldown to client name changes to prevent using it for spamming.
|
||||
- Fixed bans issued with the "banaddress" command using a client's Steam ID not working.
|
||||
@@ -214,12 +318,12 @@ Bugfixes:
|
||||
- Fixed PUCS not beeping when you're underwater without a tank if you're inside a hull that has oxygen in it.
|
||||
- Fixed some issues in sonar AITargets which made monsters hear the sonar when they shouldn't: switching to passive would immediately make the current directional ping cover 360 degrees, and whether the ping was directional or not would actually depend on whether the previous ping was directional, not what the mode is now.
|
||||
- Fixed items getting autofilled into non-interactable containers in wrecks and outposts.
|
||||
- Fixes to ID card tag issues in wrecks (prevented accessing the secure containers with the ID cards looted from the corpses).
|
||||
- Fixed ID cards looted from the corpses of a wreck not giving access to the secure containers in the wreck.
|
||||
- Fixed verifying file integrity on Steam resetting the server settings file.
|
||||
- Fixed crashing if you try to open an access-restricted directory in the file selection dialog.
|
||||
- Fixed a typo in physicorium shell's damage config, causing it to not do bleeding damage.
|
||||
- Fixed money gain/lose popups no longer showing in the campaign.
|
||||
- Fixed bloodloss and drunkenness never fully healing, just dropping below the threshold at which the icon appears. Caused e.g. drunkenness and bloodloss to never fully go away, causing issues with some talent effects.
|
||||
- Fixed bloodloss and drunkenness never fully healing, just dropping below the threshold at which the icon appears. As a result e.g. drunkenness and bloodloss never fully went away, which caused issues with some talent effects.
|
||||
- Fixed bots always opening the door/hatch they're trying to repair.
|
||||
- Fixed power indicator not rotating with batteries.
|
||||
- Fixed lights on welding tools and plasma cutters emitting light the next round if the round ends while using them.
|
||||
@@ -227,8 +331,8 @@ Bugfixes:
|
||||
- Fixed Berilia's bottom EDC not being wired to a supercapacitor and a loose wire between the flak cannon and the right supercapacitor.
|
||||
- Fixed status effects targeting "NearbyCharacters" or "NearbyItems" being applied twice. Modders: if you used this, double the effects (e.g. damage) to get the same results as previously.
|
||||
- Fixed a rounding error that caused Health Scanner HUD to display every level of bleeding below 100% as "minor".
|
||||
- Fixed speech impediment from the husk infection making the bots unable to register any new targets autonomously (= without orders).
|
||||
- Fixed bots having unintentionally long reaction times on reporting the issues, causing them to ignore any new enemies when they first envounter them.
|
||||
- Fixed speech impediment from the husk infection making the bots unable to register any new targets on their own (= without being ordered).
|
||||
- Fixed bots having unintentionally long reaction times on reporting the issues, causing them to ignore new enemies for a while when they first encounter them, unless being attacked.
|
||||
- Fixed the default aim assist being 50% instead of 5%. Fixed aim assist not resetting when the reset button is pressed on the settings window.
|
||||
- Fixed other players not seeing the spray particles when someone uses a sprayer in multiplayer.
|
||||
- Fixed ability to "fire" (just dropping the projectile) hardpoints that are connected to a periscope and loader.
|
||||
@@ -245,23 +349,23 @@ Bugfixes:
|
||||
- Adjusted railgun, coilgun and double coilgun firing offsets to make the projectile spawn closer to the end of the barrel.
|
||||
- Fixed loot sometimes spawning in vending machines' output slots.
|
||||
- Fixed water level sometimes "flickering" up and down when water is leaking to a room from the left or right.
|
||||
- Fixed resetting UI position doing nothing to equipped items' UIs (e.g. handheld status monitor).
|
||||
- Fixed resetting the UI position doing nothing to equipped items' UIs (e.g. handheld status monitor).
|
||||
- Fixed items equipped in the health interface slot being sellable.
|
||||
- Fixed inconsistent view ranges of large turrets.
|
||||
- Fixed SMG magazine shape being inconsistent with the shape of the mag well on the SMG sprite.
|
||||
- Fixed character portrait and health bar buttons being clickable (despite being hidden) when the health interface is open.
|
||||
- Attempt to fix occasional crashes due to location store being null when teleporting from location to another with console commands.
|
||||
- Fixed occasional crashes due to location store being null when teleporting from location to another with console commands.
|
||||
- Fixes to impact-sensitive items exploding at the start of the round (e.g. at the start of explosive transport missions or when purchasing explosives).
|
||||
- Attempt to fix bots occasionally being unable to operate turrets when starting a new round until they're re-ordered to man the turret.
|
||||
- Fixed bots occasionally being unable to operate turrets when starting a new round until they're re-ordered to man the turret.
|
||||
- Fixed focus staying on the highlighted item/character indefinitely if you keep holding LMB, even if you're outside interaction range.
|
||||
- Prevented spawning of genetic materials outside creature inventories when the inventory size was too small, by increasing inventory sizes.
|
||||
- Fixed some creatures not having enough space in their inventory for the genetic materials to spawn into.
|
||||
- Fixed minerals still sometimes being placed outside the level in mineral missions.
|
||||
- Yet another fix to cave tunnels sometimes being too narrow to pass through.
|
||||
- Fixed cave tunnels sometimes being too narrow to pass through.
|
||||
- Fixed "man and his raptor" outpost event giving 1000 marks in an incorrect branch of the dialog (the one where you immediately accept the NPC on board, instead of the one where the NPC says they'll pay you 1000 mk).
|
||||
- Fixed cases of interaction texts for focused item (most notoriously, the planter) not being updated correctly.
|
||||
- Fixed "snap to grid" causing door gaps to get misaligned.
|
||||
- Fixed weird equipping behavior on fruit and paints, causing them to be equipped in both hands when trying to unequip.
|
||||
- Fixed junction boxes not getting damaged by water since the power rework.
|
||||
- Fixed junction boxes not getting damaged by water.
|
||||
- Fixed opiate withdrawal only reducing down to 20%, but never fully healing by itself.
|
||||
- Fixed engines reverting back to the non-damaged sprite when they're damaged badly enough that the sprite starts shaking.
|
||||
- Fixed walls being set up incorrectly in vertical abandoned outpost hallway modules, causing them to stick out into the connected modules.
|
||||
@@ -272,16 +376,16 @@ Bugfixes:
|
||||
|
||||
Modding:
|
||||
- Added a button to the main menu that can be used to update all installed mods when there's updates available.
|
||||
- Mods with errors can no longer be enabled.
|
||||
- Mods with errors can no longer be enabled. This change should encourage modders to fix errors in their mods and report bugs, as well as discourage players from ignoring errors.
|
||||
- Removed most of the debug console error spam seen when launching the game or opening the settings menu when faulty mods are installed.
|
||||
- Fixed mods failing to show up in the mods list at all when they have certain kinds of errors.
|
||||
- Implemented the status effect type "OnSuccess" where "OnUse" was used instead. Changed "OnUse" to be neutral: always triggers, regardless of the (skill) requirements. You may need to switch using "OnSuccess" instead of "OnUse", if it's intended for the status effect to trigger only when the requirements are matched.
|
||||
- Implemented the status effect type "OnSuccess" where "OnUse" was used instead. Changed "OnUse" to be neutral, meaning it always triggers, regardless of the (skill) requirements. You may need to switch using "OnSuccess" instead of "OnUse", if it's intended for the status effect to trigger only when the (skill) requirements are matched.
|
||||
- Fixed increasing an item's HealthMultiplier making the items appear damaged in existing subs/saves (e.g. if you doubled an item's maximum condition, the items would remain in the old maximum condition and appear 50% damaged).
|
||||
- Fixed crashing if a talent is triggered when the character receives some affliction, and that talent applies the same affliction on the character.
|
||||
- Fixed crashing if the ingredient of a fabrication recipe can't be found.
|
||||
- Fixed inability to sync properties of ItemComponents that the item has multiple of (meaning that it was only possible to e.g. edit the light color of the item's first LightComponent if it has multiple).
|
||||
- Allow 'launchimpulse' on RangedWeapon to affect projectile's speed (sum of launch impulses).
|
||||
- Allow 'penetration' on RangedWeapon to affect projectile's penetration (sum of penetration).
|
||||
- Added 'launchimpulse' on RangedWeapon to affect projectile's speed (sum of launch impulses).
|
||||
- Added 'penetration' on RangedWeapon to affect projectile's penetration (sum of penetration).
|
||||
- Added 'DontApplyToHands' property to Propulsion, preventing extra force applying to hands when the item is held in hands (instead applying only to the character's whole body).
|
||||
- Added a skill requirement conditional for StatusEffect, example: <Conditional skillrequirement="true" weapons="lt 35" /> to make a status effect occur only if the target has less than 35 weapon skill.
|
||||
- Added ReloadSkillRequirement and ReloadNoSkill to RangedWeapon. E.g. a weapon with reload=2s, ReloadSkillRequirement=40, ReloadNoSkill=5s will have a character with 20 weapons skill reload at 3.5 s.
|
||||
@@ -290,8 +394,9 @@ Modding:
|
||||
- Added UseEnvironment.None to Propulsion component.
|
||||
- Fixed the debug console command "head" causing the character to disappear. The command can be used for changing the appearance of the character at runtime.
|
||||
- Status effects of type "OnUse" on projectiles now trigger when the projectile is launched. Previously it launched when the projectile hit the target. Use OnImpact (or OnSuccess/OnFailure) when you want something to happen when the projectile hits the target.
|
||||
- Added an option to multiply the damage by max vitality (relative damage) per affliction definition, in addition to the "multiplyAfflictionsByMaxVitality" attribute defined for the status effects. If you want to define it for an affliction separately, leave the status effect level definition off, because it'd override the affliction specific value.
|
||||
- Added an option to multiply the damage by max vitality (relative damage) per affliction definition, in addition to the "multiplyAfflictionsByMaxVitality" attribute defined for the status effects. If you want to define it for an affliction separately, leave the status effect level definition undefined, because it'd override the affliction specific value.
|
||||
- Fixed item's OnSpawn effects being applied twice.
|
||||
- Fixed item components that inherit the status effects from another item component (e.g. the medical syringes) triggering the status effects twice when the effect is triggered via the item and not via the item component. Didn’t cause issues with the vanilla items, but might affect some mods. If your item e.g. suddenly does only half of the damage after the update, it’s possible that it was affected by the bug. Just double the effect to fix it.
|
||||
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v0.19.14.0
|
||||
|
||||
Reference in New Issue
Block a user