Merge remote-tracking branch 'upstream/master' into develop

This commit is contained in:
EvilFactory
2024-12-11 10:44:53 -03:00
257 changed files with 4793 additions and 1653 deletions
@@ -38,6 +38,8 @@ namespace Barotrauma
public const float MaxHighlightDistance = 150.0f;
public const float MaxDragDistance = 200.0f;
public override ContentPackage ContentPackage => Prefab?.ContentPackage;
partial void UpdateLimbLightSource(Limb limb);
private bool enabled = true;
@@ -666,9 +668,23 @@ namespace Barotrauma
public bool RequireConsciousnessForCustomInteract = true;
public bool AllowCustomInteract
{
get { return (!RequireConsciousnessForCustomInteract || (!IsIncapacitated && Stun <= 0.0f)) && !Removed; }
get
{
if (CampaignMode.HostileFactionDisablesInteraction(CampaignInteractionType) &&
AIController is HumanAIController humanAi && humanAi.IsInHostileFaction())
{
return false;
}
return (!RequireConsciousnessForCustomInteract || (!IsIncapacitated && Stun <= 0.0f)) && !Removed;
}
}
public bool ShouldShowCustomInteractText =>
!CustomInteractHUDText.IsNullOrEmpty() &&
AllowCustomInteract &&
(AIController is not HumanAIController humanAi || humanAi.AllowCampaignInteraction());
private float lockHandsTimer;
public bool LockHands
{
@@ -1212,6 +1228,7 @@ namespace Barotrauma
}
public CampaignMode.InteractionType CampaignInteractionType;
public Identifier MerchantIdentifier;
private bool accessRemovedCharacterErrorShown;
@@ -1265,6 +1282,10 @@ namespace Barotrauma
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
public bool IsInPlayerSub => Submarine != null && Submarine.Info.IsPlayer;
/// <summary>
/// Alias for <see cref="IsInPlayerSub"/>, so the same property name works on both items and characters.
/// </summary>
public bool InPlayerSubmarine => IsInPlayerSub;
public float AITurretPriority
{
@@ -1412,7 +1433,7 @@ namespace Barotrauma
if (characterInfo?.HumanPrefabIds is { } prefabIds &&
prefabIds.NpcSetIdentifier != default && prefabIds.NpcIdentifier != default)
{
humanPrefab = NPCSet.Get(
HumanPrefab = NPCSet.Get(
characterInfo.HumanPrefabIds.NpcSetIdentifier,
characterInfo.HumanPrefabIds.NpcIdentifier);
}
@@ -1765,6 +1786,11 @@ namespace Barotrauma
}
}
if (this == Controlled && inputType == InputType.Run && ToggleRun)
{
return true;
}
return keys[(int)inputType].Held;
}
@@ -1964,6 +1990,8 @@ namespace Barotrauma
}
}
public bool ToggleRun;
public bool CanRunWhileDragging()
{
if (selectedCharacter is not { IsDraggable: true }) { return true; }
@@ -2169,8 +2197,9 @@ namespace Barotrauma
SmoothedCursorPosition = cursorPosition - smoothedCursorDiff;
}
bool aiControlled = this is AICharacter && Controlled != this && !IsRemotelyControlled;
if (!aiControlled)
bool aiControlled = this is AICharacter && Controlled != this && !IsRemotePlayer;
bool controlledByServer = GameMain.NetworkMember is { IsClient: true } && IsRemotelyControlled;
if (!aiControlled && !controlledByServer)
{
Vector2 targetMovement = GetTargetMovement();
AnimController.TargetMovement = targetMovement;
@@ -2199,7 +2228,8 @@ namespace Barotrauma
{
AnimController.TargetDir = Direction.Right;
}
else
//only humanoids' flipping is controlled by the cursor, monster flipping is driven by their movement in FishAnimController
else if (AnimController is HumanoidAnimController)
{
if (CursorPosition.X < AnimController.Collider.Position.X - cursorFollowMargin)
{
@@ -2262,15 +2292,9 @@ namespace Barotrauma
}
else if (IsKeyDown(InputType.Attack))
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Controlled != this)
{
if ((currentAttackTarget.DamageTarget as Entity)?.Removed ?? false)
{
currentAttackTarget = default;
}
currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _);
}
else if (IsPlayer)
//normally the attack target, where to aim the attack and such is handled by EnemyAIController,
//but in the case of player-controlled monsters, we handle it here
if (IsPlayer)
{
float dist = -1;
Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position);
@@ -2315,13 +2339,16 @@ namespace Barotrauma
}
}
var currentContexts = GetAttackContexts();
var validLimbs = AnimController.Limbs.Where(l =>
var attackLimbs = AnimController.Limbs.Where(static l => l.attack != null);
bool hasAttacksWithoutRootForce = attackLimbs.Any(static l=> !l.attack.HasRootForce);
var validLimbs = attackLimbs.Where(l =>
{
if (l.IsSevered || l.IsStuck) { return false; }
if (l.Disabled) { return false; }
var attack = l.attack;
if (attack == null) { return false; }
if (attack.CoolDownTimer > 0) { return false; }
//disallow attacks with root force if there's any other attacks available
if (hasAttacksWithoutRootForce && attack.HasRootForce) { return false; }
if (!attack.IsValidContext(currentContexts)) { return false; }
if (attackTarget != null)
{
@@ -2359,6 +2386,14 @@ namespace Barotrauma
}
}
}
else if (GameMain.NetworkMember is { IsClient: true } && Controlled != this)
{
if (currentAttackTarget.DamageTarget is Entity { Removed: true })
{
currentAttackTarget = default;
}
currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _);
}
}
if (Inventory != null)
@@ -2479,119 +2514,11 @@ namespace Barotrauma
seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb();
if (target is Character targetCharacter)
{
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
return ISpatialEntity.IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
}
else
{
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
}
}
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
{
if (seeingEntity is Character seeingCharacter)
{
return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing);
}
if (target is Character targetCharacter)
{
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
}
else
{
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
}
}
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 (seeingEntity == null) { return false; }
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)
Limb leftExtremity = null, rightExtremity = null;
float leftMostDot = 0.0f, rightMostDot = 0.0f;
Vector2 dir = target.WorldPosition - seeingEntity.WorldPosition;
Vector2 leftDir = new Vector2(dir.Y, -dir.X);
Vector2 rightDir = new Vector2(-dir.Y, dir.X);
foreach (Limb limb in target.AnimController.Limbs)
{
if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; }
if (limb.Hidden) { continue; }
Vector2 limbDir = limb.WorldPosition - seeingEntity.WorldPosition;
float leftDot = Vector2.Dot(limbDir, leftDir);
if (leftDot > leftMostDot)
{
leftMostDot = leftDot;
leftExtremity = limb;
continue;
}
float rightDot = Vector2.Dot(limbDir, rightDir);
if (rightDot > rightMostDot)
{
rightMostDot = rightDot;
rightExtremity = limb;
continue;
}
}
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 seeThroughWindows = true, bool checkFacing = false)
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null) { return false; }
if (seeingEntity == null) { return false; }
// TODO: Could we just use the method below? If not, let's refactor it so that we can.
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition);
if (checkFacing && seeingEntity is Character seeingCharacter)
{
if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; }
}
//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)
{
return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null;
}
//we're outside, the other character inside
else if (seeingEntity.Submarine == null)
{
return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
}
//both inside different subs
else
{
return
Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null &&
Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
}
bool IsBlocking(Fixture f)
{
var body = f.Body;
if (body == null) { return false; }
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 && seeThroughWindows)
{
if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; }
}
return item != target;
}
return true;
return ISpatialEntity.CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
}
}
@@ -2740,7 +2667,7 @@ namespace Barotrauma
public bool CanBeDraggedBy(Character character)
{
if (!IsDraggable) { return false; }
return IsKnockedDown || LockHands || IsPet || (IsBot && character.TeamID == TeamID);
return IsKnockedDown || LockHands || (IsPet && character.IsFriendly(this)) || (IsBot && character.TeamID == TeamID);
}
/// <summary>
@@ -3669,7 +3596,12 @@ namespace Barotrauma
{
humanAnimController.Crouching = false;
}
if (IsRagdolled) { AnimController.IgnorePlatforms = true; }
//ragdolling manually makes the character go through platforms
//EXCEPT for clients, they rely on the server telling whether platforms should be ignored or not
if (IsRagdolled && GameMain.NetworkMember is not { IsClient: true })
{
AnimController.IgnorePlatforms = true;
}
AnimController.ResetPullJoints();
SelectedItem = SelectedSecondaryItem = null;
return;
@@ -4121,6 +4053,7 @@ namespace Barotrauma
if (character.TeamID != TeamID) { continue; }
if (character.AIController is not HumanAIController) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
if (character.Info == null) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
if (currentOrder == null) { continue; }
@@ -4136,12 +4069,15 @@ namespace Barotrauma
case OrderCategory.Movement:
// If there character has another movement order, dismiss that order
Order orderToReplace = null;
foreach (var currentOrder in CurrentOrders)
if (CurrentOrders != null)
{
if (currentOrder == null) { continue; }
if (currentOrder.Category != OrderCategory.Movement) { continue; }
orderToReplace = currentOrder;
break;
foreach (var currentOrder in CurrentOrders)
{
if (currentOrder == null) { continue; }
if (currentOrder.Category != OrderCategory.Movement) { continue; }
orderToReplace = currentOrder;
break;
}
}
if (orderToReplace is { AutoDismiss: true })
{
@@ -4177,6 +4113,7 @@ namespace Barotrauma
private void AddCurrentOrder(Order newOrder)
{
if (CurrentOrders == null) { return; }
if (newOrder == null || newOrder.Identifier == "dismissed")
{
if (newOrder.Option != Identifier.Empty)
@@ -4218,9 +4155,9 @@ namespace Barotrauma
}
}
private bool RemoveDuplicateOrders(Order order)
private void RemoveDuplicateOrders(Order order)
{
bool removed = false;
if (CurrentOrders == null) { return; }
int? priorityOfRemoved = null;
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
{
@@ -4229,12 +4166,11 @@ namespace Barotrauma
{
priorityOfRemoved = orderInfo.ManualPriority;
CurrentOrders.RemoveAt(i);
removed = true;
break;
}
}
if (!priorityOfRemoved.HasValue) { return removed; }
if (!priorityOfRemoved.HasValue) { return; }
for (int i = 0; i < CurrentOrders.Count; i++)
{
@@ -4245,11 +4181,9 @@ namespace Barotrauma
}
}
CurrentOrders.RemoveAll(order => order.ManualPriority <= 0);
CurrentOrders.RemoveAll(o => o.ManualPriority <= 0);
// Sort the current orders so the one with the highest priority comes first
CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
return removed;
}
public Order GetCurrentOrderWithTopPriority()
@@ -4334,6 +4268,30 @@ namespace Barotrauma
aiChatMessageQueue.Add(new AIChatMessage(message, messageType, identifier, delay));
}
#if CLIENT
public void SendSinglePlayerMessage(AIChatMessage message, bool canUseRadio, WifiComponent radio)
{
if (message.MessageType == null)
{
message.MessageType = canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default;
}
if (GameMain.GameSession?.CrewManager is { IsSinglePlayer: true } crewManager)
{
string modifiedMessage = ChatMessage.ApplyDistanceEffect(message.Message, message.MessageType.Value, this, Controlled);
if (!string.IsNullOrEmpty(modifiedMessage))
{
crewManager.AddSinglePlayerChatMessage(Name, modifiedMessage, message.MessageType.Value, this);
}
if (canUseRadio)
{
Signal s = new Signal(modifiedMessage, sender: this, source: radio.Item);
radio.TransmitSignal(s, sentFromChat: true);
}
}
ShowSpeechBubble(ChatMessage.MessageColor[(int)message.MessageType.Value], message.Message);
}
#endif
private void UpdateAIChatMessages(float deltaTime)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
@@ -4350,28 +4308,13 @@ namespace Barotrauma
message.MessageType = canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default;
}
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
{
string modifiedMessage = ChatMessage.ApplyDistanceEffect(message.Message, message.MessageType.Value, this, Controlled);
if (!string.IsNullOrEmpty(modifiedMessage))
{
GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(Name, modifiedMessage, message.MessageType.Value, this);
}
if (canUseRadio)
{
Signal s = new Signal(modifiedMessage, sender: this, source: radio.Item);
radio.TransmitSignal(s, sentFromChat: true);
}
}
SendSinglePlayerMessage(message, canUseRadio, radio);
#endif
#if SERVER
if (GameMain.Server != null && message.MessageType != ChatMessageType.Order)
{
GameMain.Server.SendChatMessage(message.Message, message.MessageType.Value, null, this);
}
#endif
#if CLIENT
ShowSpeechBubble(ChatMessage.MessageColor[(int)message.MessageType.Value], message.Message);
#endif
sentMessages.Add(message);
}
@@ -4451,10 +4394,12 @@ namespace Barotrauma
{
attackAfflictions = attack.Afflictions.Keys;
}
float damageMultiplier = attack.DamageMultiplier * attackData.DamageMultiplier;
var attackResult = targetLimb == null ?
AddDamage(worldPosition, attackAfflictions, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier * attackData.DamageMultiplier) :
DamageLimb(worldPosition, targetLimb, attackAfflictions, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier * attackData.DamageMultiplier, penetration: penetration + attackData.AddedPenetration, shouldImplode: attackData.ShouldImplode);
AddDamage(worldPosition, attackAfflictions, attack.Stun, playSound, attackImpulse, out limbHit, attacker, damageMultiplier) :
DamageLimb(worldPosition, targetLimb, attackAfflictions, attack.Stun, playSound, attackImpulse, attacker, damageMultiplier, penetration: penetration + attackData.AddedPenetration, shouldImplode: attackData.ShouldImplode);
if (attacker != null)
{
@@ -5337,7 +5282,7 @@ namespace Barotrauma
{
SpawnInventoryItemsRecursive(inventory, itemData, new List<Item>());
}
private void SpawnInventoryItemsRecursive(Inventory inventory, ContentXElement element, List<Item> extraDuffelBags)
{
foreach (var itemElement in element.Elements())
@@ -5352,8 +5297,8 @@ namespace Barotrauma
}
#if SERVER
newItem.GetComponent<Terminal>()?.SyncHistory();
if (newItem.GetComponent<WifiComponent>() is WifiComponent wifiComponent) { newItem.CreateServerEvent(wifiComponent); }
if (newItem.GetComponent<GeneticMaterial>() is GeneticMaterial geneticMaterial) { newItem.CreateServerEvent(geneticMaterial); }
SyncInGameEditables(newItem);
#endif
int[] slotIndices = itemElement.GetAttributeIntArray("i", new int[] { 0 });
if (!slotIndices.Any())
@@ -5576,7 +5521,7 @@ namespace Barotrauma
/// <summary>
/// Removes the talents the character has unlocked in their talent tree.
/// </summary>
public void ResetTalents(bool applyXpPenalty)
public void ResetTalents(int talentPointReduction)
{
characterTalents.Clear();
abilityResistances.Clear();
@@ -5584,13 +5529,17 @@ namespace Barotrauma
CharacterHealth.RemoveAfflictions(affliction => affliction.Prefab.AfflictionType == Tags.AfflictionTypeTalentBuff);
statValues.Clear();
if (applyXpPenalty)
for (int i = 0; i < talentPointReduction; i++)
{
int currentLevel = info.GetCurrentLevel();
if (currentLevel > 0)
{
info.SetExperience(info.ExperiencePoints - CharacterInfo.ExperienceRequiredPerLevel(currentLevel));
}
else
{
break;
}
}
}
@@ -5945,7 +5894,7 @@ namespace Barotrauma
}
// NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance
return hadResistance ? resistance : 1f;
return hadResistance ? Math.Max(0, resistance) : 1f;
}
public float GetAbilityResistance(AfflictionPrefab affliction)
@@ -5964,7 +5913,7 @@ namespace Barotrauma
}
// NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance
return hadResistance ? resistance : 1f;
return hadResistance ? Math.Max(0, resistance) : 1f;
}
public void ChangeAbilityResistance(TalentResistanceIdentifier identifier, float value)
@@ -6001,7 +5950,7 @@ namespace Barotrauma
// NPCs are friendly to the same team and the friendly NPCs
CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
// Friendly NPCs are friendly to both player teams
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
CharacterTeamType.FriendlyNPC => otherTeam is CharacterTeamType.Team1 or CharacterTeamType.Team2,
// None (bandits and such) consider friendly NPCs friendly, not attacking them unless they attack first
// Otherwise bandits would for example attach the hostages.
CharacterTeamType.None => otherTeam == CharacterTeamType.FriendlyNPC,