Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2025-04-10 10:37:09 -03:00
296 changed files with 8420 additions and 2945 deletions
@@ -147,13 +147,23 @@ namespace Barotrauma
public bool IsRemotePlayer { get; set; }
public bool IsLocalPlayer => Controlled == this;
public bool IsPlayer => Controlled == this || IsRemotePlayer;
public bool IsPlayer => IsLocalPlayer || IsRemotePlayer;
/// <summary>
/// Is the character player or does it have an active ship command manager (an AI controlled sub)? Bots in the player team are not treated as commanders.
/// </summary>
public bool IsCommanding => IsPlayer || AIController is HumanAIController { ShipCommandManager.Active: true };
/// <summary>
/// Is the character actively controlled by a human AI?
/// </summary>
public bool IsBot => !IsPlayer && AIController is HumanAIController { Enabled: true };
/// <summary>
/// Is the character actively controlled by an AI?
/// </summary>
public bool IsAIControlled => !IsPlayer && AIController is { Enabled: true };
public bool IsEscorted { get; set; }
public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty;
@@ -381,11 +391,11 @@ namespace Barotrauma
public bool IsOnPlayerTeam =>
teamID == CharacterTeamType.Team1 ||
(teamID == CharacterTeamType.Team2 && !IsFriendlyNPCTurnedHostile);
(teamID == CharacterTeamType.Team2 && !IsFriendlyNPCTurnedHostile); // Some events use Team2 as a hostile team for NPCs, so we shouldn't treat those to be in any player team. Normally Team2 means a player team in PvP.
public bool IsOriginallyOnPlayerTeam => originalTeamID == CharacterTeamType.Team1 || originalTeamID == CharacterTeamType.Team2;
public bool IsOriginallyOnPlayerTeam => originalTeamID is CharacterTeamType.Team1 or CharacterTeamType.Team2;
public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && (teamID == CharacterTeamType.Team2 || teamID == CharacterTeamType.None);
public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && teamID is CharacterTeamType.Team2 or CharacterTeamType.None;
public bool IsInstigator => CombatAction is { IsInstigator: true };
@@ -398,6 +408,12 @@ namespace Barotrauma
/// </summary>
public bool IsCriminal;
/// <summary>
/// A flag for the guards to remember that the character has used weapons or tools offensively, so that they know to confiscate those.
/// Intentionally not set from stealing or fleeing.
/// </summary>
public bool IsActingOffensively;
/// <summary>
/// Set true only, if the character is turned hostile from an escort mission (See <see cref="EscortMission"/>).
/// </summary>
@@ -752,11 +768,24 @@ namespace Barotrauma
set
{
if (value == selectedCharacter) { return; }
if (selectedCharacter != null) { selectedCharacter.selectedBy = null; }
if (selectedCharacter != null) { selectedCharacter.selectedBy = null; }
selectedCharacter = value;
if (selectedCharacter != null) {selectedCharacter.selectedBy = this; }
if (selectedCharacter != null) { selectedCharacter.selectedBy = this; }
#if CLIENT
CharacterHealth.SetHealthBarVisibility(value == null);
if (IsLocalPlayer && !GUI.IsUltrawide && GUI.IsHUDScaled)
{
if (value != null)
{
// Scaled HUD on non-ultra-wide -> hide the chatbox, so that it doesn't overlap with the inventory.
ChatBox.AutoHideChatBox();
}
else
{
ChatBox.ResetChatBoxOpenState();
}
}
#endif
bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
CheckTalents(AbilityEffectType.OnLootCharacter, new AbilityCharacterLoot(value));
@@ -937,10 +966,7 @@ namespace Barotrauma
get { return IsHuman && HasEquippedItem(Tags.HandLockerItem); }
}
public bool IsPet
{
get { return AIController is EnemyAIController enemyController && enemyController.PetBehavior != null; }
}
public bool IsPet => Params.IsPet;
public float Oxygen
{
@@ -1085,18 +1111,18 @@ namespace Barotrauma
}
#if CLIENT
HintManager.OnSetSelectedItem(this, prevSelectedItem, _selectedItem);
if (Controlled == this)
if (IsLocalPlayer)
{
_selectedItem?.GetComponent<Fabricator>()?.RefreshSelectedItem();
if (_selectedItem == null)
{
GameMain.GameSession?.CrewManager?.ResetCrewList();
}
else if (!_selectedItem.IsLadder)
if (_selectedItem != null)
{
GameMain.GameSession?.CrewManager?.AutoHideCrewList();
}
else
{
GameMain.GameSession?.CrewManager?.ResetCrewListOpenState();
}
_selectedItem?.GetComponent<CircuitBox>()?.OnViewUpdateProjSpecific();
}
@@ -1199,7 +1225,7 @@ namespace Barotrauma
{
get
{
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsKnockedDown && (!IsRagdolled || AnimController.IsHoldingToRope);
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsKnockedDownOrRagdolled && (!IsRagdolled || AnimController.IsHoldingToRope);
}
}
@@ -1430,12 +1456,9 @@ namespace Barotrauma
//no longer a new hire after spawning (only displayed as a new hire at the end of the outpost round, when the character hasn't spawned yet)
Info.IsNewHire = false;
}
if (characterInfo?.HumanPrefabIds is { } prefabIds &&
prefabIds.NpcSetIdentifier != default && prefabIds.NpcIdentifier != default)
if (characterInfo?.HumanPrefabIds is { NpcSetIdentifier.IsEmpty: false, NpcIdentifier.IsEmpty: false })
{
HumanPrefab = NPCSet.Get(
characterInfo.HumanPrefabIds.NpcSetIdentifier,
characterInfo.HumanPrefabIds.NpcIdentifier);
HumanPrefab = characterInfo.HumanPrefab;
}
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
@@ -1831,12 +1854,12 @@ namespace Barotrauma
if (info == null) { return; }
if (info.HumanPrefabIds != default)
{
var humanPrefab = NPCSet.Get(info.HumanPrefabIds.NpcSetIdentifier, info.HumanPrefabIds.NpcIdentifier);
if (humanPrefab == null)
var prefab = info.HumanPrefab;
if (prefab == null)
{
DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\".");
}
else if (humanPrefab.GiveItems(this, spawnPoint?.Submarine ?? Submarine, spawnPoint))
else if (prefab.GiveItems(this, spawnPoint?.Submarine ?? Submarine, spawnPoint))
{
return;
}
@@ -1905,23 +1928,9 @@ namespace Barotrauma
if (skillIdentifier != null)
{
foreach (Item item in Inventory.AllItems)
if (wearableSkillModifiers.TryGetValue(skillIdentifier, out float skillValue))
{
if (item?.GetComponent<Wearable>() is Wearable wearable &&
!Inventory.IsInLimbSlot(item, InvSlotType.Any))
{
foreach (var allowedSlot in wearable.AllowedSlots)
{
if (allowedSlot == InvSlotType.Any) { continue; }
if (!Inventory.IsInLimbSlot(item, allowedSlot)) { continue; }
if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue))
{
skillLevel += skillValue;
break;
}
}
}
skillLevel += skillValue;
}
}
@@ -2667,7 +2676,7 @@ namespace Barotrauma
public bool CanBeDraggedBy(Character character)
{
if (!IsDraggable) { return false; }
return IsKnockedDown || LockHands || (IsPet && character.IsFriendly(this)) || (IsBot && character.TeamID == TeamID);
return IsKnockedDownOrRagdolled || LockHands || (IsPet && character.IsOnFriendlyTeam(this)) || (IsBot && character.TeamID == TeamID);
}
/// <summary>
@@ -3051,12 +3060,7 @@ namespace Barotrauma
public void DoInteractionUpdate(float deltaTime, Vector2 mouseSimPos)
{
bool isLocalPlayer = Controlled == this;
if (!isLocalPlayer && (this is AICharacter && !IsRemotePlayer))
{
return;
}
if (IsAIControlled) { return; }
if (DisableInteract)
{
@@ -3077,7 +3081,7 @@ namespace Barotrauma
}
#if CLIENT
if (isLocalPlayer)
if (IsLocalPlayer)
{
if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this) && !DisableFocusingOnEntities)
{
@@ -3597,23 +3601,25 @@ namespace Barotrauma
humanAnimController.Crouching = false;
}
//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 })
//EXCEPT if the character is controlled by the server (i.e. remote player or bot),
//in that case the server decides whether platforms should be ignored or not
bool isControlledByRemotelyByServer = GameMain.NetworkMember is { IsClient: true } && IsRemotelyControlled;
if (IsRagdolled &&
!isControlledByRemotelyByServer)
{
AnimController.IgnorePlatforms = true;
}
AnimController.ResetPullJoints();
SelectedItem = SelectedSecondaryItem = null;
SelectedCharacter = null;
return;
}
//AI and control stuff
Control(deltaTime, cam);
bool isNotControlled = Controlled != this;
if (isNotControlled && (!(this is AICharacter) || IsRemotePlayer))
if (IsRemotePlayer)
{
Vector2 mouseSimPos = ConvertUnits.ToSimUnits(cursorPosition);
DoInteractionUpdate(deltaTime, mouseSimPos);
@@ -3880,7 +3886,9 @@ namespace Barotrauma
//don't spawn duffel bags in PvP modes that include respawning, because it can lead to a ton of accumulated items in the sub/outpost
bool pvpWithRespawning = GameMain.GameSession?.GameMode is PvPMode && GameMain.NetworkMember?.RespawnManager != null;
if (!despawnContainerId.IsEmpty && !pvpWithRespawning)
if (!despawnContainerId.IsEmpty && !pvpWithRespawning &&
//don't duffelbag disconnected character's items (the items should disappear with the character, and reappear if they rejoin later)
CauseOfDeath?.Type != CauseOfDeathType.Disconnected)
{
var containerPrefab =
MapEntityPrefab.FindByIdentifier(despawnContainerId) as ItemPrefab ??
@@ -3928,6 +3936,16 @@ namespace Barotrauma
}
else
{
#if SERVER
if (CauseOfDeath?.Type == CauseOfDeathType.Disconnected)
{
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign)
{
//refresh campaign data so the disconnected player gets to keep the items that were in their inventory
mpCampaign.RefreshCharacterCampaignData(this, refreshHealthData: false);
}
}
#endif
Spawner.AddEntityToRemoveQueue(this);
}
}
@@ -4720,17 +4738,24 @@ namespace Barotrauma
healer.Info?.ApplySkillGain(Tags.MedicalItem, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed);
}
}
public bool IsKnockedDownOrRagdolled => (IsRagdolled && !AnimController.IsHangingWithRope) || IsKnockedDown;
/// <summary>
/// Is the character knocked down regardless whether the technical state is dead, unconcious, paralyzed, or stunned.
/// With stunning, the parameter uses an one second delay before the character is treated as knocked down. The purpose of this is to ignore minor stunning. If you don't want to to ignore any stun, use the Stun property.
/// With stunning, the parameter uses a one-second delay before the character is treated as knocked down. The purpose of this is to ignore minor stunning. If you don't want to to ignore any stun, use the Stun property.
/// </summary>
public bool IsKnockedDown => (IsRagdolled && !AnimController.IsHangingWithRope) || CharacterHealth.StunTimer > 1.0f || IsIncapacitated;
public bool IsKnockedDown => CharacterHealth.StunTimer > 1.0f || IsIncapacitated;
public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
if (Screen.Selected != GameMain.GameScreen) { return; }
if (GodMode)
{
CharacterHealth.Stun = 0;
return;
}
if (newStun > 0 && Params.Health.StunImmunity)
{
if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
@@ -5008,6 +5033,8 @@ namespace Barotrauma
{
characterInfo.PermanentlyDead = true;
}
GameMain.GameSession.RefreshAnyOpenPlayerInfo();
#endif
#if SERVER
@@ -5016,8 +5043,10 @@ namespace Barotrauma
Info.LastRewardDistribution = Option.Some(Wallet.RewardDistribution);
}
#endif
if (GameAnalyticsManager.SendUserStatistics && Prefab?.ContentPackage == ContentPackageManager.VanillaCorePackage)
//we don't need info of every kill, we can get a good sample size just by logging 5%
if (GameAnalyticsManager.SendUserStatistics &&
Prefab?.ContentPackage == ContentPackageManager.VanillaCorePackage &&
GameAnalyticsManager.ShouldLogRandomSample())
{
string causeOfDeathStr = causeOfDeathAffliction == null ?
causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Identifier.Value.Replace(" ", "");
@@ -5297,6 +5326,7 @@ 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
@@ -5511,7 +5541,7 @@ namespace Barotrauma
/// </summary>
public bool IsProtectedFromPressure => IsImmuneToPressure || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure);
public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure) || GodMode;
#region Talents
private readonly List<CharacterTalent> characterTalents = new List<CharacterTalent>();
@@ -5772,9 +5802,14 @@ namespace Barotrauma
/// </summary>
private readonly Dictionary<StatTypes, float> wearableStatValues = new Dictionary<StatTypes, float>();
/// <summary>
/// A dictionary with temporary values, updated when the character equips/unequips wearables. Used to reduce unnecessary inventory checking.
/// </summary>
private readonly Dictionary<Identifier, float> wearableSkillModifiers = new Dictionary<Identifier, float>();
public float GetStatValue(StatTypes statType, bool includeSaved = true)
{
if (!IsHuman) { return 0f; }
if (Info == null) { return 0f; }
float statValue = 0f;
if (statValues.TryGetValue(statType, out float value))
@@ -5785,7 +5820,7 @@ namespace Barotrauma
{
statValue += CharacterHealth.GetStatValue(statType);
}
if (Info != null && includeSaved)
if (includeSaved)
{
// could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change
statValue += Info.GetSavedStatValue(statType);
@@ -5807,12 +5842,17 @@ namespace Barotrauma
public void OnWearablesChanged()
{
HashSet<Wearable> handledWearables = new HashSet<Wearable>();
wearableStatValues.Clear();
wearableSkillModifiers.Clear();
for (int i = 0; i < Inventory.Capacity; i++)
{
if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.SlotTypes[i] != InvSlotType.LeftHand && Inventory.SlotTypes[i] != InvSlotType.RightHand
&& Inventory.GetItemAt(i)?.GetComponent<Wearable>() is Wearable wearable)
{
if (handledWearables.Contains(wearable)) { continue; }
handledWearables.Add(wearable);
foreach (var statValuePair in wearable.WearableStatValues)
{
if (wearableStatValues.ContainsKey(statValuePair.Key))
@@ -5824,6 +5864,17 @@ namespace Barotrauma
wearableStatValues.Add(statValuePair.Key, statValuePair.Value);
}
}
foreach (var skillModifier in wearable.SkillModifiers)
{
if (wearableSkillModifiers.ContainsKey(skillModifier.Key))
{
wearableSkillModifiers[skillModifier.Key] += skillModifier.Value;
}
else
{
wearableSkillModifiers.Add(skillModifier.Key, skillModifier.Value);
}
}
}
}
}