Release 1.11.4.1 (Winter Update)

This commit is contained in:
Markus Isberg
2025-12-08 14:56:47 +00:00
parent 21e34e5cd8
commit 598966f200
121 changed files with 1614 additions and 819 deletions
@@ -404,7 +404,7 @@ namespace Barotrauma
return;
}
DamageCharacters(worldPosition, Attack, force, damageSource, attacker);
DamageCharacters(worldPosition, Attack, force, damageSource, attacker, displayRange);
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
@@ -465,12 +465,12 @@ namespace Barotrauma
partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull);
private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker)
private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker, float range)
{
if (attack.Range <= 0.0f) { return; }
if (range <= 0.0f) { return; }
//long range for the broad distance check, because large characters may still be in range even if their collider isn't
float broadRange = Math.Max(attack.Range * 10.0f, 10000.0f);
float broadRange = Math.Max(range * 10.0f, 10000.0f);
foreach (Character c in Character.CharacterList)
{
@@ -518,7 +518,7 @@ namespace Barotrauma
float limbRadius = limb.body.GetMaxExtent();
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius));
if (dist > attack.Range) { continue; }
if (dist > range) { continue; }
float distFactor =
DistanceFalloff ?
@@ -371,7 +371,7 @@ namespace Barotrauma
if (!IsInDamageRange(c, DamageRange)) { continue; }
//GetApproximateDistance returns float.MaxValue if there's no path through open gaps between the hulls (e.g. if there's a door/wall in between)
if (hull.GetApproximateDistance(Position, c.Position, c.CurrentHull, 10000.0f) > size.X + DamageRange + FlameHeight)
if (hull.GetApproximateDistance(Position, c.Position, c.CurrentHull, maxDistance: 10000.0f, minimumGapOpenness: Structure.LargeGapOpenness) > size.X + DamageRange + FlameHeight)
{
continue;
}
@@ -1133,54 +1133,72 @@ namespace Barotrauma
}
/// <summary>
/// Approximate distance from this hull to the target hull, moving through open gaps without passing through walls.
/// Uses a greedy algo and may not use the most optimal path. Returns float.MaxValue if no path is found.
/// Used in <see cref="GetApproximateDistance"/>
/// </summary>
public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance, float distanceMultiplierPerClosedDoor = 0)
private static readonly Dictionary<Hull, float> cachedDistances = [];
/// <summary>
/// Used in <see cref="GetApproximateDistance"/>
/// </summary>
private static readonly PriorityQueue<(Hull hull, Vector2 pos), float> priorityQueue = new PriorityQueue<(Hull hull, Vector2 pos), float>();
/// <summary>
/// Approximate distance from this hull to the target hull, moving through open gaps without passing through walls.
/// Uses a Dijkstra's algorithm to find the shortest path.
/// </summary>
/// <param name="minimumGapOpenness">The gap's <see cref="Gap.Open">openness</see> must be larger than or equal to this to be considered valid for the path.</param>
public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance, float distanceMultiplierPerClosedDoor = 0, float minimumGapOpenness = 0.5f)
{
return GetApproximateHullDistance(startPos, endPos, new HashSet<Hull>(), targetHull, 0.0f, maxDistance, distanceMultiplierPerClosedDoor);
}
cachedDistances.Clear();
priorityQueue.Clear();
private float GetApproximateHullDistance(Vector2 startPos, Vector2 endPos, HashSet<Hull> connectedHulls, Hull target, float distance, float maxDistance, float distanceMultiplierFromDoors = 0)
{
if (distance >= maxDistance) { return float.MaxValue; }
if (this == target)
cachedDistances[this] = 0f;
priorityQueue.Enqueue((this, startPos), 0f);
while (priorityQueue.TryDequeue(out var current, out float currentDist))
{
return distance + Vector2.Distance(startPos, endPos);
}
Hull currentHull = current.hull;
Vector2 currentPos = current.pos;
connectedHulls.Add(this);
if (currentDist > maxDistance) { return float.MaxValue; }
foreach (Gap g in ConnectedGaps)
{
float distanceMultiplier = 1;
if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken)
// If we've reached the target, add the final segment from hull to endPos
if (currentHull == targetHull)
{
//gap blocked if the door is closed, and we haven't made any predictions of it opening client-side
if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.PredictedState.HasValue) ||
//OR we've predicted that the door is closed client-side
(g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
return currentDist + Vector2.Distance(currentPos, endPos);
}
foreach (Gap g in ConnectedGaps)
{
float distanceMultiplier = 1;
if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken)
{
if (g.ConnectedDoor.OpenState < 0.1f)
//gap blocked if the door is closed, and we haven't made any predictions of it opening client-side
if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.PredictedState.HasValue) ||
//OR we've predicted that the door is closed client-side
(g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
{
if (distanceMultiplierFromDoors <= 0) { continue; }
distanceMultiplier *= distanceMultiplierFromDoors;
if (g.ConnectedDoor.OpenState < 0.1f)
{
if (distanceMultiplierPerClosedDoor <= 0) { continue; }
distanceMultiplier *= distanceMultiplierPerClosedDoor;
}
}
}
}
else if (g.Open <= 0.0f)
{
continue;
}
for (int i = 0; i < 2 && i < g.linkedTo.Count; i++)
{
if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull))
else if (g.Open < minimumGapOpenness)
{
float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position) * distanceMultiplier, maxDistance);
if (dist < float.MaxValue)
continue;
}
for (int i = 0; i < 2 && i < g.linkedTo.Count; i++)
{
if (g.linkedTo[i] is Hull nextHull && nextHull != currentHull)
{
return dist;
float newDist = currentDist + Vector2.Distance(currentPos, g.Position) * distanceMultiplier;
if (!cachedDistances.TryGetValue(nextHull, out float oldDist) || newDist < oldDist)
{
cachedDistances[nextHull] = newDist;
priorityQueue.Enqueue((nextHull, g.Position), newDist);
}
}
}
}
@@ -1274,7 +1292,7 @@ namespace Barotrauma
}
/// <summary>
/// Recursively find all the hulls linked to the specified hull.
/// Recursively find all the hulls linked to the specified hull, including the hull itself.
/// </summary>
public void GetLinkedHulls(List<Hull> linkedHulls, bool includeHiddenHulls = false)
{
@@ -7,7 +7,7 @@ using System.Linq;
namespace Barotrauma
{
class LevelGenerationParams : PrefabWithUintIdentifier, ISerializableEntity
internal partial class LevelGenerationParams : PrefabWithUintIdentifier, ISerializableEntity
{
public readonly static PrefabCollection<LevelGenerationParams> LevelParams = new PrefabCollection<LevelGenerationParams>();
@@ -378,7 +378,14 @@ namespace Barotrauma
if (characters.Any())
{
price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false));
price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValueWithAll(StatTypes.StoreSellMultiplier, tag)));
price *= 1f + characters.Max(c => GetMultiplierForItem(c, item));
float GetMultiplierForItem(Character character, ItemPrefab item)
{
return
item.Tags.Sum(tag => character.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag)) +
character.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, Tags.StatIdentifierTargetAll);
}
}
// Price should never go below 1 mk
@@ -588,7 +595,7 @@ namespace Barotrauma
public Location(Vector2 mapPosition, int? zone, Identifier? biomeId, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
{
Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, biomeId, requireOutpost);
CreateRandomName(Type, rand, existingLocations);
AssignRandomName(Type, rand, existingLocations);
MapPosition = mapPosition;
PortraitId = ToolBox.StringToInt(nameIdentifier.Value);
Connections = new List<LocationConnection>();
@@ -1210,7 +1217,7 @@ namespace Barotrauma
HireManager.AvailableCharacters = hireableCharacters.ToList();
}
private void CreateRandomName(LocationType type, Random rand, IEnumerable<Location> existingLocations)
public void AssignRandomName(LocationType type, Random rand, IEnumerable<Location> existingLocations)
{
if (!type.ForceLocationName.IsEmpty)
{
@@ -735,7 +735,7 @@ namespace Barotrauma
Location startLocation = Locations.MinBy(l => l.MapPosition.X);
if (LocationType.Prefabs.TryGet("outpost", out LocationType startLocationType))
{
startLocation.ChangeType(campaign, startLocationType, createStores: false);
mapLocationTypeGenerator.ChangeLocationTypeAndName(campaign, startLocation, startLocationType);
mapLocationTypeGenerator.AddToFilled(startLocation);
}
@@ -155,13 +155,17 @@ namespace Barotrauma
return filledLocations.Contains(location);
}
public static void ChangeLocationTypeAndName(CampaignMode campaign, Location location, LocationType suitableLocationType)
public void ChangeLocationTypeAndName(CampaignMode campaign, Location location, LocationType suitableLocationType)
{
location.ChangeType(campaign, suitableLocationType, createStores: false, unlockInitialMissions: false);
if (!suitableLocationType.ForceLocationName.IsEmpty)
{
location.ForceName(suitableLocationType.ForceLocationName);
}
else
{
location.AssignRandomName(location.Type, Rand.GetRNG(Rand.RandSync.ServerAndClient), existingLocations: map.Locations);
}
}
public void AssignForcedBiomeGateTypes(IEnumerable<Location> gateLocations)
@@ -792,6 +792,7 @@ namespace Barotrauma
ItemPrefab itemPrefab = ItemPrefab.Find(name, identifier);
if (itemPrefab != null)
{
DebugConsole.AddWarning($"Could not find a structure with the identifier {identifier}, but there's a matching item with the identifier. Converting to an item.");
t = typeof(Item);
}
}
@@ -53,6 +53,16 @@ namespace Barotrauma
const float LeakThreshold = 0.1f;
const float BigGapThreshold = 0.7f;
/// <summary>
/// How <see cref="Gap.open">open</see> the gap on a partially broken wall section is at most (when it's below <see cref="BigGapThreshold"/>, after which it lerps up to <see cref="LargeGapOpenness"/>).
/// </summary>
public const float SmallGapOpenness = 0.35f;
/// <summary>
/// How <see cref="Gap.open">open</see> the gap on a fully broken wall section is.
/// </summary>
public const float LargeGapOpenness = 0.75f;
public override ContentPackage ContentPackage => Prefab?.ContentPackage;
#if CLIENT
@@ -64,6 +74,9 @@ namespace Barotrauma
private static Explosion explosionOnBroken;
public delegate void OnHealthChangedHandler(Character attacker, float damage);
public OnHealthChangedHandler OnHealthChanged;
[Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody)]
public bool Indestructible
{
@@ -1332,11 +1345,11 @@ namespace Barotrauma
float gapOpen = 0;
if (damageRatio > BigGapThreshold)
{
gapOpen = MathHelper.Lerp(0.35f, 0.75f, MathUtils.InverseLerp(BigGapThreshold, 1.0f, damageRatio));
gapOpen = MathHelper.Lerp(SmallGapOpenness, LargeGapOpenness, MathUtils.InverseLerp(BigGapThreshold, 1.0f, damageRatio));
}
else if (damageRatio > LeakThreshold)
{
gapOpen = MathHelper.Lerp(0f, 0.35f, MathUtils.InverseLerp(LeakThreshold, BigGapThreshold, damageRatio));
gapOpen = MathHelper.Lerp(0f, SmallGapOpenness, MathUtils.InverseLerp(LeakThreshold, BigGapThreshold, damageRatio));
}
gap.Open = gapOpen;
@@ -1355,16 +1368,20 @@ namespace Barotrauma
Sections[sectionIndex].damage = MathHelper.Clamp(damage, 0.0f, MaxHealth);
HasDamage = Sections.Any(s => s.damage > 0.0f);
if (attacker != null && damageDiff != 0.0f)
if (damageDiff != 0.0f)
{
HumanAIController.StructureDamaged(this, damageDiff, attacker);
OnHealthChangedProjSpecific(attacker, damageDiff);
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
OnHealthChanged?.Invoke(attacker, damageDiff);
if (attacker != null)
{
if (damageDiff < 0.0f)
HumanAIController.StructureDamaged(this, damageDiff, attacker);
OnHealthChangedProjSpecific(attacker, damageDiff);
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
attacker.Info?.ApplySkillGain(Barotrauma.Tags.MechanicalSkill,
-damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage);
if (damageDiff < 0.0f)
{
attacker.Info?.ApplySkillGain(Barotrauma.Tags.MechanicalSkill,
-damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage);
}
}
}
}
@@ -1775,9 +1792,9 @@ namespace Barotrauma
//3. not found, attempt to find a prefab that uses the previous name as an identifier
if (prefab == null) { prefab = MapEntityPrefab.Find(null, name) as StructurePrefab; }
}
else
else if (StructurePrefab.Prefabs.TryGet(identifier, out StructurePrefab structurePrefab))
{
prefab = MapEntityPrefab.Find(null, identifier) as StructurePrefab;
prefab = structurePrefab;
}
return prefab;
}
@@ -1400,6 +1400,8 @@ namespace Barotrauma
if (item.Submarine != this) { continue; }
var pump = item.GetComponent<Pump>();
if (pump == null || item.CurrentHull == null) { continue; }
//if the pump has no connection panel, it must be something else than a ballast pump (e.g. a weak point which uses a pump component to pump water in)
if (item.GetComponent<ConnectionPanel>() == null) { continue; }
if (!item.HasTag(Tags.Ballast) && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; }
pump.FlowPercentage = 0.0f;
ballastHulls.Add(item.CurrentHull);
@@ -29,7 +29,7 @@ namespace Barotrauma
const float VerticalDrag = 0.05f;
const float MaxDrag = 0.1f;
private const float ImpactDamageMultiplier = 10.0f;
private const float ImpactDamageMultiplier = 3.0f;
//limbs with a mass smaller than this won't cause an impact when they hit the sub
private const float MinImpactLimbMass = 10.0f;
@@ -886,6 +886,11 @@ namespace Barotrauma
}
float wallImpact = Vector2.Dot(impact.Velocity, -impact.Normal);
if (wallImpact < MinCollisionImpact) { return; }
//magic number to make wall impacts on par with monster impacts (the latter are affected by the mass of the monster)
const float WallImpactMultiplier = 3.0f;
wallImpact *= WallImpactMultiplier;
ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos);
foreach (Submarine dockedSub in submarine.DockedTo)
@@ -1070,17 +1075,20 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
if (item.Submarine != submarine) { continue; }
if (Timing.TotalTimeUnpaused < item.LastSubmarineImpactTime + Item.SubmarineImpactCooldown) { continue; }
if (item.body is not { BodyType: BodyType.Dynamic })
{
if (!item.Prefab.ReceiveSubmarineImpacts) { continue; }
item.ReceiveImpact(impact, recursive: false);
item.LastSubmarineImpactTime = Timing.TotalTimeUnpaused;
}
if (!item.body.Enabled || item.CurrentHull == null || item.body.Mass > impulseMagnitude) { continue; }
item.body.ApplyLinearImpulse(impulse, 10.0f);
item.PositionUpdateInterval = 0.0f;
item.LastSubmarineImpactTime = Timing.TotalTimeUnpaused;
}
float dmg = applyDamage ? impact * ImpactDamageMultiplier : 0.0f;