Build 0.21.1.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>
|
||||
@@ -351,6 +351,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)
|
||||
|
||||
@@ -355,6 +355,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;
|
||||
@@ -365,7 +369,7 @@ namespace Barotrauma
|
||||
{
|
||||
targetingTag = "husk";
|
||||
}
|
||||
else if (!Character.IsFriendly(targetCharacter))
|
||||
else if (!Character.IsSameSpeciesOrGroup(targetCharacter))
|
||||
{
|
||||
if (enemy.CombatStrength > CombatStrength)
|
||||
{
|
||||
@@ -687,12 +691,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.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.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)
|
||||
|
||||
+1
-1
@@ -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++;
|
||||
}
|
||||
|
||||
+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));
|
||||
|
||||
@@ -371,7 +371,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));
|
||||
}
|
||||
|
||||
@@ -381,7 +382,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)
|
||||
{
|
||||
|
||||
@@ -994,7 +994,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
|
||||
@@ -1014,7 +1014,7 @@ namespace Barotrauma
|
||||
if (l.IsSevered) { continue; }
|
||||
|
||||
float rotation = l.body.Rotation;
|
||||
if (l.DoesFlip)
|
||||
if (l.DoesMirror)
|
||||
{
|
||||
if (RagdollParams.IsSpritesheetOrientationHorizontal)
|
||||
{
|
||||
|
||||
+55
-21
@@ -150,8 +150,10 @@ namespace Barotrauma
|
||||
|
||||
private readonly float movementLerp;
|
||||
|
||||
private float cprAnimTimer;
|
||||
private float cprPump;
|
||||
private float cprAnimTimer,cprPump;
|
||||
|
||||
private float fallingProneAnimTimer;
|
||||
const float FallingProneAnimDuration = 1.0f;
|
||||
|
||||
private bool swimming;
|
||||
//time until the character can switch from walking to swimming or vice versa
|
||||
@@ -268,7 +270,8 @@ namespace Barotrauma
|
||||
if (deathAnimTimer < deathAnimDuration)
|
||||
{
|
||||
deathAnimTimer += deltaTime;
|
||||
UpdateDying(deltaTime);
|
||||
//the force/torque used to move the limbs goes from 1 to 0 during the death anim duration
|
||||
UpdateFallingProne(1.0f - deathAnimTimer / deathAnimDuration);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -278,6 +281,11 @@ namespace Barotrauma
|
||||
|
||||
if (!character.CanMove)
|
||||
{
|
||||
if (fallingProneAnimTimer < FallingProneAnimDuration)
|
||||
{
|
||||
fallingProneAnimTimer += deltaTime;
|
||||
UpdateFallingProne(1.0f);
|
||||
}
|
||||
levitatingCollider = false;
|
||||
Collider.FarseerBody.FixedRotation = false;
|
||||
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
||||
@@ -291,18 +299,20 @@ namespace Barotrauma
|
||||
}
|
||||
return;
|
||||
}
|
||||
fallingProneAnimTimer = 0.0f;
|
||||
|
||||
//re-enable collider
|
||||
if (!Collider.Enabled)
|
||||
{
|
||||
var lowestLimb = FindLowestLimb();
|
||||
|
||||
|
||||
Collider.SetTransform(new Vector2(
|
||||
Collider.SimPosition.X,
|
||||
Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)),
|
||||
Collider.Rotation);
|
||||
|
||||
Collider.FarseerBody.ResetDynamics();
|
||||
Collider.FarseerBody.LinearVelocity = MainLimb.LinearVelocity;
|
||||
Collider.Enabled = true;
|
||||
}
|
||||
|
||||
@@ -431,7 +441,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (Timing.TotalTime > LockFlippingUntil && TargetDir != dir && !IsStuck)
|
||||
if (Timing.TotalTime > FlipLockTime && TargetDir != dir && !IsStuck)
|
||||
{
|
||||
Flip();
|
||||
}
|
||||
@@ -1292,10 +1302,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateDying(float deltaTime)
|
||||
void UpdateFallingProne(float strength)
|
||||
{
|
||||
//the force/torque used to move the limbs goes from 1 to 0 during the death anim duration
|
||||
float strength = 1.0f - deathAnimTimer / deathAnimDuration;
|
||||
if (strength <= 0.0f) { return; }
|
||||
|
||||
Limb head = GetLimb(LimbType.Head);
|
||||
Limb torso = GetLimb(LimbType.Torso);
|
||||
@@ -1319,6 +1328,19 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
if (torso == null) { return; }
|
||||
|
||||
//make the torso tip over
|
||||
//otherwise it tends to just drop straight down, pinning the characters legs in a weird pose
|
||||
if (!InWater)
|
||||
{
|
||||
//prefer tipping over in the same direction the torso is rotating
|
||||
//or moving
|
||||
//or lastly, in the direction it's facing if it's not moving/rotating
|
||||
float fallDirection = Math.Sign(torso.body.AngularVelocity - torso.body.LinearVelocity.X - Dir * 0.01f);
|
||||
float torque = MathF.Cos(torso.Rotation) * fallDirection * 5.0f * strength;
|
||||
torso.body.ApplyTorque(torque * torso.body.Mass);
|
||||
}
|
||||
|
||||
//attempt to make legs stay in a straight line with the torso to prevent the character from doing a split
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
@@ -1503,12 +1525,12 @@ namespace Barotrauma
|
||||
Limb rightHand = GetLimb(LimbType.RightHand);
|
||||
|
||||
Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm);
|
||||
if (targetLeftHand == null) targetLeftHand = target.AnimController.GetLimb(LimbType.Torso);
|
||||
if (targetLeftHand == null) targetLeftHand = target.AnimController.MainLimb;
|
||||
if (targetLeftHand == null) { targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); }
|
||||
if (targetLeftHand == null) { targetLeftHand = target.AnimController.MainLimb; }
|
||||
|
||||
Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm);
|
||||
if (targetRightHand == null) targetRightHand = target.AnimController.GetLimb(LimbType.Torso);
|
||||
if (targetRightHand == null) targetRightHand = target.AnimController.MainLimb;
|
||||
if (targetRightHand == null) { targetRightHand = target.AnimController.GetLimb(LimbType.Torso); }
|
||||
if (targetRightHand == null) { targetRightHand = target.AnimController.MainLimb; }
|
||||
|
||||
if (!target.AllowInput)
|
||||
{
|
||||
@@ -1644,18 +1666,24 @@ namespace Barotrauma
|
||||
pullLimb.PullJointEnabled = true;
|
||||
if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb)
|
||||
{
|
||||
Vector2 pullLimbAnchor = targetLimb.SimPosition;
|
||||
pullLimb.PullJointMaxForce = 5000.0f;
|
||||
if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging))
|
||||
{
|
||||
targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f);
|
||||
}
|
||||
|
||||
Vector2 shoulderPos = rightShoulder.WorldAnchorA;
|
||||
Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY;
|
||||
if (!MathUtils.IsValid(dragDir)) { dragDir = Vector2.UnitY; }
|
||||
|
||||
targetAnchor = shoulderPos - dragDir * ConvertUnits.ToSimUnits(upperArmLength + forearmLength);
|
||||
Vector2 shoulderPos = rightShoulder.WorldAnchorA;
|
||||
float targetDist = Vector2.Distance(targetLimb.SimPosition, shoulderPos);
|
||||
Vector2 dragDir = (targetLimb.SimPosition - shoulderPos) / targetDist;
|
||||
if (!MathUtils.IsValid(dragDir)) { dragDir = -Vector2.UnitY; }
|
||||
if (!InWater)
|
||||
{
|
||||
//lerp the arm downwards when not swimming
|
||||
dragDir = Vector2.Lerp(dragDir, -Vector2.UnitY, 0.5f);
|
||||
}
|
||||
|
||||
Vector2 pullLimbAnchor = shoulderPos + dragDir * Math.Min(targetDist, (upperArmLength + forearmLength) * 2);
|
||||
targetAnchor = shoulderPos + dragDir * (upperArmLength + forearmLength);
|
||||
targetForce = 200.0f;
|
||||
if (target.Submarine != character.Submarine)
|
||||
{
|
||||
@@ -1723,7 +1751,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 +1850,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
|
||||
@@ -873,7 +877,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();
|
||||
@@ -1428,6 +1432,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);
|
||||
|
||||
@@ -492,37 +492,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)
|
||||
@@ -532,18 +547,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,47 +606,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly static List<Character> CharacterList = new List<Character>();
|
||||
|
||||
public const float MaxHighlightDistance = 150.0f;
|
||||
public const float MaxDragDistance = 200.0f;
|
||||
|
||||
partial void UpdateLimbLightSource(Limb limb);
|
||||
|
||||
private bool enabled = true;
|
||||
@@ -354,9 +357,15 @@ namespace Barotrauma
|
||||
public readonly CharacterPrefab Prefab;
|
||||
|
||||
public readonly CharacterParams Params;
|
||||
|
||||
public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier();
|
||||
|
||||
public Identifier Group => Params.Group;
|
||||
|
||||
public bool IsHumanoid => Params.Humanoid;
|
||||
|
||||
public bool IsMachine => Params.IsMachine;
|
||||
|
||||
public bool IsHusk => Params.Husk;
|
||||
|
||||
public bool IsMale => info?.IsMale ?? false;
|
||||
@@ -805,6 +814,11 @@ namespace Barotrauma
|
||||
public float HealthPercentage => CharacterHealth.HealthPercentage;
|
||||
public float MaxVitality => CharacterHealth.MaxVitality;
|
||||
public float MaxHealth => MaxVitality;
|
||||
|
||||
/// <summary>
|
||||
/// Was the character in full health at the beginning of the frame?
|
||||
/// </summary>
|
||||
public bool WasFullHealth => CharacterHealth.WasInFullHealth;
|
||||
public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
|
||||
public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached;
|
||||
public float EmpVulnerability => Params.Health.EmpVulnerability;
|
||||
@@ -1741,7 +1755,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();
|
||||
@@ -2201,7 +2215,9 @@ namespace Barotrauma
|
||||
|
||||
if (SelectedCharacter != null)
|
||||
{
|
||||
if (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > 90000.0f || !SelectedCharacter.CanBeSelected)
|
||||
if (!SelectedCharacter.CanBeSelected ||
|
||||
(Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > MaxDragDistance * MaxDragDistance &&
|
||||
SelectedCharacter.GetDistanceToClosestLimb(SimPosition) > ConvertUnits.ToSimUnits(MaxDragDistance)))
|
||||
{
|
||||
DeselectCharacter();
|
||||
}
|
||||
@@ -2494,8 +2510,12 @@ namespace Barotrauma
|
||||
|
||||
if (!skipDistanceCheck)
|
||||
{
|
||||
maxDist = ConvertUnits.ToSimUnits(maxDist);
|
||||
if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) { return false; }
|
||||
maxDist = Math.Max(ConvertUnits.ToSimUnits(maxDist), c.AnimController.Collider.GetMaxExtent());
|
||||
if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist &&
|
||||
Vector2.DistanceSquared(SimPosition, c.AnimController.MainLimb.SimPosition) > maxDist * maxDist)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return checkVisibility ? CanSeeCharacter(c) : true;
|
||||
@@ -3138,56 +3158,57 @@ namespace Barotrauma
|
||||
|
||||
UpdateAIChatMessages(deltaTime);
|
||||
|
||||
//Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us!
|
||||
bool allowRagdoll = GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true;
|
||||
bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 8.0f * 8.0f;
|
||||
bool wasRagdolled = false;
|
||||
bool selfRagdolled = false;
|
||||
|
||||
if (IsForceRagdolled)
|
||||
if (GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true)
|
||||
{
|
||||
IsRagdolled = IsForceRagdolled;
|
||||
}
|
||||
else if (this != Controlled)
|
||||
{
|
||||
wasRagdolled = IsRagdolled;
|
||||
IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll);
|
||||
}
|
||||
//Keep us ragdolled if we were forced or we're too speedy to unragdoll
|
||||
else if (allowRagdoll && (!IsRagdolled || !tooFastToUnragdoll))
|
||||
{
|
||||
if (ragdollingLockTimer > 0.0f)
|
||||
bool wasRagdolled = IsRagdolled;
|
||||
if (IsForceRagdolled)
|
||||
{
|
||||
SetInput(InputType.Ragdoll, false, true);
|
||||
ragdollingLockTimer -= deltaTime;
|
||||
IsRagdolled = IsForceRagdolled;
|
||||
}
|
||||
else if (this != Controlled)
|
||||
{
|
||||
wasRagdolled = IsRagdolled;
|
||||
IsRagdolled = IsKeyDown(InputType.Ragdoll);
|
||||
}
|
||||
else
|
||||
{
|
||||
wasRagdolled = IsRagdolled;
|
||||
IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
|
||||
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.5f; }
|
||||
bool tooFastToUnragdoll = bodyMovingTooFast(AnimController.Collider) || bodyMovingTooFast(AnimController.MainLimb.body);
|
||||
bool bodyMovingTooFast(PhysicsBody body)
|
||||
{
|
||||
return
|
||||
body.LinearVelocity.LengthSquared() > 8.0f * 8.0f ||
|
||||
//falling down counts as going too fast
|
||||
(!InWater && body.LinearVelocity.Y < -5.0f);
|
||||
}
|
||||
if (ragdollingLockTimer > 0.0f)
|
||||
{
|
||||
ragdollingLockTimer -= deltaTime;
|
||||
}
|
||||
else if (!tooFastToUnragdoll)
|
||||
{
|
||||
IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
|
||||
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; }
|
||||
}
|
||||
if (IsRagdolled)
|
||||
{
|
||||
SetInput(InputType.Ragdoll, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasRagdolled && IsRagdolled)
|
||||
{
|
||||
if (selfRagdolled)
|
||||
if (!wasRagdolled && IsRagdolled)
|
||||
{
|
||||
CheckTalents(AbilityEffectType.OnSelfRagdoll);
|
||||
CheckTalents(AbilityEffectType.OnRagdoll);
|
||||
}
|
||||
// currently does not work when you are stunned, like it should
|
||||
CheckTalents(AbilityEffectType.OnRagdoll);
|
||||
}
|
||||
|
||||
lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f);
|
||||
|
||||
//ragdoll button
|
||||
if (IsRagdolled || !CanMove)
|
||||
{
|
||||
if (AnimController is HumanoidAnimController humanAnimController)
|
||||
{
|
||||
humanAnimController.Crouching = false;
|
||||
}
|
||||
if (IsRagdolled) { AnimController.IgnorePlatforms = true; }
|
||||
AnimController.ResetPullJoints();
|
||||
SelectedItem = SelectedSecondaryItem = null;
|
||||
return;
|
||||
@@ -3365,6 +3386,20 @@ namespace Barotrauma
|
||||
return distSqr;
|
||||
}
|
||||
|
||||
public float GetDistanceToClosestLimb(Vector2 simPos)
|
||||
{
|
||||
float closestDist = float.MaxValue;
|
||||
foreach (Limb limb in AnimController.Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
float dist = Vector2.Distance(simPos, limb.SimPosition);
|
||||
dist -= limb.body.GetMaxExtent();
|
||||
closestDist = Math.Min(closestDist, dist);
|
||||
if (closestDist <= 0.0f) { return 0.0f; }
|
||||
}
|
||||
return closestDist;
|
||||
}
|
||||
|
||||
private float despawnTimer;
|
||||
private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false, bool createNetworkEvents = true)
|
||||
{
|
||||
@@ -3885,8 +3920,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);
|
||||
@@ -4484,7 +4519,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();
|
||||
@@ -5200,15 +5238,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 || other.Params.CompareGroup(me.Params.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 || other.Params.CompareGroup(me.Params.Group);
|
||||
|
||||
public void StopClimbing()
|
||||
{
|
||||
|
||||
@@ -778,9 +778,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);
|
||||
|
||||
+12
@@ -602,6 +602,18 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < effects.Count; i++)
|
||||
{
|
||||
for (int j = i + 1; j < effects.Count; j++)
|
||||
{
|
||||
var a = effects[i];
|
||||
var b = effects[j];
|
||||
if (a.MinStrength < b.MaxStrength && b.MinStrength < a.MaxStrength)
|
||||
{
|
||||
DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearEffects()
|
||||
|
||||
@@ -230,6 +230,11 @@ namespace Barotrauma
|
||||
|
||||
public float StunTimer { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Was the character in full health at the beginning of the frame?
|
||||
/// </summary>
|
||||
public bool WasInFullHealth { get; private set; }
|
||||
|
||||
public Affliction PressureAffliction
|
||||
{
|
||||
get { return pressureAffliction; }
|
||||
@@ -772,6 +777,8 @@ namespace Barotrauma
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
WasInFullHealth = vitality >= MaxVitality;
|
||||
|
||||
UpdateOxygen(deltaTime);
|
||||
|
||||
StunTimer = Stun > 0 ? StunTimer + deltaTime : 0;
|
||||
@@ -878,7 +885,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;
|
||||
@@ -980,6 +991,8 @@ namespace Barotrauma
|
||||
UpdateLimbAfflictionOverlays();
|
||||
UpdateSkinTint();
|
||||
Character.Kill(type, affliction);
|
||||
|
||||
WasInFullHealth = false;
|
||||
#if CLIENT
|
||||
DisplayVitalityDelay = 0.0f;
|
||||
DisplayedVitality = Vitality;
|
||||
@@ -1024,17 +1037,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:
|
||||
|
||||
-1
@@ -28,7 +28,6 @@ namespace Barotrauma.Abilities
|
||||
conditionals.Add(new PropertyConditional(attribute));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-13
@@ -37,19 +37,7 @@ namespace Barotrauma.Abilities
|
||||
|
||||
if (itemPrefab != null)
|
||||
{
|
||||
if (category != MapEntityCategory.None)
|
||||
{
|
||||
if (!itemPrefab.Category.HasFlag(category)) { return false; }
|
||||
}
|
||||
|
||||
if (identifiers.Any())
|
||||
{
|
||||
if (!identifiers.Any(t => itemPrefab.Identifier == t))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p));
|
||||
return MatchesItem(itemPrefab);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -57,5 +45,22 @@ namespace Barotrauma.Abilities
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool MatchesItem(ItemPrefab itemPrefab)
|
||||
{
|
||||
if (category != MapEntityCategory.None)
|
||||
{
|
||||
if (!itemPrefab.Category.HasFlag(category)) { return false; }
|
||||
}
|
||||
|
||||
if (identifiers.Any())
|
||||
{
|
||||
if (!identifiers.Any(t => itemPrefab.Identifier == t))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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)
|
||||
|
||||
+19
-3
@@ -1,19 +1,35 @@
|
||||
#nullable enable
|
||||
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
internal sealed class CharacterAbilityRemoveRandomIngredient : CharacterAbility
|
||||
{
|
||||
public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { }
|
||||
private readonly AbilityConditionItem? condition;
|
||||
|
||||
public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
var conditionElement = abilityElement.GetChildElement(nameof(AbilityConditionItem));
|
||||
if (conditionElement != null)
|
||||
{
|
||||
condition = new AbilityConditionItem(CharacterTalent, conditionElement);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
{
|
||||
if (abilityObject is not Fabricator.AbilityFabricationItemIngredients { Items.Count: > 0 } ingredients) { return; }
|
||||
|
||||
int randomIndex = Rand.Int(ingredients.Items.Count, Rand.RandSync.Unsynced);
|
||||
ingredients.Items.RemoveAt(randomIndex);
|
||||
List<Item> applicableIngredients = condition == null ?
|
||||
ingredients.Items.ToList() :
|
||||
ingredients.Items.Where(it => condition.MatchesItem(it.Prefab)).ToList();
|
||||
if (applicableIngredients.None()) { return; }
|
||||
|
||||
ingredients.Items.Remove(applicableIngredients.GetRandom(Rand.RandSync.Unsynced));
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -288,9 +288,7 @@ namespace Barotrauma
|
||||
|
||||
if (errorCatcher.Errors.Any())
|
||||
{
|
||||
yield return ContentPackageManager.LoadProgress.Failure(
|
||||
ContentPackageManager.LoadProgress.Error
|
||||
.Reason.ConsoleErrorsThrown);
|
||||
yield return ContentPackageManager.LoadProgress.Failure(errorCatcher.Errors.Select(e => e.Text));
|
||||
yield break;
|
||||
}
|
||||
yield return ContentPackageManager.LoadProgress.Progress((i + indexOffset) / (float)Files.Length);
|
||||
|
||||
@@ -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;
|
||||
@@ -435,22 +437,19 @@ namespace Barotrauma
|
||||
public readonly record struct LoadProgress(Result<float, LoadProgress.Error> Result)
|
||||
{
|
||||
public readonly record struct Error(
|
||||
Error.Reason ErrorReason,
|
||||
Option<Exception> Exception)
|
||||
Either<ImmutableArray<string>, Exception> ErrorsOrException)
|
||||
{
|
||||
public enum Reason { Exception, ConsoleErrorsThrown }
|
||||
|
||||
public Error(Reason reason) : this(reason, Option.None) { }
|
||||
public Error(Exception exception) : this(Reason.Exception, Option.Some(exception)) { }
|
||||
public Error(IEnumerable<string> errorMessages) : this(ErrorsOrException: errorMessages.ToImmutableArray()) { }
|
||||
public Error(Exception exception) : this(ErrorsOrException: exception) { }
|
||||
}
|
||||
|
||||
public static LoadProgress Failure(Exception exception)
|
||||
=> new LoadProgress(
|
||||
Result<float, Error>.Failure(new Error(exception)));
|
||||
|
||||
public static LoadProgress Failure(Error.Reason reason)
|
||||
public static LoadProgress Failure(IEnumerable<string> errorMessages)
|
||||
=> new LoadProgress(
|
||||
Result<float, Error>.Failure(new Error(reason)));
|
||||
Result<float, Error>.Failure(new Error(errorMessages)));
|
||||
|
||||
public static LoadProgress Progress(float value)
|
||||
=> new LoadProgress(
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -1312,10 +1312,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));
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ namespace Barotrauma
|
||||
OnUseRangedWeapon,
|
||||
OnReduceAffliction,
|
||||
OnAddDamageAffliction,
|
||||
OnSelfRagdoll,
|
||||
OnRagdoll,
|
||||
OnRoundEnd,
|
||||
OnLootCharacter,
|
||||
@@ -168,7 +167,7 @@ namespace Barotrauma
|
||||
PumpSpeed,
|
||||
PumpMaxFlow,
|
||||
ReactorMaxOutput,
|
||||
ReactorFuelEfficiency,
|
||||
ReactorFuelConsumption,
|
||||
DeconstructorSpeed,
|
||||
FabricationSpeed
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ namespace Barotrauma
|
||||
|
||||
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
|
||||
|
||||
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos, giveTags: true);
|
||||
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos);
|
||||
|
||||
if (spawnPos is WayPoint wp)
|
||||
{
|
||||
|
||||
@@ -97,7 +97,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 ||
|
||||
|
||||
@@ -404,7 +404,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
|
||||
@@ -545,17 +545,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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -46,15 +46,15 @@ namespace Barotrauma
|
||||
public static void Init()
|
||||
{
|
||||
#if CLIENT
|
||||
Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), true);
|
||||
DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), true);
|
||||
SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), true);
|
||||
TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), true);
|
||||
Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), isSinglePlayer: true);
|
||||
DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), isSinglePlayer: true);
|
||||
SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), isSinglePlayer: true);
|
||||
TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), isSinglePlayer: true);
|
||||
#endif
|
||||
Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), false);
|
||||
Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), false);
|
||||
PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), false);
|
||||
MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), false, false);
|
||||
Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), isSinglePlayer: false);
|
||||
Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), isSinglePlayer: false);
|
||||
PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), isSinglePlayer: false);
|
||||
MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), isSinglePlayer: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -359,7 +359,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;
|
||||
}
|
||||
@@ -459,7 +460,10 @@ namespace Barotrauma.Items.Components
|
||||
ce = ce.Next;
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -213,6 +213,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;
|
||||
}
|
||||
|
||||
@@ -856,7 +856,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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,7 +767,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;
|
||||
}
|
||||
@@ -775,7 +775,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 })
|
||||
{
|
||||
@@ -882,7 +888,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;
|
||||
}
|
||||
@@ -953,8 +959,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)
|
||||
{
|
||||
@@ -965,14 +971,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);
|
||||
}
|
||||
|
||||
+18
-13
@@ -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
|
||||
}
|
||||
@@ -187,6 +187,8 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
public float TemporaryFlickerTimer;
|
||||
|
||||
public override void Move(Vector2 amount, bool ignoreContacts = false)
|
||||
{
|
||||
#if CLIENT
|
||||
@@ -205,7 +207,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (base.IsActive == value) { return; }
|
||||
base.IsActive = isOn = value;
|
||||
SetLightSourceState(value, value ? lightBrightness : 0.0f);
|
||||
SetLightSourceState(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +238,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)
|
||||
@@ -263,8 +265,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 +286,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,12 +303,14 @@ namespace Barotrauma.Items.Components
|
||||
PhysicsBody body = ParentBody ?? item.body;
|
||||
if (body != null && !body.Enabled)
|
||||
{
|
||||
SetLightSourceState(false, 0.0f);
|
||||
SetLightSourceState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
TemporaryFlickerTimer -= deltaTime;
|
||||
|
||||
//currPowerConsumption = powerConsumption;
|
||||
if (Rand.Range(0.0f, 1.0f) < 0.05f && Voltage < Rand.Range(0.0f, MinVoltage))
|
||||
if (Rand.Range(0.0f, 1.0f) < 0.05f && (Voltage < Rand.Range(0.0f, MinVoltage) || TemporaryFlickerTimer > 0.0f))
|
||||
{
|
||||
#if CLIENT
|
||||
if (Voltage > 0.1f)
|
||||
@@ -325,7 +330,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 +362,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 +380,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()
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable, IClientSerializable
|
||||
{
|
||||
partial class WireSection
|
||||
public partial class WireSection
|
||||
{
|
||||
private Vector2 start;
|
||||
private Vector2 end;
|
||||
@@ -775,20 +775,25 @@ namespace Barotrauma.Items.Components
|
||||
UpdateSections();
|
||||
}
|
||||
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
|
||||
public static IEnumerable<Vector2> ExtractNodes(XElement element)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap);
|
||||
|
||||
string nodeString = componentElement.GetAttributeString("nodes", "");
|
||||
if (nodeString == "") return;
|
||||
string nodeString = element.GetAttributeString("nodes", "");
|
||||
if (nodeString.IsNullOrWhiteSpace()) { yield break; }
|
||||
|
||||
string[] nodeCoords = nodeString.Split(';');
|
||||
for (int i = 0; i < nodeCoords.Length / 2; i++)
|
||||
{
|
||||
float.TryParse(nodeCoords[i * 2], NumberStyles.Float, CultureInfo.InvariantCulture, out float x);
|
||||
float.TryParse(nodeCoords[i * 2 + 1], NumberStyles.Float, CultureInfo.InvariantCulture, out float y);
|
||||
nodes.Add(new Vector2(x, y));
|
||||
float.TryParse(nodeCoords[i * 2].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float x);
|
||||
float.TryParse(nodeCoords[i * 2 + 1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float y);
|
||||
yield return new Vector2(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap);
|
||||
|
||||
nodes.AddRange(ExtractNodes(componentElement));
|
||||
|
||||
Drawable = nodes.Any();
|
||||
}
|
||||
|
||||
@@ -445,7 +445,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;
|
||||
@@ -1581,7 +1582,7 @@ namespace Barotrauma
|
||||
tags.Add(newTag);
|
||||
}
|
||||
|
||||
public IEnumerable<Identifier> GetTags()
|
||||
public IReadOnlyCollection<Identifier> GetTags()
|
||||
{
|
||||
return tags;
|
||||
}
|
||||
@@ -1743,15 +1744,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);
|
||||
@@ -1765,7 +1765,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);
|
||||
@@ -1793,8 +1793,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
LastConditionChange = condition - prev;
|
||||
LastConditionChange = condition - prevCondition;
|
||||
ConditionLastUpdated = Timing.TotalTime;
|
||||
prevCondition = condition;
|
||||
|
||||
static void flagChangedConnections(Dictionary<string, Connection> connections)
|
||||
{
|
||||
@@ -2159,8 +2160,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; }
|
||||
}
|
||||
@@ -2711,7 +2713,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; }
|
||||
}
|
||||
@@ -2742,7 +2744,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; }
|
||||
}
|
||||
@@ -2781,8 +2783,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 })
|
||||
{
|
||||
@@ -3448,7 +3450,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();
|
||||
|
||||
@@ -3522,15 +3524,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;
|
||||
|
||||
@@ -568,9 +568,11 @@ namespace Barotrauma
|
||||
|
||||
public ImmutableDictionary<Identifier, FixedQuantityResourceInfo> LevelQuantity { get; private set; }
|
||||
|
||||
public bool CanSpriteFlipX { get; private set; }
|
||||
private bool canSpriteFlipX;
|
||||
public override bool CanSpriteFlipX => canSpriteFlipX;
|
||||
|
||||
public bool CanSpriteFlipY { get; private set; }
|
||||
private bool canSpriteFlipY;
|
||||
public override bool CanSpriteFlipY => canSpriteFlipY;
|
||||
|
||||
/// <summary>
|
||||
/// Can the item be chosen as extra cargo in multiplayer. If not set, the item is available if it can be bought from outposts in the campaign.
|
||||
@@ -884,8 +886,8 @@ namespace Barotrauma
|
||||
case "sprite":
|
||||
string spriteFolder = GetTexturePath(subElement, variantOf);
|
||||
|
||||
CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true);
|
||||
CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true);
|
||||
canSpriteFlipX = subElement.GetAttributeBool("canflipx", true);
|
||||
canSpriteFlipY = subElement.GetAttributeBool("canflipy", true);
|
||||
|
||||
sprite = new Sprite(subElement, spriteFolder, lazyLoad: true);
|
||||
if (subElement.GetAttribute("sourcerect") == null &&
|
||||
|
||||
@@ -188,6 +188,12 @@ namespace Barotrauma
|
||||
item.Condition -= item.MaxCondition * EmpStrength * distFactor;
|
||||
}
|
||||
|
||||
var lightComponent = item.GetComponent<LightComponent>();
|
||||
if (lightComponent != null)
|
||||
{
|
||||
lightComponent.TemporaryFlickerTimer = Math.Min(EmpStrength * distFactor, 10.0f);
|
||||
}
|
||||
|
||||
//discharge batteries
|
||||
var powerContainer = item.GetComponent<PowerContainer>();
|
||||
if (powerContainer != null)
|
||||
|
||||
@@ -860,6 +860,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++)
|
||||
@@ -1099,6 +1100,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);
|
||||
@@ -3176,7 +3178,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;
|
||||
}
|
||||
@@ -3232,10 +3234,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())
|
||||
{
|
||||
@@ -3288,10 +3289,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);
|
||||
@@ -3334,14 +3341,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);
|
||||
|
||||
@@ -171,7 +171,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,7 +188,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
int endLocationindex = element.GetAttributeInt("endlocation", -1);
|
||||
if (endLocationindex > 0 && endLocationindex < Locations.Count)
|
||||
if (endLocationindex >= 0 && endLocationindex < Locations.Count)
|
||||
{
|
||||
EndLocation = Locations[endLocationindex];
|
||||
}
|
||||
|
||||
@@ -174,6 +174,9 @@ namespace Barotrauma
|
||||
|
||||
public abstract Sprite Sprite { get; }
|
||||
|
||||
public virtual bool CanSpriteFlipX { get; } = false;
|
||||
public virtual bool CanSpriteFlipY { get; } = false;
|
||||
|
||||
public abstract string OriginalName { get; }
|
||||
|
||||
public abstract LocalizedString Name { get; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -20,8 +20,8 @@ namespace Barotrauma
|
||||
|
||||
public readonly ContentXElement ConfigElement;
|
||||
|
||||
public readonly bool CanSpriteFlipX;
|
||||
public readonly bool CanSpriteFlipY;
|
||||
public override bool CanSpriteFlipX { get; }
|
||||
public override bool CanSpriteFlipY { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If null, the orientation is determined automatically based on the dimensions of the structure instances
|
||||
|
||||
@@ -1310,7 +1310,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;
|
||||
@@ -1381,65 +1381,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;
|
||||
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)
|
||||
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)
|
||||
@@ -1481,7 +1481,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")))
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace Barotrauma
|
||||
get { return submarine; }
|
||||
}
|
||||
|
||||
public SubmarineBody(Submarine sub, bool showWarningMessages = true)
|
||||
public SubmarineBody(Submarine sub, bool showErrorMessages = true)
|
||||
{
|
||||
this.submarine = sub;
|
||||
|
||||
@@ -119,9 +119,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
|
||||
|
||||
@@ -197,7 +197,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
public T GetVote<T>(VoteType voteType)
|
||||
{
|
||||
return (votes[(int)voteType] is T) ? (T)votes[(int)voteType] : default(T);
|
||||
return (votes[(int)voteType] is T t) ? t : default;
|
||||
}
|
||||
|
||||
public void SetVote(VoteType voteType, object value)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1093,7 +1093,7 @@ namespace Barotrauma.Networking
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int index = msg.ReadUInt16();
|
||||
if (index < 0 || index >= subList.Count) { continue; }
|
||||
if (index >= subList.Count) { continue; }
|
||||
string submarineName = subList[index].Name;
|
||||
HiddenSubs.Add(submarineName);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
public enum VoteState { None = 0, Started = 1, Running = 2, Passed = 3, Failed = 4 };
|
||||
|
||||
private IReadOnlyDictionary<T, int> GetVoteCounts<T>(VoteType voteType, IEnumerable<Client> voters)
|
||||
private static IReadOnlyDictionary<T, int> GetVoteCounts<T>(VoteType voteType, IEnumerable<Client> voters)
|
||||
{
|
||||
Dictionary<T, int> voteList = new Dictionary<T, int>();
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Barotrauma
|
||||
return voteList;
|
||||
}
|
||||
|
||||
public T HighestVoted<T>(VoteType voteType, List<Client> voters)
|
||||
public static T HighestVoted<T>(VoteType voteType, IEnumerable<Client> voters)
|
||||
{
|
||||
if (voteType == VoteType.Sub && !GameMain.NetworkMember.ServerSettings.AllowSubVoting) { return default; }
|
||||
if (voteType == VoteType.Mode && !GameMain.NetworkMember.ServerSettings.AllowModeVoting) { return default; }
|
||||
|
||||
@@ -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();
|
||||
@@ -895,6 +904,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;
|
||||
|
||||
@@ -1760,12 +1760,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)
|
||||
|
||||
@@ -257,10 +257,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)
|
||||
{
|
||||
@@ -329,6 +378,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)
|
||||
{
|
||||
@@ -341,6 +391,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)
|
||||
@@ -383,6 +434,11 @@ namespace Barotrauma
|
||||
maxLevels.Add(new UpgradeMaxLevelMod(subElement));
|
||||
break;
|
||||
}
|
||||
case "resourcecost":
|
||||
{
|
||||
resourceCosts.Add(new UpgradeResourceCost(subElement));
|
||||
break;
|
||||
}
|
||||
#if CLIENT
|
||||
case "decorativesprite":
|
||||
{
|
||||
@@ -414,6 +470,7 @@ namespace Barotrauma
|
||||
|
||||
this.targetProperties = targetProperties;
|
||||
MaxLevelsMods = maxLevels.ToImmutableArray();
|
||||
ResourceCosts = resourceCosts.ToImmutableHashSet();
|
||||
|
||||
upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray("categories", Array.Empty<Identifier>())?
|
||||
.ToImmutableHashSet() ?? ImmutableHashSet<Identifier>.Empty;
|
||||
@@ -456,6 +513,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)
|
||||
|
||||
@@ -212,8 +212,13 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return ShortRepresentation.GetHashCode(StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool operator ==(Md5Hash? a, Md5Hash? b)
|
||||
=> (a is null == b is null) && (a?.Equals(b) ?? true);
|
||||
=> Equals(a, b);
|
||||
|
||||
public static bool operator !=(Md5Hash? a, Md5Hash? b) => !(a == b);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace Barotrauma.Networking;
|
||||
*/
|
||||
|
||||
[NetworkSerialize]
|
||||
public readonly record struct Segment<T>(T Identifier, UInt16 Pointer) : INetSerializableStruct where T : struct;
|
||||
public readonly record struct Segment<T>(T Identifier, int Pointer) : INetSerializableStruct where T : struct;
|
||||
|
||||
readonly ref struct SegmentTableWriter<T> where T : struct
|
||||
{
|
||||
@@ -94,7 +94,7 @@ readonly ref struct SegmentTableWriter<T> where T : struct
|
||||
public static SegmentTableWriter<T> StartWriting(IWriteMessage msg)
|
||||
{
|
||||
var retVal = new SegmentTableWriter<T>(msg, msg.BitPosition);
|
||||
msg.WriteUInt16(0); //reserve space for the table pointer
|
||||
msg.WriteInt32(0); //reserve space for the table pointer
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@@ -104,28 +104,22 @@ readonly ref struct SegmentTableWriter<T> where T : struct
|
||||
{
|
||||
throw new InvalidOperationException($"Too many segments in SegmentTable<{typeof(T).Name}>");
|
||||
}
|
||||
|
||||
if (message.BitPosition - PointerLocation > UInt16.MaxValue)
|
||||
{
|
||||
throw new OverflowException(
|
||||
$"Too much data is being stored in SegmentTable<{typeof(T).Name}> ({segments.Count} segments)");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void StartNewSegment(T value)
|
||||
{
|
||||
ThrowOnInvalidState();
|
||||
segments.Add(new Segment<T>(value, (UInt16)(message.BitPosition-PointerLocation)));
|
||||
segments.Add(new Segment<T>(value, message.BitPosition - PointerLocation));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ThrowOnInvalidState();
|
||||
int tablePosition = message.BitPosition;
|
||||
|
||||
|
||||
//rewrite the table pointer now that we know where the table ends
|
||||
message.BitPosition = PointerLocation;
|
||||
message.WriteUInt16((UInt16)(tablePosition-PointerLocation));
|
||||
message.WriteInt32(tablePosition - PointerLocation);
|
||||
|
||||
//write the table
|
||||
message.BitPosition = tablePosition;
|
||||
@@ -274,7 +268,7 @@ readonly ref struct SegmentTableReader<T> where T : struct
|
||||
ExceptionHandler? exceptionHandler = null)
|
||||
{
|
||||
int pointerLocation = msg.BitPosition;
|
||||
int tablePointer = msg.ReadUInt16();
|
||||
int tablePointer = msg.ReadInt32();
|
||||
int tableLocation = pointerLocation + tablePointer;
|
||||
|
||||
int returnPosition = msg.BitPosition;
|
||||
|
||||
@@ -1,3 +1,104 @@
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
v0.21.1.0 (unstable)
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
|
||||
Changes:
|
||||
- Added translations for the submarine and character editors.
|
||||
- Some submarine upgrades cost materials in addition to money.
|
||||
- Miscellaneous fixes to all translations.
|
||||
- The "max missions" campaign setting is restricted to a maximum of 3.
|
||||
- Minor improvements to ragdoll animation when falling prone due to stun/unconsciousness/ragdolling.
|
||||
- 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.
|
||||
- Changed Alarm Buzzer sound to differentiate it from the diving suit warning beep.
|
||||
- Depth charge tweaks: require UEX, increase non-contained item explosion radius and damage, allow quality to affect explosion, increase pricing.
|
||||
- Reduceed Flak Cannon effectiveness (in particular Spreader Ammo and Explosive Ammo), against large enemies in particular.
|
||||
- Lights flicker when hit by EMP.
|
||||
- Large monsters no longer drop the loot when they die. This was implemented as a workaround to it being difficult to grab large monsters, but now they're much easier to grab - you can grab them anywhere near their main limb, instead of having to find the "origin" somewhere at the center of the monster.
|
||||
|
||||
Talents:
|
||||
- Fixed "Down With the Ship" sometimes having an incorrect description.
|
||||
- Fixed "Bounty Hunter" and "Logistics Expert" not giving 15% experience bonus.
|
||||
- Pet raptor eggs can be fabricated from raptor eggs of any size.
|
||||
- Fixed Flamer being craftable by anyone, even though it should only be possible with the "Pyromaniac" talent.
|
||||
- Fixed ability to move in an unpowered exosuit when you've got any speed buff.
|
||||
- Fixed "True Potential" and "Chonky Honks" not requiring clown power to work.
|
||||
- Reduced the chance of finding genetic materials with the "Gene Harvester" talent, fixed genetic materials sometimes being found on defense bots.
|
||||
- Fixed "Spec Ops" not working properly with shotguns and other weapons that fire multiple projectiles per shot. Only the first projectile that hit did double damage.
|
||||
- Fixed some parts of Hemulen and Venture not flooding properly.
|
||||
- "Mass production" talent only applies to items in the "material" category. Fixes e.g. recycle recipes sometimes allowing you to keep the original item.
|
||||
- Fixed "Lone Wolf" not giving any damage/stun resistance.
|
||||
- Fixed "Scrap Savant" having a 80% chance of finding scrap instead of 20%.
|
||||
- Fixed "Steady Tune" not doing what the description says (giving a constantly diminishing 7.5% resistance instead of 60%), made the talent give 100% immunity instead.
|
||||
- Fixed "Multifunctional" talent not giving a boost to crowbar damage.
|
||||
- Fixed inaccurate "Unstoppable Curiosity" and "Ph.D in Nuclear Physics" descriptions.
|
||||
|
||||
Multiplayer:
|
||||
- Better support for playing the MP campaign without a host or someone with campaign management permissions on the server:
|
||||
- You can vote to end the round in the campaign too.
|
||||
- Automatic restart works in the campaign mode too.
|
||||
- Anyone can manage salaries if there's no-one allowed to do it.
|
||||
- If there's no host or anyone with permissions in the server, anyone is able to setup a new campaign.
|
||||
- Campaign can be voted for when game mode voting is enabled.
|
||||
- Made ragdoll syncing more robust: reduces cases of teleportation/desync when manually ragdolling the character in multiplayer.
|
||||
- Fixed rejoining clients not regaining control of their character even if the character is still alive, if the client's IP address has changed. A respawn would also not trigger.
|
||||
- Fixed "failed to write a network message, too much data is being stored in SegmentTable" errors that could occur in various situations: for example when the host has a large ban list, lots of submarines, and when rewiring a device with lots of connections.
|
||||
- Fixed "Create new character" button not appearing in the tab menu when dead or spectating.
|
||||
- Fixed clients not entering the server lobby if they accept a server invite during a single player round or tutorial.
|
||||
- Fixed inability to join IPv4 servers when IPv6 is disabled.
|
||||
- Fixed hidden submarine list sometimes desyncing if you have specific custom submarines.
|
||||
|
||||
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.
|
||||
- Fixed ragdolling affecting the character's velocity, allowing it to be used as a way to avoid fall damage.
|
||||
- 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.
|
||||
- Fixed "save as item assembly" and "snap to grid" buttons taking cursor focus in the sub editor even when they're not visible.
|
||||
- Fixed inability to launch custom dedicated server executables from the main menu on mac and linux.
|
||||
- Fixed inability to drag and drop items from the entity list to small containers (such as battery charging docks) in the sub editor.
|
||||
- Fixed item condition not decreasing client-side if the condition decreases very slowly: for example when using a thorium rod with the "Cruisin'" talent.
|
||||
- Fixed flares still emitting light after running out.
|
||||
- Fixed Electrical Discharge Coil preview not working in the sub editor.
|
||||
- Fixed alien flares not activating when clicking LMB.
|
||||
- Fixed crawler's arms getting broken when the character flips in water.
|
||||
- Fixed the recycle recipe of flak explosive ammo.
|
||||
- Fixed misaligned shells in wrecked railgun shell rack.
|
||||
- Fixed misaligned light component light sprite.
|
||||
- Fixed crashing if the select audio device is disconnected while in the initial loading screen.
|
||||
- Fixed inability to sell genetic materials that aren't 100% refined.
|
||||
- Fixed liquid oxygenite exploding too easily.
|
||||
|
||||
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 and Projectile 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".
|
||||
- The use target can now be a character, an item, or a structure, depending on the context. This allows effects that weren't previously possible, but due to it we'll now need to introduce some restrictions in the definitions in some cases. For example, we might want to use a conditional to check whether the target is of the right type, before triggering the status effect (<Conditional entitytype="eq Character"/>).
|
||||
- Added a new attribute for the MeleeWeapon component, "HitOnlyCharacters", which can be used for ignoring the hits to walls and items entirely.
|
||||
- Due to the changes, some status effects that previously worked, might now need the "AllowWhenBroken" set to true in the definition to keep them working as they used to. So e.g. the "OnImpact" doesn't work anymore on your custom explosive, try that.
|
||||
- Fixed crashing when trying to place a wreck with no hulls in a level.
|
||||
- Fixed mod descriptions getting truncated to 255 characters when selecting an already-published item in the Mods menu.
|
||||
- Fixed HMG's requiring hmgmagazine instead of any item with the type "hmgammo", making the use of modded ammo impossible without overriding the gun too.
|
||||
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
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
|
||||
---------------------------------------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user