Unstable 1.2.4.0

This commit is contained in:
Markus Isberg
2023-11-30 13:53:00 +02:00
parent 8a2e2ea0ae
commit fb5ea537bf
210 changed files with 4201 additions and 1283 deletions
@@ -91,6 +91,11 @@ namespace Barotrauma
private IEnumerable<Hull> visibleHulls;
private float hullVisibilityTimer;
const float hullVisibilityInterval = 0.5f;
/// <summary>
/// Returns hulls that are visible to the character, including the current hull.
/// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls.
/// </summary>
public IEnumerable<Hull> VisibleHulls
{
get
@@ -353,7 +358,7 @@ namespace Barotrauma
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, int? unequipMax = null)
{
var inventory = parentItem.OwnInventory;
if (inventory == null) { return; }
if (inventory == null || !inventory.Container.DrawInventory) { return; }
int removed = 0;
if (predicate == null || inventory.AllItems.Any(predicate))
{
@@ -262,7 +262,7 @@ namespace Barotrauma
if (aiElements.Count == 0)
{
DebugConsole.ThrowError("Error in file \"" + c.Params.File + "\" - no AI element found.",
DebugConsole.ThrowError("Error in file \"" + c.Params.File.Path + "\" - no AI element found.",
contentPackage: c.Prefab?.ContentPackage);
outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, false, false);
@@ -312,7 +312,7 @@ namespace Barotrauma
}
ReevaluateAttacks();
outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors);
insideSteering = new IndoorsSteeringManager(this, AIParams.CanOpenDoors, canAttackDoors);
steeringManager = outsideSteering;
State = AIState.Idle;
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
@@ -322,6 +322,10 @@ namespace Barotrauma
}
private CharacterParams.AIParams _aiParams;
/// <summary>
/// Shorthand for <see cref="Character.Params.AI"/> with null checking.
/// </summary>
/// <returns><see cref="Character.Params.AI"/> or an empty params. Does not return nulls.</returns>
public CharacterParams.AIParams AIParams
{
get
@@ -565,7 +569,7 @@ namespace Barotrauma
}
}
if (Character.Params.UsePathFinding && Character.Params.AI.UsePathFindingToGetInside && AIParams.CanOpenDoors)
if (Character.Params.UsePathFinding && AIParams.UsePathFindingToGetInside && AIParams.CanOpenDoors)
{
// Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk).
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
@@ -3097,7 +3101,10 @@ namespace Barotrauma
break;
}
valueModifier *= targetMemory.Priority / (float)Math.Sqrt(dist);
valueModifier *=
targetMemory.Priority /
//sqrt = the further the target is, the less the distance matters
MathF.Sqrt(dist);
if (valueModifier > targetValue)
{
@@ -685,8 +685,8 @@ namespace Barotrauma
}
if (removeDivingSuit)
{
var divingSuit = Character.Inventory.FindItemByTag(Tags.HeavyDivingGear);
if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors))
var divingSuit = Character.Inventory.FindEquippedItemByTag(Tags.HeavyDivingGear);
if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors) && divingSuit.IsInteractable(Character))
{
if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
@@ -727,54 +727,51 @@ namespace Barotrauma
}
}
if (takeMaskOff)
{
if (Character.HasEquippedItem(Tags.LightDivingGear))
{
var mask = Character.Inventory.FindEquippedItemByTag(Tags.LightDivingGear);
if (mask != null)
{
var mask = Character.Inventory.FindItemByTag(Tags.LightDivingGear);
if (mask != null)
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
}
}
else
{
ReequipUnequipped();
}
}
}
else
{
ReequipUnequipped();
}
}
}
}
}
@@ -784,13 +781,11 @@ namespace Barotrauma
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
{
for (int i = 0; i < 2; i++)
foreach (Item item in Character.HeldItems)
{
var hand = i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand;
Item item = Character.Inventory.GetItemInLimbSlot(hand);
if (item == null) { continue; }
if (item == null || !item.IsInteractable(Character)) { continue; }
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID )
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, CharacterInventory.AnySlot) && Character.Submarine?.TeamID == Character.TeamID)
{
if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Bag })) { continue; }
findItemState = FindItemState.OtherItem;
@@ -1389,7 +1384,10 @@ namespace Barotrauma
// Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist.
continue;
}
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
bool isWitnessing =
otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) ||
otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) ||
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
if (!isWitnessing)
{
//if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
@@ -1754,11 +1752,11 @@ namespace Barotrauma
if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead ||
otherCharacter.Info?.Job == null ||
otherCharacter.AIController is not HumanAIController otherHumanAI ||
!otherHumanAI.VisibleHulls.Contains(character.CurrentHull))
Vector2.DistanceSquared(otherCharacter.WorldPosition, character.WorldPosition) > 1000.0f * 1000.0f)
{
continue;
}
if (!otherCharacter.CanSeeTarget(character)) { continue; }
if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; }
if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); }
float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
@@ -1840,13 +1838,13 @@ namespace Barotrauma
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f ||
otherCharacter.Info?.Job == null || !(otherCharacter.AIController is HumanAIController otherHumanAI) ||
!otherHumanAI.VisibleHulls.Contains(thief.CurrentHull))
otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI ||
Vector2.DistanceSquared(otherCharacter.WorldPosition, thief.WorldPosition) > 1000.0f * 1000.0f)
{
continue;
}
//if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; }
if (!otherCharacter.CanSeeTarget(thief)) { continue; }
if (!otherCharacter.CanSeeTarget(thief, seeThroughWindows: true)) { continue; }
// Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding
// -> allow them to use the emergency items
if (thief.Submarine != null)
@@ -1070,7 +1070,8 @@ namespace Barotrauma
{
// Try reload ammunition from inventory
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio);
Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
Item ammunition = character.Inventory.FindItem(i =>
i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i) && i.IsInteractable(character), recursive: true);
if (ammunition != null)
{
var container = Weapon.GetComponent<ItemContainer>();
@@ -1089,6 +1090,9 @@ namespace Barotrauma
}
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
{
// Inventory not drawn = it's not interactable
// If the weapon is empty and the inventory is inaccessible, it can't be reloaded
if (!Weapon.OwnInventory.Container.DrawInventory) { return false; }
SeekAmmunition(ammunitionIdentifiers);
}
return false;
@@ -14,7 +14,7 @@ namespace Barotrauma
private int escapeProgress;
private bool isBeingWatched;
private bool shouldSwitchTeams;
private readonly bool shouldSwitchTeams;
const string EscapeTeamChangeIdentifier = "escape";
@@ -88,10 +88,12 @@ namespace Barotrauma
escapeProgress += Rand.Range(2, 5);
if (escapeProgress > 15)
{
Item handcuffs = character.Inventory.FindItemByTag(Tags.HandLockerItem);
if (handcuffs != null)
foreach (var it in character.HeldItems)
{
handcuffs.Drop(character);
if (it.HasTag(Tags.HandLockerItem) && it.IsInteractable(character))
{
it.Drop(character);
}
}
}
escapeTimer = EscapeIntervalTimer * Rand.Range(0.75f, 1.25f);
@@ -114,7 +114,7 @@ namespace Barotrauma
if (!IsValidTarget(target, character)) { continue; }
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
if (target.Inventory.AllItems.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing && target.HasEquippedItem(it)) &&
character.CanSeeTarget(target))
character.CanSeeTarget(target, seeThroughWindows: true))
{
AIObjectiveCheckStolenItems? existingObjective =
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.TargetCharacter == target);
@@ -120,7 +120,7 @@ namespace Barotrauma
// The intention behind this is to reduce unnecessary path finding calls in cases where the bot can't find a path.
timerMargin += 0.5f;
timerMargin = Math.Min(timerMargin, newTargetIntervalMin);
newTargetTimer = Math.Min(newTargetTimer, timerMargin);
newTargetTimer = Math.Max(newTargetTimer, timerMargin);
}
private void SetTargetTimerHigh()
@@ -664,6 +664,13 @@ namespace Barotrauma
WallSectionIndex = wallSectionIndex ?? other.WallSectionIndex;
UseController = useController ?? other.UseController;
#if DEBUG
if (UseController && ConnectedController == null)
{
DebugConsole.ThrowError($"AI: Created an Order {Identifier} that's set to use a Controller, but a Controller was not specified.\n{Environment.StackTrace.CleanupStackTrace()}");
}
#endif
}
public Order WithOption(Identifier option)
@@ -713,7 +720,12 @@ namespace Barotrauma
public Order WithItemComponent(Item item, ItemComponent component = null)
{
return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item));
Controller controller = null;
if (UseController)
{
controller = item?.FindController(tags: ControllerTags);
}
return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item), connectedController: controller);
}
public Order WithWallSection(Structure wall, int? sectionIndex)
@@ -10,6 +10,17 @@ namespace Barotrauma
{
class HumanoidAnimController : AnimController
{
private const float SteepestWalkableSlopeAngleDegrees = 50f;
private const float SlowlyWalkableSlopeAngleDegrees = 30f;
private static readonly float SteepestWalkableSlopeNormalX =
MathF.Sin(MathHelper.ToRadians(SteepestWalkableSlopeAngleDegrees));
private static readonly float SlowlyWalkableSlopeNormalX =
MathF.Sin(MathHelper.ToRadians(SlowlyWalkableSlopeAngleDegrees));
private const float MaxSpeedOnStairs = 1.7f;
private const float SteepSlopePushMagnitude = MaxSpeedOnStairs;
public override RagdollParams RagdollParams
{
get { return HumanRagdollParams; }
@@ -501,10 +512,14 @@ namespace Barotrauma
Limb leftLeg = GetLimb(LimbType.LeftLeg);
Limb rightLeg = GetLimb(LimbType.RightLeg);
bool onSlopeThatMakesSlow = Math.Abs(floorNormal.X) > SlowlyWalkableSlopeNormalX;
bool slowedDownBySlope = onSlopeThatMakesSlow && Math.Sign(floorNormal.X) == -Math.Sign(TargetMovement.X);
bool onSlopeTooSteepToClimb = Math.Abs(floorNormal.X) > SteepestWalkableSlopeNormalX;
float walkCycleMultiplier = 1.0f;
if (Stairs != null)
if (Stairs != null || slowedDownBySlope)
{
TargetMovement = new Vector2(MathHelper.Clamp(TargetMovement.X, -1.7f, 1.7f), TargetMovement.Y);
TargetMovement = new Vector2(MathHelper.Clamp(TargetMovement.X, -MaxSpeedOnStairs, MaxSpeedOnStairs), TargetMovement.Y);
walkCycleMultiplier *= 1.5f;
}
@@ -586,6 +601,15 @@ namespace Barotrauma
bool movingHorizontally = !MathUtils.NearlyEqual(TargetMovement.X, 0.0f);
if (Stairs == null && onSlopeTooSteepToClimb)
{
if (Math.Sign(targetMovement.X) != Math.Sign(floorNormal.X))
{
targetMovement.X = Math.Sign(floorNormal.X) * SteepSlopePushMagnitude;
movement = targetMovement;
}
}
if (Stairs != null || onSlope)
{
torso.PullJointWorldAnchorB = new Vector2(
@@ -764,7 +788,7 @@ namespace Barotrauma
{
footPos = new Vector2(colliderPos.X + stepSize.X * i * 0.2f, colliderPos.Y - 0.1f);
}
if (Stairs == null)
if (Stairs == null && !onSlopeThatMakesSlow)
{
footPos.Y = Math.Max(Math.Min(FloorY, footPos.Y + 0.5f), footPos.Y);
}
@@ -1536,7 +1560,7 @@ namespace Barotrauma
target.CharacterHealth.CalculateVitality();
if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f)
{
character.Info?.IncreaseSkillLevel("medical".ToIdentifier(), SkillSettings.Current.SkillIncreasePerCprRevive);
character.Info?.ApplySkillGain(Tags.MedicalSkill, SkillSettings.Current.SkillIncreasePerCprRevive);
SteamAchievementManager.OnCharacterRevived(target, character);
lastReviveTime = (float)Timing.TotalTime;
#if SERVER
@@ -1741,7 +1765,23 @@ namespace Barotrauma
targetAnchor += target.Submarine.SimPosition;
}
}
pullLimb.PullJointWorldAnchorB = pullLimbAnchor;
if (Vector2.DistanceSquared(pullLimb.PullJointWorldAnchorA, pullLimbAnchor) > 50.0f * 50.0f)
{
//there's a similar error check in the PullJointWorldAnchorB setter, but we seem to be getting quite a lot of
//errors specifically from this method, so let's use a more consistent error message here to prevent clogging GA with
//different error messages that all include a different coordinate
string errorMsg =
$"Attempted to move the anchor B of a limb's pull joint extremely far from the limb in {nameof(DragCharacter)}. " +
$"Character in sub: {character.Submarine != null}, target in sub: {target.Submarine != null}.";
GameAnalyticsManager.AddErrorEventOnce("DragCharacter:PullJointTooFar", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
}
else
{
pullLimb.PullJointWorldAnchorB = pullLimbAnchor;
}
}
else
{
@@ -572,6 +572,10 @@ namespace Barotrauma
public void AddJoint(JointParams jointParams)
{
if (!checkLimbIndex(jointParams.Limb2, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2"))
{
return;
}
LimbJoint joint = new LimbJoint(Limbs[jointParams.Limb1], Limbs[jointParams.Limb2], jointParams, this);
GameMain.World.Add(joint.Joint);
for (int i = 0; i < LimbJoints.Length; i++)
@@ -582,6 +586,21 @@ namespace Barotrauma
}
Array.Resize(ref LimbJoints, LimbJoints.Length + 1);
LimbJoints[LimbJoints.Length - 1] = joint;
bool checkLimbIndex(int index, string debugName)
{
if (index < 0 || index >= limbs.Length)
{
string errorMsg = $"Failed to add a joint to character {character.Name}. {debugName} out of bounds (index: {index}, limbs: {limbs.Length}.";
DebugConsole.ThrowError(errorMsg, contentPackage: jointParams.Element?.ContentPackage);
if (jointParams.Element?.ContentPackage == GameMain.VanillaContent)
{
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.AddJoint:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
}
return false;
}
return true;
}
}
protected void AddLimb(LimbParams limbParams)
@@ -668,6 +687,13 @@ namespace Barotrauma
}
}
private enum LimbStairCollisionResponse
{
DontClimbStairs,
ClimbWithoutLimbCollision,
ClimbWithLimbCollision
}
public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact)
{
if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) { return false; }
@@ -710,37 +736,53 @@ namespace Barotrauma
}
else if (structure.StairDirection != Direction.None)
{
Stairs = null;
//don't collider with stairs if
//1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards
float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10);
if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return false; }
//2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards
if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return false; }
//3. collided with the stairs from below
if (contact.Manifold.LocalNormal.Y < 0.0f) { return false; }
//4. contact points is above the bottom half of the collider
contact.GetWorldManifold(out Vector2 normal, out FarseerPhysics.Common.FixedArray2<Vector2> points);
if (points[0].Y > Collider.SimPosition.Y) { return false; }
//5. in water
if (inWater && targetMovement.Y < 0.5f) { return false; }
//---------------
//set stairs to that of the one dragging us
if (character.SelectedBy != null)
{
Stairs = character.SelectedBy.AnimController.Stairs;
}
else
Stairs = structure;
{
var collisionResponse = handleLimbStairCollision();
if (collisionResponse == LimbStairCollisionResponse.ClimbWithLimbCollision)
{
Stairs = structure;
}
else
{
if (collisionResponse == LimbStairCollisionResponse.DontClimbStairs) { Stairs = null; }
if (Stairs == null)
return false;
return false;
}
}
LimbStairCollisionResponse handleLimbStairCollision()
{
//don't collide with stairs if
//1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards
float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10);
if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; }
//2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards
if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return LimbStairCollisionResponse.DontClimbStairs; }
//3. collided with the stairs from below
if (contact.Manifold.LocalNormal.Y < 0.0f)
{
return Stairs != structure
? LimbStairCollisionResponse.DontClimbStairs
: LimbStairCollisionResponse.ClimbWithoutLimbCollision;
}
//4. contact points is above the bottom half of the collider
contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2<Vector2> points);
if (points[0].Y > Collider.SimPosition.Y) { return LimbStairCollisionResponse.DontClimbStairs; }
//5. in water
if (inWater && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; }
return LimbStairCollisionResponse.ClimbWithLimbCollision;
}
}
lock (impactQueue)
@@ -1335,17 +1377,28 @@ namespace Barotrauma
if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance)
{
float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor;
if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f)
const float LevitationSpeedMultiplier = 5f;
// If the character is walking down a slope, target a position that moves along it
float slopePull = 0f;
if (floorNormal.Y is > 0f and < 1f
&& Math.Sign(movement.X) == Math.Sign(floorNormal.X))
{
if (Stairs != null)
slopePull = Math.Abs(movement.X * floorNormal.X / floorNormal.Y) / LevitationSpeedMultiplier;
}
if (Math.Abs(Collider.SimPosition.Y - targetY - slopePull) > 0.01f)
{
float yVelocity = (targetY - Collider.SimPosition.Y) * LevitationSpeedMultiplier;
if (Stairs != null && targetY < Collider.SimPosition.Y)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X,
(targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f);
}
else
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f);
yVelocity = Math.Sign(yVelocity);
}
yVelocity -= slopePull * LevitationSpeedMultiplier;
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, yVelocity);
}
}
else
@@ -1593,10 +1646,10 @@ namespace Barotrauma
// Force check floor y at least once a second so that we'll drop through gaps that we are standing upon.
private const float FloorYStaleTime = 1;
private float floorYCheckTimer;
private void RefreshFloorY(float deltaTime, Limb refLimb = null, bool ignoreStairs = false)
private void RefreshFloorY(float deltaTime, bool ignoreStairs = false)
{
floorYCheckTimer -= deltaTime;
PhysicsBody refBody = refLimb == null ? Collider : refLimb.body;
PhysicsBody refBody = Collider;
if (floorYCheckTimer < 0 ||
lastFloorCheckIgnoreStairs != ignoreStairs ||
lastFloorCheckIgnorePlatforms != IgnorePlatforms ||
@@ -1621,7 +1674,7 @@ namespace Barotrauma
if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) { height = Math.Max(height, HeadPosition.Value); }
if (TorsoPosition.HasValue && MathUtils.IsValid(TorsoPosition.Value)) { height = Math.Max(height, TorsoPosition.Value); }
Vector2 rayEnd = rayStart - new Vector2(0.0f, height);
Vector2 rayEnd = rayStart - new Vector2(0.0f, height * 2f);
Vector2 colliderBottomDisplay = ConvertUnits.ToDisplayUnits(GetColliderBottom());
Fixture standOnFloorFixture = null;
@@ -1700,7 +1753,7 @@ namespace Barotrauma
}
}
if (closestFraction == 1) //raycast didn't hit anything
if (closestFraction >= 1) //raycast didn't hit anything
{
floorNormal = Vector2.UnitY;
if (CurrentHull == null)
@@ -67,10 +67,18 @@ namespace Barotrauma
}
foreach (var item in HeldItems)
{
if (item.body != null)
if (item.body == null) { continue; }
if (!enabled)
{
item.body.Enabled = enabled;
item.body.Enabled = false;
}
else if (item.GetComponent<Holdable>() is { IsActive: true })
{
//held items includes all items in hand slots
//we only want to enable the physics body if it's an actual holdable item, not e.g. a wearable item like handcuffs
item.body.Enabled = true;
}
}
AnimController.Collider.Enabled = value;
}
@@ -936,10 +944,16 @@ namespace Barotrauma
{
var prevSelectedItem = _selectedItem;
_selectedItem = value;
if (value is not null)
{
CheckTalents(AbilityEffectType.OnItemSelected, new AbilityItemSelected(value));
}
#if CLIENT
HintManager.OnSetSelectedItem(this, prevSelectedItem, _selectedItem);
if (Controlled == this)
{
_selectedItem?.GetComponent<Fabricator>()?.RefreshSelectedItem();
if (_selectedItem == null)
{
GameMain.GameSession?.CrewManager?.ResetCrewList();
@@ -1694,31 +1708,21 @@ namespace Barotrauma
info.Job?.GiveJobItems(this, spawnPoint);
}
public void GiveIdCardTags(WayPoint spawnPoint, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false)
public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false)
{
GiveIdCardTags(spawnPoint.ToEnumerable(), requireSpawnPointTagsNotGiven, createNetworkEvent);
}
public void GiveIdCardTags(IEnumerable<WayPoint> spawnPoints, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false)
{
if (info?.Job == null || spawnPoints == null) { return; }
if (info?.Job == null || spawnPoint == null) { return; }
foreach (Item item in Inventory.AllItems)
{
if (item?.GetComponent<IdCard>() is not IdCard idCard) { continue; }
if (requireSpawnPointTagsNotGiven)
var idCard = item?.GetComponent<IdCard>();
if (idCard == null) { continue; }
//if the card belongs to someone else, don't add any tags.
//otherwise you can gain access to places you shouldn't by temporarily giving the card to someone (e.g. a captain bot) at the end of the round
if (idCard.OwnerName != info.Name) { continue; }
foreach (string s in spawnPoint.IdCardTags)
{
if (idCard.SpawnPointTagsGiven) { continue; }
item.AddTag(s);
}
foreach (var spawnPoint in spawnPoints)
{
foreach (string s in spawnPoint.IdCardTags)
{
item.AddTag(s);
}
}
idCard.SpawnPointTagsGiven = true;
if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item));
@@ -2303,40 +2307,40 @@ namespace Barotrauma
return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb;
}
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool checkFacing = false)
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool seeThroughWindows = false, bool checkFacing = false)
{
seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb();
if (target is Character targetCharacter)
{
return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing);
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
}
else
{
return CheckVisibility(target, seeingEntity, checkFacing);
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
}
}
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false)
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
{
if (seeingEntity is Character seeingCharacter)
{
return seeingCharacter.CanSeeTarget(target, checkFacing: checkFacing);
return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing);
}
if (target is Character targetCharacter)
{
return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing);
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
}
else
{
return CheckVisibility(target, seeingEntity, checkFacing);
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
}
}
private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool checkFacing = false)
private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null || target.Removed) { return false; }
if (CheckVisibility(target, seeingEntity, checkFacing)) { return true; }
if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
if (!target.AnimController.SimplePhysicsEnabled)
{
//find the limbs that are furthest from the target's position (from the viewer's point of view)
@@ -2365,13 +2369,13 @@ namespace Barotrauma
continue;
}
}
if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, checkFacing)) { return true; }
if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, checkFacing)) { return true; }
if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
}
return false;
}
private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false)
private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = true, bool checkFacing = false)
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null) { return false; }
@@ -2382,39 +2386,37 @@ namespace Barotrauma
{
if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; }
}
Body closestBody;
//both inside the same sub (or both outside)
//OR the we're inside, the other character outside
if (target.Submarine == seeingEntity.Submarine || target.Submarine == null)
{
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null;
}
//we're outside, the other character inside
else if (seeingEntity.Submarine == null)
{
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
}
//both inside different subs
else
{
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
if (!IsBlocking(closestBody))
{
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
}
return
Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null &&
Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
}
return !IsBlocking(closestBody);
bool IsBlocking(Body body)
bool IsBlocking(Fixture f)
{
var body = f.Body;
if (body == null) { return false; }
if (body.UserData is Structure wall && wall.CastShadow)
if (body.UserData is Structure wall)
{
if (!wall.CastShadow && seeThroughWindows) { return false; }
return wall != target;
}
else if (body.UserData is Item item)
{
if (item.GetComponent<Door>() is { HasWindow: true } door)
if (item.GetComponent<Door>() is { HasWindow: true } door && seeThroughWindows)
{
if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; }
}
@@ -2513,9 +2515,21 @@ namespace Barotrauma
if (inventory.Owner is Item item)
{
if (!CanInteractWith(item) && !item.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; }
ItemContainer container = item.GetComponents<ItemContainer>().FirstOrDefault(ic => ic.Inventory == inventory);
if (container != null && !container.HasRequiredItems(this, addMessage: false)) { return false; }
if (!CanInteractWith(item))
{
//could be simplified with LINQ, but that'd require capturing variables which we shouldn't do in a method that's called as frequently as this
foreach (var linkedEntity in item.linkedTo)
{
if (linkedEntity is Item linkedItem && linkedItem.DisplaySideBySideWhenLinked && CanInteractWith(linkedItem)) { return true; }
}
return false;
}
ItemContainer container = (inventory as ItemInventory)?.Container;
if (container != null)
{
if (!container.HasRequiredItems(this, addMessage: false)) { return false; }
if (!container.DrawInventory) { return false; }
}
}
return true;
}
@@ -3584,6 +3598,8 @@ namespace Barotrauma
private void Despawn(bool createNetworkEvents = true)
{
if (!EnableDespawn) { return; }
Identifier despawnContainerId =
IsHuman ?
"despawncontainer".ToIdentifier() :
@@ -3665,10 +3681,12 @@ namespace Barotrauma
float massFactor = (float)Math.Sqrt(Mass / 20);
float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange);
float newRange = MathHelper.SmoothStep(aiTarget.SightRange, targetRange, deltaTime * aiTargetChangeSpeed);
newRange *= 1.0f + GetStatValue(StatTypes.SightRangeMultiplier);
if (!float.IsNaN(newRange))
{
aiTarget.SightRange = newRange;
}
}
private void UpdateSoundRange(float deltaTime)
@@ -3683,6 +3701,7 @@ namespace Barotrauma
float massFactor = (float)Math.Sqrt(Mass / 10);
float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange);
float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed);
newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
if (!float.IsNaN(newRange))
{
aiTarget.SoundRange = newRange;
@@ -4340,19 +4359,16 @@ namespace Barotrauma
}
if (medicalDamage > 0)
{
IncreaseSkillLevel("medical".ToIdentifier(), medicalDamage);
IncreaseSkillLevel(Tags.MedicalSkill, medicalDamage);
}
if (weaponDamage > 0)
{
IncreaseSkillLevel("weapons".ToIdentifier(), weaponDamage);
IncreaseSkillLevel(Tags.WeaponsSkill, weaponDamage);
}
void IncreaseSkillLevel(Identifier skill, float damage)
{
float attackerSkillLevel = attacker.GetSkillLevel(skill);
// The formula is too generous on low skill levels, hence the minimum divider.
float minSkillDivider = 15f;
attacker.Info?.IncreaseSkillLevel(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, minSkillDivider));
attacker.Info?.ApplySkillGain(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage, false, 1f);
}
}
@@ -4366,12 +4382,10 @@ namespace Barotrauma
{
medicalGain += affliction.Strength * affliction.Prefab.MedicalSkillGain;
}
if (medicalGain <= 0) { return; }
Identifier skill = new Identifier("medical");
float attackerSkillLevel = healer.GetSkillLevel(skill);
// The formula is too generous on low skill levels, hence the minimum divider.
float minSkillDivider = 15f;
healer.Info?.IncreaseSkillLevel(skill, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, minSkillDivider));
if (medicalGain > 0)
{
healer.Info?.ApplySkillGain(Tags.MedicalItem, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed);
}
}
/// <summary>
@@ -5005,8 +5019,10 @@ namespace Barotrauma
private readonly List<Hull> visibleHulls = new List<Hull>();
private readonly HashSet<Hull> tempList = new HashSet<Hull>();
/// <summary>
/// Returns hulls that are visible to the player, including the current hull.
/// Returns hulls that are visible to the character, including the current hull.
/// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls.
/// Can be heavy if used every frame.
/// </summary>
public List<Hull> GetVisibleHulls()
@@ -5021,7 +5037,7 @@ namespace Barotrauma
foreach (var hull in adjacentHulls)
{
if (hull.ConnectedGaps.Any(g =>
(g.Open > 0.9f || g.ConnectedDoor is { HasWindow: true }) &&
g.Open > 0.9f &&
g.linkedTo.Contains(CurrentHull) &&
Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2)))
{
@@ -5041,7 +5057,7 @@ namespace Barotrauma
else
{
if (h.ConnectedGaps.Any(g =>
(g.Open > 0.9f || g.ConnectedDoor is { HasWindow: true }) &&
g.Open > 0.9f &&
Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2) &&
CanSeeTarget(g)))
{
@@ -5582,4 +5598,12 @@ namespace Barotrauma
public Character Character { get; set; }
}
class AbilityItemSelected : AbilityObject, IAbilityItem
{
public AbilityItemSelected(Item item)
{
Item = item;
}
public Item Item { get; set; }
}
}
@@ -1214,6 +1214,21 @@ namespace Barotrauma
return (int)(salary * Job.Prefab.PriceMultiplier);
}
/// <summary>
/// Increases the characters skill at a rate proportional to their current skill.
/// If you want to increase the skill level by a specific amount instead, use <see cref="IncreaseSkillLevel"/>
/// </summary>
public void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility = false, float maxGain = 2f)
{
float skillLevel = Job.GetSkillLevel(skillIdentifier);
// The formula is too generous on low skill levels, hence the minimum divider.
float skillDivider = MathF.Pow(Math.Max(skillLevel, 15f), SkillSettings.Current.SkillIncreaseExponent);
IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility);
}
/// <summary>
/// Increase the skill by a specific amount. Talents may affect the actual, final skill increase.
/// </summary>
public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false)
{
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
@@ -1222,9 +1237,7 @@ namespace Barotrauma
{
increase *= SkillSettings.Current.AssistantSkillIncreaseMultiplier;
}
increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed);
increase = GetSkillSpecificGain(increase, skillIdentifier);
float prevLevel = Job.GetSkillLevel(skillIdentifier);
@@ -1902,13 +1915,30 @@ namespace Barotrauma
}
}
/// <summary>
/// Get the combined stat value of the identifier "all" and the specified identifier.
/// </summary>
/// <remarks>
/// The "all" identifier works like the "any" identifier in outpost modules where it doesn't literally mean everything but
/// is an unique identifier that indicates that it should target everything. For example if we wanted to make a talent
/// that increases the fabrication quality of every single item we could use something like:
/// <CharacterAbilityGivePermanentStat stattype="IncreaseFabricationQuality" statidentifier="all" />
/// (Granted IncreaseFabricationQuality doesn't support the "all" identifier so if we need this in vanilla it needs to be implemented in code)
/// </remarks>
public float GetSavedStatValueWithAll(StatTypes statType, Identifier statIdentifier)
=> GetSavedStatValue(statType, Tags.StatIdentifierTargetAll) +
GetSavedStatValue(statType, statIdentifier);
public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier)
=> GetSavedStatValueWithBotsInMp(statType, statIdentifier, GameSession.GetSessionCrewCharacters(CharacterType.Bot));
public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier, IReadOnlyCollection<Character> bots)
{
float statValue = GetSavedStatValue(statType, statIdentifier);
if (GameMain.NetworkMember is null) { return statValue; }
foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot))
foreach (Character bot in bots)
{
int botStatValue = (int)bot.Info.GetSavedStatValue(statType, statIdentifier);
statValue = Math.Max(statValue, botStatValue);
@@ -1026,6 +1026,10 @@ namespace Barotrauma
}
}
/// <summary>
/// Removes all the effects of the prefab (including the sounds and other assets defined in them).
/// Note that you need to call LoadAllEffectsAndTreatmentSuitabilities before trying to use the affliction again!
/// </summary>
public static void ClearAllEffects()
{
Prefabs.ForEach(p => p.ClearEffects());
@@ -1313,6 +1313,8 @@ namespace Barotrauma
public void Remove()
{
RemoveProjSpecific();
afflictionsToRemove.Clear();
afflictionsToUpdate.Clear();
}
partial void RemoveProjSpecific();
@@ -136,6 +136,12 @@ namespace Barotrauma
public readonly List<ParticleParams> DamageEmitters = new List<ParticleParams>();
public readonly List<InventoryParams> Inventories = new List<InventoryParams>();
public HealthParams Health { get; private set; }
/// <summary>
/// Parameters for EnemyAIController. Not used by HumanAIController.
/// </summary>
/// <returns>
/// AIParams or null. Use <see cref="EnemyAIController.AIParams"/>, if you don't expect nulls.
/// </returns>
public AIParams AI { get; private set; }
public CharacterParams(CharacterFile file)
@@ -603,7 +609,8 @@ namespace Barotrauma
public void AddItem(string identifier = null)
{
identifier = identifier ?? "";
if (Element == null) { return; }
identifier ??= "";
var element = CreateElement("item", new XAttribute("identifier", identifier));
Element.Add(element);
var item = new InventoryItem(element, Character);
@@ -732,6 +739,11 @@ namespace Barotrauma
public bool TryAddNewTarget(Identifier tag, AIState state, float priority, out TargetParams targetParams)
{
if (Element == null)
{
targetParams = null;
return false;
}
var element = TargetParams.CreateNewElement(Character, tag, state, priority);
if (TryAddTarget(element, out targetParams))
{
@@ -100,6 +100,13 @@ namespace Barotrauma
set;
}
[Serialize(1.5f, IsPropertySaveable.Yes)]
public float SkillIncreaseExponent
{
get;
set;
}
public SkillSettings(XElement element, SkillSettingsFile file) : base(file, "SkillSettings".ToIdentifier())
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -11,6 +10,12 @@ namespace Barotrauma.Abilities
private readonly List<PropertyConditional> conditionals = new List<PropertyConditional>();
/// <summary>
/// If enabled, the conditional is checked on the target of the ability (e.g. the character that was killed if the effect type is OnKillCharacter).
/// Defaults to true, except in the case of <see cref="AbilityConditionHasPermanentStat"/>, which by default targets the character who has the talent.
/// </summary>
private readonly bool targetAbilityTarget = false;
public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
targetTypes = ParseTargetTypes(
@@ -25,30 +30,47 @@ namespace Barotrauma.Abilities
}
}
if (!targetTypes.Any() && !conditionals.Any())
//don't log this error if this is a subclass of AbilityConditionCharacter
//(in that case not having any conditionals here is ok)
if (!targetTypes.Any() && !conditionals.Any() && GetType() == typeof(AbilityConditionCharacter))
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character.",
contentPackage: conditionElement.ContentPackage);
}
targetAbilityTarget = conditionElement.GetAttributeBool(nameof(targetAbilityTarget), this is not AbilityConditionHasPermanentStat);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
public sealed override bool MatchesCondition()
{
if (abilityObject is IAbilityCharacter abilityCharacter)
//by default data-reliant conditions don't accept null, but in this case it's ok,
//because we can assume it's the character who has the talent
return MatchesCondition(abilityObject: null);
}
public sealed override bool MatchesCondition(AbilityObject abilityObject)
{
return invert ? !MatchesConditionSpecific(abilityObject) : MatchesConditionSpecific(abilityObject);
}
protected sealed override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
Character targetCharacter =
targetAbilityTarget ?
(abilityObject as IAbilityCharacter)?.Character ?? character :
character;
if (targetCharacter is null) { return false; }
if (!IsViableTarget(targetTypes, targetCharacter)) { return false; }
foreach (var conditional in conditionals)
{
if (abilityCharacter.Character is not Character character) { return false; }
if (!IsViableTarget(targetTypes, character)) { return false; }
foreach (var conditional in conditionals)
{
if (!conditional.Matches(character)) { return false; }
}
return true;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityCharacter));
return false;
if (!conditional.Matches(targetCharacter)) { return false; }
}
return MatchesCharacter(targetCharacter);
}
protected virtual bool MatchesCharacter(Character character)
{
return true;
}
}
}
@@ -1,6 +1,6 @@
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionData
internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionCharacter
{
private readonly Identifier identifier;
@@ -9,11 +9,9 @@ namespace Barotrauma.Abilities
identifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
protected override bool MatchesCharacter(Character character)
{
if (abilityObject is not IAbilityCharacter ability) { return false; }
return !ability.Character.MarkedAsLooted.Contains(identifier);
return character != null &&!character.MarkedAsLooted.Contains(identifier);
}
}
}
@@ -2,15 +2,13 @@
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionData
internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionCharacter
{
public AbilityConditionCharacterUnconcious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
protected override bool MatchesCharacter(Character character)
{
if (abilityObject is not IAbilityCharacter targetCharacter) { return false; }
return targetCharacter.Character.IsUnconscious;
return character is { IsUnconscious: true };
}
}
}
@@ -0,0 +1,19 @@
using Barotrauma.Items.Components;
namespace Barotrauma.Abilities
{
class AbilityConditionItemIsStatic : AbilityConditionData
{
public AbilityConditionItemIsStatic(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is IAbilityItem { Item: var item })
{
return item.GetComponent<Holdable>() is null && item.GetComponent<Wearable>() is null;
}
return false;
}
}
}
@@ -1,6 +1,8 @@
namespace Barotrauma.Abilities
using System;
namespace Barotrauma.Abilities
{
class AbilityConditionHasPermanentStat : AbilityConditionDataless
class AbilityConditionHasPermanentStat : AbilityConditionCharacter
{
private readonly Identifier statIdentifier;
private readonly StatTypes statType;
@@ -21,8 +23,13 @@
placeholder = conditionElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
}
protected override bool MatchesConditionSpecific()
protected override bool MatchesCharacter(Character character)
{
if (character?.Info == null)
{
DebugConsole.AddWarning($"Error in {nameof(AbilityConditionHasPermanentStat.MatchesCharacter)}: character {character} has no CharacterInfo. Are you trying to use the condition on a non-player character?\n{Environment.StackTrace.CleanupStackTrace()}");
return false;
}
Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier);
return character.Info.GetSavedStatValue(statType, identifier) >= min;
}
@@ -2,21 +2,18 @@
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionLowestLevel : AbilityConditionDataless
internal sealed class AbilityConditionLowestLevel : AbilityConditionCharacter
{
public AbilityConditionLowestLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific()
protected override bool MatchesCharacter(Character character)
{
int ownLevel = character.Info.GetCurrentLevel();
foreach (Character crew in GameSession.GetSessionCrewCharacters(CharacterType.Both))
foreach (Character otherCharacter in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (crew == character) { continue; }
if (crew.Info.GetCurrentLevel() < ownLevel) { return false; }
if (otherCharacter == character) { continue; }
if (otherCharacter.Info.GetCurrentLevel() < ownLevel) { return false; }
}
return true;
}
}
@@ -38,7 +38,11 @@ internal sealed class CharacterAbilityGiveExperience : CharacterAbility
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter)
if (abilityObject is AbilityCharacterKill { Killer: { } killer })
{
ApplyEffectSpecific(killer);
}
else if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter)
{
ApplyEffectSpecific(targetCharacter);
}
@@ -7,12 +7,14 @@ namespace Barotrauma.Abilities
private readonly ItemTalentStats stat;
private readonly float value;
private readonly bool stackable;
private readonly bool save;
public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
value = abilityElement.GetAttributeFloat("value", 0f);
stackable = abilityElement.GetAttributeBool("stackable", true);
save = abilityElement.GetAttributeBool("save", false);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
@@ -27,7 +29,7 @@ namespace Barotrauma.Abilities
{
if (abilityObject is not IAbilityItem ability) { return; }
ability.Item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
ability.Item.StatManager.ApplyStat(stat, stackable, save, value, CharacterTalent);
}
}
}
@@ -10,6 +10,7 @@ namespace Barotrauma.Abilities
private readonly float value;
private readonly ImmutableHashSet<Identifier> tags;
private readonly bool stackable;
private readonly bool save;
public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
@@ -17,6 +18,7 @@ namespace Barotrauma.Abilities
value = abilityElement.GetAttributeFloat("value", 0f);
tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
stackable = abilityElement.GetAttributeBool("stackable", true);
save = abilityElement.GetAttributeBool("save", false);
}
public override void InitializeAbility(bool addingFirstTime)
@@ -44,7 +46,7 @@ namespace Barotrauma.Abilities
if (item.Submarine?.TeamID != Character.TeamID) { continue; }
if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier))
{
item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
item.StatManager.ApplyStat(stat, stackable, save, value, CharacterTalent);
}
}
}
@@ -1,4 +1,6 @@
namespace Barotrauma.Abilities
using System;
namespace Barotrauma.Abilities
{
public enum PermanentStatPlaceholder
{
@@ -19,7 +21,12 @@
private readonly bool setValue;
private readonly PermanentStatPlaceholder placeholder;
//private readonly float maximumValue;
/// <summary>
/// If enabled, the effect is applied on the target of the ability (e.g. the character that was killed if the effect type is OnKillCharacter).
/// Defaults to false (= targets the character who has the talent).
/// </summary>
private readonly bool targetAbilityTarget = false;
public override bool AllowClientSimulation => true;
public override bool AppliesEffectOnIntervalUpdate => true;
@@ -40,27 +47,28 @@
giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None);
setValue = abilityElement.GetAttributeBool("setvalue", false);
placeholder = abilityElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
targetAbilityTarget = abilityElement.GetAttributeBool(nameof(targetAbilityTarget), false);
}
public override void InitializeAbility(bool addingFirstTime)
{
if (giveOnAddingFirstTime && addingFirstTime)
{
ApplyEffectSpecific();
ApplyEffectSpecific(abilityObject: null);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffectSpecific();
ApplyEffectSpecific(abilityObject);
}
protected override void ApplyEffect()
{
ApplyEffectSpecific();
ApplyEffectSpecific(abilityObject: null);
}
private void ApplyEffectSpecific()
private void ApplyEffectSpecific(AbilityObject abilityObject)
{
Identifier identifier = HandlePlaceholders(placeholder, statIdentifier);
if (targetAllies)
@@ -72,7 +80,21 @@
}
else
{
Character?.Info?.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
Character targetCharacter =
targetAbilityTarget ?
(abilityObject as IAbilityCharacter)?.Character ?? Character :
Character;
if (targetCharacter == null)
{
DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityGivePermanentStat.ApplyEffectSpecific)}: character was null.\n{Environment.StackTrace.CleanupStackTrace()}");
return;
}
if (targetCharacter?.Info == null)
{
DebugConsole.AddWarning($"Error in {nameof(CharacterAbilityGivePermanentStat.ApplyEffectSpecific)}: character {targetCharacter} has no CharacterInfo. Are you trying to use the condition on a non-player character?\n{Environment.StackTrace.CleanupStackTrace()}");
return;
}
targetCharacter.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
}
}
@@ -83,7 +105,7 @@
switch (placeholder)
{
case PermanentStatPlaceholder.LocationName when map.CurrentLocation is { } location:
return original.Replace("[placeholder]", location.Name);
return original.Replace("[placeholder]", location.NameIdentifier.Value);
case PermanentStatPlaceholder.LocationIndex:
return original.Replace("[placeholder]", map.CurrentLocationIndex.ToString());
}
@@ -44,9 +44,13 @@ namespace Barotrauma.Abilities
case "fallbackabilities":
LoadFallbackAbilities(subElement);
break;
case "condition":
case "conditions":
LoadConditions(subElement);
break;
default:
DebugConsole.ThrowError($"Error in talent {characterTalent.Prefab.Identifier}: unrecognized xml element \"{subElement.Name}\".");
break;
}
}
@@ -39,6 +39,23 @@ namespace Barotrauma
}
public Item? FindItemInContainer(ItemContainer? container)
=> container?.Inventory.GetItemsAt(Slot).ElementAt(StackIndex);
{
var items = container?.Inventory.GetItemsAt(Slot);
if (items != null && StackIndex >= 0 && StackIndex < items.Count())
{
return items.ElementAt(StackIndex);
}
else
{
string errorMsg =
$"Circuit box error: failed to find an item in the container {container?.Item.Name ?? "null"}.";
DebugConsole.ThrowError(
errorMsg +
$" Items: {items?.Count().ToString() ?? "null"}, " +
$" Slot: {Slot}, StackIndex: {StackIndex}");
GameAnalyticsManager.AddErrorEventOnce("ItemSlotIndexPair.FindItemInContainer", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return null;
}
}
}
}
@@ -19,13 +19,12 @@ namespace Barotrauma
LanguageIdentifier language = languageName.ToLanguageIdentifier();
if (!TextManager.TextPacks.ContainsKey(language))
{
TextManager.TextPacks.TryAdd(language, ImmutableHashSet<TextPack>.Empty);
TextManager.TextPacks.TryAdd(language, ImmutableList<TextPack>.Empty);
}
var newPack = new TextPack(this, mainElement, language);
var newHashSet = TextManager.TextPacks[language].Add(newPack);
var newList = TextManager.TextPacks[language].Add(newPack);
TextManager.TextPacks.TryRemove(language, out _);
TextManager.TextPacks.TryAdd(language, newHashSet);
TextManager.TextPacks.TryAdd(language, newList);
TextManager.IncrementLanguageVersion();
}
@@ -33,9 +32,9 @@ namespace Barotrauma
{
foreach (var kvp in TextManager.TextPacks.ToArray())
{
var newHashSet = kvp.Value.Where(p => p.ContentFile != this).ToImmutableHashSet();
var newList = kvp.Value.Where(p => p.ContentFile != this).ToImmutableList();
TextManager.TextPacks.TryRemove(kvp.Key, out _);
if (newHashSet.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newHashSet); }
if (newList.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newList); }
}
TextManager.IncrementLanguageVersion();
if (!TextManager.TextPacks.ContainsKey(GameSettings.CurrentConfig.Language))
@@ -49,7 +48,11 @@ namespace Barotrauma
public override void Sort()
{
//Overrides for text packs don't exist! Should we change this?
foreach (var language in TextManager.TextPacks.Keys.ToList())
{
TextManager.TextPacks[language] =
TextManager.TextPacks[language].Sort((t1, t2) => (t1.ContentFile.ContentPackage?.Index ?? int.MaxValue) - (t2.ContentFile.ContentPackage?.Index ?? int.MaxValue));
}
}
}
}
@@ -155,6 +155,7 @@ namespace Barotrauma
.Distinct(new TypeComparer<ContentFile>())
.ForEach(f => f.Sort());
MergedHash = Md5Hash.MergeHashes(All.Select(cp => cp.Hash));
TextManager.IncrementLanguageVersion();
}
public static int IndexOf(ContentPackage contentPackage)
@@ -858,7 +858,13 @@ namespace Barotrauma
commands.Add(new Command("debugevent", "debugevent [identifier]: outputs debug info about a specific event that's currently active. Mainly intended for debugging events in multiplayer: in single player, the same information is available by enabling debugdraw.", (string[] args) =>
{
if (GameMain.GameSession?.EventManager is EventManager eventManager && args.Length > 0)
if (args.Length == 0)
{
ThrowError($"Please specify the identifier of the event you want to debug.");
return;
}
if (GameMain.GameSession?.EventManager is EventManager eventManager)
{
var ev = eventManager.ActiveEvents.FirstOrDefault(ev => ev.Prefab?.Identifier == args[0]);
if (ev == null)
@@ -877,9 +883,18 @@ namespace Barotrauma
}
}, isCheat: true, getValidArgs: () =>
{
IEnumerable<EventPrefab> eventPrefabs;
if (GameMain.GameSession?.EventManager == null || GameMain.GameSession.EventManager.ActiveEvents.None())
{
eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty);
}
else
{
eventPrefabs = GameMain.GameSession.EventManager.ActiveEvents.Select(e => e.Prefab);
}
return new[]
{
GameMain.GameSession?.EventManager?.ActiveEvents.Select(ev => ev.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
eventPrefabs.Select(ev => ev.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
};
}));
@@ -1613,7 +1628,7 @@ namespace Barotrauma
int i = 0;
foreach (LocationConnection connection in campaign.Map.CurrentLocation.Connections)
{
NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).Name, Color.White);
NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).DisplayName, Color.White);
i++;
}
ShowQuestionPrompt("Select a destination (0 - " + (campaign.Map.CurrentLocation.Connections.Count - 1) + "):", (string selectedDestination) =>
@@ -1627,7 +1642,7 @@ namespace Barotrauma
}
Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation);
campaign.Map.SelectLocation(location);
NewMessage(location.Name + " selected.", Color.White);
NewMessage(location.DisplayName + " selected.", Color.White);
});
}
else
@@ -1641,7 +1656,7 @@ namespace Barotrauma
}
Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation);
campaign.Map.SelectLocation(location);
NewMessage(location.Name + " selected.", Color.White);
NewMessage(location.DisplayName + " selected.", Color.White);
}
}));
@@ -1913,7 +1928,7 @@ namespace Barotrauma
{
if (location.Stores != null)
{
var msg = "--- Location: " + location.Name + " ---";
var msg = "--- Location: " + location.DisplayName + " ---";
foreach (var store in location.Stores)
{
msg += $"\nStore identifier: {store.Value.Identifier}";
@@ -2417,11 +2432,7 @@ namespace Barotrauma
public static void LogError(string msg, Color? color = null, ContentPackage contentPackage = null)
{
if (contentPackage != null)
{
string colorStr = XMLExtensions.ToStringHex(Color.MediumPurple);
msg = $"‖color:{colorStr}‖[{contentPackage.Name}]‖color:end‖ {msg}";
}
msg = AddContentPackageInfoToMessage(msg, contentPackage);
color ??= Color.Red;
NewMessage(msg, color.Value, isCommand: false, isError: true);
}
@@ -2557,11 +2568,7 @@ namespace Barotrauma
public static void ThrowError(string error, Exception e = null, ContentPackage contentPackage = null, bool createMessageBox = false, bool appendStackTrace = false)
{
if (contentPackage != null)
{
string color = XMLExtensions.ToStringHex(Color.MediumPurple);
error = $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {error}";
}
error = AddContentPackageInfoToMessage(error, contentPackage);
if (e != null)
{
error += " {" + e.Message + "}\n";
@@ -2610,16 +2617,22 @@ namespace Barotrauma
public static void AddWarning(string warning, ContentPackage contentPackage = null)
{
warning = $"WARNING: {warning}";
if (contentPackage != null)
{
string color = XMLExtensions.ToStringHex(Color.MediumPurple);
warning = $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {warning}";
}
warning = AddContentPackageInfoToMessage($"WARNING: {warning}", contentPackage);
System.Diagnostics.Debug.WriteLine(warning);
NewMessage(warning, Color.Yellow);
}
private static string AddContentPackageInfoToMessage(string message, ContentPackage contentPackage)
{
if (contentPackage == null) { return message; }
#if CLIENT
string color = XMLExtensions.ToStringHex(Color.MediumPurple);
return $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {message}";
#else
return $"[{contentPackage.Name}] {message}";
#endif
}
#if CLIENT
private static IEnumerable<CoroutineStatus> CreateMessageBox(string errorMsg)
{
@@ -159,11 +159,13 @@ namespace Barotrauma
OnItemDeconstructedInventory,
OnStopTinkering,
OnItemPicked,
OnItemSelected,
OnGeneticMaterialCombinedOrRefined,
OnCrewGeneticMaterialCombinedOrRefined,
AfterSubmarineAttacked,
OnApplyTreatment,
OnStatusEffectIdentifier,
OnRepairedOutsideLeak
}
/// <summary>
@@ -550,7 +552,27 @@ namespace Barotrauma
/// <summary>
/// Can be used to prevent certain talents from being unlocked by specifying the talent's identifier via CharacterAbilityGivePermanentStat.
/// </summary>
LockedTalents
LockedTalents,
/// <summary>
/// Used to reduce or increase the cost of hiring certain jobs by a percentage.
/// </summary>
HireCostMultiplier,
/// <summary>
/// Used to increase how much items can stack in the characters inventory.
/// </summary>
InventoryExtraStackSize,
/// <summary>
/// Modifies the range of the sounds emitted by the character (can be used to make the character easier or more difficult for monsters to hear)
/// </summary>
SoundRangeMultiplier,
/// <summary>
/// Modifies how far the character can be seen from (can be used to make the character easier or more difficult for monsters to see)
/// </summary>
SightRangeMultiplier
}
internal enum ItemTalentStats
@@ -565,7 +587,8 @@ namespace Barotrauma
ReactorMaxOutput,
ReactorFuelConsumption,
DeconstructorSpeed,
FabricationSpeed
FabricationSpeed,
ExtraStackSize
}
/// <summary>
@@ -1,3 +1,6 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
@@ -8,7 +11,16 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
private PropertyConditional Conditional { get; }
[Serialize(PropertyConditional.LogicalOperatorType.Or, IsPropertySaveable.Yes)]
public PropertyConditional.LogicalOperatorType LogicalOperator { get; set; }
private ImmutableArray<PropertyConditional> Conditionals { get; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ApplyTagToLinkedHulls { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside when the item is used.")]
public Identifier ApplyTagToHull { get; set; }
public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
@@ -17,14 +29,38 @@ namespace Barotrauma
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.",
contentPackage: parentEvent.Prefab.ContentPackage);
}
Conditional = PropertyConditional.FromXElement(element, IsNotTargetTagAttribute).FirstOrDefault();
if (Conditional == null)
var conditionalElements = element.GetChildElements("Conditional");
if (conditionalElements.None())
{
//backwards compatibility
Conditionals = PropertyConditional.FromXElement(element, IsConditionalAttribute).ToImmutableArray();
}
else
{
var conditionalList = new List<PropertyConditional>();
foreach (ContentXElement subElement in conditionalElements)
{
conditionalList.AddRange(PropertyConditional.FromXElement(subElement));
break;
}
Conditionals = conditionalList.ToImmutableArray();
}
if (Conditionals.None())
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.",
contentPackage: parentEvent.Prefab.ContentPackage);
}
static bool IsNotTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() != "targettag";
static bool IsConditionalAttribute(XAttribute attribute)
{
var nameAsIdentifier = attribute.NameAsIdentifier();
return
nameAsIdentifier != nameof(TargetTag) &&
nameAsIdentifier != nameof(LogicalOperator) &&
nameAsIdentifier != nameof(ApplyTagToLinkedHulls) &&
nameAsIdentifier != nameof(ApplyTagToHull);
}
}
private string GetEventName()
@@ -34,32 +70,65 @@ namespace Barotrauma
protected override bool? DetermineSuccess()
{
ISerializableEntity target = null;
IEnumerable<ISerializableEntity> targets = null;
if (!TargetTag.IsEmpty)
{
foreach (var t in ParentEvent.GetTargets(TargetTag))
{
if (t is ISerializableEntity e)
{
target = e;
break;
}
}
targets = ParentEvent.GetTargets(TargetTag).OfType<ISerializableEntity>();
}
if (target == null)
if (targets.None())
{
DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.",
contentPackage: ParentEvent.Prefab.ContentPackage);
}
if (target == null || Conditional == null)
if (targets.None() || Conditionals.None())
{
foreach (var target in targets)
{
ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls);
}
return true;
}
else
{
bool success = false;
foreach (var target in targets)
{
if (ConditionalsMatch(target))
{
success = true;
ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls);
}
}
return success;
}
}
private bool ConditionalsMatch(ISerializableEntity target)
{
if (LogicalOperator == PropertyConditional.LogicalOperatorType.And)
{
return Conditionals.All(c => ConditionalMatches(target, c));
}
else
{
return Conditionals.Any(c => ConditionalMatches(target, c));
}
}
private static bool ConditionalMatches(ISerializableEntity target, PropertyConditional conditional)
{
if (target is Item item)
{
return item.ConditionalMatches(Conditional);
if (!conditional.TargetItemComponent.IsNullOrEmpty() &&
item.Components.None(ic => ic.Name == conditional.TargetItemComponent))
{
return false;
}
return item.ConditionalMatches(conditional);
}
return Conditional.Matches(target);
return conditional.Matches(target);
}
}
}
@@ -1,5 +1,6 @@
#nullable enable
using Microsoft.Xna.Framework;
using System.Linq;
namespace Barotrauma
{
@@ -8,12 +9,18 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check from.")]
public Identifier EntityTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Entities that also have this tag are excluded.")]
public Identifier ExcludedEntityTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check to.")]
public Identifier TargetTag { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Does the entity need to be facing the target? Only valid if the entity is a character.")]
public bool CheckFacing { get; set; }
[Serialize(1000.0f, IsPropertySaveable.Yes, description: "Maximum distance between the targets.")]
public float MaxDistance { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the entity who saw the target when the check succeeds.")]
public Identifier ApplyTagToEntity { get; set; }
@@ -31,11 +38,17 @@ namespace Barotrauma
{
foreach (var entity in ParentEvent.GetTargets(EntityTag))
{
if (!ExcludedEntityTag.IsEmpty)
{
if (ParentEvent.GetTargets(ExcludedEntityTag).Contains(entity)) { continue; }
}
foreach (var target in ParentEvent.GetTargets(TargetTag))
{
if (!AllowSameEntity && entity == target) { continue; }
if (Character.IsTargetVisible(target, entity, CheckFacing))
{
if (Vector2.DistanceSquared(target.WorldPosition, entity.WorldPosition) > MaxDistance * MaxDistance) { continue; }
if (Character.IsTargetVisible(target, entity, seeThroughWindows: true, CheckFacing))
{
if (!ApplyTagToEntity.IsEmpty)
{
ParentEvent.AddTarget(ApplyTagToEntity, entity);
@@ -103,7 +103,7 @@ namespace Barotrauma
public EventAction(ScriptedEvent parentEvent, ContentXElement element)
{
ParentEvent = parentEvent ?? throw new ArgumentNullException(nameof(parentEvent));
ParentEvent = parentEvent;
SerializableProperty.DeserializeProperties(this, element);
}
@@ -141,7 +141,11 @@ namespace Barotrauma
Identifier typeName = element.Name.ToString().ToIdentifier();
if (typeName == "TutorialSegmentAction")
{
typeName = "EventObjectiveAction".ToIdentifier();
typeName = nameof(EventObjectiveAction).ToIdentifier();
}
else if (typeName == "TutorialHighlightAction")
{
typeName = nameof(HighlightAction).ToIdentifier();
}
actionType = Type.GetType("Barotrauma." + typeName, throwOnError: true, ignoreCase: true);
if (actionType == null) { throw new NullReferenceException(); }
@@ -170,6 +174,30 @@ namespace Barotrauma
}
}
protected void ApplyTagsToHulls(Entity entity, Identifier hullTag, Identifier linkedHullTag)
{
var currentHull = entity switch
{
Item item => item.CurrentHull,
Character character => character.CurrentHull,
_ => null,
};
if (currentHull == null) { return; }
if (!hullTag.IsEmpty)
{
ParentEvent.AddTarget(hullTag, currentHull);
}
if (!linkedHullTag.IsEmpty)
{
ParentEvent.AddTarget(linkedHullTag, currentHull);
foreach (var linkedHull in currentHull.GetLinkedEntities<Hull>())
{
ParentEvent.AddTarget(linkedHullTag, linkedHull);
}
}
}
/// <summary>
/// Rich test to display in debugdraw
/// </summary>
@@ -2,7 +2,7 @@ namespace Barotrauma
{
partial class EventObjectiveAction : EventAction
{
public enum SegmentActionType { Trigger, Add, Complete, CompleteAndRemove, Remove, Fail, FailAndRemove };
public enum SegmentActionType { Trigger, Add, AddIfNotFound, Complete, CompleteAndRemove, Remove, Fail, FailAndRemove };
[Serialize(SegmentActionType.Trigger, IsPropertySaveable.Yes)]
public SegmentActionType Type { get; set; }
@@ -5,17 +5,31 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public string Name { get; set; }
[Serialize(-1, IsPropertySaveable.Yes)]
public int MaxTimes { get; set; }
private int counter;
public GoTo(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
public override bool IsFinished(ref string goTo)
{
goTo = Name;
if (counter < MaxTimes || MaxTimes <= 0)
{
goTo = Name;
counter++;
}
return true;
}
public override string ToDebugString()
{
return $"[-] Go to label \"{Name}\"";
string msg = $"[-] Go to label \"{Name}\"";
if (MaxTimes > 0)
{
msg += $" ({counter}/{MaxTimes})";
}
return msg;
}
public override void Reset() { }
@@ -0,0 +1,43 @@
#nullable enable
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma;
partial class HighlightAction : EventAction
{
private static readonly Color highlightColor = Color.Orange;
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Only the player controlling this character will see the highlight. If empty, all players will see it.")]
public Identifier TargetCharacter { get; set; }
[Serialize(true, IsPropertySaveable.Yes)]
public bool State { get; set; }
private bool isFinished;
public HighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
var targetCharacters = TargetCharacter.IsEmpty ? null : ParentEvent.GetTargets(TargetCharacter).OfType<Character>();
foreach (var target in ParentEvent.GetTargets(TargetTag))
{
SetHighlightProjSpecific(target, targetCharacters);
}
isFinished = true;
}
partial void SetHighlightProjSpecific(Entity entity, IEnumerable<Character>? targetCharacters);
public override bool IsFinished(ref string goToLabel) => isFinished;
public override void Reset() => isFinished = false;
}
@@ -123,11 +123,11 @@ namespace Barotrauma
campaign.Map.Discover(unlockLocation, checkTalents: false);
if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] == null)
{
DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.Name}\".");
DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.DisplayName}\".");
}
else
{
DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the connection from \"{unlockedMission.Locations[0].Name}\" to \"{unlockedMission.Locations[1].Name}\".");
DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the connection from \"{unlockedMission.Locations[0].DisplayName}\" to \"{unlockedMission.Locations[1].DisplayName}\".");
}
#if CLIENT
new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", unlockedMission.Name),
@@ -12,7 +12,7 @@ namespace Barotrauma
public Identifier Type { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public string Name { get; set; }
public Identifier Name { get; set; }
private bool isFinished;
@@ -77,9 +77,9 @@ namespace Barotrauma
location.ChangeType(campaign, locationType);
}
}
if (!string.IsNullOrEmpty(Name))
if (!Name.IsEmpty)
{
location.ForceName(TextManager.Get(Name).Fallback(Name).Value);
location.ForceName(Name);
}
}
}
@@ -101,7 +101,7 @@ namespace Barotrauma
WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human);
if (subWaypoint != null)
{
npc.GiveIdCardTags(subWaypoint, requireSpawnPointTagsNotGiven: false, createNetworkEvent: true);
npc.GiveIdCardTags(subWaypoint, createNetworkEvent: true);
}
}
}
@@ -90,31 +90,46 @@ namespace Barotrauma
private void TagPlayers()
{
AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters));
AddTargetPredicate(
Tag,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters));
}
private void TagTraitors()
{
AddTargetPredicate(Tags.Traitor, e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated);
AddTargetPredicate(
Tags.Traitor,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated);
}
private void TagNonTraitors()
{
AddTargetPredicate(Tags.NonTraitor, e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
AddTargetPredicate(
Tags.NonTraitor,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
}
private void TagNonTraitorPlayers()
{
AddTargetPredicate(Tags.NonTraitorPlayer, e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
AddTargetPredicate(
Tags.NonTraitorPlayer,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
}
private void TagBots(bool playerCrewOnly)
{
AddTargetPredicate(Tag, e =>
e is Character c &&
c.IsBot &&
(!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) &&
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1));
AddTargetPredicate(
Tag,
ScriptedEvent.TargetPredicate.EntityType.Character,
e =>
e is Character c &&
c.IsBot &&
(!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) &&
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1));
}
private void TagCrew()
@@ -139,42 +154,67 @@ namespace Barotrauma
private void TagStructuresByIdentifier(Identifier identifier)
{
AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier);
AddTargetPredicate(
Tag,
ScriptedEvent.TargetPredicate.EntityType.Structure,
e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier);
}
private void TagStructuresBySpecialTag(Identifier tag)
{
AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag);
AddTargetPredicate(
Tag,
ScriptedEvent.TargetPredicate.EntityType.Structure,
e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag);
}
private void TagItemsByIdentifier(Identifier identifier)
{
AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier);
AddTargetPredicate(
Tag,
ScriptedEvent.TargetPredicate.EntityType.Item,
e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier);
}
private void TagItemsByTag(Identifier tag)
{
AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag));
AddTargetPredicate(
Tag,
ScriptedEvent.TargetPredicate.EntityType.Item,
e => e is Item it && IsValidItem(it) && it.HasTag(tag));
}
private void TagHulls()
{
AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine));
AddTargetPredicate(
Tag,
ScriptedEvent.TargetPredicate.EntityType.Hull,
e => e is Hull h && SubmarineTypeMatches(h.Submarine));
}
private void TagHullsByName(Identifier name)
{
AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase));
AddTargetPredicate(
Tag,
ScriptedEvent.TargetPredicate.EntityType.Hull,
e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase));
}
private void TagSubmarinesByType(Identifier type)
{
AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier()));
AddTargetPredicate(
Tag,
ScriptedEvent.TargetPredicate.EntityType.Submarine,
e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier()));
}
private bool IsValidItem(Item it)
{
return (!it.HiddenInGame || AllowHiddenItems) && SubmarineTypeMatches(it.Submarine);
return
(!it.HiddenInGame || AllowHiddenItems) &&
//if the item has just spawned, it may be in a hull but not moved into the coordinate space of the hull yet
//= it.Submarine still null
SubmarineTypeMatches(it.Submarine ?? it.CurrentHull?.Submarine ?? it.ParentInventory?.Owner?.Submarine);
}
private bool SubmarineTypeMatches(Submarine sub)
@@ -197,7 +237,7 @@ namespace Barotrauma
}
}
private void AddTargetPredicate(Identifier tag, Predicate<Entity> predicate)
private void AddTargetPredicate(Identifier tag, ScriptedEvent.TargetPredicate.EntityType entityType, Predicate<Entity> predicate)
{
if (ChoosePercentage > 0.0f)
{
@@ -209,7 +249,7 @@ namespace Barotrauma
}
else
{
ParentEvent.AddTargetPredicate(tag, predicate);
ParentEvent.AddTargetPredicate(tag, entityType, predicate);
mustRecheckTargets = true;
}
}
@@ -1,34 +0,0 @@
namespace Barotrauma;
partial class TutorialHighlightAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
[Serialize(true, IsPropertySaveable.Yes)]
public bool State { get; set; }
private bool isFinished;
public TutorialHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (GameMain.NetworkMember != null)
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(TutorialHighlightAction)} is not supported in multiplayer.",
contentPackage: element.ContentPackage);
}
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
UpdateProjSpecific();
isFinished = true;
}
partial void UpdateProjSpecific();
public override bool IsFinished(ref string goToLabel) => isFinished;
public override void Reset() => isFinished = false;
}
@@ -30,11 +30,16 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside, and all the hulls it's linked to, when the item is used.")]
public Identifier ApplyTagToLinkedHulls { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
public int RequiredUseCount { get; set; }
private bool isFinished;
private readonly HashSet<Entity> targets = new HashSet<Entity>();
private readonly HashSet<ItemComponent> targetComponents = new HashSet<ItemComponent>();
private int useCount = 0;
private Identifier onUseEventIdentifier;
private Identifier OnUseEventIdentifier
{
@@ -58,6 +63,14 @@ namespace Barotrauma
private void OnItemUsed(Item item, Character user)
{
if (!UserTag.IsEmpty)
{
if (!ParentEvent.GetTargets(UserTag).Contains(user)) { return; }
}
useCount++;
if (useCount < RequiredUseCount) { return; }
if (!ApplyTagToItem.IsEmpty)
{
ParentEvent.AddTarget(ApplyTagToItem, item);
@@ -66,22 +79,7 @@ namespace Barotrauma
{
ParentEvent.AddTarget(ApplyTagToUser, user);
}
if (item.CurrentHull != null)
{
if (!ApplyTagToHull.IsEmpty)
{
ParentEvent.AddTarget(ApplyTagToHull, item.CurrentHull);
}
if (!ApplyTagToLinkedHulls.IsEmpty)
{
ParentEvent.AddTarget(ApplyTagToLinkedHulls, item.CurrentHull);
foreach (var linkedHull in item.CurrentHull.GetLinkedEntities<Hull>())
{
ParentEvent.AddTarget(ApplyTagToLinkedHulls, linkedHull);
}
}
}
ApplyTagsToHulls(item, ApplyTagToHull, ApplyTagToLinkedHulls);
DeregisterTargets();
isFinished = true;
}
@@ -142,6 +140,7 @@ namespace Barotrauma
public override void Reset()
{
isFinished = false;
useCount = 0;
DeregisterTargets();
}
@@ -412,6 +412,7 @@ namespace Barotrauma
preloadedSprites.ForEach(s => s.Remove());
preloadedSprites.Clear();
timeStamps.Clear();
pathFinder = null;
}
@@ -905,7 +906,18 @@ namespace Barotrauma
activeEvents.Add(QueuedEvents.Dequeue());
}
}
public void EntitySpawned(Entity entity)
{
foreach (var ev in activeEvents)
{
if (ev is ScriptedEvent scriptedEvent)
{
scriptedEvent.EntitySpawned(entity);
}
}
}
private void CalculateCurrentIntensity(float deltaTime)
{
intensityUpdateTimer -= deltaTime;
@@ -57,7 +57,7 @@ namespace Barotrauma
{
for (int n = 0; n < 2; n++)
{
descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].Name);
descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].DisplayName);
}
}
@@ -185,7 +185,7 @@ namespace Barotrauma
for (int n = 0; n < 2; n++)
{
string locationName = $"‖color:gui.orange‖{locations[n].Name}‖end‖";
string locationName = $"‖color:gui.orange‖{locations[n].DisplayName}‖end‖";
if (description != null) { description = description.Replace("[location" + (n + 1) + "]", locationName); }
if (successMessage != null) { successMessage = successMessage.Replace("[location" + (n + 1) + "]", locationName); }
if (failureMessage != null) { failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locationName); }
@@ -431,8 +431,7 @@ namespace Barotrauma
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
// use multipliers here so that we can easily add them together without introducing multiplicative XP stacking
var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier));
var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f, character: null);
crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier));
DistributeExperienceToCrew(crewCharacters, (int)(baseExperienceGain * experienceGainMultiplier.Value));
@@ -652,16 +651,18 @@ namespace Barotrauma
public Mission Mission { get; set; }
}
class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission
class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission, IAbilityCharacter
{
public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier)
public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier, Character character)
{
Value = missionExperienceGainMultiplier;
Mission = mission;
Character = character;
}
public float Value { get; set; }
public Mission Mission { get; set; }
public Character Character { get; set; }
}
}
@@ -673,7 +673,7 @@ namespace Barotrauma
monster.AnimController.SetPosition(FarseerPhysics.ConvertUnits.ToSimUnits(pos));
var eventManager = GameMain.GameSession.EventManager;
if (eventManager != null)
if (eventManager != null && monster.Params.AI != null)
{
if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath))
{
@@ -700,7 +700,7 @@ namespace Barotrauma
//this will do nothing if the monsters have no swarm behavior defined,
//otherwise it'll make the spawned characters act as a swarm
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI.CombatStrength))}.", Color.LightBlue, debugOnly: true);
DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI?.CombatStrength ?? 0))}.", Color.LightBlue, debugOnly: true);
}
if (GameMain.GameSession != null)
@@ -7,7 +7,21 @@ namespace Barotrauma
{
class ScriptedEvent : Event
{
private readonly Dictionary<Identifier, List<Predicate<Entity>>> targetPredicates = new Dictionary<Identifier, List<Predicate<Entity>>>();
public sealed record TargetPredicate(
TargetPredicate.EntityType Type,
Predicate<Entity> Predicate)
{
public enum EntityType
{
Character,
Hull,
Item,
Structure,
Submarine
}
}
private readonly Dictionary<Identifier, List<TargetPredicate>> targetPredicates = new Dictionary<Identifier, List<TargetPredicate>>();
private readonly Dictionary<Identifier, List<Entity>> cachedTargets = new Dictionary<Identifier, List<Entity>>();
@@ -17,7 +31,7 @@ namespace Barotrauma
/// </summary>
private readonly Dictionary<Identifier, int> initialAmounts = new Dictionary<Identifier, int>();
private int prevEntityCount;
private bool newEntitySpawned;
private int prevPlayerCount, prevBotCount;
private Character prevControlled;
@@ -191,13 +205,13 @@ namespace Barotrauma
}
}
public void AddTargetPredicate(Identifier tag, Predicate<Entity> predicate)
public void AddTargetPredicate(Identifier tag, TargetPredicate.EntityType entityType, Predicate<Entity> predicate)
{
if (!targetPredicates.ContainsKey(tag))
{
targetPredicates.Add(tag, new List<Predicate<Entity>>());
targetPredicates.Add(tag, new List<TargetPredicate>());
}
targetPredicates[tag].Add(predicate);
targetPredicates[tag].Add(new TargetPredicate(entityType, predicate));
// force re-search for this tag
if (cachedTargets.ContainsKey(tag))
{
@@ -229,7 +243,6 @@ namespace Barotrauma
}
List<Entity> targetsToReturn = new List<Entity>();
if (Targets.ContainsKey(tag))
{
foreach (Entity e in Targets[tag])
@@ -240,11 +253,24 @@ namespace Barotrauma
}
if (targetPredicates.ContainsKey(tag))
{
foreach (Entity entity in Entity.GetEntities())
foreach (var targetPredicate in targetPredicates[tag])
{
if (targetPredicates[tag].Any(p => p(entity)) && !targetsToReturn.Contains(entity))
IEnumerable<Entity> entityList = targetPredicate.Type switch
{
targetsToReturn.Add(entity);
TargetPredicate.EntityType.Character => Character.CharacterList,
TargetPredicate.EntityType.Item => Item.ItemList,
TargetPredicate.EntityType.Structure => MapEntity.MapEntityList.Where(m => m is Structure),
TargetPredicate.EntityType.Hull => Hull.HullList,
TargetPredicate.EntityType.Submarine => Submarine.Loaded,
_ => Entity.GetEntities(),
};
foreach (Entity entity in entityList)
{
if (targetsToReturn.Contains(entity)) { continue; }
if (targetPredicate.Predicate(entity))
{
targetsToReturn.Add(entity);
}
}
}
}
@@ -293,14 +319,8 @@ namespace Barotrauma
{
int botCount = 0;
int playerCount = 0;
bool forceRefreshTargets = false;
foreach (Character c in Character.CharacterList)
{
if (c.Removed)
{
forceRefreshTargets = true;
continue;
}
if (c.IsPlayer)
{
playerCount++;
@@ -310,10 +330,11 @@ namespace Barotrauma
botCount++;
}
}
if (forceRefreshTargets || Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled)
if (botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled || NeedsToRefreshCachedTargets())
{
cachedTargets.Clear();
prevEntityCount = Entity.EntityCount;
newEntitySpawned = false;
prevBotCount = botCount;
prevPlayerCount = playerCount;
prevControlled = Character.Controlled;
@@ -369,6 +390,47 @@ namespace Barotrauma
}
}
private bool NeedsToRefreshCachedTargets()
{
if (newEntitySpawned) { return true; }
foreach (var cachedTargetList in cachedTargets.Values)
{
foreach (var target in cachedTargetList)
{
//one of the previously cached entities has been removed -> force refresh
if (target.Removed)
{
return true;
}
}
}
return false;
}
public void EntitySpawned(Entity entity)
{
if (newEntitySpawned) { return; }
if (entity is Character character &&
Level.Loaded?.StartOutpost != null &&
Level.Loaded.StartOutpost.Info.OutpostNPCs.Values.Any(npcList => npcList.Contains(character)))
{
newEntitySpawned = true;
return;
}
//new entity matches one of the existing predicates -> force refresh
foreach (var targetPredicateList in targetPredicates.Values)
{
foreach (var targetPredicate in targetPredicateList)
{
if (targetPredicate.Predicate(entity))
{
newEntitySpawned = true;
return;
}
}
}
}
public override bool LevelMeetsRequirements()
{
if (requiredDestinationTypes == null) { return true; }
@@ -294,6 +294,11 @@ namespace Barotrauma.Extensions
.Where(nullable => nullable.HasValue)
.Select(nullable => nullable.Value);
public static IEnumerable<T> NotNull<T>(this IEnumerable<T> source) where T : class
=> source
.Where(nullable => nullable != null)
.Select(nullable => nullable!);
public static IEnumerable<T> NotNone<T>(this IEnumerable<Option<T>> source)
{
foreach (var o in source)
@@ -262,7 +262,7 @@ namespace Barotrauma
while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count)
{
spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]);
}
}
}
if (spawnWaypoints == null || !spawnWaypoints.Any())
{
@@ -306,15 +306,16 @@ namespace Barotrauma
}
character.LoadTalents();
character.GiveIdCardTags(new List<WayPoint>() { mainSubWaypoints[i], spawnWaypoints[i] });
character.Info.StartItemsGiven = true;
character.GiveIdCardTags(mainSubWaypoints[i]);
character.GiveIdCardTags(spawnWaypoints[i]);
character.Info.StartItemsGiven = true;
if (character.Info.OrderData != null)
{
character.Info.ApplyOrderData();
}
}
AddCharacter(character, sortCrewList: false);
#if CLIENT
if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; }
@@ -45,7 +45,14 @@ namespace Barotrauma
}
else
{
data.Add(identifier, Convert.ChangeType(value, type, NumberFormatInfo.InvariantInfo));
try
{
data.Add(identifier, Convert.ChangeType(value, type, NumberFormatInfo.InvariantInfo));
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to change the type of the value \"{value}\" to {type}.", e);
}
}
}
}
@@ -57,7 +57,7 @@ namespace Barotrauma
if (increase != 0 && Character.Controlled != null)
{
Character.Controlled.AddMessage(
TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name).Value,
TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.DisplayName ?? Faction.Prefab.Name).Value,
increase > 0 ? GUIStyle.Green : GUIStyle.Red,
playSound: true, Identifier, increase, lifetime: 5.0f);
}
@@ -558,8 +558,8 @@ namespace Barotrauma
if (availableTransition == TransitionType.None)
{
DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " +
"(current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
"(current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " +
"selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " +
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
"at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" +
@@ -570,8 +570,8 @@ namespace Barotrauma
{
DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " +
"(transition type: " + availableTransition + ", " +
"current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
"current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " +
"selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " +
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
"at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" +
@@ -582,8 +582,8 @@ namespace Barotrauma
ShowCampaignUI = ForceMapUI = false;
#endif
DebugConsole.NewMessage("Transitioning to " + (nextLevel?.Seed ?? "null") +
" (current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
" (current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " +
"selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " +
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
"at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ", " +
@@ -1024,7 +1024,7 @@ namespace Barotrauma
return ToolBox.SelectWeightedRandom(factionsList, weights, random);
}
public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Client client = null)
public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Character hirer, Client client = null)
{
if (characterInfo == null) { return false; }
if (characterInfo.MinReputationToHire.factionId != Identifier.Empty)
@@ -1034,7 +1034,8 @@ namespace Barotrauma
return false;
}
}
if (!TryPurchase(client, characterInfo.Salary)) { return false; }
if (!TryPurchase(client, HireManager.GetSalaryFor(characterInfo))) { return false; }
characterInfo.IsNewHire = true;
characterInfo.Title = null;
location.RemoveHireableCharacter(characterInfo);
@@ -1263,7 +1264,7 @@ namespace Barotrauma
{
DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White);
DebugConsole.NewMessage(" Money: " + Bank.Balance, Color.White);
DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.Name, Color.White);
DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.DisplayName, Color.White);
DebugConsole.NewMessage(" Available destinations: ", Color.White);
for (int i = 0; i < map.CurrentLocation.Connections.Count; i++)
@@ -1271,11 +1272,11 @@ namespace Barotrauma
Location destination = map.CurrentLocation.Connections[i].OtherLocation(map.CurrentLocation);
if (destination == map.SelectedLocation)
{
DebugConsole.NewMessage(" " + i + ". " + destination.Name + " [SELECTED]", Color.White);
DebugConsole.NewMessage(" " + i + ". " + destination.DisplayName + " [SELECTED]", Color.White);
}
else
{
DebugConsole.NewMessage(" " + i + ". " + destination.Name, Color.White);
DebugConsole.NewMessage(" " + i + ". " + destination.DisplayName, Color.White);
}
}
@@ -1307,7 +1308,7 @@ namespace Barotrauma
{
if (NumberOfMissionsAtLocation(location) > Settings.TotalMaxMissionCount)
{
DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.Name}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions.");
DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.DisplayName}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions.");
foreach (Mission mission in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(Settings.TotalMaxMissionCount).ToList())
{
currentLocation.DeselectMission(mission);
@@ -161,16 +161,18 @@ namespace Barotrauma
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
#if CLIENT
case "gamemode": //legacy support
case "singleplayercampaign":
#if CLIENT
CrewManager = new CrewManager(true);
var campaign = SinglePlayerCampaign.Load(subElement);
campaign.LoadNewLevel();
GameMode = campaign;
InitOwnedSubs(submarineInfo, ownedSubmarines);
break;
#else
throw new Exception("The server cannot load a single player campaign.");
#endif
break;
case "multiplayercampaign":
CrewManager = new CrewManager(false);
var mpCampaign = MultiPlayerCampaign.LoadNew(subElement);
@@ -567,7 +569,7 @@ namespace Barotrauma
if (EndLocation != null && levelData != null)
{
GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false);
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false);
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.DisplayName), Color.CadetBlue, playSound: false);
var missionsToShow = missions.Where(m => m.Prefab.ShowStartMessage);
if (missionsToShow.Count() > 1)
{
@@ -582,7 +584,7 @@ namespace Barotrauma
}
else
{
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.Name), Color.CadetBlue, playSound: false);
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.DisplayName), Color.CadetBlue, playSound: false);
}
}
@@ -871,6 +873,7 @@ namespace Barotrauma
//Clear the grids to allow for garbage collection
Powered.Grids.Clear();
Powered.ChangedConnections.Clear();
try
{
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
@@ -20,6 +21,23 @@ namespace Barotrauma
AvailableCharacters.Remove(character);
}
public static int GetSalaryFor(IReadOnlyCollection<CharacterInfo> hires)
{
return hires.Sum(hire => GetSalaryFor(hire));
}
public static int GetSalaryFor(CharacterInfo hire)
{
IEnumerable<Character> crew = GameSession.GetSessionCrewCharacters(CharacterType.Both);
float multiplier = 0;
foreach (var character in crew)
{
multiplier += character?.Info?.GetSavedStatValueWithAll(StatTypes.HireCostMultiplier, hire.Job.Prefab.Identifier) ?? 0;
}
float finalMultiplier = 1f + MathF.Max(multiplier, -1f);
return (int)(hire.Salary * finalMultiplier);
}
public void GenerateCharacters(Location location, int amount)
{
AvailableCharacters.ForEach(c => c.Remove());
@@ -151,6 +151,19 @@ namespace Barotrauma
partial void InitProjSpecific(XElement element);
public Item FindEquippedItemByTag(Identifier tag)
{
if (tag.IsEmpty) { return null; }
for (int i = 0; i < slots.Length; i++)
{
if (SlotTypes[i] == InvSlotType.Any) { continue; }
var item = slots[i].FirstOrDefault();
if (item != null && item.HasTag(tag)) { return item; }
}
return null;
}
public int FindLimbSlot(InvSlotType limbSlot)
{
for (int i = 0; i < slots.Length; i++)
@@ -343,7 +343,12 @@ namespace Barotrauma.Items.Components
OnFailedToOpen();
return;
}
toggleCooldownTimer = ToggleCoolDown;
if (ToggleWhenClicked)
{
//do not activate cooldown at this point if the door doesn't get toggled when clicked
//(i.e. if it just sends out a signal that might get passed back to the door and try to toggle it)
toggleCooldownTimer = ToggleCoolDown;
}
if (IsStuck || IsJammed)
{
#if CLIENT
@@ -404,7 +409,7 @@ namespace Barotrauma.Items.Components
position.Y >= item.Rect.Y + Window.Y &&
position.Y <= item.Rect.Y + Window.Y + Window.Height &&
position.X >= item.Rect.X - maxPerpendicularDistance &&
position.Y <= item.Rect.Right + maxPerpendicularDistance;
position.X <= item.Rect.Right + maxPerpendicularDistance;
}
}
@@ -80,9 +80,6 @@ namespace Barotrauma.Items.Components
[Serialize("0,0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public Vector2 OwnerSheetIndex { get; set; }
[Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public bool SpawnPointTagsGiven { get; set; }
public IdCard(Item item, ContentXElement element) : base(item, element) { }
public void Initialize(WayPoint spawnPoint, Character character)
@@ -439,9 +439,10 @@ namespace Barotrauma.Items.Components
if (targetItem.Removed) { return; }
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
#if CLIENT
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar)
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
(user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
{
Character.Controlled?.UpdateHUDProgressBar(targetItem,
Character.Controlled.UpdateHUDProgressBar(targetItem,
targetItem.WorldPosition,
targetItem.Condition / targetItem.MaxCondition,
emptyColor: GUIStyle.HealthBarColorLow,
@@ -200,7 +200,7 @@ namespace Barotrauma.Items.Components
}
#if CLIENT
if (requiredTime < float.MaxValue)
if (requiredTime < float.MaxValue && picker == Character.Controlled)
{
Character.Controlled?.UpdateHUDProgressBar(
this,
@@ -572,8 +572,15 @@ namespace Barotrauma.Items.Components
structureFixAmount *= 1 + item.GetQualityModifier(Quality.StatType.RepairToolStructureDamageMultiplier);
}
var didLeak = targetStructure.SectionIsLeakingFromOutside(sectionIndex);
targetStructure.AddDamage(sectionIndex, -structureFixAmount * degreeOfSuccess, user);
if (didLeak && !targetStructure.SectionIsLeakingFromOutside(sectionIndex))
{
user.CheckTalents(AbilityEffectType.OnRepairedOutsideLeak);
}
//if the next section is small enough, apply the effect to it as well
//(to make it easier to fix a small "left-over" section)
for (int i = -1; i < 2; i += 2)
@@ -660,9 +667,10 @@ namespace Barotrauma.Items.Components
float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier));
levelResource.DeattachTimer += addedDetachTime;
#if CLIENT
if (targetItem.Prefab.ShowHealthBar)
if (targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
(user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
{
Character.Controlled?.UpdateHUDProgressBar(
Character.Controlled.UpdateHUDProgressBar(
this,
targetItem.WorldPosition,
levelResource.DeattachTimer / levelResource.DeattachDuration,
@@ -60,12 +60,19 @@ namespace Barotrauma.Items.Components
set
{
if (parent == value) { return; }
if (parent != null) { parent.OnActiveStateChanged -= SetActiveState; }
if (value != null) { value.OnActiveStateChanged += SetActiveState; }
if (InheritParentIsActive)
{
if (parent != null) { parent.OnActiveStateChanged -= SetActiveState; }
if (value != null) { value.OnActiveStateChanged += SetActiveState; }
}
parent = value;
}
}
[Serialize(true, IsPropertySaveable.No, description: "If this is a child component of another component, should this component inherit the IsActive state of the parent?")]
public bool InheritParentIsActive { get; set; }
public readonly ContentXElement originalElement;
protected const float CorrectionDelay = 1.0f;
@@ -394,8 +401,11 @@ namespace Barotrauma.Items.Components
if (ic == null) { break; }
ic.Parent = this;
ic.IsActive = isActive;
OnActiveStateChanged += ic.SetActiveState;
if (ic.InheritParentIsActive)
{
ic.IsActive = isActive;
OnActiveStateChanged += ic.SetActiveState;
}
item.AddComponent(ic);
break;
@@ -236,6 +236,12 @@ namespace Barotrauma.Items.Components
get => Inventory.AllItems.Count(it => it.Condition > 0.0f);
}
public int ExtraStackSize
{
get => Inventory.ExtraStackSize;
set => Inventory.ExtraStackSize = value;
}
private readonly ImmutableArray<SlotRestrictions> slotRestrictions;
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
@@ -298,7 +304,10 @@ namespace Barotrauma.Items.Components
}
}
Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow);
// we have to assign this here because the fields are serialized before the inventory is created otherwise
ExtraStackSize = element.GetAttributeInt(nameof(ExtraStackSize), 0);
List<SlotRestrictions> newSlotRestrictions = new List<SlotRestrictions>(totalCapacity);
for (int i = 0; i < capacity; i++)
{
@@ -389,6 +398,7 @@ namespace Barotrauma.Items.Components
public void OnItemContained(Item containedItem)
{
int index = Inventory.FindIndex(containedItem);
RelatedItem relatedItem = null;
if (index >= 0 && index < slotRestrictions.Length)
{
if (slotRestrictions[index].ContainableItems != null)
@@ -397,6 +407,8 @@ namespace Barotrauma.Items.Components
foreach (var containableItem in slotRestrictions[index].ContainableItems)
{
if (!containableItem.MatchesItem(containedItem)) { continue; }
//the 1st matching ContainableItem of the slot determines the hiding, position and rotation of the item
relatedItem ??= containableItem;
foreach (StatusEffect effect in containableItem.StatusEffects)
{
activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition));
@@ -405,7 +417,6 @@ namespace Barotrauma.Items.Components
}
}
var relatedItem = FindContainableItem(containedItem);
var containedItemInfo = new ContainedItem(containedItem,
Hide: relatedItem?.Hide ?? false,
ItemPos: relatedItem?.ItemPos,
@@ -786,12 +797,9 @@ namespace Barotrauma.Items.Components
private RelatedItem FindContainableItem(Item item)
{
var relatedItem = ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item));
if (relatedItem == null && AllSubContainableItems != null)
{
relatedItem = AllSubContainableItems.FirstOrDefault(ci => ci.MatchesItem(item));
}
return relatedItem;
int index = Inventory.FindIndex(item);
if (index == -1 ) { return null; }
return slotRestrictions[index]?.ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item));
}
/// <summary>
@@ -1095,6 +1103,7 @@ namespace Barotrauma.Items.Components
itemIds[i].Add(idRemap.GetOffsetId(id));
}
}
ExtraStackSize = componentElement.GetAttributeInt(nameof(ExtraStackSize), 0);
}
public override XElement Save(XElement parentElement)
@@ -1107,6 +1116,7 @@ namespace Barotrauma.Items.Components
itemIdStrings[i] = string.Join(';', items.Select(it => it.ID.ToString()));
}
componentElement.Add(new XAttribute("contained", string.Join(',', itemIdStrings)));
componentElement.Add(new XAttribute(nameof(ExtraStackSize), ExtraStackSize));
return componentElement;
}
}
@@ -104,7 +104,7 @@ namespace Barotrauma.Items.Components
// doesn't quite work properly, remaining time changes if tinkering stops
float deconstructionSpeedModifier = userDeconstructorSpeedMultiplier * (1f + tinkeringStrength * TinkeringSpeedIncrease);
float deconstructionSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed);
float deconstructionSpeed = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed);
if (DeconstructItemsSimultaneously)
{
@@ -149,13 +149,13 @@ namespace Barotrauma.Items.Components
{
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100));
}
currForce *= item.StatManager.GetAdjustedValue(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier;
currForce *= item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier;
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
{
currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease;
}
currForce = item.StatManager.GetAdjustedValue(ItemTalentStats.EngineSpeed, currForce);
currForce = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineSpeed, currForce);
//less effective when in a bad condition
currForce *= MathHelper.Lerp(0.5f, 2.0f, condition);
@@ -469,7 +469,7 @@ namespace Barotrauma.Items.Components
character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount);
}
user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount);
quality = GetFabricatedItemQuality(fabricatedItem, user);
quality = GetFabricatedItemQuality(fabricatedItem, user).RollQuality();
}
int amount = (int)fabricationitemAmount.Value;
@@ -534,12 +534,10 @@ namespace Barotrauma.Items.Components
{
foreach (Skill skill in fabricatedItem.RequiredSkills)
{
float userSkill = user.GetSkillLevel(skill.Identifier);
float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f);
float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill;
var addedSkillValue = new AbilityFabricatorSkillGain(skill.Identifier, addedSkill);
user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue);
user.Info.IncreaseSkillLevel(
user.Info.ApplySkillGain(
skill.Identifier,
addedSkillValue.Value);
}
@@ -576,10 +574,52 @@ namespace Barotrauma.Items.Components
return currPowerConsumption;
}
private static int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user)
public static float CalculateBonusRollPercentage(float skillLevel, float target)
=> Math.Clamp((skillLevel - target) / (100f - target) * 100f, min: 0, max: 100);
public readonly record struct QualityResult(int Quality, float PlusOnePercentage, float PlusTwoPercentage)
{
if (user?.Info == null) { return 0; }
if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; }
public static readonly QualityResult Empty = new QualityResult(0, 0, 0);
public bool HasRandomQualityRollChance => PlusOnePercentage > 0f || PlusTwoPercentage > 0f;
// The total real world percentage for a roll to succeed, taking into account that +1 needs to succeed for +2 to be attempted and
// that the chance for only +1 goes down as +2 increases since some of the +1's will turn into +2s
public float TotalPlusOnePercentage => Math.Clamp(PlusOnePercentage * (100f - PlusTwoPercentage) / 100f, min: 0, max: 100);
public float TotalPlusTwoPercentage => Math.Clamp(PlusOnePercentage * PlusTwoPercentage / 100f, min: 0, max: 100);
public int RollQuality()
{
int additionalQuality = 0;
if (Roll(PlusOnePercentage))
{
additionalQuality++;
if (Roll(PlusTwoPercentage))
{
additionalQuality++;
}
}
return Quality + additionalQuality;
static bool Roll(float percentage)
=> percentage >= Rand.Range(0, 100, Rand.RandSync.Unsynced);
}
}
public const int PlusOneQualityBonusThreshold = 50,
PlusTwoQualityBonusThreshold = 75;
public const int PlusOneTarget = 100,
PlusTwoTarget = 125;
public const float PlusOneLerp = 0.2f,
PlusTwoLerp = 0.4f;
private static QualityResult GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user)
{
if (user?.Info == null) { return QualityResult.Empty; }
if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return QualityResult.Empty; }
int quality = 0;
float floatQuality = 0.0f;
floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality, includeSaved: false);
@@ -593,34 +633,63 @@ namespace Barotrauma.Items.Components
}
quality = (int)floatQuality;
const int MaxCraftingSkill = 100;
// Use Option here instead of 0 because we want the lowest value and a value of 0 would always be lower than any other chance
Option<float> plusOne = Option.None,
plusTwo = Option.None;
//having a higher-than-100 skill (e.g. due to talents) gives +1 quality
quality += fabricatedItem.RequiredSkills.All(s => user.GetSkillLevel(s.Identifier) >= MaxCraftingSkill) ? 1 : 0;
foreach (var skill in fabricatedItem.RequiredSkills)
{
//+1 quality if the character's skill level is >20% from the min requirement towards max skill
//e.g. if the skill requirement is 10 -> 28
//40 -> 52
//90 -> 92
float skillRequirement = MathHelper.Lerp(skill.Level, MaxCraftingSkill, 0.2f);
if (user.GetSkillLevel(skill.Identifier) > skillRequirement)
float skillLevel = user.GetSkillLevel(skill.Identifier);
if (skillLevel >= PlusOneQualityBonusThreshold)
{
quality += 1;
//+1 quality chance if the character's skill level is >20% from the min requirement towards max skill as well as higher than 50
//e.g. if the skill requirement is 10 -> 28 (but minimum 50 threshold)
//40 -> 52
//90 -> 92
var bonusChance1 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusOneTarget, PlusOneLerp));
plusOne = OverrideChanceIfLess(plusOne, bonusChance1);
if (skillLevel >= PlusTwoQualityBonusThreshold)
{
var bonusChance2 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusTwoTarget, PlusTwoLerp));
plusTwo = OverrideChanceIfLess(plusTwo, bonusChance2);
}
else
{
break;
}
}
else
{
break;
}
static Option<float> OverrideChanceIfLess(Option<float> original, float bonusChance)
{
if (original.TryUnwrap(out var originalChance))
{
return originalChance > bonusChance ? Option.Some(bonusChance) : original;
}
return Option.Some(bonusChance);
}
}
return quality;
return new QualityResult(quality,
PlusOnePercentage: plusOne.Match(some: static f => f, none: static () => 0f),
PlusTwoPercentage: plusTwo.Match(some: static f => f, none: static () => 0f));
}
partial void UpdateRequiredTimeProjSpecific();
private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item)
{
return
return
(user != null && user.HasRecipeForItem(item.Identifier)) ||
GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier));
}
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
{
if (fabricableItem == null) { return false; }
@@ -698,7 +767,7 @@ namespace Barotrauma.Items.Components
//fabricating takes 100 times longer if degree of success is close to 0
//characters with a higher skill than required can fabricate up to 100% faster
float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValue(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f);
float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f);
if (user?.Info is { } info && fabricableItem.TargetItem is { } it)
{
@@ -138,14 +138,14 @@ namespace Barotrauma.Items.Components
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValue(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor;
currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor;
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
{
currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease;
}
currFlow = item.StatManager.GetAdjustedValue(ItemTalentStats.PumpSpeed, currFlow);
currFlow = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpSpeed, currFlow);
//less effective when in a bad condition
currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition);
@@ -875,7 +875,7 @@ namespace Barotrauma.Items.Components
}
}
private float GetMaxOutput() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput);
private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate);
private float GetMaxOutput() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput);
private float GetFuelConsumption() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate);
}
}
@@ -336,8 +336,7 @@ namespace Barotrauma.Items.Components
{
showIceSpireWarning = false;
if (user != null && user.Info != null &&
user.SelectedItem == item &&
controlledSub != null && controlledSub.Velocity.LengthSquared() > 0.01f)
user.SelectedItem == item)
{
IncreaseSkillLevel(user, deltaTime);
}
@@ -402,14 +401,15 @@ namespace Barotrauma.Items.Components
private void IncreaseSkillLevel(Character user, float deltaTime)
{
if (controlledSub == null) { return; }
if (controlledSub.Velocity.LengthSquared() < 0.01f) { return; }
if (user?.Info == null) { return; }
// Do not increase the helm skill when "steering" the sub while docked into something static (e.g. outpost or wreck)
if (GameMain.GameSession?.Campaign != null && controlledSub != null && controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; }
if (GameMain.GameSession?.Campaign != null&& controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; }
float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f;
user.Info.IncreaseSkillLevel(
"helm".ToIdentifier(),
SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime);
float speedMultiplier = MathHelper.Clamp(TargetVelocity.Length() / 100.0f, 0.0f, 1.0f);
user.Info.ApplySkillGain(Tags.HelmSkill,
SkillSettings.Current.SkillIncreasePerSecondWhenSteering * speedMultiplier * deltaTime);
}
private void UpdateAutoPilot(float deltaTime)
@@ -395,6 +395,6 @@ namespace Barotrauma.Items.Components
}
}
public float GetCapacity() => item.StatManager.GetAdjustedValue(ItemTalentStats.BatteryCapacity, Capacity);
public float GetCapacity() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.BatteryCapacity, Capacity);
}
}
@@ -454,6 +454,7 @@ namespace Barotrauma.Items.Components
base.RemoveComponentSpecific();
connectedRecipients?.Clear();
connectionDirty?.Clear();
recipientsToRefresh.Clear();
}
}
}
@@ -1017,9 +1017,10 @@ namespace Barotrauma.Items.Components
{
attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f);
#if CLIENT
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar)
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
(User == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
{
Character.Controlled?.UpdateHUDProgressBar(targetItem,
Character.Controlled.UpdateHUDProgressBar(targetItem,
targetItem.WorldPosition,
targetItem.Condition / targetItem.MaxCondition,
emptyColor: GUIStyle.HealthBarColorLow,
@@ -495,9 +495,7 @@ namespace Barotrauma.Items.Components
{
foreach (Skill skill in requiredSkills)
{
float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier);
CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier,
SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f));
CurrentFixer.Info?.ApplySkillGain(skill.Identifier, SkillSettings.Current.SkillIncreasePerRepair);
}
SteamAchievementManager.OnItemRepaired(item, CurrentFixer);
CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete, new AbilityRepairable(item));
@@ -570,7 +568,7 @@ namespace Barotrauma.Items.Components
if (item.ConditionPercentage > MinDeteriorationCondition)
{
float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed);
float deteriorationSpeed = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed);
if (ForceDeteriorationTimer > 0.0f) { deteriorationSpeed = Math.Max(deteriorationSpeed, 1.0f); }
item.Condition -= deteriorationSpeed * deltaTime;
}
@@ -157,7 +157,7 @@ namespace Barotrauma.Items.Components
delayedElementToLoad = Option.None;
}
private void LoadFromXML(ContentXElement loadElement)
public void LoadFromXML(ContentXElement loadElement)
{
foreach (var subElement in loadElement.Elements())
{
@@ -383,6 +383,12 @@ namespace Barotrauma.Items.Components
wire.RemoveConnection(item);
}
}
c.Grid = null;
}
foreach (var connection in Connections)
{
Powered.ChangedConnections.Remove(connection);
connection.Recipients.Clear();
}
Connections.Clear();
@@ -305,16 +305,17 @@ namespace Barotrauma.Items.Components
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
(IsActiveConditionals == null || IsActiveConditionals.Count == 0))
{
if (item.body != null && !item.body.Enabled)
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
}
else
if (item.body == null || item.body.Enabled ||
(item.ParentInventory is ItemInventory itemInventory && !itemInventory.Container.HideItems))
{
lightBrightness = 1.0f;
SetLightSourceState(true, lightBrightness);
}
else
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
}
isOn = true;
SetLightSourceTransformProjSpecific();
base.IsActive = false;
@@ -341,8 +342,22 @@ namespace Barotrauma.Items.Components
#if CLIENT
Light.ParentSub = item.Submarine;
#endif
bool visibleInContainer;
var ownerCharacter = item.GetRootInventoryOwner() as Character;
if ((item.Container != null && ownerCharacter == null) ||
if (ownerCharacter != null && item.RootContainer?.GetComponent<Holdable>() is not { IsActive: true })
{
//if the item is in a character inventory, the light should only be visible if the character is holding the item
//(not if it's e.q. inside a wearable item, or in a rifle worn on the back)
visibleInContainer = false;
}
else
{
visibleInContainer = item.FindParentInventory(static it => it is ItemInventory { Container.HideItems: true }) == null;
}
if ((item.Container != null && !visibleInContainer && ownerCharacter == null) ||
(ownerCharacter != null && ownerCharacter.InvisibleTimer > 0.0f))
{
lightBrightness = 0.0f;
@@ -352,7 +367,7 @@ namespace Barotrauma.Items.Components
SetLightSourceTransformProjSpecific();
PhysicsBody body = ParentBody ?? item.body;
if (body != null && !body.Enabled)
if (body != null && !body.Enabled && !visibleInContainer)
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
@@ -432,6 +447,11 @@ namespace Barotrauma.Items.Components
target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness);
}
public override void Drop(Character dropper, bool setTransform = true)
{
SetLightSourceTransform();
}
partial void SetLightSourceState(bool enabled, float brightness);
public void SetLightSourceTransform()
@@ -275,6 +275,15 @@ namespace Barotrauma.Items.Components
}
}
protected override void RemoveComponentSpecific()
{
if (PhysicsBody != null)
{
PhysicsBody.Remove();
PhysicsBody = null;
}
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
base.ReceiveSignal(signal, connection);
@@ -61,6 +61,22 @@ namespace Barotrauma.Items.Components
private const float CrewAiFindTargetMaxInterval = 1.0f;
private const float CrewAIFindTargetMinInverval = 0.2f;
/// <summary>
/// Bots consider the projectile to move at least this fast when calculating how far ahead a moving target they need to aim.
/// Aiming ahead doesn't work reliably with very slow projectiles, because we'd need to take into account drag and gravity,
/// and the target would most likely move in a different direction anyway before the projectile reaches it.
/// </summary>
private const float MinimumProjectileVelocityForAimAhead = 20.0f;
/// <summary>
/// Bots don't try to aim ahead a moving target by more than this amount. If the target is very fast and/or the projectile very slow,
/// we'd need to aim so far ahead it'd most likely fail anyway.
/// </summary>
private const float MaximumAimAhead = 10.0f;
private float projectileSpeed;
private Item previousAmmo;
private int currentLoaderIndex;
private const float TinkeringPowerCostReduction = 0.2f;
@@ -563,8 +579,9 @@ namespace Barotrauma.Items.Components
// Do not increase the weapons skill when operating a turret in an outpost level
if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedFriendlyOutpost))
{
user.Info.IncreaseSkillLevel("weapons".ToIdentifier(),
SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f));
user.Info.ApplySkillGain(
Tags.WeaponsSkill,
SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime);
}
float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f);
@@ -664,6 +681,11 @@ namespace Barotrauma.Items.Components
return GetAvailableInstantaneousBatteryPower() >= GetPowerRequiredToShoot();
}
private Vector2 GetBarrelDir()
{
return new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
}
private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false)
{
tryingToCharge = true;
@@ -709,7 +731,8 @@ namespace Barotrauma.Items.Components
ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
if (projectileContainer != null && projectileContainer.Item != item)
{
projectileContainer?.Item.Use(deltaTime);
//user needs to be null because the ammo boxes shouldn't be directly usable by characters
projectileContainer?.Item.Use(deltaTime, user: null, userForOnUsedEvent: user);
}
}
else
@@ -735,7 +758,7 @@ namespace Barotrauma.Items.Components
ItemContainer projectileContainer = containerItem.GetComponent<ItemContainer>();
if (projectileContainer != null)
{
containerItem.Use(deltaTime);
containerItem.Use(deltaTime, user: null, userForOnUsedEvent: user);
projectiles = GetLoadedProjectiles();
if (projectiles.Any()) { return true; }
}
@@ -930,6 +953,7 @@ namespace Barotrauma.Items.Components
Projectile projectileComponent = projectile.GetComponent<Projectile>();
if (projectileComponent != null)
{
TryDetermineProjectileSpeed(projectileComponent);
projectileComponent.Launcher = item;
projectileComponent.Attacker = projectileComponent.User = user;
if (projectileComponent.Attack != null)
@@ -960,6 +984,16 @@ namespace Barotrauma.Items.Components
LaunchProjSpecific();
}
private void TryDetermineProjectileSpeed(Projectile projectile)
{
if (projectile != null && !projectile.Hitscan)
{
projectileSpeed =
ConvertUnits.ToDisplayUnits(
MathHelper.Clamp((projectile.LaunchImpulse + LaunchImpulse) / projectile.Item.body.Mass, MinimumProjectileVelocityForAimAhead, NetConfig.MaxPhysicsBodyVelocity));
}
}
partial void LaunchProjSpecific();
private static void ShiftItemsInProjectileContainer(ItemContainer container)
@@ -1143,7 +1177,7 @@ namespace Barotrauma.Items.Components
if (target is Hull targetHull)
{
Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
Vector2 barrelDir = GetBarrelDir();
if (!MathUtils.GetLineRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _))
{
return;
@@ -1244,8 +1278,24 @@ namespace Barotrauma.Items.Components
if (container != null)
{
maxProjectileCount += container.Capacity;
int projectiles = projectileContainer.ContainedItems.Count(it => it.Condition > 0.0f);
usableProjectileCount += projectiles;
var projectiles = projectileContainer.ContainedItems.Where(it => it.Condition > 0.0f);
var firstProjectile = projectiles.FirstOrDefault();
if (firstProjectile?.Prefab != previousAmmo?.Prefab)
{
//assume the projectiles are infinitely fast (no aiming ahead of the target) if we can't find projectiles to calculate the speed based on,
//and if the projectile type isn't the same as before
projectileSpeed = float.PositiveInfinity;
}
previousAmmo = firstProjectile;
if (projectiles.Any())
{
var projectile =
firstProjectile.GetComponent<Projectile>() ??
firstProjectile.ContainedItems.FirstOrDefault()?.GetComponent<Projectile>();
TryDetermineProjectileSpeed(projectile);
usableProjectileCount += projectiles.Count();
}
}
}
}
@@ -1409,6 +1459,7 @@ namespace Barotrauma.Items.Components
targetPos = currentTarget.WorldPosition;
}
bool iceSpireSpotted = false;
Vector2 targetVelocity = Vector2.Zero;
// Adjust the target character position (limb or submarine)
if (currentTarget is Character targetCharacter)
{
@@ -1424,20 +1475,39 @@ namespace Barotrauma.Items.Components
else
{
// Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head.
float closestDist = closestDistance;
float closestDistSqr = closestDistance;
foreach (Limb limb in targetCharacter.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.Hidden) { continue; }
if (!IsWithinAimingRadius(limb.WorldPosition)) { continue; }
float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition);
if (dist < closestDist)
float distSqr = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition);
if (distSqr < closestDistSqr)
{
closestDist = dist;
closestDistSqr = distSqr;
if (limb == targetCharacter.AnimController.MainLimb)
{
//prefer main limb (usually a much better target than the extremities that are often the closest limbs)
closestDistSqr *= 0.5f;
}
targetPos = limb.WorldPosition;
}
}
if (closestDist > shootDistance * shootDistance)
if (projectileSpeed < float.PositiveInfinity && targetPos.HasValue)
{
//lead the target (aim where the target will be in the future)
float dist = MathF.Sqrt(closestDistSqr);
float projectileMovementTime = dist / projectileSpeed;
targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity;
Vector2 movementAmount = targetVelocity * projectileMovementTime;
//don't try to compensate more than 10 meters - if the target is so fast or the projectile so slow we need to go beyond that,
//it'd most likely fail anyway
movementAmount = ConvertUnits.ToDisplayUnits(movementAmount.ClampLength(MaximumAimAhead));
Vector2 futurePosition = targetPos.Value + movementAmount;
targetPos = Vector2.Lerp(targetPos.Value, futurePosition, DegreeOfSuccess(character));
}
if (closestDistSqr > shootDistance * shootDistance)
{
aiFindTargetTimer = CrewAIFindTargetMinInverval;
ResetTarget();
@@ -1512,7 +1582,9 @@ namespace Barotrauma.Items.Components
if (targetPos == null) { return false; }
// Force the highest priority so that we don't change the objective while targeting enemies.
objective.ForceHighestPriority = true;
#if CLIENT
debugDrawTargetPos = targetPos.Value;
#endif
if (closestEnemy != null && character.AIController.SelectedAiTarget != closestEnemy.AiTarget)
{
if (character.IsOnPlayerTeam)
@@ -1563,8 +1635,28 @@ namespace Barotrauma.Items.Components
if (IsPointingTowards(targetPos.Value))
{
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
Vector2 barrelDir = GetBarrelDir();
Vector2 aimStartPos = item.WorldPosition;
Vector2 aimEndPos = item.WorldPosition + barrelDir * shootDistance;
bool allowShootingIfNothingInWay = false;
if (currentTarget != null)
{
Vector2 targetStartPos = currentTarget.WorldPosition;
Vector2 targetEndPos = currentTarget.WorldPosition + targetVelocity * ConvertUnits.ToDisplayUnits(MaximumAimAhead);
//if there's nothing in the way (not even the target we're trying to aim towards),
//shooting should only be allowed if we're aiming ahead of the target, in which case it's to be expected that we're aiming at "thin air"
allowShootingIfNothingInWay =
targetVelocity.LengthSquared() > 0.001f &&
MathUtils.LineSegmentsIntersect(
aimStartPos, aimEndPos,
targetStartPos, targetEndPos) &&
//target needs to be moving roughly perpendicular to us for aiming ahead of it to make sense
Math.Abs(Vector2.Dot(Vector2.Normalize(aimEndPos - aimStartPos), Vector2.Normalize(targetEndPos - targetStartPos))) < 0.5f;
}
Vector2 start = ConvertUnits.ToSimUnits(aimStartPos);
Vector2 end = ConvertUnits.ToSimUnits(aimEndPos);
// Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target.
Body worldTarget = CheckLineOfSight(start, end);
if (closestEnemy != null && closestEnemy.Submarine != null)
@@ -1572,11 +1664,13 @@ namespace Barotrauma.Items.Components
start -= closestEnemy.Submarine.SimPosition;
end -= closestEnemy.Submarine.SimPosition;
Body transformedTarget = CheckLineOfSight(start, end);
canShoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character));
canShoot =
CanShoot(transformedTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay) &&
(worldTarget == null || CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay));
}
else
{
canShoot = CanShoot(worldTarget, character);
canShoot = CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay);
}
if (!canShoot) { return false; }
if (character.IsOnPlayerTeam)
@@ -1666,9 +1760,13 @@ namespace Barotrauma.Items.Components
}
}
private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true)
private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true, bool allowShootingIfNothingInWay = false)
{
if (targetBody == null) { return false; }
if (targetBody == null)
{
//nothing in the way (not even the target we're trying to shoot) -> no point in firing at thin air
return allowShootingIfNothingInWay;
}
Character targetCharacter = null;
if (targetBody.UserData is Character c)
{
@@ -559,6 +559,7 @@ namespace Barotrauma.Items.Components
foreach (WearableSprite wearableSprite in wearableSprites)
{
wearableSprite?.Sprite?.Remove();
wearableSprite.Picker = null;
}
}
@@ -230,11 +230,19 @@ namespace Barotrauma
protected readonly int capacity;
protected readonly ItemSlot[] slots;
public bool Locked;
protected float syncItemsDelay;
private int extraStackSize;
public int ExtraStackSize
{
get => extraStackSize;
set => extraStackSize = MathHelper.Max(value, 0);
}
/// <summary>
/// All items contained in the inventory. Stacked items are returned as individual instances. DO NOT modify the contents of the inventory while enumerating this list.
/// </summary>
@@ -383,7 +383,7 @@ namespace Barotrauma
public float RotationRad { get; private set; }
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, MinValueFloat = 0.0f, MaxValueFloat = 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, IsPropertySaveable.Yes)]
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)]
public float Rotation
{
get
@@ -393,7 +393,7 @@ namespace Barotrauma
set
{
if (!Prefab.AllowRotatingInEditor) { return; }
RotationRad = MathHelper.ToRadians(value);
RotationRad = MathUtils.WrapAnglePi(MathHelper.ToRadians(value));
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen)
{
@@ -1327,8 +1327,11 @@ namespace Barotrauma
}
}
if (FlippedX) clone.FlipX(false);
if (FlippedY) clone.FlipY(false);
if (FlippedX) { clone.FlipX(false); }
if (FlippedY) { clone.FlipY(false); }
// Flipping an item tampers with its rotation, so restore it
clone.Rotation = Rotation;
foreach (ItemComponent component in clone.components)
{
@@ -1640,6 +1643,9 @@ namespace Barotrauma
return transformedRect;
}
public override Quad2D GetTransformedQuad()
=> Quad2D.FromSubmarineRectangle(rect).Rotated(-RotationRad);
/// <summary>
/// goes through every item and re-checks which hull they are in
/// </summary>
@@ -2516,7 +2522,7 @@ namespace Barotrauma
if (Prefab.AllowRotatingInEditor)
{
RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad);
RotationRad = MathUtils.WrapAnglePi(-RotationRad);
}
#if CLIENT
if (Prefab.CanSpriteFlipX)
@@ -2543,6 +2549,10 @@ namespace Barotrauma
return;
}
if (Prefab.AllowRotatingInEditor)
{
RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad);
}
#if CLIENT
if (Prefab.CanSpriteFlipY)
{
@@ -3043,7 +3053,10 @@ namespace Barotrauma
return -1;
}
public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null)
/// <param name="userForOnUsedEvent">User to pass to the OnUsed event. May need to be different than the user in cases like loaders using ammo boxes:
/// the box is technically being used by the loader, and doesn't allow a character to use it, but we may still need to know which character caused
/// the box to be used.</param>
public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null, Character userForOnUsedEvent = null)
{
if (RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim)))
{
@@ -3068,7 +3081,7 @@ namespace Barotrauma
ic.PlaySound(ActionType.OnUse, user);
#endif
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, user, targetLimb, useTarget: useTarget, user: user);
ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user));
ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user ?? userForOnUsedEvent));
if (ic.DeleteOnUse) { remove = true; }
}
}
@@ -3526,7 +3539,26 @@ namespace Barotrauma
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
if (!CanClientAccess(sender) || !(property.GetAttribute<ConditionallyEditable>()?.IsEditable(this) ?? true))
bool conditionAllowsEditing = true;
if (property.GetAttribute<ConditionallyEditable>() is { } condition)
{
conditionAllowsEditing = condition.IsEditable(this);
}
bool canAccess = false;
if (Container?.GetComponent<CircuitBox>() != null &&
Container.CanClientAccess(sender))
{
//items inside circuit boxes are inaccessible by "normal" means,
//but the properties can still be edited through the circuit box UI
canAccess = true;
}
else
{
canAccess = CanClientAccess(sender);
}
if (!canAccess || !conditionAllowsEditing)
{
allowEditing = false;
}
@@ -3799,6 +3831,11 @@ namespace Barotrauma
}
break;
}
case "itemstats":
{
item.StatManager.Load(subElement);
break;
}
default:
{
ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString());
@@ -3924,7 +3961,7 @@ namespace Barotrauma
foreach (ItemComponent component in item.components)
{
if (component.Parent != null) { component.IsActive = component.Parent.IsActive; }
if (component.Parent != null && component.InheritParentIsActive) { component.IsActive = component.Parent.IsActive; }
component.OnItemLoaded();
}
@@ -3994,6 +4031,8 @@ namespace Barotrauma
upgrade.Save(element);
}
statManager?.Save(element);
element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture)));
var conditionAttribute = element.GetAttribute("condition");
@@ -23,9 +23,10 @@ namespace Barotrauma
Upgrade = 8,
ItemStat = 9,
DroppedStack = 10,
SetHighlight = 11,
MinValue = 0,
MaxValue = 10
MaxValue = 11
}
public interface IEventData : NetEntityEvent.IData
@@ -871,24 +871,43 @@ namespace Barotrauma
public int GetMaxStackSize(Inventory inventory)
{
int extraStackSize = inventory switch
{
ItemInventory { Owner: Item it } i => (int)it.StatManager.GetAdjustedValueAdditive(ItemTalentStats.ExtraStackSize, i.ExtraStackSize),
CharacterInventory { Owner: Character { Info: { } info } } i => i.ExtraStackSize + (int)info.GetSavedStatValueWithAll(StatTypes.InventoryExtraStackSize, Category.ToIdentifier()),
not null => inventory.ExtraStackSize,
null => 0
};
if (inventory is CharacterInventory && maxStackSizeCharacterInventory > 0)
{
return maxStackSizeCharacterInventory;
return MaxStackWithExtra(maxStackSizeCharacterInventory, extraStackSize);
}
else if (inventory?.Owner is Item item &&
(item.GetComponent<Holdable>() is { Attachable: false } || item.GetComponent<Wearable>() != null))
{
if (maxStackSizeHoldableOrWearableInventory > 0)
{
return maxStackSizeHoldableOrWearableInventory;
return MaxStackWithExtra(maxStackSizeHoldableOrWearableInventory, extraStackSize);
}
else if (maxStackSizeCharacterInventory > 0)
{
//if maxStackSizeHoldableOrWearableInventory is not set, it defaults to maxStackSizeCharacterInventory
return maxStackSizeCharacterInventory;
return MaxStackWithExtra(maxStackSizeCharacterInventory, extraStackSize);
}
}
return maxStackSize;
return MaxStackWithExtra(maxStackSize, extraStackSize);
static int MaxStackWithExtra(int maxStackSize, int extraStackSize)
{
extraStackSize = Math.Max(extraStackSize, 0);
if (maxStackSize == 1)
{
return Math.Min(maxStackSize, Inventory.MaxPossibleStackSize);
}
return Math.Min(maxStackSize + extraStackSize, Inventory.MaxPossibleStackSize);
}
}
[Serialize(false, IsPropertySaveable.No)]
@@ -1138,10 +1157,13 @@ namespace Barotrauma
if (fabricationRecipes.TryGetValue(newRecipe.RecipeHash, out var prevRecipe))
{
//the errors below may be caused by a mod overriding a base item instead of this one, log the package of the base item in that case
var packageToLog = GetParentModPackageOrThisPackage();
var packageToLog =
(variantOf.ContentPackage != null && variantOf.ContentPackage != ContentPackageManager.VanillaCorePackage) ?
variantOf.ContentPackage :
GetParentModPackageOrThisPackage();
int prevRecipeIndex = loadedRecipes.IndexOf(prevRecipe);
DebugConsole.ThrowError(
DebugConsole.AddWarning(
$"Error in item prefab \"{ToString()}\": " +
$"Fabrication recipe #{loadedRecipes.Count + 1} has the same hash as recipe #{prevRecipeIndex + 1}. This is most likely caused by identical, duplicate recipes. " +
$"This will cause issues with fabrication.",
@@ -2,24 +2,46 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
[NetworkSerialize]
internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option<UInt32> UniqueCharacterId) : INetSerializableStruct
internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option<UInt32> UniqueCharacterId, bool Save) : INetSerializableStruct
{
/// <summary>
/// Stackable identifiers feature a unique ID to allow multiple stats applied by the same talent from different characters to coexist.
/// </summary>
public static TalentStatIdentifier CreateStackable(ItemTalentStats stat, Identifier talentIdentifier, UInt32 characterId)
=> new(stat, talentIdentifier, Option<UInt32>.Some(characterId));
=> new(stat, talentIdentifier, Option<UInt32>.Some(characterId), Save: false);
/// <summary>
/// Unstackable identifiers do not have a unique ID causing them to be identical to other stats applied by the same talent from different characters and thus only one of them will be applied.
/// <see cref="ItemStatManager.ApplyStat"/> will always use the highest value for unstackable stats.
/// </summary>
public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier)
=> new(stat, talentIdentifier, Option.None);
public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier, bool Save)
=> new(stat, talentIdentifier, Option.None, Save);
public XElement Serialize()
=> new XElement("Stat",
new XAttribute("type", Stat),
new XAttribute("talent", TalentIdentifier));
public static Option<TalentStatIdentifier> TryLoadFromXML(XElement element)
{
var stat = element.GetAttributeEnum("type", ItemTalentStats.None);
var talentIdentifier = element.GetAttributeIdentifier("talent", Identifier.Empty);
if (stat == ItemTalentStats.None || talentIdentifier == Identifier.Empty)
{
var error = $"Failed to load talent stat identifier from XML {element}";
DebugConsole.ThrowError(error);
GameAnalyticsManager.AddErrorEventOnce("ItemStatManager.TryLoadFromXML:Invalid", GameAnalyticsManager.ErrorSeverity.Error, error);
return Option.None;
}
return Option.Some(CreateUnstackable(stat, talentIdentifier, true));
}
}
internal sealed class ItemStatManager
@@ -29,14 +51,14 @@ namespace Barotrauma
public ItemStatManager(Item item) => this.item = item;
public void ApplyStat(ItemTalentStats stat, bool stackable, float value, CharacterTalent talent)
public void ApplyStat(ItemTalentStats stat, bool stackable, bool save, float value, CharacterTalent talent)
{
if (talent.Character?.ID is not { } characterId ||
talent.Prefab?.Identifier is not { } talentIdentifier) { return; }
var identifier = stackable
? TalentStatIdentifier.CreateStackable(stat, talentIdentifier, characterId)
: TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier);
: TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier, save);
if (!stackable)
{
@@ -57,12 +79,45 @@ namespace Barotrauma
#endif
}
public void Save(XElement parent)
{
var element = new XElement("itemstats");
foreach (var (key, value) in talentStats)
{
if (!key.Save) { continue; }
var statElement = key.Serialize();
statElement.Add(new XAttribute("value", value));
element.Add(statElement);
}
parent.Add(element);
}
public void Load(XElement element)
{
foreach (XElement statElement in element.Elements())
{
if (!TalentStatIdentifier.TryLoadFromXML(statElement).TryUnwrap(out var identifier)) { continue; }
var value = statElement.GetAttributeFloat("value", 0f);
ApplyStatDirect(identifier, value);
}
}
/// <summary>
/// Used for setting the value value from network packet; bypassing all validity checks.
/// </summary>
public void ApplyStatDirect(TalentStatIdentifier identifier, float value) => talentStats[identifier] = value;
public void ApplyStatDirect(TalentStatIdentifier identifier, float value)
=> talentStats[identifier] = value;
public float GetAdjustedValue(ItemTalentStats stat, float originalValue)
/// <summary>
/// Adjusts the value by multiplying it with the value of the talent stat
/// </summary>
public float GetAdjustedValueMultiplicative(ItemTalentStats stat, float originalValue)
{
float total = originalValue;
@@ -74,5 +129,21 @@ namespace Barotrauma
return total;
}
/// <summary>
/// Adjusts the value by adding the value of the talent stat instead of multiplying it
/// </summary>
public float GetAdjustedValueAdditive(ItemTalentStats stat, float originalValue)
{
float total = originalValue;
foreach (var (key, value) in talentStats)
{
if (key.Stat != stat) { continue; }
total += value;
}
return total;
}
}
}
@@ -442,6 +442,11 @@ namespace Barotrauma
public List<DummyFireSource> FakeFireSources { get; private set; }
/// <summary>
/// Can be used by conditionals
/// </summary>
public int FireCount => FireSources?.Count ?? 0;
public BallastFloraBehavior BallastFlora { get; set; }
public Hull(Rectangle rectangle)
@@ -633,14 +633,14 @@ namespace Barotrauma
{
endHole = new Tunnel(
TunnelType.SidePath,
new List<Point>() { startPosition, startExitPosition, new Point(0, Size.Y) },
new List<Point>() { startPosition, new Point(0, startPosition.Y) },
minWidth, parentTunnel: mainPath);
}
else
{
endHole = new Tunnel(
TunnelType.SidePath,
new List<Point>() { endPosition, endExitPosition, Size },
new List<Point>() { endPosition, new Point(Size.X, endPosition.Y) },
minWidth, parentTunnel: mainPath);
}
Tunnels.Add(endHole);
@@ -4122,7 +4122,7 @@ namespace Barotrauma
if (location != null)
{
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.Name}, level type: {LevelData.Type})");
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.DisplayName}, level type: {LevelData.Type})");
outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost);
}
else
@@ -4230,7 +4230,20 @@ namespace Barotrauma
}
}
spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1);
Vector2 preferredSpawnPos = i == 0 ? StartPosition : EndPosition;
//if we're placing the outpost at the end of the level, close to the bottom-right,
//and there's a hole leading out the right side of the level, move the spawn position towards that hole.
//Makes outpost placement a little nicer in levels with lots of verticality: if there's a tall vertical
//shaft leading down to the end position, we don't want the outpost to be placed all the way up to wherever the
//ceiling is at the top of that shaft.
if (i == 1 && GenerationParams.CreateHoleNextToEnd &&
preferredSpawnPos.X > Size.X * 0.75f &&
preferredSpawnPos.Y < Size.Y * 0.25f)
{
preferredSpawnPos.X = (preferredSpawnPos.X + Size.X) / 2;
}
spawnPos = outpost.FindSpawnPos(preferredSpawnPos, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1);
if (Type == LevelData.LevelType.Outpost)
{
spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2);
@@ -4254,7 +4267,7 @@ namespace Barotrauma
if (StartLocation != null)
{
outpost.TeamID = StartLocation.Type.OutpostTeam;
outpost.Info.Name = StartLocation.Name;
outpost.Info.Name = StartLocation.DisplayName.Value;
}
}
else
@@ -4263,7 +4276,7 @@ namespace Barotrauma
if (EndLocation != null)
{
outpost.TeamID = EndLocation.Type.OutpostTeam;
outpost.Info.Name = EndLocation.Name;
outpost.Info.Name = EndLocation.DisplayName.Value;
}
}
}
@@ -241,7 +241,7 @@ namespace Barotrauma
/// </summary>
public LevelData(Location location, Map map, float difficulty)
{
Seed = location.BaseName + map.Locations.IndexOf(location);
Seed = location.NameIdentifier.Value + map.Locations.IndexOf(location);
Biome = location.Biome;
Type = LevelType.Outpost;
Difficulty = difficulty;
@@ -639,6 +639,7 @@ namespace Barotrauma
public override void Remove()
{
objectsInRange.Clear();
if (objects != null)
{
foreach (LevelObject obj in objects)
@@ -55,8 +55,17 @@ namespace Barotrauma
public readonly List<LocationConnection> Connections = new List<LocationConnection>();
private string baseName;
public LocalizedString DisplayName { get; private set; }
public Identifier NameIdentifier => nameIdentifier;
private int nameFormatIndex;
private Identifier nameIdentifier;
/// <summary>
/// For backwards compatibility: a non-localizable name from the old text files.
/// </summary>
private string rawName;
private LocationType addInitialMissionsForType;
@@ -75,10 +84,6 @@ namespace Barotrauma
public bool DisallowLocationTypeChanges;
public string BaseName { get => baseName; }
public string Name { get; private set; }
public Biome Biome { get; set; }
public Vector2 MapPosition { get; private set; }
@@ -309,7 +314,7 @@ namespace Barotrauma
if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive)
{
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false));
price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, new Identifier("all")));
price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, Tags.StatIdentifierTargetAll));
price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag)));
}
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false));
@@ -484,7 +489,7 @@ namespace Barotrauma
{
if (missionIndex < 0 || missionIndex >= availableMissions.Count)
{
DebugConsole.ThrowError($"Failed to select a mission in location \"{Name}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})");
DebugConsole.ThrowError($"Failed to select a mission in location \"{DisplayName}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})");
break;
}
selectedMissions.Add(availableMissions[missionIndex]);
@@ -536,15 +541,15 @@ namespace Barotrauma
public override string ToString()
{
return $"Location ({Name ?? "null"})";
return $"Location ({DisplayName ?? "null"})";
}
public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
{
Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost);
Name = RandomName(Type, rand, existingLocations);
CreateRandomName(Type, rand, existingLocations);
MapPosition = mapPosition;
PortraitId = ToolBox.StringToInt(Name);
PortraitId = ToolBox.StringToInt(nameIdentifier.Value);
Connections = new List<LocationConnection>();
}
@@ -561,9 +566,21 @@ namespace Barotrauma
GetTypeOrFallback(originalLocationTypeId, out LocationType originalType);
OriginalType = originalType;
baseName = element.GetAttributeString("basename", "");
Name = element.GetAttributeString("name", "");
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
nameIdentifier = element.GetAttributeIdentifier(nameof(nameIdentifier), "");
if (nameIdentifier.IsEmpty)
{
//backwards compatibility
rawName = element.GetAttributeString("basename", "");
nameIdentifier = rawName.ToIdentifier();
DisplayName = element.GetAttributeString("name", "");
}
else
{
nameFormatIndex = element.GetAttributeInt(nameof(nameFormatIndex), 0);
DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
}
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false);
@@ -641,7 +658,7 @@ namespace Barotrauma
LevelData = new LevelData(element.GetChildElement("Level"), clampDifficultyToBiome: true);
PortraitId = ToolBox.StringToInt(Name);
PortraitId = ToolBox.StringToInt(!rawName.IsNullOrEmpty() ? rawName : nameIdentifier.Value);
LoadStores(element);
LoadMissions(element);
@@ -687,7 +704,7 @@ namespace Barotrauma
int locationTypeChangeIndex = subElement.GetAttributeInt("index", 0);
if (locationTypeChangeIndex < 0 || locationTypeChangeIndex >= Type.CanChangeTo.Count)
{
DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{Name}\". Location index out of bounds ({locationTypeChangeIndex}).");
DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{DisplayName}\". Location index out of bounds ({locationTypeChangeIndex}).");
continue;
}
PendingLocationTypeChange = (Type.CanChangeTo[locationTypeChangeIndex], timer, null);
@@ -698,7 +715,7 @@ namespace Barotrauma
var mission = MissionPrefab.Prefabs[missionIdentifier];
if (mission == null)
{
DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{Name}\". Matching mission not found.");
DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{DisplayName}\". Matching mission not found.");
continue;
}
PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission);
@@ -735,14 +752,27 @@ namespace Barotrauma
if (newType == null)
{
DebugConsole.ThrowError($"Failed to change the type of the location \"{Name}\" to null.\n" + Environment.StackTrace.CleanupStackTrace());
DebugConsole.ThrowError($"Failed to change the type of the location \"{DisplayName}\" to null.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType);
Type = newType;
Name = Type.NameFormats == null || !Type.NameFormats.Any() ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
if (rawName != null)
{
DebugConsole.Log($"Location {rawName} changed it's type from {Type} to {newType}");
DisplayName =
Type.NameFormats == null || !Type.NameFormats.Any() ?
rawName :
Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", rawName);
}
else
{
DebugConsole.Log($"Location {DisplayName.Value} changed it's type from {Type} to {newType}");
DisplayName =
Type.NameFormats == null || !Type.NameFormats.Any() ?
TextManager.Get(nameIdentifier) :
Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", TextManager.Get(nameIdentifier).Value);
}
if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
{
@@ -1058,12 +1088,12 @@ namespace Barotrauma
{
if (!Type.HasHireableCharacters)
{
DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace());
DebugConsole.ThrowError("Cannot hire a character from location \"" + DisplayName + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
if (HireManager == null)
{
DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace());
DebugConsole.ThrowError("Cannot hire a character from location \"" + DisplayName + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
@@ -1086,22 +1116,52 @@ namespace Barotrauma
return HireManager.AvailableCharacters;
}
private string RandomName(LocationType type, Random rand, IEnumerable<Location> existingLocations)
private void CreateRandomName(LocationType type, Random rand, IEnumerable<Location> existingLocations)
{
if (!type.ForceLocationName.IsNullOrEmpty())
if (!type.ForceLocationName.IsEmpty)
{
baseName = type.ForceLocationName.Value;
return baseName;
nameIdentifier = type.ForceLocationName;
DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value);
return;
}
nameIdentifier = type.GetRandomNameId(rand, existingLocations);
if (nameIdentifier.IsEmpty)
{
rawName = type.GetRandomRawName(rand, existingLocations);
if (rawName.IsNullOrEmpty())
{
DebugConsole.ThrowError($"Failed to generate a name for a location of the type {type.Identifier}. No names found in localization files or the .txt files.");
rawName = "none";
}
nameIdentifier = rawName.ToIdentifier();
DisplayName = rawName;
}
else
{
if (type.NameFormats == null || !type.NameFormats.Any())
{
DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value);
return;
}
nameFormatIndex = rand.Next() % type.NameFormats.Count;
DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
}
baseName = type.GetRandomName(rand, existingLocations);
if (type.NameFormats == null || !type.NameFormats.Any()) { return baseName; }
nameFormatIndex = rand.Next() % type.NameFormats.Count;
return type.NameFormats[nameFormatIndex].Replace("[name]", baseName);
}
public void ForceName(string name)
private static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
{
baseName = Name = name;
if (type?.NameFormats == null || !type.NameFormats.Any())
{
return TextManager.Get(nameId);
}
return type.NameFormats[nameFormatIndex % type.NameFormats.Count].Replace("[name]", TextManager.Get(nameId).Value);
}
public void ForceName(Identifier nameId)
{
rawName = string.Empty;
nameIdentifier = nameId;
DisplayName = TextManager.Get(nameId).Fallback(nameId.Value);
}
public void LoadStores(XElement locationElement)
@@ -1125,7 +1185,7 @@ namespace Barotrauma
}
else
{
string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": duplicate identifier.";
string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": duplicate identifier.";
DebugConsole.ThrowError(msg);
GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:DuplicateStoreInfo", GameAnalyticsManager.ErrorSeverity.Error, msg);
continue;
@@ -1133,7 +1193,7 @@ namespace Barotrauma
}
else
{
string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier.";
string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier.";
DebugConsole.ThrowError(msg);
GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:IncorrectStoreIdentifier", GameAnalyticsManager.ErrorSeverity.Error, msg);
continue;
@@ -1444,8 +1504,9 @@ namespace Barotrauma
var locationElement = new XElement("location",
new XAttribute("type", Type.Identifier),
new XAttribute("originaltype", (Type ?? OriginalType).Identifier),
new XAttribute("basename", BaseName),
new XAttribute("name", Name),
/*not used currently (we load the nameIdentifier instead),
* but could make sense to include still for backwards compatibility reasons*/
new XAttribute("name", DisplayName),
new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty),
new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
new XAttribute("pricemultiplier", PriceMultiplier),
@@ -1455,6 +1516,16 @@ namespace Barotrauma
new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation),
new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated));
if (!rawName.IsNullOrEmpty())
{
locationElement.Add(new XAttribute(nameof(rawName), rawName));
}
else
{
locationElement.Add(new XAttribute(nameof(nameIdentifier), nameIdentifier));
locationElement.Add(new XAttribute(nameof(nameFormatIndex), nameFormatIndex));
}
if (Faction != null)
{
locationElement.Add(new XAttribute("faction", Faction.Prefab.Identifier));
@@ -1491,7 +1562,7 @@ namespace Barotrauma
changeElement.Add(new XAttribute("index", index));
if (index == -1)
{
DebugConsole.AddWarning($"Invalid location type change in the location \"{Name}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType}).");
DebugConsole.AddWarning($"Invalid location type change in the location \"{DisplayName}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType}).");
}
else
{
@@ -1,4 +1,5 @@
using Barotrauma.IO;
using Barotrauma.Extensions;
using Barotrauma.IO;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -13,7 +14,7 @@ namespace Barotrauma
{
public static readonly PrefabCollection<LocationType> Prefabs = new PrefabCollection<LocationType>();
private readonly ImmutableArray<string> names;
private readonly ImmutableArray<string> rawNames;
private readonly ImmutableArray<Sprite> portraits;
//<name, commonness>
@@ -26,7 +27,7 @@ namespace Barotrauma
public readonly LocalizedString Name;
public readonly LocalizedString Description;
public readonly LocalizedString ForceLocationName;
public readonly Identifier ForceLocationName;
public readonly float BeaconStationChance;
@@ -54,12 +55,20 @@ namespace Barotrauma
private set;
}
private readonly ImmutableArray<Identifier>? nameIdentifiers = null;
private LanguageIdentifier nameFormatLanguage;
private ImmutableArray<string>? nameFormats = null;
public IReadOnlyList<string> NameFormats
{
get
{
nameFormats ??= TextManager.GetAll($"LocationNameFormat.{Identifier}").ToImmutableArray();
if (nameFormats == null || GameSettings.CurrentConfig.Language != nameFormatLanguage)
{
nameFormats = TextManager.GetAll($"LocationNameFormat.{Identifier}").ToImmutableArray();
nameFormatLanguage = GameSettings.CurrentConfig.Language;
}
return nameFormats;
}
}
@@ -143,29 +152,37 @@ namespace Barotrauma
if (element.GetAttribute("name") != null)
{
ForceLocationName = TextManager.Get(element.GetAttributeString("name", string.Empty));
ForceLocationName = element.GetAttributeIdentifier("name", string.Empty);
}
else
{
string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" });
var names = new List<string>();
foreach (string rawPath in rawNamePaths)
//backwards compatibility for location names defined in a text file
string[] rawNamePaths = element.GetAttributeStringArray("namefile", Array.Empty<string>());
if (rawNamePaths.Any())
{
try
foreach (string rawPath in rawNamePaths)
{
var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim());
names.AddRange(File.ReadAllLines(path.Value).ToList());
try
{
var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim());
names.AddRange(File.ReadAllLines(path.Value).ToList());
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e);
}
}
catch (Exception e)
if (!names.Any())
{
DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e);
names.Add("ERROR: No names found");
}
this.rawNames = names.ToImmutableArray();
}
if (!names.Any())
else
{
names.Add("ERROR: No names found");
nameIdentifiers = element.GetAttributeIdentifierArray("nameidentifiers", new Identifier[] { Identifier }).ToImmutableArray();
}
this.names = names.ToImmutableArray();
}
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty<string>());
@@ -259,17 +276,64 @@ namespace Barotrauma
return portraits[Math.Abs(randomSeed) % portraits.Length];
}
public string GetRandomName(Random rand, IEnumerable<Location> existingLocations)
public Identifier GetRandomNameId(Random rand, IEnumerable<Location> existingLocations)
{
if (nameIdentifiers == null)
{
return Identifier.Empty;
}
List<Identifier> nameIds = new List<Identifier>();
foreach (var nameId in nameIdentifiers)
{
int index = 0;
while (true)
{
Identifier tag = $"LocationName.{nameId}.{index}".ToIdentifier();
if (TextManager.ContainsTag(tag, TextManager.DefaultLanguage))
{
nameIds.Add(tag);
index++;
}
else
{
if (index == 0)
{
DebugConsole.ThrowError($"Could not find any location names for the location type {Identifier}. Name identifier: {nameId}");
}
break;
}
}
}
if (nameIds.None())
{
return Identifier.Empty;
}
if (existingLocations != null)
{
var unusedNames = names.Where(name => !existingLocations.Any(l => l.BaseName == name)).ToList();
var unusedNameIds = nameIds.FindAll(nameId => existingLocations.None(l => l.NameIdentifier == nameId));
if (unusedNameIds.Count > 0)
{
return unusedNameIds[rand.Next() % unusedNameIds.Count];
}
}
return nameIds[rand.Next() % nameIds.Count];
}
/// <summary>
/// For backwards compatibility. Chooses a random name from the names defined in the .txt name files (<see cref="rawNamePaths"/>).
/// </summary>
public string GetRandomRawName(Random rand, IEnumerable<Location> existingLocations)
{
if (rawNames == null || rawNames.None()) { return string.Empty; }
if (existingLocations != null)
{
var unusedNames = rawNames.Where(name => !existingLocations.Any(l => l.DisplayName.Value == name)).ToList();
if (unusedNames.Count > 0)
{
return unusedNames[rand.Next() % unusedNames.Count];
}
}
return names[rand.Next() % names.Length];
return rawNames[rand.Next() % rawNames.Length];
}
public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func<LocationType, bool> predicate = null)
@@ -262,9 +262,9 @@ namespace Barotrauma
foreach (var endLocation in EndLocations)
{
if (endLocation.Type?.ForceLocationName != null &&
!endLocation.Type.ForceLocationName.IsNullOrEmpty())
!endLocation.Type.ForceLocationName.IsEmpty)
{
endLocation.ForceName(endLocation.Type.ForceLocationName.Value);
endLocation.ForceName(endLocation.Type.ForceLocationName);
}
}
@@ -1005,10 +1005,10 @@ namespace Barotrauma
CurrentLocation.CreateStores();
OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation));
if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } })
if (GameMain.GameSession is { Campaign.CampaignMetadata: { } metadata })
{
metadata.SetValue("campaign.location.id".ToIdentifier(), CurrentLocationIndex);
metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.Name);
metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.NameIdentifier.Value);
metadata.SetValue("campaign.location.biome".ToIdentifier(), CurrentLocation.Biome?.Identifier ?? "null".ToIdentifier());
metadata.SetValue("campaign.location.type".ToIdentifier(), CurrentLocation.Type?.Identifier ?? "null".ToIdentifier());
}
@@ -1077,7 +1077,7 @@ namespace Barotrauma
if (SelectedConnection?.Locked ?? false)
{
string errorMsg =
$"A locked connection was selected ({SelectedConnection.Locations[0].Name} -> {SelectedConnection.Locations[1].Name}." +
$"A locked connection was selected ({SelectedConnection.Locations[0].DisplayName} -> {SelectedConnection.Locations[1].DisplayName}." +
$" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n"
+ Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
@@ -1093,7 +1093,7 @@ namespace Barotrauma
{
if (!Locations.Contains(location))
{
string errorMsg = "Failed to select a location. " + (location?.Name ?? "null") + " not found in the map.";
string errorMsg = $"Failed to select a location. {location?.DisplayName ?? "null"} not found in the map.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Map.SelectLocation:LocationNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return;
@@ -1301,11 +1301,11 @@ namespace Barotrauma
private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change)
{
string prevName = location.Name;
LocalizedString prevName = location.DisplayName;
if (!LocationType.Prefabs.TryGet(change.ChangeToType, out var newType))
{
DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found.");
DebugConsole.ThrowError($"Failed to change the type of the location \"{location.DisplayName}\". Location type \"{change.ChangeToType}\" not found.");
return false;
}
@@ -1372,7 +1372,7 @@ namespace Barotrauma
}
partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change);
partial void ChangeLocationTypeProjSpecific(Location location, LocalizedString prevName, LocationTypeChange change);
partial void ClearAnimQueue();
@@ -1498,7 +1498,7 @@ namespace Barotrauma
}
Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty);
string prevLocationName = location.Name;
LocalizedString prevLocationName = location.DisplayName;
LocationType prevLocationType = location.Type;
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First();
location.ChangeType(campaign, newLocationType);
@@ -1619,7 +1619,7 @@ namespace Barotrauma
//this should not be possible, you can't enter non-outpost locations (= natural formations)
if (CurrentLocation != null && !CurrentLocation.Type.HasOutpost && SelectedConnection == null)
{
DebugConsole.AddWarning($"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.Name}). Loading the first adjacent connection...");
DebugConsole.AddWarning($"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.DisplayName}). Loading the first adjacent connection...");
SelectLocation(CurrentLocation.Connections[0].OtherLocation(CurrentLocation));
}
}
@@ -576,15 +576,10 @@ namespace Barotrauma
base.Remove();
MapEntityList.Remove(this);
#if CLIENT
Submarine.ForceRemoveFromVisibleEntities(this);
if (SelectedList.Contains(this))
{
SelectedList = SelectedList.Where(e => e != this).ToHashSet();
}
SelectedList.Remove(this);
#endif
if (aiTarget != null)
{
aiTarget.Remove();
@@ -686,6 +681,9 @@ namespace Barotrauma
Move(-relative * 2.0f);
}
public virtual Quad2D GetTransformedQuad()
=> Quad2D.FromSubmarineRectangle(rect);
public static List<MapEntity> LoadAll(Submarine submarine, XElement parentElement, string filePath, int idOffset)
{
IdRemap idRemap = new IdRemap(parentElement, idOffset);
@@ -733,6 +733,12 @@ namespace Barotrauma
}
}
public override Quad2D GetTransformedQuad()
=> Quad2D.FromSubmarineRectangle(rect).Rotated(
FlippedX != FlippedY
? rotationRad
: -rotationRad);
/// <summary>
/// Checks if there's a structure items can be attached to at the given position and returns it.
/// </summary>
@@ -912,6 +918,12 @@ namespace Barotrauma
return Sections[sectionIndex].damage >= MaxHealth * LeakThreshold;
}
public bool SectionIsLeakingFromOutside(int sectionIndex)
{
if (sectionIndex < 0 || sectionIndex >= Sections.Length) { return false; }
return SectionIsLeaking(sectionIndex) && !Sections[sectionIndex].gap.IsRoomToRoom;
}
public int SectionLength(int sectionIndex)
{
if (sectionIndex < 0 || sectionIndex >= Sections.Length) return 0;
@@ -1304,8 +1316,8 @@ namespace Barotrauma
{
if (damageDiff < 0.0f)
{
attacker.Info?.IncreaseSkillLevel("mechanical".ToIdentifier(),
-damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f));
attacker.Info?.ApplySkillGain(Barotrauma.Tags.MechanicalSkill,
-damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage);
}
}
}
@@ -505,51 +505,58 @@ namespace Barotrauma
minWidth += padding;
minHeight += padding;
Vector2 limits = GetHorizontalLimits(spawnPos, minWidth, minHeight, 0);
if (verticalMoveDir != 0)
int iterations = 0;
const int maxIterations = 5;
do
{
verticalMoveDir = Math.Sign(verticalMoveDir);
//do a raycast towards the top/bottom of the level depending on direction
Vector2 potentialPos = new Vector2(spawnPos.X, verticalMoveDir > 0 ? Level.Loaded.Size.Y : 0);
//3 raycasts (left, middle and right side of the sub, so we don't accidentally raycast up a passage too narrow for the sub)
for (int x = -1; x <= 1; x++)
Vector2 potentialPos = spawnPos;
if (verticalMoveDir != 0)
{
Vector2 xOffset = Vector2.UnitX * minWidth / 2 * x;
if (PickBody(
ConvertUnits.ToSimUnits(spawnPos + xOffset),
ConvertUnits.ToSimUnits(potentialPos + xOffset),
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null)
verticalMoveDir = Math.Sign(verticalMoveDir);
//do a raycast towards the top/bottom of the level depending on direction
Vector2 rayEnd = new Vector2(potentialPos.X, verticalMoveDir > 0 ? Level.Loaded.Size.Y : 0);
Vector2 closestPickedPos = rayEnd;
//multiple raycast across the width of the sub (so we don't accidentally raycast up a passage too narrow for the sub)
for (float x = -1; x <= 1; x += 0.2f)
{
int offsetFromWall = 10 * -verticalMoveDir;
//if the raycast hit a wall, attempt to place the spawnpos there
if (verticalMoveDir > 0)
Vector2 xOffset = Vector2.UnitX * minWidth / 2 * x;
xOffset.X += subDockingPortOffset;
if (PickBody(
ConvertUnits.ToSimUnits(potentialPos + xOffset),
ConvertUnits.ToSimUnits(rayEnd + xOffset),
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
customPredicate: (Fixture f) =>
{
return f.UserData is not VoronoiCell { IsDestructible: true };
}) != null)
{
potentialPos.Y = Math.Min(potentialPos.Y, ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall);
}
else
{
potentialPos.Y = Math.Max(potentialPos.Y, ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall);
//if the raycast hit a wall, attempt to place the spawnpos there
int offsetFromWall = 10 * -verticalMoveDir;
float pickedPos = ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall;
closestPickedPos.Y =
verticalMoveDir > 0 ?
Math.Min(closestPickedPos.Y, pickedPos) :
Math.Max(closestPickedPos.Y, pickedPos);
}
}
potentialPos.Y = closestPickedPos.Y;
}
//step away from the top/bottom of the level, or from whatever wall the raycast hit,
//until we found a spot where there's enough room to place the sub
float dist = Math.Abs(potentialPos.Y - spawnPos.Y);
for (float d = dist; d > 0; d -= 100.0f)
Vector2 limits = GetHorizontalLimits(new Vector2(potentialPos.X, potentialPos.Y - (dockedBorders.Height * 0.5f * verticalMoveDir)),
maxHorizontalMoveAmount: minWidth, minHeight, verticalMoveDir, padding);
if (limits.Y - limits.X >= minWidth)
{
float y = spawnPos.Y + verticalMoveDir * d;
limits = GetHorizontalLimits(new Vector2(spawnPos.X, y), minWidth, minHeight, verticalMoveDir);
if (limits.Y - limits.X > minWidth)
{
spawnPos = new Vector2(spawnPos.X, y - (dockedBorders.Height * 0.5f * verticalMoveDir));
break;
}
}
}
Vector2 newSpawnPos = new Vector2(spawnPos.X, potentialPos.Y - (dockedBorders.Height * 0.5f * verticalMoveDir));
bool couldMoveInVerticalMoveDir = Math.Sign(newSpawnPos.Y - spawnPos.Y) == Math.Sign(verticalMoveDir);
if (!couldMoveInVerticalMoveDir) { break; }
spawnPos = ClampToHorizontalLimits(newSpawnPos, limits);
}
static Vector2 GetHorizontalLimits(Vector2 spawnPos, float minWidth, float minHeight, int verticalMoveDir)
iterations++;
} while (iterations < maxIterations);
Vector2 GetHorizontalLimits(Vector2 spawnPos, float maxHorizontalMoveAmount, float minHeight, int verticalMoveDir, int padding)
{
Vector2 refPos = spawnPos - Vector2.UnitY * minHeight * 0.5f * Math.Sign(verticalMoveDir);
@@ -580,34 +587,44 @@ namespace Barotrauma
if (Math.Abs(ruin.Area.Center.Y - refPos.Y) > (minHeight + ruin.Area.Height) * 0.5f) { continue; }
if (ruin.Area.Center.X < refPos.X)
{
minX = Math.Max(minX, ruin.Area.Right + 100.0f);
minX = Math.Max(minX, ruin.Area.Right + padding);
}
else
{
maxX = Math.Min(maxX, ruin.Area.X - 100.0f);
maxX = Math.Min(maxX, ruin.Area.X - padding);
}
}
return new Vector2(Math.Max(minX, spawnPos.X - minWidth), Math.Min(maxX, spawnPos.X + minWidth));
minX += subDockingPortOffset;
maxX += subDockingPortOffset;
return new Vector2(
Math.Max(Math.Max(minX, spawnPos.X - maxHorizontalMoveAmount - padding), 0),
Math.Min(Math.Min(maxX, spawnPos.X + maxHorizontalMoveAmount + padding), Level.Loaded.Size.X));
}
if (limits.X < 0.0f && limits.Y > Level.Loaded.Size.X)
Vector2 ClampToHorizontalLimits(Vector2 spawnPos, Vector2 limits)
{
//no walls found at either side, just use the initial spawnpos and hope for the best
}
else if (limits.X < 0)
{
//no wall found at the left side, spawn to the left from the right-side wall
spawnPos.X = limits.Y - minWidth * 0.5f - 100.0f + subDockingPortOffset;
}
else if (limits.Y > Level.Loaded.Size.X)
{
//no wall found at right side, spawn to the right from the left-side wall
spawnPos.X = limits.X + minWidth * 0.5f + 100.0f + subDockingPortOffset;
}
else
{
//walls found at both sides, use their midpoint
spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset;
if (limits.X < 0.0f && limits.Y > Level.Loaded.Size.X)
{
//no walls found at either side, just use the initial spawnpos and hope for the best
}
else if (limits.X < 0)
{
//no wall found at the left side, spawn to the left from the right-side wall
spawnPos.X = limits.Y - minWidth * 0.5f - 100.0f + subDockingPortOffset;
}
else if (limits.Y > Level.Loaded.Size.X)
{
//no wall found at right side, spawn to the right from the left-side wall
spawnPos.X = limits.X + minWidth * 0.5f + 100.0f + subDockingPortOffset;
}
else
{
//walls found at both sides, use their midpoint
spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset;
}
return spawnPos;
}
spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - padding * 2);
@@ -929,11 +946,17 @@ namespace Barotrauma
return true;
}
/// <summary>
/// check visibility between two points (in sim units)
/// Check visibility between two points (in sim units).
/// </summary>
/// <returns>a physics body that was between the points (or null)</returns>
public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true)
///
/// <param name="ignoreBranches">Should plants' branches be ignored?</param>
/// <param name="blocksVisibilityPredicate">If the predicate returns false, the fixture is ignored even if it would normally block visibility.</param>
/// <returns>A physics body that was between the points (or null)</returns>
public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true,
Predicate<Fixture> blocksVisibilityPredicate = null)
{
Body closestBody = null;
float closestFraction = 1.0f;
@@ -968,7 +991,10 @@ namespace Barotrauma
if (sectionIndex > -1 && structure.SectionBodyDisabled(sectionIndex)) { return -1; }
}
}
if (blocksVisibilityPredicate != null && !blocksVisibilityPredicate(fixture))
{
return -1;
}
if (fraction < closestFraction)
{
closestBody = fixture.Body;
@@ -1885,12 +1911,11 @@ namespace Barotrauma
Unloading = true;
try
{
#if CLIENT
RoundSound.RemoveAllRoundSounds();
GameMain.LightManager?.ClearLights();
depthSortedDamageable.Clear();
#endif
var _loaded = new List<Submarine>(loaded);
foreach (Submarine sub in _loaded)
{
@@ -1925,9 +1950,11 @@ namespace Barotrauma
Ragdoll.RemoveAll();
PhysicsBody.RemoveAll();
StatusEffect.StopAll();
GameMain.World = null;
Powered.Grids.Clear();
Powered.ChangedConnections.Clear();
GC.Collect();
@@ -1947,6 +1974,7 @@ namespace Barotrauma
outdoorNodes?.Clear();
outdoorNodes = null;
obstructedNodes.Clear();
GameMain.GameSession?.Campaign?.UpgradeManager?.OnUpgradesChanged?.TryDeregister(upgradeEventIdentifier);
@@ -1958,11 +1986,17 @@ namespace Barotrauma
visibleEntities = null;
bodyDist.Clear();
bodies.Clear();
if (MainSub == this) { MainSub = null; }
if (MainSubs[1] == this) { MainSubs[1] = null; }
ConnectedDockingPorts?.Clear();
Powered.ChangedConnections.Clear();
Powered.Grids.Clear();
loaded.Remove(this);
}
@@ -169,7 +169,12 @@ namespace Barotrauma
bool hasCollider = wall.HasBody && !wall.IsPlatform && wall.StairDirection == Direction.None;
Rectangle rect = wall.Rect;
SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider);
var transformedQuad = wall.GetTransformedQuad();
AddPointToExtents(transformedQuad.A, hasCollider: hasCollider);
AddPointToExtents(transformedQuad.B, hasCollider: hasCollider);
AddPointToExtents(transformedQuad.C, hasCollider: hasCollider);
AddPointToExtents(transformedQuad.D, hasCollider: hasCollider);
if (hasCollider)
{
farseerBody.CreateRectangle(
@@ -188,7 +193,8 @@ namespace Barotrauma
if (hull.Submarine != submarine || hull.IdFreed) { continue; }
Rectangle rect = hull.Rect;
SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider: true);
AddPointToExtents(new Vector2(rect.X, rect.Y - rect.Height), hasCollider: true);
AddPointToExtents(new Vector2(rect.Right, rect.Y), hasCollider: true);
farseerBody.CreateRectangle(
ConvertUnits.ToSimUnits(rect.Width),
@@ -221,33 +227,42 @@ namespace Barotrauma
float simWidth = ConvertUnits.ToSimUnits(width);
float simHeight = ConvertUnits.ToSimUnits(height);
if (radius > 0f || (width > 0f && height > 0f))
{
var transformedQuad = item.GetTransformedQuad();
AddPointToExtents(transformedQuad.A, hasCollider: true);
AddPointToExtents(transformedQuad.B, hasCollider: true);
AddPointToExtents(transformedQuad.C, hasCollider: true);
AddPointToExtents(transformedQuad.D, hasCollider: true);
}
if (width > 0.0f && height > 0.0f)
{
item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos, collisionCategory, collidesWith));
SetExtents(item.Position - new Vector2(width, height) / 2, item.Position + new Vector2(width, height) / 2, hasCollider: true);
AddPointToExtents(item.Position - new Vector2(width, height) / 2, hasCollider: true);
AddPointToExtents(item.Position + new Vector2(width, height) / 2, hasCollider: true);
}
else if (radius > 0.0f && width > 0.0f)
{
item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos, collisionCategory, collidesWith));
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith));
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith));
SetExtents(item.Position - new Vector2(width / 2 + radius, height / 2), item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true);
AddPointToExtents(item.Position - new Vector2(width / 2 + radius, height / 2), hasCollider: true);
AddPointToExtents(item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true);
}
else if (radius > 0.0f && height > 0.0f)
{
item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos, collisionCategory, collidesWith));
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith));
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith));
SetExtents(item.Position - new Vector2(width / 2, height / 2 + radius), item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true);
AddPointToExtents(item.Position - new Vector2(width / 2, height / 2 + radius), hasCollider: true);
AddPointToExtents(item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true);
}
else if (radius > 0.0f)
{
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos, collisionCategory, collidesWith));
visibleMinExtents.X = Math.Min(item.Position.X - radius, visibleMinExtents.X);
visibleMinExtents.Y = Math.Min(item.Position.Y - radius, visibleMinExtents.Y);
visibleMaxExtents.X = Math.Max(item.Position.X + radius, visibleMaxExtents.X);
visibleMaxExtents.Y = Math.Max(item.Position.Y + radius, visibleMaxExtents.Y);
SetExtents(item.Position - new Vector2(radius, radius), item.Position + new Vector2(radius, radius), hasCollider: true);
AddPointToExtents(item.Position - new Vector2(radius, radius), hasCollider: true);
AddPointToExtents(item.Position + new Vector2(radius, radius), hasCollider: true);
}
item.StaticFixtures.ForEach(f => f.UserData = item);
}
@@ -268,18 +283,18 @@ namespace Barotrauma
Body = new PhysicsBody(farseerBody);
void SetExtents(Vector2 min, Vector2 max, bool hasCollider)
void AddPointToExtents(Vector2 point, bool hasCollider)
{
visibleMinExtents.X = Math.Min(min.X, visibleMinExtents.X);
visibleMinExtents.Y = Math.Min(min.Y, visibleMinExtents.Y);
visibleMaxExtents.X = Math.Max(max.X, visibleMaxExtents.X);
visibleMaxExtents.Y = Math.Max(max.Y, visibleMaxExtents.Y);
visibleMinExtents.X = Math.Min(point.X, visibleMinExtents.X);
visibleMinExtents.Y = Math.Min(point.Y, visibleMinExtents.Y);
visibleMaxExtents.X = Math.Max(point.X, visibleMaxExtents.X);
visibleMaxExtents.Y = Math.Max(point.Y, visibleMaxExtents.Y);
if (hasCollider)
{
minExtents.X = Math.Min(min.X, minExtents.X);
minExtents.Y = Math.Min(min.Y, minExtents.Y);
maxExtents.X = Math.Max(max.X, maxExtents.X);
maxExtents.Y = Math.Max(max.Y, maxExtents.Y);
minExtents.X = Math.Min(point.X, minExtents.X);
minExtents.Y = Math.Min(point.Y, minExtents.Y);
maxExtents.X = Math.Max(point.X, maxExtents.X);
maxExtents.Y = Math.Max(point.Y, maxExtents.Y);
}
}
}
@@ -1,11 +1,10 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Steamworks.ServerList;
namespace Barotrauma
{
@@ -111,7 +110,7 @@ namespace Barotrauma
public void OnSpawned(Entity spawnedItem)
{
if (!(spawnedItem is Item item)) { throw new ArgumentException($"The entity passed to ItemSpawnInfo.OnSpawned must be an Item (value was {spawnedItem?.ToString() ?? "null"})."); }
if (spawnedItem is not Item item) { throw new ArgumentException($"The entity passed to ItemSpawnInfo.OnSpawned must be an Item (value was {spawnedItem?.ToString() ?? "null"})."); }
onSpawned?.Invoke(item);
}
}
@@ -443,6 +442,7 @@ namespace Barotrauma
CreateNetworkEventProjSpecific(new SpawnEntity(spawnedEntity));
}
spawnInfo.OnSpawned(spawnedEntity);
GameMain.GameSession?.EventManager?.EntitySpawned(spawnedEntity);
}
}
}
@@ -195,7 +195,7 @@ namespace Barotrauma.Networking
int orderPriority = msg.ReadByte();
OrderTarget orderTargetPosition = null;
Order.OrderTargetType orderTargetType = (Order.OrderTargetType)msg.ReadByte();
int wallSectionIndex = 0;
int? wallSectionIndex = null;
if (msg.ReadBoolean())
{
float x = msg.ReadSingle();
@@ -252,6 +252,13 @@ namespace Barotrauma.Networking
continue;
}
#if CLIENT
foreach (var itemComponent in item.Components)
{
itemComponent.StopLoopingSound();
}
#endif
//restore other items to full condition and recharge batteries
item.Condition = item.MaxCondition;
item.GetComponent<Repairable>()?.ResetDeterioration();
@@ -848,6 +848,13 @@ namespace Barotrauma.Networking
private set;
}
[Serialize(10.0f, IsPropertySaveable.Yes)]
public float MinimumMidRoundSyncTimeout
{
get;
private set;
}
private bool karmaEnabled;
[Serialize(false, IsPropertySaveable.Yes)]
public bool KarmaEnabled

Some files were not shown because too many files have changed in this diff Show More