Unstable 1.8.4.0
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Steam;
|
||||
using FarseerPhysics;
|
||||
@@ -15,6 +16,84 @@ namespace Barotrauma
|
||||
|
||||
static class AchievementManager
|
||||
{
|
||||
private static readonly ImmutableHashSet<Identifier> SupportedAchievements = ImmutableHashSet.Create(
|
||||
"killmoloch".ToIdentifier(),
|
||||
"killhammerhead".ToIdentifier(),
|
||||
"killendworm".ToIdentifier(),
|
||||
"artifactmission".ToIdentifier(),
|
||||
"combatmission1".ToIdentifier(),
|
||||
"combatmission2".ToIdentifier(),
|
||||
"healcrit".ToIdentifier(),
|
||||
"repairdevice".ToIdentifier(),
|
||||
"traitorwin".ToIdentifier(),
|
||||
"killtraitor".ToIdentifier(),
|
||||
"killclown".ToIdentifier(),
|
||||
"healopiateaddiction".ToIdentifier(),
|
||||
"survivecrushdepth".ToIdentifier(),
|
||||
"survivereactormeltdown".ToIdentifier(),
|
||||
"healhusk".ToIdentifier(),
|
||||
"killpoison".ToIdentifier(),
|
||||
"killnuke".ToIdentifier(),
|
||||
"killtool".ToIdentifier(),
|
||||
"clowncostume".ToIdentifier(),
|
||||
"lastmanstanding".ToIdentifier(),
|
||||
"lonesailor".ToIdentifier(),
|
||||
"subhighvelocity".ToIdentifier(),
|
||||
"nodamagerun".ToIdentifier(),
|
||||
"subdeep".ToIdentifier(),
|
||||
"maxintensity".ToIdentifier(),
|
||||
"discovercoldcaverns".ToIdentifier(),
|
||||
"discovereuropanridge".ToIdentifier(),
|
||||
"discoverhydrothermalwastes".ToIdentifier(),
|
||||
"discovertheaphoticplateau".ToIdentifier(),
|
||||
"discoverthegreatsea".ToIdentifier(),
|
||||
"travel10".ToIdentifier(),
|
||||
"travel100".ToIdentifier(),
|
||||
"xenocide".ToIdentifier(),
|
||||
"genocide".ToIdentifier(),
|
||||
"cargomission".ToIdentifier(),
|
||||
"subeditor24h".ToIdentifier(),
|
||||
"crewaway".ToIdentifier(),
|
||||
"captainround".ToIdentifier(),
|
||||
"securityofficerround".ToIdentifier(),
|
||||
"engineerround".ToIdentifier(),
|
||||
"mechanicround".ToIdentifier(),
|
||||
"medicaldoctorround".ToIdentifier(),
|
||||
"assistantround".ToIdentifier(),
|
||||
"campaigncompleted".ToIdentifier(),
|
||||
"salvagewreckmission".ToIdentifier(),
|
||||
"escortmission".ToIdentifier(),
|
||||
"killcharybdis".ToIdentifier(),
|
||||
"killlatcher".ToIdentifier(),
|
||||
"killspineling_giant".ToIdentifier(),
|
||||
"killcrawlerbroodmother".ToIdentifier(),
|
||||
"ascension".ToIdentifier(),
|
||||
"campaignmetadata_pathofthebikehorn_7".ToIdentifier(),
|
||||
"campaignmetadata_coalitionspecialhire1_hired_true".ToIdentifier(),
|
||||
"campaignmetadata_coalitionspecialhire2_hired_true".ToIdentifier(),
|
||||
"campaignmetadata_separatistspecialhire1_hired_true".ToIdentifier(),
|
||||
"campaignmetadata_separatistspecialhire2_hired_true".ToIdentifier(),
|
||||
"campaignmetadata_huskcultspecialhire1_hired_true".ToIdentifier(),
|
||||
"campaignmetadata_clownspecialhire1_hired_true".ToIdentifier(),
|
||||
"scanruin".ToIdentifier(),
|
||||
"clearruin".ToIdentifier(),
|
||||
"beaconmission".ToIdentifier(),
|
||||
"abandonedoutpostrescue".ToIdentifier(),
|
||||
"abandonedoutpostassassinate".ToIdentifier(),
|
||||
"abandonedoutpostdestroyhumans".ToIdentifier(),
|
||||
"abandonedoutpostdestroymonsters".ToIdentifier(),
|
||||
"nestmission".ToIdentifier(),
|
||||
"miningmission".ToIdentifier(),
|
||||
"combatmissionseparatistsvscoalition".ToIdentifier(),
|
||||
"combatmissioncoalitionvsseparatists".ToIdentifier(),
|
||||
"getoutalive".ToIdentifier(),
|
||||
"abyssbeckons".ToIdentifier(),
|
||||
"europasfinest".ToIdentifier(),
|
||||
"kingofthehull".ToIdentifier(),
|
||||
"killmantis".ToIdentifier(),
|
||||
"ancientnovelty".ToIdentifier(),
|
||||
"whatsmirksbelow".ToIdentifier());
|
||||
|
||||
private const float UpdateInterval = 1.0f;
|
||||
|
||||
private static readonly HashSet<Identifier> unlockedAchievements = new HashSet<Identifier>();
|
||||
@@ -42,7 +121,30 @@ namespace Barotrauma
|
||||
private static PathFinder pathFinder;
|
||||
private static readonly Dictionary<Character, CachedDistance> cachedDistances = new Dictionary<Character, CachedDistance>();
|
||||
|
||||
public static void OnStartRound()
|
||||
static AchievementManager()
|
||||
{
|
||||
#if DEBUG
|
||||
if (SteamManager.IsInitialized && SteamManager.TryGetAllAvailableAchievements(out var achievements) && achievements.Any())
|
||||
{
|
||||
foreach (var achievement in achievements)
|
||||
{
|
||||
if (!SupportedAchievements.Contains(achievement.Identifier.ToIdentifier()))
|
||||
{
|
||||
DebugConsole.ThrowError($"Achievement \"{achievement.Identifier}\" is present on Steam's backend but not in achievements supported by {nameof(AchievementManager)}.");
|
||||
}
|
||||
}
|
||||
foreach (Identifier achievementId in SupportedAchievements)
|
||||
{
|
||||
if (achievements.None(a => a.Identifier.ToIdentifier() == achievementId))
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find achievement \"{achievementId}\" on Steam's backend.");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void OnStartRound(Biome biome = null)
|
||||
{
|
||||
roundData = new RoundData();
|
||||
foreach (Item item in Item.ItemList)
|
||||
@@ -53,12 +155,32 @@ namespace Barotrauma
|
||||
}
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList, false);
|
||||
cachedDistances.Clear();
|
||||
|
||||
#if CLIENT
|
||||
// If this is a multiplayer game, the client should let the server handle achievements
|
||||
if (GameMain.Client != null) { return; }
|
||||
#endif
|
||||
|
||||
if (biome != null && GameMain.GameSession?.GameMode is CampaignMode)
|
||||
{
|
||||
string shortBiomeIdentifier = biome.Identifier.Value.Replace(" ", "");
|
||||
UnlockAchievement($"discover{shortBiomeIdentifier}".ToIdentifier(), unlockClients: true);
|
||||
|
||||
// Just got out of Cold Caverns
|
||||
if (shortBiomeIdentifier == "europanridge".ToIdentifier() &&
|
||||
GameMain.NetworkMember?.ServerSettings?.RespawnMode == RespawnMode.Permadeath)
|
||||
{
|
||||
UnlockAchievement("getoutalive".ToIdentifier(), unlockClients: true,
|
||||
clientConditions: static client => GameMain.GameSession.PermadeathCountForAccount(client.AccountId) <= 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Update(float deltaTime)
|
||||
{
|
||||
if (GameMain.GameSession == null) { return; }
|
||||
#if CLIENT
|
||||
// If this is a multiplayer game, the client should let the server handle achievements
|
||||
if (GameMain.Client != null) { return; }
|
||||
#endif
|
||||
|
||||
@@ -73,7 +195,7 @@ namespace Barotrauma
|
||||
UnlockAchievement(
|
||||
identifier: "maxintensity".ToIdentifier(),
|
||||
unlockClients: true,
|
||||
conditions: static c => c is { IsDead: false, IsUnconscious: false });
|
||||
characterConditions: static c => c is { IsDead: false, IsUnconscious: false });
|
||||
}
|
||||
|
||||
foreach (Character c in Character.CharacterList)
|
||||
@@ -221,11 +343,6 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void OnBiomeDiscovered(Biome biome)
|
||||
{
|
||||
UnlockAchievement($"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier());
|
||||
}
|
||||
|
||||
public static void OnCampaignMetadataSet(Identifier identifier, object value, bool unlockClients = false)
|
||||
{
|
||||
if (identifier.IsEmpty || value is null) { return; }
|
||||
@@ -235,6 +352,7 @@ namespace Barotrauma
|
||||
public static void OnItemRepaired(Item item, Character fixer)
|
||||
{
|
||||
#if CLIENT
|
||||
// If this is a multiplayer game, the client should let the server handle achievements
|
||||
if (GameMain.Client != null) { return; }
|
||||
#endif
|
||||
if (fixer == null) { return; }
|
||||
@@ -242,11 +360,27 @@ namespace Barotrauma
|
||||
UnlockAchievement(fixer, "repairdevice".ToIdentifier());
|
||||
UnlockAchievement(fixer, $"repair{item.Prefab.Identifier}".ToIdentifier());
|
||||
}
|
||||
|
||||
public static void OnButtonTerminalSignal(Item item, Character user)
|
||||
{
|
||||
if (item == null || user == null) { return; }
|
||||
|
||||
#if CLIENT
|
||||
// If this is a multiplayer game, the client should let the server handle achievements
|
||||
if (GameMain.Client != null) { return; }
|
||||
#endif
|
||||
if ((item.Prefab.Identifier == "alienterminal" || item.Prefab.Identifier == "alienterminal_new") &&
|
||||
item.Condition <= 0)
|
||||
{
|
||||
UnlockAchievement(user, "ancientnovelty".ToIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnAfflictionReceived(Affliction affliction, Character character)
|
||||
{
|
||||
if (affliction.Prefab.AchievementOnReceived.IsEmpty) { return; }
|
||||
#if CLIENT
|
||||
// If this is a multiplayer game, the client should let the server handle achievements
|
||||
if (GameMain.Client != null) { return; }
|
||||
#endif
|
||||
UnlockAchievement(character, affliction.Prefab.AchievementOnReceived);
|
||||
@@ -257,6 +391,7 @@ namespace Barotrauma
|
||||
if (affliction.Prefab.AchievementOnRemoved.IsEmpty) { return; }
|
||||
|
||||
#if CLIENT
|
||||
// If this is a multiplayer game, the client should let the server handle achievements
|
||||
if (GameMain.Client != null) { return; }
|
||||
#endif
|
||||
UnlockAchievement(character, affliction.Prefab.AchievementOnRemoved);
|
||||
@@ -265,6 +400,7 @@ namespace Barotrauma
|
||||
public static void OnCharacterRevived(Character character, Character reviver)
|
||||
{
|
||||
#if CLIENT
|
||||
// If this is a multiplayer game, the client should let the server handle achievements
|
||||
if (GameMain.Client != null) { return; }
|
||||
#endif
|
||||
if (reviver == null) { return; }
|
||||
@@ -274,6 +410,7 @@ namespace Barotrauma
|
||||
public static void OnCharacterKilled(Character character, CauseOfDeath causeOfDeath)
|
||||
{
|
||||
#if CLIENT
|
||||
// If this is a multiplayer game, the client should let the server handle achievements
|
||||
if (GameMain.Client != null || GameMain.GameSession == null) { return; }
|
||||
|
||||
if (character != Character.Controlled &&
|
||||
@@ -310,6 +447,17 @@ namespace Barotrauma
|
||||
UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}indoors".ToIdentifier());
|
||||
}
|
||||
}
|
||||
#if SERVER
|
||||
if (character.SpeciesName == "Jove" &&
|
||||
GameMain.GameSession.Campaign is MultiPlayerCampaign &&
|
||||
GameMain.Server?.ServerSettings is { IronmanModeActive: true })
|
||||
{
|
||||
UnlockAchievement(
|
||||
identifier: "europasfinest".ToIdentifier(),
|
||||
unlockClients: true,
|
||||
characterConditions: static c => c is { IsDead: false });
|
||||
}
|
||||
#endif
|
||||
|
||||
if (character.HasEquippedItem("clownmask".ToIdentifier()) &&
|
||||
character.HasEquippedItem("clowncostume".ToIdentifier()) &&
|
||||
@@ -317,6 +465,12 @@ namespace Barotrauma
|
||||
{
|
||||
UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier());
|
||||
}
|
||||
|
||||
if (character.CharacterHealth?.GetAffliction("psychoclown") != null &&
|
||||
character.CurrentHull?.Submarine.Info is { Type: SubmarineType.BeaconStation })
|
||||
{
|
||||
UnlockAchievement(causeOfDeath.Killer, "whatsmirksbelow".ToIdentifier());
|
||||
}
|
||||
|
||||
// TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest.
|
||||
if (character.CharacterHealth?.GetAffliction("morbusinepoisoning") != null)
|
||||
@@ -344,8 +498,14 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.Server?.ServerSettings?.RespawnMode == RespawnMode.Permadeath &&
|
||||
causeOfDeath.Type != CauseOfDeathType.Disconnected)
|
||||
{
|
||||
UnlockAchievement(character, "abyssbeckons".ToIdentifier());
|
||||
}
|
||||
|
||||
if (GameMain.Server?.TraitorManager != null)
|
||||
{
|
||||
if (GameMain.Server.TraitorManager.IsTraitor(character))
|
||||
@@ -359,6 +519,7 @@ namespace Barotrauma
|
||||
public static void OnTraitorWin(Character character)
|
||||
{
|
||||
#if CLIENT
|
||||
// If this is a multiplayer game, the client should let the server handle achievements
|
||||
if (GameMain.Client != null || GameMain.GameSession == null) { return; }
|
||||
#endif
|
||||
UnlockAchievement(character, "traitorwin".ToIdentifier());
|
||||
@@ -400,16 +561,21 @@ namespace Barotrauma
|
||||
|
||||
foreach (Mission mission in gameSession.Missions)
|
||||
{
|
||||
if (mission is CombatMission combatMission && GameMain.GameSession.WinningTeam.HasValue)
|
||||
// For PvP missions, all characters on the winning team that are still alive get achievements (if available)
|
||||
if (mission is CombatMission && GameMain.GameSession.WinningTeam.HasValue)
|
||||
{
|
||||
//all characters that are alive and in the winning team get an achievement
|
||||
// Attempt unlocking team-specific achievement (if one has been set in the achievement backend)
|
||||
var achvIdentifier =
|
||||
$"{mission.Prefab.AchievementIdentifier}{(int) GameMain.GameSession.WinningTeam}"
|
||||
.ToIdentifier();
|
||||
UnlockAchievement(achvIdentifier, true,
|
||||
c => c != null && !c.IsDead && !c.IsUnconscious && combatMission.IsInWinningTeam(c));
|
||||
c => c != null && !c.IsDead && !c.IsUnconscious && CombatMission.IsInWinningTeam(c));
|
||||
|
||||
// Attempt unlocking mission-specific achievement (if one has been set in the achievement backend)
|
||||
UnlockAchievement(mission.Prefab.AchievementIdentifier, true,
|
||||
c => c != null && !c.IsDead && !c.IsUnconscious && CombatMission.IsInWinningTeam(c));
|
||||
}
|
||||
else if (mission.Completed)
|
||||
else if (mission is not CombatMission && mission.Completed)
|
||||
{
|
||||
//all characters get an achievement
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
@@ -424,7 +590,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
//made it to the destination
|
||||
if (gameSession.Submarine.AtEndExit)
|
||||
if (gameSession.Submarine != null && gameSession.Submarine.AtEndExit)
|
||||
{
|
||||
bool noDamageRun = !roundData.SubWasDamaged && !gameSession.Casualties.Any();
|
||||
|
||||
@@ -454,7 +620,7 @@ namespace Barotrauma
|
||||
|
||||
if (charactersInSub.Count == 1)
|
||||
{
|
||||
//there must be some casualties to get the last mant standing achievement
|
||||
//there must be some casualties to get the last man standing achievement
|
||||
if (gameSession.Casualties.Any())
|
||||
{
|
||||
UnlockAchievement(charactersInSub[0], "lastmanstanding".ToIdentifier());
|
||||
@@ -517,26 +683,28 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func<Character, bool> conditions = null)
|
||||
public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func<Character, bool> characterConditions = null, Func<Client, bool> clientConditions = null)
|
||||
{
|
||||
if (CheatsEnabled) { return; }
|
||||
if (Screen.Selected is { IsEditor: true }) { return; }
|
||||
if (!SupportedAchievements.Contains(identifier)) { return; }
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
|
||||
#endif
|
||||
#if SERVER
|
||||
if (unlockClients && GameMain.Server != null)
|
||||
{
|
||||
foreach (Client c in GameMain.Server.ConnectedClients)
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
if (conditions != null && !conditions(c.Character)) { continue; }
|
||||
GameMain.Server.GiveAchievement(c, identifier);
|
||||
if (clientConditions != null && !clientConditions(client)) { continue; }
|
||||
if (characterConditions != null && !characterConditions(client.Character)) { continue; }
|
||||
GameMain.Server.GiveAchievement(client, identifier);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if CLIENT
|
||||
if (conditions != null && !conditions(Character.Controlled)) { return; }
|
||||
if (characterConditions != null && !characterConditions(Character.Controlled)) { return; }
|
||||
#endif
|
||||
|
||||
UnlockAchievementsOnPlatforms(identifier);
|
||||
|
||||
@@ -25,4 +25,6 @@ namespace Barotrauma
|
||||
Vector2.DistanceSquared(EndWorldPos, currentEndWorldPos) > minDistSquared;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct CachedLocation(Vector2 Location, double RecalculationTime);
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (dropOtherIfCannotMove)
|
||||
{
|
||||
if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags))
|
||||
if (otherItem.Prefab.Identifier == item.Prefab.Identifier || (targetTags != null && otherItem.HasIdentifierOrTags(targetTags)))
|
||||
{
|
||||
bool switchingToBetterSuit =
|
||||
targetTags != null &&
|
||||
@@ -358,13 +358,23 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea = true) => UnequipEmptyItems(Character, parentItem, avoidDroppingInSea);
|
||||
/// <param name="avoidDroppingInSea">When enabled, items are first put in the inventory and dropped only if that fails, unless the character is inside a friendly submarine.</param>
|
||||
/// <param name="allowDestroying">Allows destroying of the items when unequipped (instead of dropping them). Used only with infinite spawns.</param>
|
||||
public void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea = true, bool allowDestroying = false) => UnequipEmptyItems(Character, parentItem, avoidDroppingInSea, allowDestroying);
|
||||
|
||||
public void UnequipContainedItems(Item parentItem, Func<Item, bool> predicate = null, bool avoidDroppingInSea = true, int? unequipMax = null) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea, unequipMax);
|
||||
/// <param name="avoidDroppingInSea">When enabled, items are first put in the inventory and dropped only if that fails, unless the character is inside a friendly submarine.</param>
|
||||
/// <param name="allowDestroying">Allows destroying of the items when unequipped (instead of dropping them). Used only with infinite spawns.</param>
|
||||
/// <param name="unequipMax">Optional max amount for items to be unequipped.</param>
|
||||
public void UnequipContainedItems(Item parentItem, Func<Item, bool> predicate = null, bool avoidDroppingInSea = true, bool allowDestroying = false, int? unequipMax = null) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea, allowDestroying, unequipMax);
|
||||
|
||||
public static void UnequipEmptyItems(Character character, Item parentItem, bool avoidDroppingInSea = true) => UnequipContainedItems(character, parentItem, it => it.Condition <= 0, avoidDroppingInSea);
|
||||
|
||||
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, int? unequipMax = null)
|
||||
/// <param name="avoidDroppingInSea">When enabled, items are first put in the inventory and dropped only if that fails, unless the character is inside a friendly submarine.</param>
|
||||
/// <param name="allowDestroying">Allows destroying of the items when unequipped (instead of dropping them). Used only with infinite spawns.</param>
|
||||
public static void UnequipEmptyItems(Character character, Item parentItem, bool avoidDroppingInSea = true, bool allowDestroying = false) => UnequipContainedItems(character, parentItem, it => it.Condition <= 0, avoidDroppingInSea, allowDestroying);
|
||||
|
||||
/// <param name="avoidDroppingInSea">When enabled, items are first put in the inventory and dropped only if that fails, unless the character is inside a friendly submarine.</param>
|
||||
/// <param name="allowDestroying">Allows destroying of the items when unequipped (instead of dropping them). Used only with infinite spawns.</param>
|
||||
/// <param name="unequipMax">Optional max amount for items to be unequipped.</param>
|
||||
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, bool allowDestroying = false, int? unequipMax = null)
|
||||
{
|
||||
var inventory = parentItem.OwnInventory;
|
||||
if (inventory == null || !inventory.Container.DrawInventory) { return; }
|
||||
@@ -376,21 +386,36 @@ namespace Barotrauma
|
||||
if (containedItem == null) { continue; }
|
||||
if (predicate == null || predicate(containedItem))
|
||||
{
|
||||
if (avoidDroppingInSea && !character.IsInFriendlySub)
|
||||
if (allowDestroying && GameMain.NetworkMember is not { IsClient: true } && character.AIController.HasInfiniteItemSpawns(containedItem.Prefab.Identifier))
|
||||
{
|
||||
// If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
|
||||
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot))
|
||||
{
|
||||
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
|
||||
continue;
|
||||
}
|
||||
Entity.Spawner?.AddItemToRemoveQueue(containedItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (avoidDroppingInSea && !character.IsInFriendlySub)
|
||||
{
|
||||
// If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
|
||||
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot))
|
||||
{
|
||||
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
|
||||
continue;
|
||||
}
|
||||
}
|
||||
containedItem.Drop(character);
|
||||
}
|
||||
containedItem.Drop(character);
|
||||
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasInfiniteItemSpawns(IEnumerable<Identifier> itemIdentifiers)
|
||||
=> (Character.HumanPrefab?.InfiniteItems.Any(it => itemIdentifiers.Contains(it.Identifier) || it.Tags.Any(itemIdentifiers.Contains)) ?? false)
|
||||
|| (Character.Info?.Job?.HasJobItem(jobItem => jobItem.Infinite && itemIdentifiers.Contains(jobItem.GetItemIdentifier(Character.TeamID, isPvPMode: GameMain.GameSession.GameMode is PvPMode))) ?? false);
|
||||
|
||||
public bool HasInfiniteItemSpawns(Identifier itemIdentifier)
|
||||
=> (Character.HumanPrefab?.InfiniteItems.Any(it => it.Identifier == itemIdentifier || it.Tags.Contains(itemIdentifier)) ?? false)
|
||||
|| (Character.Info?.Job?.HasJobItem(jobItem => jobItem.Infinite && jobItem.GetItemIdentifier(Character.TeamID, isPvPMode: GameMain.GameSession.GameMode is PvPMode) == itemIdentifier) ?? false);
|
||||
|
||||
public void ReequipUnequipped()
|
||||
{
|
||||
|
||||
@@ -191,7 +191,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (InDetectable) { return true; }
|
||||
if (Entity == null) { return true; }
|
||||
if (Level.Loaded != null && WorldPosition.Y > Level.Loaded.Size.Y)
|
||||
if (Level.IsPositionAboveLevel(WorldPosition))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -317,18 +317,21 @@ namespace Barotrauma
|
||||
{
|
||||
obstacleRaycastTimer = obstacleRaycastIntervalShort;
|
||||
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
|
||||
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
|
||||
if (Submarine.MainSub != null)
|
||||
{
|
||||
if (connectedSub == Submarine.MainSub) { continue; }
|
||||
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
|
||||
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
|
||||
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
|
||||
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
|
||||
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
|
||||
{
|
||||
PathSteering.CurrentPath.Unreachable = true;
|
||||
break;
|
||||
if (connectedSub == Submarine.MainSub) { continue; }
|
||||
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
|
||||
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
|
||||
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
|
||||
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
|
||||
{
|
||||
PathSteering.CurrentPath.Unreachable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,7 +348,7 @@ namespace Barotrauma
|
||||
enemyCheckTimer -= deltaTime;
|
||||
if (enemyCheckTimer < 0)
|
||||
{
|
||||
CheckEnemies();
|
||||
SpotEnemies();
|
||||
enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
|
||||
}
|
||||
}
|
||||
@@ -440,7 +443,6 @@ namespace Barotrauma
|
||||
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
|
||||
{
|
||||
ReportProblems();
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -464,31 +466,7 @@ namespace Barotrauma
|
||||
bool run = !currentObjective.ForceWalk && (currentObjective.ForceRun || objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority);
|
||||
if (currentObjective is AIObjectiveGoTo goTo)
|
||||
{
|
||||
if (run && goTo == objectiveManager.ForcedOrder && goTo.IsWaitOrder && !Character.IsOnPlayerTeam)
|
||||
{
|
||||
// NPCs with a wait order don't run.
|
||||
run = false;
|
||||
}
|
||||
else if (goTo.Target != null)
|
||||
{
|
||||
if (Character.CurrentHull == null)
|
||||
{
|
||||
run = Vector2.DistanceSquared(Character.WorldPosition, goTo.Target.WorldPosition) > 300 * 300;
|
||||
}
|
||||
else
|
||||
{
|
||||
float yDiff = goTo.Target.WorldPosition.Y - Character.WorldPosition.Y;
|
||||
if (Math.Abs(yDiff) > 100)
|
||||
{
|
||||
run = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
float xDiff = goTo.Target.WorldPosition.X - Character.WorldPosition.X;
|
||||
run = Math.Abs(xDiff) > 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
run = goTo.ShouldRun(run);
|
||||
}
|
||||
|
||||
//if someone is grabbing the bot and the bot isn't trying to run anywhere, let them keep dragging and "control" the bot
|
||||
@@ -561,17 +539,19 @@ namespace Barotrauma
|
||||
ShipCommandManager?.Update(deltaTime);
|
||||
}
|
||||
|
||||
private void CheckEnemies()
|
||||
private void SpotEnemies()
|
||||
{
|
||||
//already in combat, no need to check
|
||||
if (objectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return; }
|
||||
if (objectiveManager.HasActiveObjective<AIObjectiveCombat>()) { return; }
|
||||
|
||||
float closestDistance = 0;
|
||||
Character closestEnemy = null;
|
||||
bool shouldActOffensively = ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>();
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.Submarine != Character.Submarine) { continue; }
|
||||
if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; }
|
||||
if (c.Removed || c.IsDead || c.IsIncapacitated || c.InDetectable) { continue; }
|
||||
if (IsFriendly(c)) { continue; }
|
||||
Vector2 toTarget = c.WorldPosition - WorldPosition;
|
||||
float dist = toTarget.LengthSquared();
|
||||
@@ -593,8 +573,8 @@ namespace Barotrauma
|
||||
}
|
||||
if (closestEnemy != null)
|
||||
{
|
||||
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
|
||||
}
|
||||
AddCombatObjective(shouldActOffensively ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
|
||||
}
|
||||
}
|
||||
|
||||
private void UnequipUnnecessaryItems()
|
||||
@@ -603,7 +583,7 @@ namespace Barotrauma
|
||||
if (ObjectiveManager.CurrentObjective == null) { return; }
|
||||
if (Character.CurrentHull == null) { return; }
|
||||
bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, Tags.OxygenSource, out _, conditionPercentage: 1);
|
||||
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
|
||||
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveMoveItem>();
|
||||
|
||||
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
|
||||
{
|
||||
@@ -618,8 +598,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (findItemState != FindItemState.OtherItem)
|
||||
{
|
||||
var decontain = ObjectiveManager.GetActiveObjectives<AIObjectiveDecontainItem>().LastOrDefault();
|
||||
if (decontain != null && decontain.TargetItem != null && decontain.TargetItem.HasTag(Tags.HeavyDivingGear) &&
|
||||
var moveItemObjective = ObjectiveManager.GetLastActiveObjective<AIObjectiveMoveItem>();
|
||||
if (moveItemObjective is { TargetItem: not null } && moveItemObjective.TargetItem.HasTag(Tags.HeavyDivingGear) &&
|
||||
ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective))
|
||||
{
|
||||
// Don't try to put the diving suit in a locker if the suit would be needed in any hull in the path to the locker.
|
||||
@@ -725,17 +705,17 @@ namespace Barotrauma
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
|
||||
var moveItemObjective = new AIObjectiveMoveItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
|
||||
{
|
||||
DropIfFails = false
|
||||
};
|
||||
decontainObjective.Abandoned += () =>
|
||||
moveItemObjective.Abandoned += () =>
|
||||
{
|
||||
ReequipUnequipped();
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
decontainObjective.Completed += () => ReequipUnequipped();
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
moveItemObjective.Completed += () => ReequipUnequipped();
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(moveItemObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
@@ -761,7 +741,7 @@ namespace Barotrauma
|
||||
HandleRelocation(mask);
|
||||
ReequipUnequipped();
|
||||
}
|
||||
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
|
||||
else if (findItemState is FindItemState.None or FindItemState.DivingMask)
|
||||
{
|
||||
findItemState = FindItemState.DivingMask;
|
||||
if (FindSuitableContainer(mask, out Item targetContainer))
|
||||
@@ -770,14 +750,14 @@ namespace Barotrauma
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () =>
|
||||
var moveItemObjective = new AIObjectiveMoveItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
moveItemObjective.Abandoned += () =>
|
||||
{
|
||||
ReequipUnequipped();
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
decontainObjective.Completed += () => ReequipUnequipped();
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
moveItemObjective.Completed += ReequipUnequipped;
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(moveItemObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
@@ -801,36 +781,41 @@ namespace Barotrauma
|
||||
if (isCarrying) { return; }
|
||||
if (!ObjectiveManager.CurrentObjective.AllowAutomaticItemUnequipping || !ObjectiveManager.GetActiveObjective().AllowAutomaticItemUnequipping) { return; }
|
||||
|
||||
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
|
||||
if (Character.Submarine?.TeamID == Character.TeamID && findItemState is FindItemState.None or FindItemState.OtherItem)
|
||||
{
|
||||
// Only unequip other items inside a friendly sub.
|
||||
foreach (Item item in Character.HeldItems)
|
||||
{
|
||||
if (item == null || !item.IsInteractable(Character)) { continue; }
|
||||
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, CharacterInventory.AnySlot) && Character.Submarine?.TeamID == Character.TeamID)
|
||||
if (Character.TryPutItemInAnySlot(item)) { continue; }
|
||||
if (Character.TryPutItemInBag(item)) { continue; }
|
||||
if (item.HasTag(Tags.Weapon))
|
||||
{
|
||||
if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Bag })) { continue; }
|
||||
findItemState = FindItemState.OtherItem;
|
||||
if (FindSuitableContainer(item, out Item targetContainer))
|
||||
// Don't store weapons in containers, because it could be that we are holding a weapon that cannot be placed on back (if we have a toolbelt) nor in any slot, such as an HMG.
|
||||
// Could check that we only ignore weapons when we've had an order to find a weapon, but it could also be that we picked the weapon for self-defence, on ad-hoc basis.
|
||||
// And I don't think it would make sense to move those weapons in containers either.
|
||||
continue;
|
||||
}
|
||||
findItemState = FindItemState.OtherItem;
|
||||
if (FindSuitableContainer(item, out Item targetContainer))
|
||||
{
|
||||
findItemState = FindItemState.None;
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
findItemState = FindItemState.None;
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
var moveItemObjective = new AIObjectiveMoveItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
moveItemObjective.Abandoned += () =>
|
||||
{
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () =>
|
||||
{
|
||||
ReequipUnequipped();
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Drop(Character);
|
||||
HandleRelocation(item);
|
||||
}
|
||||
ReequipUnequipped();
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(moveItemObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Drop(Character);
|
||||
HandleRelocation(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -842,7 +827,7 @@ namespace Barotrauma
|
||||
public void HandleRelocation(Item item)
|
||||
{
|
||||
if (item.SpawnedInCurrentOutpost) { return; }
|
||||
if (item.Submarine == null) { return; }
|
||||
if (item.Submarine == null || Submarine.MainSub == null) { return; }
|
||||
// Only affects bots in the player team
|
||||
if (!Character.IsOnPlayerTeam) { return; }
|
||||
// Don't relocate if the item is on a sub of the same team
|
||||
@@ -869,6 +854,7 @@ namespace Barotrauma
|
||||
if (item == null || item.Removed) { return; }
|
||||
if (!itemsToRelocate.Contains(item)) { return; }
|
||||
var mainSub = Submarine.MainSub;
|
||||
if (mainSub == null) { return; }
|
||||
Entity owner = item.GetRootInventoryOwner();
|
||||
if (owner != null)
|
||||
{
|
||||
@@ -1036,13 +1022,13 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Character target in Character.CharacterList)
|
||||
{
|
||||
if (target.CurrentHull != hull || !target.Enabled) { continue; }
|
||||
if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false))
|
||||
if (target.CurrentHull != hull || !target.Enabled || target.InDetectable) { continue; }
|
||||
if (AIObjectiveFightIntruders.IsValidTarget(target, Character, targetCharactersInOtherSubs: false))
|
||||
{
|
||||
if (!target.IsHandcuffed && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
|
||||
if (AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
|
||||
{
|
||||
var orderPrefab = OrderPrefab.Prefabs["reportintruders"];
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
if (target.IsEscorted)
|
||||
{
|
||||
@@ -1060,6 +1046,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Character.CombatAction == null && !isFighting)
|
||||
{
|
||||
// Immediately react to enemies when they are spotted. AIObjectiveFightIntruders and AIObjectiveFindSafety would make the bot react to the threats,
|
||||
// but the reaction is delayed (and doesn't necessarily target this enemy), and in many cases the reaction would come only when the enemy attacks and triggers AIObjectiveCombat.
|
||||
AddCombatObjective(ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>() ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (AIObjectiveExtinguishFires.IsValidTarget(hull, Character))
|
||||
@@ -1067,14 +1059,14 @@ namespace Barotrauma
|
||||
if (AddTargets<AIObjectiveExtinguishFires, Hull>(Character, hull) && newOrder == null)
|
||||
{
|
||||
var orderPrefab = OrderPrefab.Prefabs["reportfire"];
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
}
|
||||
}
|
||||
if (IsBallastFloraNoticeable(Character, hull) && newOrder == null)
|
||||
{
|
||||
var orderPrefab = OrderPrefab.Prefabs["reportballastflora"];
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
}
|
||||
if (!isFighting)
|
||||
@@ -1086,7 +1078,7 @@ namespace Barotrauma
|
||||
if (AddTargets<AIObjectiveFixLeaks, Gap>(Character, gap) && newOrder == null && !gap.IsRoomToRoom)
|
||||
{
|
||||
var orderPrefab = OrderPrefab.Prefabs["reportbreach"];
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
}
|
||||
}
|
||||
@@ -1101,7 +1093,7 @@ namespace Barotrauma
|
||||
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && (!Character.IsMedic || Character == target) && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
|
||||
{
|
||||
var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"];
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
}
|
||||
}
|
||||
@@ -1126,20 +1118,19 @@ namespace Barotrauma
|
||||
}
|
||||
if (newOrder != null && speak)
|
||||
{
|
||||
string msg = newOrder.GetChatMessage(string.Empty, targetHull?.DisplayName?.Value ?? string.Empty, givingOrderToSelf: false);
|
||||
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
|
||||
identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
|
||||
minDurationBetweenSimilar: 60.0f);
|
||||
Character.Speak(msg, ChatMessageType.Default, identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(), minDurationBetweenSimilar: 60f);
|
||||
}
|
||||
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
|
||||
Character.Speak(msg, messageType: ChatMessageType.Order);
|
||||
#if SERVER
|
||||
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
|
||||
.WithManualPriority(CharacterInfo.HighestManualOrderPriority)
|
||||
.WithTargetEntity(targetHull)
|
||||
.WithOrderGiver(Character), "", null, Character));
|
||||
.WithOrderGiver(Character), msg, targetCharacter: null, sender: Character));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1163,8 +1154,19 @@ namespace Barotrauma
|
||||
public static void ReportProblem(Character reporter, Order order, Hull targetHull = null)
|
||||
{
|
||||
if (reporter == null || order == null) { return; }
|
||||
var visibleHulls = targetHull is null ? new List<Hull>(reporter.GetVisibleHulls()) : new List<Hull> { targetHull };
|
||||
foreach (var hull in visibleHulls)
|
||||
if (targetHull == null)
|
||||
{
|
||||
foreach (var hull in reporter.GetVisibleHulls())
|
||||
{
|
||||
Report(hull);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Report(targetHull);
|
||||
}
|
||||
|
||||
void Report(Hull hull)
|
||||
{
|
||||
PropagateHullSafety(reporter, hull);
|
||||
RefreshTargets(reporter, order, hull);
|
||||
@@ -1295,7 +1297,13 @@ namespace Barotrauma
|
||||
//if (Character.LastDamageSource == null) { return; }
|
||||
//AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
|
||||
}
|
||||
if (realDamage <= 0 && (attacker.IsBot || attacker.TeamID == Character.TeamID))
|
||||
|
||||
bool sameTeam =
|
||||
attacker.TeamID == Character.TeamID ||
|
||||
// consider escorted characters to be in the same team (otherwise accidental damage or side-effects from healing trigger them too easily)
|
||||
(attacker.TeamID == CharacterTeamType.Team1 && Character.IsEscorted);
|
||||
|
||||
if (realDamage <= 0 && (attacker.IsBot || sameTeam))
|
||||
{
|
||||
// Don't react to damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player
|
||||
return;
|
||||
@@ -1307,9 +1315,9 @@ namespace Barotrauma
|
||||
}
|
||||
bool isAttackerInfected = false;
|
||||
bool isAttackerFightingEnemy = false;
|
||||
float minorDamageThreshold = 1;
|
||||
float minorDamageThreshold = 5;
|
||||
float majorDamageThreshold = 20;
|
||||
if (attacker.TeamID == Character.TeamID && !attacker.IsInstigator)
|
||||
if (sameTeam && !attacker.IsInstigator)
|
||||
{
|
||||
minorDamageThreshold = 10;
|
||||
majorDamageThreshold = 40;
|
||||
@@ -1338,7 +1346,7 @@ namespace Barotrauma
|
||||
// Inform other NPCs
|
||||
if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
|
||||
{
|
||||
if (GameMain.IsMultiplayer || !attacker.IsPlayer || Character.TeamID != attacker.TeamID)
|
||||
if (!attacker.IsPlayer || Character.TeamID != attacker.TeamID)
|
||||
{
|
||||
InformOtherNPCs(cumulativeDamage);
|
||||
}
|
||||
@@ -1406,7 +1414,12 @@ namespace Barotrauma
|
||||
if (otherCharacter.IsPlayer) { continue; }
|
||||
if (otherCharacter.AIController is not HumanAIController otherHumanAI) { continue; }
|
||||
if (!otherHumanAI.IsFriendly(Character)) { continue; }
|
||||
if (attacker.AIController is EnemyAIController enemyAI && otherHumanAI.IsFriendly(attacker))
|
||||
if (otherHumanAI.objectiveManager.IsCurrentObjective<AIObjectiveCombat>() || otherHumanAI.objectiveManager.HasActiveObjective<AIObjectiveCombat>())
|
||||
{
|
||||
// Already in combat, don't change target (because we are not attacked by the enemy)
|
||||
return;
|
||||
}
|
||||
if (attacker.AIController is EnemyAIController && otherHumanAI.IsFriendly(attacker))
|
||||
{
|
||||
// Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist.
|
||||
continue;
|
||||
@@ -1417,15 +1430,33 @@ namespace Barotrauma
|
||||
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)
|
||||
//don't react to the attack
|
||||
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID || !CheckReportRange(Character, otherCharacter, ReportRange))
|
||||
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID)
|
||||
{
|
||||
// Dead or in different team -> cannot report.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (otherHumanAI.objectiveManager.HasOrders())
|
||||
{
|
||||
// Unless witnessing the attack, don't react, if have been ordered to do something.
|
||||
// The combat objective would take a higher prio than the order.
|
||||
continue;
|
||||
}
|
||||
if (!CheckReportRange(Character, otherCharacter, ReportRange))
|
||||
{
|
||||
// Not inside report range -> cannot report.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing);
|
||||
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
|
||||
if (!isWitnessing)
|
||||
{
|
||||
if (combatMode is AIObjectiveCombat.CombatMode.Defensive or AIObjectiveCombat.CombatMode.Retreat)
|
||||
{
|
||||
// Ignore defensive and retreating behavior, unless witnessing the attack.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 3.0f, Rand.RandSync.Unsynced);
|
||||
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
|
||||
}
|
||||
}
|
||||
@@ -1461,12 +1492,8 @@ namespace Barotrauma
|
||||
}
|
||||
if (attacker.IsPlayer && c.TeamID == attacker.TeamID)
|
||||
{
|
||||
if (GameMain.IsSingleplayer || c.TeamID != attacker.TeamID)
|
||||
{
|
||||
// Bots in the player team never act aggressively in single player when attacked by the player
|
||||
// In multiplayer, they react only to players attacking them or other crew members
|
||||
return Character == c && cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
|
||||
}
|
||||
// Bots in the player team never act aggressively when attacked by the player
|
||||
return Character == c && cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
|
||||
}
|
||||
if (c.Submarine == null || !c.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
|
||||
{
|
||||
@@ -1511,16 +1538,18 @@ namespace Barotrauma
|
||||
// Already targeting the attacker -> treat as a more serious threat.
|
||||
cumulativeDamage *= 2;
|
||||
currentCombatObjective.AllowHoldFire = false;
|
||||
c.IsCriminal = true;
|
||||
attacker.IsCriminal = true;
|
||||
attacker.IsActingOffensively = true;
|
||||
}
|
||||
if (c.IsCriminal)
|
||||
if (attacker.IsCriminal)
|
||||
{
|
||||
// Always react if the attacker has been misbehaving earlier.
|
||||
cumulativeDamage = Math.Max(cumulativeDamage, minorDamageThreshold);
|
||||
}
|
||||
if (cumulativeDamage > majorDamageThreshold)
|
||||
{
|
||||
c.IsCriminal = true;
|
||||
attacker.IsCriminal = true;
|
||||
attacker.IsActingOffensively = true;
|
||||
if (c.IsSecurity)
|
||||
{
|
||||
return AIObjectiveCombat.CombatMode.Offensive;
|
||||
@@ -1532,6 +1561,7 @@ namespace Barotrauma
|
||||
}
|
||||
else if (cumulativeDamage > minorDamageThreshold)
|
||||
{
|
||||
attacker.IsActingOffensively = true;
|
||||
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
|
||||
}
|
||||
else
|
||||
@@ -1596,7 +1626,6 @@ namespace Barotrauma
|
||||
{
|
||||
var objective = new AIObjectiveCombat(Character, target, mode, objectiveManager)
|
||||
{
|
||||
HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
|
||||
AbortCondition = abortCondition,
|
||||
AllowHoldFire = allowHoldFire,
|
||||
SpeakWarnings = speakWarnings
|
||||
@@ -1681,21 +1710,31 @@ namespace Barotrauma
|
||||
|
||||
public bool AllowCampaignInteraction()
|
||||
{
|
||||
if (Character == null || Character.Removed || Character.IsIncapacitated) { return false; }
|
||||
if (Character == null || Character.Removed) { return false; }
|
||||
|
||||
switch (ObjectiveManager.CurrentObjective)
|
||||
//some events might want to allow talking/examining characters that are incapacitated or in some "emergency" ai state,
|
||||
//so let's ignore those here
|
||||
var type = Character.CampaignInteractionType;
|
||||
if (type != CampaignMode.InteractionType.None &&
|
||||
type != CampaignMode.InteractionType.Talk &&
|
||||
type != CampaignMode.InteractionType.Examine)
|
||||
{
|
||||
case AIObjectiveCombat _:
|
||||
case AIObjectiveFindSafety _:
|
||||
case AIObjectiveExtinguishFires _:
|
||||
case AIObjectiveFightIntruders _:
|
||||
case AIObjectiveFixLeaks _:
|
||||
return false;
|
||||
if (Character.IsIncapacitated) { return false; }
|
||||
switch (ObjectiveManager.CurrentObjective)
|
||||
{
|
||||
case AIObjectiveCombat _:
|
||||
case AIObjectiveFindSafety _:
|
||||
case AIObjectiveExtinguishFires _:
|
||||
case AIObjectiveFightIntruders _:
|
||||
case AIObjectiveFixLeaks _:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool NeedsDivingGear(Hull hull, out bool needsSuit)
|
||||
|
||||
/// <param name="objectiveManager">Used for checking the objective.</param>
|
||||
public bool NeedsDivingGear(Hull hull, out bool needsSuit, AIObjectiveManager objectiveManager = null)
|
||||
{
|
||||
needsSuit = false;
|
||||
bool needsAir = Character.NeedsAir && Character.CharacterHealth.OxygenLowResistance < 1;
|
||||
@@ -1704,7 +1743,11 @@ namespace Barotrauma
|
||||
hull.LethalPressure > 0 ||
|
||||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
|
||||
{
|
||||
needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure;
|
||||
if (!Character.IsImmuneToPressure)
|
||||
{
|
||||
// Always require a diving suit when operating an item in a flooding room.
|
||||
needsSuit = hull == null || hull.LethalPressure > 0 || objectiveManager?.CurrentOrder is AIObjectiveOperateItem operateItem && operateItem.GetTarget().Item.CurrentHull == hull;
|
||||
}
|
||||
return needsAir || needsSuit;
|
||||
}
|
||||
if (hull.WaterPercentage > 60 || (hull.IsWetRoom && hull.WaterPercentage > 10) || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
|
||||
@@ -1839,6 +1882,7 @@ namespace Barotrauma
|
||||
if (combatMode == AIObjectiveCombat.CombatMode.Offensive)
|
||||
{
|
||||
character.IsCriminal = true;
|
||||
character.IsActingOffensively = true;
|
||||
}
|
||||
if (!TriggerSecurity(otherHumanAI, combatMode))
|
||||
{
|
||||
@@ -1879,6 +1923,11 @@ namespace Barotrauma
|
||||
public static void ItemTaken(Item item, Character thief)
|
||||
{
|
||||
if (item == null || thief == null || item.GetComponent<LevelResource>() != null) { return; }
|
||||
if (thief.IsBot && item.HasTag(AIObjectiveGetItem.AllowedItemsToTake))
|
||||
{
|
||||
// Bots are allowed to take diving gear or extinguishers, when they need them, without it being considered as stealing.
|
||||
return;
|
||||
}
|
||||
bool someoneSpoke = false;
|
||||
if (item.Illegitimate && item.GetRootInventoryOwner() is Character itemOwner && itemOwner != thief && itemOwner.TeamID == thief.TeamID)
|
||||
{
|
||||
@@ -1992,16 +2041,16 @@ namespace Barotrauma
|
||||
/// The safety levels need to be calculated for each bot individually, because the formula takes into account things like current orders.
|
||||
/// There's now a cached value per each hull, which should prevent too frequent calculations.
|
||||
/// </summary>
|
||||
public static void PropagateHullSafety(Character character, Hull hull)
|
||||
private static void PropagateHullSafety(Character character, Hull hull)
|
||||
{
|
||||
DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
|
||||
DoForEachBot(character, humanAi => humanAi.RefreshHullSafety(hull));
|
||||
}
|
||||
|
||||
public void AskToRecalculateHullSafety(Hull hull) => dirtyHullSafetyCalculations.Add(hull);
|
||||
|
||||
private void RefreshHullSafety(Hull hull)
|
||||
{
|
||||
var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.GetConnectedHulls(includingThis: true, searchDepth: 1) : VisibleHulls;
|
||||
var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.GetConnectedHulls(includingThis: true, searchDepth: 1) : null;
|
||||
float hullSafety = GetHullSafety(hull, Character, visibleHulls);
|
||||
if (hullSafety > HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
@@ -2013,7 +2062,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static void RefreshTargets(Character character, Order order, Hull hull)
|
||||
private static void RefreshTargets(Character character, Order order, Hull hull)
|
||||
{
|
||||
switch (order.Identifier.Value.ToLowerInvariant())
|
||||
{
|
||||
@@ -2044,7 +2093,7 @@ namespace Barotrauma
|
||||
foreach (var enemy in Character.CharacterList)
|
||||
{
|
||||
if (enemy.CurrentHull != hull) { continue; }
|
||||
if (AIObjectiveFightIntruders.IsValidTarget(enemy, character, false))
|
||||
if (AIObjectiveFightIntruders.IsValidTarget(enemy, character, targetCharactersInOtherSubs: false))
|
||||
{
|
||||
AddTargets<AIObjectiveFightIntruders, Character>(character, enemy);
|
||||
}
|
||||
@@ -2123,7 +2172,7 @@ namespace Barotrauma
|
||||
// Use the cached visible hulls
|
||||
visibleHulls = VisibleHulls;
|
||||
}
|
||||
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
|
||||
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires { Priority: > 0 } || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
|
||||
bool ignoreOxygen = HasDivingGear(character);
|
||||
bool ignoreEnemies = ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>();
|
||||
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies);
|
||||
@@ -2133,12 +2182,26 @@ namespace Barotrauma
|
||||
}
|
||||
return safety;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns hull safety for the character without ignoring any threats.
|
||||
/// Useful for example, when we need to calculate a safety value of the hull regardless of the protective equipment or buffs of the character.
|
||||
/// No caching involved (always recalculated).
|
||||
/// </summary>
|
||||
public static float CalculateObjectiveHullSafety(Character character) => CalculateHullSafety(
|
||||
hull: character.CurrentHull,
|
||||
visibleHulls: character.AIController?.VisibleHulls ?? character.GetVisibleHulls(),
|
||||
character,
|
||||
ignoreEnemies: false, ignoreFire: false, ignoreWater: false, ignoreOxygen: false, ignorePressureProtection: true);
|
||||
|
||||
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
|
||||
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false, bool ignorePressureProtection = false)
|
||||
{
|
||||
bool isProtectedFromPressure = character.IsProtectedFromPressure;
|
||||
if (hull == null) { return isProtectedFromPressure ? 100 : 0; }
|
||||
if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; }
|
||||
if (!ignorePressureProtection)
|
||||
{
|
||||
bool isProtectedFromPressure = character.IsProtectedFromPressure;
|
||||
if (hull == null) { return isProtectedFromPressure ? 100 : 0; }
|
||||
if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; }
|
||||
}
|
||||
// Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower.
|
||||
// With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD.
|
||||
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage));
|
||||
@@ -2157,42 +2220,56 @@ namespace Barotrauma
|
||||
waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
|
||||
}
|
||||
}
|
||||
if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1)
|
||||
if (!ignoreOxygen)
|
||||
{
|
||||
oxygenFactor = 1;
|
||||
}
|
||||
if (isProtectedFromPressure)
|
||||
{
|
||||
waterFactor = 1;
|
||||
if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1)
|
||||
{
|
||||
oxygenFactor = 1;
|
||||
}
|
||||
}
|
||||
float fireFactor = 1;
|
||||
if (!ignoreFire)
|
||||
{
|
||||
static float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
|
||||
// Even the smallest fire reduces the safety by 50%
|
||||
float fire = visibleHulls == null ? calculateFire(hull) : visibleHulls.Sum(h => calculateFire(h));
|
||||
float fire = CalculateFire(hull) + hull.linkedTo.Sum(e => CalculateFire(e as Hull));
|
||||
fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
|
||||
|
||||
float CalculateFire(Hull h)
|
||||
{
|
||||
if (h is not Hull) { return 0; }
|
||||
bool isInDamageRange = h.FireSources.Any(fs => fs.IsInDamageRange(character, fs.DamageRange));
|
||||
if (isInDamageRange) { return 1; }
|
||||
// Even the smallest fire reduces the safety by 50%
|
||||
return h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
|
||||
}
|
||||
}
|
||||
float enemyFactor = 1;
|
||||
if (!ignoreEnemies)
|
||||
{
|
||||
int enemyCount = 0;
|
||||
float enemyCount = 0;
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
float countModifier = 1.0f;
|
||||
if (c.CurrentHull == null) { continue; }
|
||||
if (visibleHulls == null)
|
||||
{
|
||||
if (c.CurrentHull != hull) { continue; }
|
||||
if (c.CurrentHull != hull && !c.CurrentHull.linkedTo.Contains(hull)) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!visibleHulls.Contains(c.CurrentHull)) { continue; }
|
||||
if (c.CurrentHull != hull && !c.CurrentHull.linkedTo.Contains(hull))
|
||||
{
|
||||
// Enemy in a visible room, but not in the same room -> a lower threat
|
||||
countModifier = 0.25f;
|
||||
}
|
||||
}
|
||||
if (IsActive(c) && !IsFriendly(character, c) && !c.IsHandcuffed)
|
||||
{
|
||||
enemyCount++;
|
||||
enemyCount += countModifier;
|
||||
}
|
||||
}
|
||||
// The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages)
|
||||
// The hull safety decreases 90% per enemy up to 100%,
|
||||
// and 22.5% per each enemy in the visible, adjacent rooms
|
||||
enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1));
|
||||
}
|
||||
float dangerousItemsFactor = 1f;
|
||||
@@ -2212,7 +2289,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (hull == null)
|
||||
{
|
||||
return CalculateHullSafety(hull, character, visibleHulls);
|
||||
return CalculateHullSafety(null, character, visibleHulls);
|
||||
}
|
||||
if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety))
|
||||
{
|
||||
@@ -2230,7 +2307,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (hull == null)
|
||||
{
|
||||
return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
|
||||
return CalculateHullSafety(null, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
|
||||
}
|
||||
HullSafety hullSafety;
|
||||
if (character.AIController is HumanAIController controller)
|
||||
@@ -2255,11 +2332,14 @@ namespace Barotrauma
|
||||
return hullSafety.safety;
|
||||
}
|
||||
|
||||
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
|
||||
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false, bool ignoreHuskDisguising = false)
|
||||
{
|
||||
if (other.IsHusk)
|
||||
if (onlySameTeam)
|
||||
{
|
||||
ignoreHuskDisguising = true;
|
||||
}
|
||||
if (other.IsHusk && !ignoreHuskDisguising)
|
||||
{
|
||||
// Disguised as husk
|
||||
return me.IsDisguisedAsHusk;
|
||||
}
|
||||
else
|
||||
@@ -2290,16 +2370,15 @@ namespace Barotrauma
|
||||
{
|
||||
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
|
||||
}
|
||||
if (GameMain.GameSession?.GameMode is CampaignMode)
|
||||
if (GameMain.GameSession?.GameMode is CampaignMode &&
|
||||
//ignore hostile faction if offering services that don't get disabled by faction hostility
|
||||
(me.CampaignInteractionType == CampaignMode.InteractionType.None || CampaignMode.HostileFactionDisablesInteraction(me.CampaignInteractionType)))
|
||||
{
|
||||
if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) ||
|
||||
(me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC))
|
||||
{
|
||||
Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other;
|
||||
|
||||
//NPCs that allow some campaign interaction are not turned hostile by low reputation
|
||||
if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; }
|
||||
|
||||
if (npc.AIController is HumanAIController npcAI)
|
||||
{
|
||||
return !npcAI.IsInHostileFaction();
|
||||
@@ -2332,7 +2411,7 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsActive(Character c) => c != null && c.Enabled && !c.IsUnconscious;
|
||||
public static bool IsActive(Character c) => c is { Enabled: true, IsUnconscious: false };
|
||||
|
||||
public static bool IsTrueForAllBotsInTheCrew(Character character, Func<HumanAIController, bool> predicate)
|
||||
{
|
||||
@@ -2344,7 +2423,7 @@ namespace Barotrauma
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2412,7 +2491,7 @@ namespace Barotrauma
|
||||
private static void DoForEachBot(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity)
|
||||
{
|
||||
if (character == null) { return; }
|
||||
foreach (var c in Character.CharacterList)
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (IsBotInTheCrew(character, c) && CheckReportRange(character, c, range))
|
||||
{
|
||||
@@ -2453,11 +2532,11 @@ namespace Barotrauma
|
||||
operatingCharacter = c;
|
||||
return true;
|
||||
}
|
||||
if (c.AIController is HumanAIController humanAI && humanAI.ObjectiveManager is AIObjectiveManager objectiveManager)
|
||||
if (c.AIController is HumanAIController { ObjectiveManager: AIObjectiveManager objectiveManager })
|
||||
{
|
||||
foreach (var objective in objectiveManager.Objectives)
|
||||
{
|
||||
if (!(objective is AIObjectiveOperateItem operateObjective)) { continue; }
|
||||
if (objective is not AIObjectiveOperateItem operateObjective) { continue; }
|
||||
if (operateObjective.Component?.Item != target.Item) { continue; }
|
||||
if (operateObjective.Priority < highestPriority) { continue; }
|
||||
if (operateObjective.PriorityModifier < highestPriorityModifier) { continue; }
|
||||
@@ -2470,135 +2549,6 @@ namespace Barotrauma
|
||||
return operatingCharacter != null;
|
||||
}
|
||||
|
||||
// There's some duplicate logic in the two methods below, but making them use the same code would require some changes in the target classes so that we could use exactly the same checks.
|
||||
// And even then there would be some differences that could end up being confusing (like the exception for steering).
|
||||
public bool IsItemOperatedByAnother(ItemComponent target, out Character other)
|
||||
{
|
||||
other = null;
|
||||
if (target?.Item == null) { return false; }
|
||||
bool isOrder = IsOrderedToOperateThis(Character.AIController);
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!IsActive(c)) { continue; }
|
||||
if (c == Character) { continue; }
|
||||
if (c.TeamID != Character.TeamID) { continue; }
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
if (c.SelectedItem == target.Item)
|
||||
{
|
||||
// If the other character is player, don't try to operate
|
||||
other = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (c.AIController is HumanAIController operatingAI)
|
||||
{
|
||||
if (operatingAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item))
|
||||
{
|
||||
// Not targeting the same item.
|
||||
continue;
|
||||
}
|
||||
bool isTargetOrdered = IsOrderedToOperateThis(c.AIController);
|
||||
if (!isOrder && isTargetOrdered)
|
||||
{
|
||||
// If the other bot is ordered to operate the item, let him do it, unless we are ordered too
|
||||
other = c;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isOrder && !isTargetOrdered)
|
||||
{
|
||||
// We are ordered and the target is not -> allow to operate
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
|
||||
{
|
||||
// The other bot is ordered to do something else
|
||||
continue;
|
||||
}
|
||||
if (target is Steering)
|
||||
{
|
||||
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
|
||||
if (Character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"))
|
||||
{
|
||||
other = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (target.DegreeOfSuccess(Character) <= target.DegreeOfSuccess(c))
|
||||
{
|
||||
other = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return other != null;
|
||||
bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item;
|
||||
}
|
||||
|
||||
public bool IsItemRepairedByAnother(Item target, out Character other)
|
||||
{
|
||||
other = null;
|
||||
if (Character == null) { return false; }
|
||||
if (target == null) { return false; }
|
||||
bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController);
|
||||
foreach (var c in Character.CharacterList)
|
||||
{
|
||||
if (!IsActive(c)) { continue; }
|
||||
if (c == Character) { continue; }
|
||||
if (c.TeamID != Character.TeamID) { continue; }
|
||||
other = c;
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
if (target.Repairables.Any(r => r.CurrentFixer == c))
|
||||
{
|
||||
// If the other character is player, don't try to repair
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (c.AIController is HumanAIController operatingAI)
|
||||
{
|
||||
var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
|
||||
if (repairItemsObjective == null) { continue; }
|
||||
if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target)
|
||||
{
|
||||
// Not targeting the same item.
|
||||
continue;
|
||||
}
|
||||
bool isTargetOrdered = IsOrderedToRepairThis(operatingAI);
|
||||
if (!isOrder && isTargetOrdered)
|
||||
{
|
||||
// If the other bot is ordered to repair the item, let him do it, unless we are ordered too
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isOrder && !isTargetOrdered)
|
||||
{
|
||||
// We are ordered and the target is not -> allow to repair
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
|
||||
{
|
||||
// The other bot is ordered to do something else
|
||||
continue;
|
||||
}
|
||||
return target.Repairables.Max(r => r.DegreeOfSuccess(Character)) <= target.Repairables.Max(r => r.DegreeOfSuccess(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
bool IsOrderedToRepairThis(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairOrder && repairOrder.PrioritizedItem == target;
|
||||
}
|
||||
|
||||
#region Wrappers
|
||||
public bool IsFriendly(Character other, bool onlySameTeam = false) => IsFriendly(Character, other, onlySameTeam);
|
||||
public bool IsTrueForAnyBotInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAnyBotInTheCrew(Character, predicate);
|
||||
|
||||
@@ -320,8 +320,7 @@ namespace Barotrauma
|
||||
Vector2 pos = host.WorldPosition;
|
||||
Vector2 diff = currentPath.CurrentNode.WorldPosition - pos;
|
||||
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
|
||||
// Only humanoids can climb ladders
|
||||
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
|
||||
bool canClimb = character.CanClimb;
|
||||
Ladder currentLadder = GetCurrentLadder();
|
||||
Ladder nextLadder = GetNextLadder();
|
||||
var ladders = currentLadder ?? nextLadder;
|
||||
@@ -559,26 +558,42 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// We'll want this to run each time, because the delegate is used to find a valid button component.
|
||||
bool canAccessButtons = false;
|
||||
foreach (var button in door.Item.GetConnectedComponents<Controller>(true, connectionFilter: c => c.Name == "toggle" || c.Name == "set_state"))
|
||||
bool buttonsFound = false;
|
||||
// Check wired controllers (e.g. buttons)
|
||||
// Always run the buttonFilter delegate (inside CanAccessButton method), if defined, because it's used for find a valid controller component that can be used for closing the door, when needed.
|
||||
// TODO: connectionFilter is ignored in the recursive searches, so it does nothing here.
|
||||
foreach (Controller button in door.Item.GetConnectedComponents<Controller>(recursive: true, connectionFilter: c => c.Name is "toggle" or "set_state"))
|
||||
{
|
||||
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
|
||||
buttonsFound = true;
|
||||
if (CanAccessButton(button))
|
||||
{
|
||||
canAccessButtons = true;
|
||||
}
|
||||
}
|
||||
foreach (var linked in door.Item.linkedTo)
|
||||
if (!canAccessButtons)
|
||||
{
|
||||
if (linked is not Item linkedItem) { continue; }
|
||||
var button = linkedItem.GetComponent<Controller>();
|
||||
if (button == null) { continue; }
|
||||
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
|
||||
// Check linked controllers (more complex circuits)
|
||||
foreach (MapEntity linked in door.Item.linkedTo)
|
||||
{
|
||||
canAccessButtons = true;
|
||||
}
|
||||
}
|
||||
return canAccessButtons || door.IsOpen || ShouldBreakDoor(door);
|
||||
if (linked is not Item linkedItem) { continue; }
|
||||
var button = linkedItem.GetComponent<Controller>();
|
||||
if (button == null) { continue; }
|
||||
buttonsFound = true;
|
||||
if (CanAccessButton(button))
|
||||
{
|
||||
canAccessButtons = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (door.IsOpen || ShouldBreakDoor(door))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// If no buttons were found, just trust it if we should have the access to the door. Could be there's some other mechanism controlling the door.
|
||||
return buttonsFound ? canAccessButtons : door.HasAccess(character);
|
||||
|
||||
bool CanAccessButton(Controller button) => button.HasAccess(character) && (buttonFilter == null || buttonFilter(button));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,12 +728,15 @@ namespace Barotrauma
|
||||
float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
|
||||
//heavily prefer buttons linked to the door, so sub builders can help the bots figure out which button to use by linking them
|
||||
if (door.Item.linkedTo.Contains(button.Item)) { distance *= 0.1f; }
|
||||
if (closestButton == null || distance < closestDist && character.CanSeeTarget(button.Item))
|
||||
if (closestButton == null || distance < closestDist)
|
||||
{
|
||||
closestButton = button;
|
||||
closestDist = distance;
|
||||
if (distance < MathUtils.Pow2(button.Item.InteractDistance + GetColliderLength()) && character.CanSeeTarget(button.Item))
|
||||
{
|
||||
closestButton = button;
|
||||
closestDist = distance;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return closestButton != null;
|
||||
});
|
||||
if (canAccess)
|
||||
{
|
||||
@@ -741,41 +759,19 @@ namespace Barotrauma
|
||||
}
|
||||
else if (closestButton != null)
|
||||
{
|
||||
if (closestDist < MathUtils.Pow2(closestButton.Item.InteractDistance + GetColliderLength()))
|
||||
if (pressButton)
|
||||
{
|
||||
if (pressButton)
|
||||
if (closestButton.Item.TryInteract(character, forceSelectKey: true))
|
||||
{
|
||||
if (closestButton.Item.TryInteract(character, forceSelectKey: true))
|
||||
{
|
||||
lastDoor = (door, shouldBeOpen);
|
||||
buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonPressTimer = 0;
|
||||
}
|
||||
lastDoor = (door, shouldBeOpen);
|
||||
buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Can't reach the button closest to the character.
|
||||
// It's possible that we could reach another buttons.
|
||||
// If this becomes an issue, we could go through them here and check if any of them are reachable
|
||||
// (would have to cache a collection of buttons instead of a single reference in the CanAccess filter method above)
|
||||
var body = Submarine.PickBody(character.SimPosition, character.GetRelativeSimPosition(closestButton.Item), collisionCategory: Physics.CollisionWall | Physics.CollisionLevel);
|
||||
if (body != null)
|
||||
else
|
||||
{
|
||||
if (body.UserData is Item item)
|
||||
{
|
||||
var d = item.GetComponent<Door>();
|
||||
if (d == null || d.IsOpen) { return; }
|
||||
}
|
||||
// The button is on the wrong side of the door or a wall
|
||||
currentPath.Unreachable = true;
|
||||
buttonPressTimer = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (shouldBeOpen)
|
||||
@@ -796,10 +792,9 @@ namespace Barotrauma
|
||||
float? penalty = GetSingleNodePenalty(nextNode);
|
||||
if (penalty == null) { return null; }
|
||||
bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y;
|
||||
//non-humanoids can't climb up ladders
|
||||
if (!(character.AnimController is HumanoidAnimController))
|
||||
if (!character.CanClimb)
|
||||
{
|
||||
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands)||
|
||||
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands) ||
|
||||
(nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
|
||||
nextNodeAboveWaterLevel)) //upper node not underwater
|
||||
{
|
||||
@@ -847,7 +842,7 @@ namespace Barotrauma
|
||||
if (!node.Waypoint.IsTraversable) { return null; }
|
||||
if (node.IsBlocked()) { return null; }
|
||||
float penalty = 0.0f;
|
||||
if (node.Waypoint.ConnectedGap != null && node.Waypoint.ConnectedGap.Open < 0.9f)
|
||||
if (node.Waypoint.ConnectedGap is { Open: < 0.9f })
|
||||
{
|
||||
var door = node.Waypoint.ConnectedDoor;
|
||||
if (door == null)
|
||||
@@ -858,19 +853,29 @@ namespace Barotrauma
|
||||
{
|
||||
if (!CanAccessDoor(door, button =>
|
||||
{
|
||||
// Ignore buttons that are on the wrong side of the door
|
||||
if (Vector2.DistanceSquared(door.Item.WorldPosition, button.Item.WorldPosition) > MathUtils.Pow2(button.Item.InteractDistance + GetColliderLength()))
|
||||
{
|
||||
// Too far from the door.
|
||||
return false;
|
||||
}
|
||||
if (!ISpatialEntity.IsTargetVisible(button.Item, door.Item))
|
||||
{
|
||||
// Obstructed.
|
||||
return false;
|
||||
}
|
||||
// Ignore buttons that are on the wrong side of the door, unless there's a motion sensor connected to the door, which can be triggered by the character.
|
||||
if (door.IsHorizontal)
|
||||
{
|
||||
if (Math.Sign(button.Item.WorldPosition.Y - door.Item.WorldPosition.Y) != Math.Sign(character.WorldPosition.Y - door.Item.WorldPosition.Y))
|
||||
{
|
||||
return false;
|
||||
return door.Item.GetDirectlyConnectedComponent<MotionSensor>() is MotionSensor ms && ms.TriggersOn(character);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Math.Sign(button.Item.WorldPosition.X - door.Item.WorldPosition.X) != Math.Sign(character.WorldPosition.X - door.Item.WorldPosition.X))
|
||||
{
|
||||
return false;
|
||||
return door.Item.GetDirectlyConnectedComponent<MotionSensor>() is MotionSensor ms && ms.TriggersOn(character);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -308,7 +308,7 @@ namespace Barotrauma
|
||||
if (enemyAI.AttackLimb == null) { break; }
|
||||
if (targetBody == null) { break; }
|
||||
if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; }
|
||||
Vector2 referencePos = TargetCharacter != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos);
|
||||
Vector2 referencePos = TargetCharacter?.WorldPosition ?? ConvertUnits.ToDisplayUnits(transformedAttachPos);
|
||||
if (Vector2.DistanceSquared(referencePos, enemyAI.AttackLimb.WorldPosition) < enemyAI.AttackLimb.attack.DamageRange * enemyAI.AttackLimb.attack.DamageRange)
|
||||
{
|
||||
AttachToBody(transformedAttachPos);
|
||||
|
||||
@@ -274,7 +274,7 @@ namespace Barotrauma
|
||||
public bool IsIgnoredAtOutpost()
|
||||
{
|
||||
if (!IgnoreAtOutpost) { return false; }
|
||||
if (!Level.IsLoadedFriendlyOutpost) { return false; }
|
||||
if (!Level.IsLoadedFriendlyOutpost && GameMain.GameSession.GameMode is not TestGameMode) { return false; }
|
||||
if (!character.IsOnPlayerTeam || character.IsFriendlyNPCTurnedHostile) { return false; }
|
||||
if (character.Submarine?.Info == null) { return false; }
|
||||
return character.Submarine.Info.IsOutpost && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC;
|
||||
@@ -513,18 +513,19 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
private bool Check()
|
||||
{
|
||||
if (isCompleted) { return true; }
|
||||
if (AbortCondition != null && AbortCondition(this))
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
return CheckObjectiveSpecific();
|
||||
return CheckObjectiveState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should return whether the objective is completed or not.
|
||||
/// </summary>
|
||||
protected abstract bool CheckObjectiveSpecific();
|
||||
protected abstract bool CheckObjectiveState();
|
||||
|
||||
private bool CheckState()
|
||||
{
|
||||
@@ -574,8 +575,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void SpeakAfterOrderReceived() { }
|
||||
|
||||
protected static bool CanPutInInventory(Character character, Item item, bool allowWearing)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
|
||||
+38
-18
@@ -45,22 +45,25 @@ namespace Barotrauma
|
||||
InitTimers();
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (character.IsClimbing)
|
||||
{
|
||||
// Target is climbing -> stop following the objective (soft abandon, without ignoring the target).
|
||||
Priority = 0;
|
||||
}
|
||||
else if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
|
||||
{
|
||||
if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
|
||||
{
|
||||
Priority = objectiveManager.GetOrderPriority(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Priority = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
if (HumanAIController.CurrentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD || HumanAIController.CalculateObjectiveHullSafety(Target) < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
// Don't do inspections in unsafe hulls, because under a threat, bots are allowed to wear diving gear or hold fire extinguishers etc. Even if they are "stolen".
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Priority = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
}
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
@@ -86,18 +89,18 @@ namespace Barotrauma
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
if (character.IsClimbing)
|
||||
if (character.IsClimbing || HumanAIController.CurrentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD || HumanAIController.CalculateObjectiveHullSafety(Target) < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
// Shouldn't start inspecting characters when they climb, nor get here, because the priority should be 0,
|
||||
// but if this still happens, we'll have to abandon the objective
|
||||
// because it's not currently possible to hold to characters and ladders at the same time.
|
||||
// Don't do inspections in unsafe hulls, because under a threat, bots are allowed to wear diving gear or hold fire extinguishers etc. Even if they are "stolen".
|
||||
// Shouldn't start inspecting characters when they climb, but we can still get here, if they start climbing while we are moving at them.
|
||||
// If that happens, let's abandon the objective, because it's not currently possible to hold to characters and ladders at the same time.
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentState = State.Inspect;
|
||||
stolenItems.Clear();
|
||||
Target.Inventory.FindAllItems(it => it.Illegitimate, recursive: true, stolenItems);
|
||||
Target.Inventory.FindAllItems(it => IsItemIllegitimate(Target, it), recursive: true, stolenItems);
|
||||
character.Speak(TextManager.Get(Target.IsCriminal ? "dialogcheckstolenitems.criminal" : "dialogcheckstolenitems").Value);
|
||||
}
|
||||
},
|
||||
@@ -190,11 +193,23 @@ namespace Barotrauma
|
||||
var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == Target);
|
||||
if (stolenItemsOnCharacter.Any())
|
||||
{
|
||||
character.Speak(TextManager.Get(character.IsCriminal ? "dialogcheckstolenitems.arrest.criminal" : "dialogcheckstolenitems.arrest").Value);
|
||||
Arrest(abortWhenItemsDropped: true, allowHoldFire: true);
|
||||
foreach (var stolenItem in stolenItemsOnCharacter)
|
||||
if (Target.IsBot)
|
||||
{
|
||||
HumanAIController.ApplyStealingReputationLoss(stolenItem);
|
||||
// Bots automatically comply and drop stolen items when being inspected.
|
||||
foreach (Item item in stolenItemsOnCharacter)
|
||||
{
|
||||
item.Drop(Target);
|
||||
}
|
||||
character.Speak(TextManager.Get("dialogcheckstolenitems.comply").Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.Speak(TextManager.Get(character.IsCriminal ? "dialogcheckstolenitems.arrest.criminal" : "dialogcheckstolenitems.arrest").Value);
|
||||
Arrest(abortWhenItemsDropped: true, allowHoldFire: true);
|
||||
foreach (var stolenItem in stolenItemsOnCharacter)
|
||||
{
|
||||
HumanAIController.ApplyStealingReputationLoss(stolenItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -242,5 +257,10 @@ namespace Barotrauma
|
||||
currentWarnDelay = Target.IsCriminal ? CriminalWarnDelay : NormalWarnDelay;
|
||||
warnTimer = currentWarnDelay;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for illegitimate item, ignoring handcuffs equipped on the owner.
|
||||
/// </summary>
|
||||
public static bool IsItemIllegitimate(Character owner, Item item) => item.Illegitimate && (!item.HasTag(Tags.HandLockerItem) || !owner.HasEquippedItem(item));
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -18,11 +18,11 @@ namespace Barotrauma
|
||||
public bool IsPriority { get; set; }
|
||||
|
||||
private readonly List<Item> ignoredContainers = new List<Item>();
|
||||
private AIObjectiveDecontainItem decontainObjective;
|
||||
private AIObjectiveMoveItem moveItemObjective;
|
||||
private int itemIndex = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Allows decontainObjective to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
|
||||
/// Allows <see cref="moveItemObjective"/> to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
|
||||
/// </summary>
|
||||
protected override bool ConcurrentObjectives => true;
|
||||
|
||||
@@ -53,9 +53,9 @@ namespace Barotrauma
|
||||
float reduction = IsPriority ? 1 : isSelected ? 2 : 3;
|
||||
float max = AIObjectiveManager.LowestOrderPriority - reduction;
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
|
||||
if (decontainObjective == null)
|
||||
if (moveItemObjective == null)
|
||||
{
|
||||
// Halve the priority until there's a decontain objective (a valid container was found).
|
||||
// Halve the priority until there's a moveItemObjective (a valid container was found).
|
||||
Priority /= 2;
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ namespace Barotrauma
|
||||
s == InvSlotType.OuterClothes ||
|
||||
s == InvSlotType.HealthInterface);
|
||||
|
||||
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
|
||||
TryAddSubObjective(ref moveItemObjective, () => new AIObjectiveMoveItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
|
||||
{
|
||||
Equip = equip,
|
||||
TakeWholeStack = true,
|
||||
@@ -99,7 +99,7 @@ namespace Barotrauma
|
||||
{
|
||||
HumanAIController.ReequipUnequipped();
|
||||
}
|
||||
if (decontainObjective != null && decontainObjective.ContainObjective != null && decontainObjective.ContainObjective.CanBeCompleted)
|
||||
if (moveItemObjective is { ContainObjective.CanBeCompleted: true })
|
||||
{
|
||||
ignoredContainers.Add(suitableContainer);
|
||||
}
|
||||
@@ -117,7 +117,7 @@ namespace Barotrauma
|
||||
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item))
|
||||
{
|
||||
@@ -144,7 +144,7 @@ namespace Barotrauma
|
||||
base.Reset();
|
||||
ignoredContainers.Clear();
|
||||
itemIndex = 0;
|
||||
decontainObjective = null;
|
||||
moveItemObjective = null;
|
||||
}
|
||||
|
||||
public void DropTarget()
|
||||
|
||||
+2
@@ -81,6 +81,8 @@ namespace Barotrauma
|
||||
|
||||
public static bool IsItemInsideValidSubmarine(Item item, Character character)
|
||||
{
|
||||
if (item == null || item.Removed) { return false; }
|
||||
if (character == null || character.Removed) { return false; }
|
||||
if (item.CurrentHull == null) { return false; }
|
||||
if (item.Submarine == null) { return false; }
|
||||
if (item.Submarine.TeamID != character.TeamID) { return false; }
|
||||
|
||||
+266
-116
@@ -4,15 +4,18 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using static Barotrauma.AIObjectiveFindSafety;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using FarseerPhysics;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveCombat : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "combat".ToIdentifier();
|
||||
|
||||
public override string DebugTag => $"{Identifier} ({Mode})";
|
||||
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
@@ -35,10 +38,9 @@ namespace Barotrauma
|
||||
private bool allowCooldown;
|
||||
|
||||
public Character Enemy { get; private set; }
|
||||
public bool HoldPosition { get; set; }
|
||||
|
||||
|
||||
private Item _weapon;
|
||||
private Item Weapon
|
||||
public Item Weapon
|
||||
{
|
||||
get { return _weapon; }
|
||||
set
|
||||
@@ -48,6 +50,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
private ItemComponent _weaponComponent;
|
||||
private bool hasValidRangedWeapon;
|
||||
private ItemComponent WeaponComponent
|
||||
{
|
||||
get
|
||||
@@ -74,7 +77,6 @@ namespace Barotrauma
|
||||
private float pathBackTimer;
|
||||
private const float DefaultCoolDown = 10.0f;
|
||||
private const float PathBackCheckTime = 1.0f;
|
||||
private IEnumerable<Body> myBodies;
|
||||
private float aimTimer;
|
||||
private float reloadTimer;
|
||||
private float spreadTimer;
|
||||
@@ -88,7 +90,9 @@ namespace Barotrauma
|
||||
private const float DistanceCheckInterval = 0.2f;
|
||||
private float distanceTimer;
|
||||
|
||||
private const float CloseDistanceThreshold = 300;
|
||||
private const float CloseDistance = 300;
|
||||
private const float MeleeDistance = 125;
|
||||
private const float TooCloseToShoot = 100;
|
||||
private const float FloorHeightApproximate = 100;
|
||||
|
||||
public bool AllowHoldFire;
|
||||
@@ -169,13 +173,7 @@ namespace Barotrauma
|
||||
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = DefaultCoolDown)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
if (mode == CombatMode.None)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Combat mode == None");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
Debug.Assert(mode != CombatMode.None);
|
||||
Enemy = enemy;
|
||||
coolDownTimer = coolDown;
|
||||
findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
|
||||
@@ -230,6 +228,7 @@ namespace Barotrauma
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
isAimBlocked = false;
|
||||
ignoreWeaponTimer -= deltaTime;
|
||||
checkWeaponsTimer -= deltaTime;
|
||||
if (reloadTimer > 0)
|
||||
@@ -257,7 +256,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine)
|
||||
{
|
||||
@@ -332,68 +331,155 @@ namespace Barotrauma
|
||||
pathBackTimer -= deltaTime;
|
||||
}
|
||||
}
|
||||
if (standUpTimer > 0)
|
||||
{
|
||||
standUpTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Crouch by default so that others can shoot from behind. Disabled when the line of sight is blocked and while moving.
|
||||
allowCrouching = true;
|
||||
}
|
||||
if (HumanAIController.DebugAI)
|
||||
{
|
||||
BlockedPositions ??= new List<Vector2>();
|
||||
BlockedPositions.Clear();
|
||||
}
|
||||
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
|
||||
{
|
||||
if (Mode != CombatMode.Retreat && TryArm())
|
||||
{
|
||||
OperateWeapon(deltaTime);
|
||||
}
|
||||
if (HoldPosition)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
else if (seekAmmunitionObjective == null && seekWeaponObjective == null)
|
||||
isMoving = false;
|
||||
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
|
||||
{
|
||||
Move(deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private bool isMoving;
|
||||
private void Move(float deltaTime)
|
||||
{
|
||||
switch (Mode)
|
||||
if (Mode == CombatMode.Retreat)
|
||||
{
|
||||
case CombatMode.Offensive:
|
||||
case CombatMode.Arrest:
|
||||
Retreat(deltaTime);
|
||||
}
|
||||
else if (character.IsOnPlayerTeam && !Enemy.IsPlayer && objectiveManager.CurrentOrder is AIObjectiveGoTo gotoObjective)
|
||||
{
|
||||
if (gotoObjective.IsWaitOrder && WeaponComponent is MeleeWeapon && IsEnemyClose(CloseDistance))
|
||||
{
|
||||
// Ordered to wait near the enemy with a melee weapon -> engage.
|
||||
Engage(deltaTime);
|
||||
break;
|
||||
case CombatMode.Defensive:
|
||||
if (character.IsOnPlayerTeam && !Enemy.IsPlayer && objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ordered to follow -> keep following.
|
||||
if (!gotoObjective.IsCloseEnough)
|
||||
{
|
||||
if ((character.CurrentHull == null || character.CurrentHull == Enemy.CurrentHull) && sqrDistance < 200 * 200)
|
||||
isMoving = true;
|
||||
}
|
||||
gotoObjective.FaceTargetOnCompleted = false;
|
||||
gotoObjective.ForceAct(deltaTime);
|
||||
if (!character.AnimController.InWater && IsEnemyClose(CloseDistance))
|
||||
{
|
||||
// If close to the enemy, face it, so that we can attack it.
|
||||
HumanAIController.FaceTarget(Enemy);
|
||||
HumanAIController.AutoFaceMovement = false;
|
||||
if (!gotoObjective.ShouldRun(true))
|
||||
{
|
||||
Engage(deltaTime);
|
||||
ForceWalk = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case CombatMode.Defensive:
|
||||
Retreat(deltaTime);
|
||||
break;
|
||||
case CombatMode.Offensive when hasValidRangedWeapon && IsEnemyClose(CloseDistance):
|
||||
// Too close to the enemy -> try to back off.
|
||||
Hull currentHull = character.CurrentHull;
|
||||
bool backOff = currentHull != null;
|
||||
Vector2 escapeVel = Vector2.Zero;
|
||||
if (backOff)
|
||||
{
|
||||
int previousEnemyDir = 0;
|
||||
foreach (Character enemy in Character.CharacterList)
|
||||
{
|
||||
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsHandcuffed) { continue; }
|
||||
if (enemy.CurrentHull == null) { continue; }
|
||||
if (currentHull != enemy.CurrentHull && !currentHull.linkedTo.Contains(enemy.CurrentHull)) { continue; }
|
||||
Vector2 dir = character.Position - enemy.Position;
|
||||
int enemyDir = Math.Sign(dir.X);
|
||||
if (enemyDir == 0)
|
||||
{
|
||||
// Exactly at the same pos.
|
||||
if (previousEnemyDir != 0)
|
||||
{
|
||||
// Another enemy at either side -> Ignore this enemy.
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Just choose either direction.
|
||||
enemyDir = Rand.Value() > 0.5f ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (previousEnemyDir != 0 && enemyDir != previousEnemyDir)
|
||||
{
|
||||
// Don't back off when there are enemies in different directions, because that's doomed.
|
||||
backOff = false;
|
||||
break;
|
||||
}
|
||||
previousEnemyDir = enemyDir;
|
||||
// This formula is slightly modified from AIObjectiveFindSafety.UpdateSimpleEscape().
|
||||
float distMultiplier = MathHelper.Clamp(MeleeDistance / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
|
||||
escapeVel += new Vector2(enemyDir * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
|
||||
}
|
||||
if (escapeVel == Vector2.Zero)
|
||||
{
|
||||
backOff = false;
|
||||
}
|
||||
if (backOff)
|
||||
{
|
||||
// Only move if we haven't reached the edge of the room.
|
||||
float left = currentHull.Rect.X + 50;
|
||||
float right = currentHull.Rect.Right - 50;
|
||||
backOff = escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right;
|
||||
}
|
||||
}
|
||||
if (backOff)
|
||||
{
|
||||
BackOff();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep following the goto target
|
||||
var gotoObjective = objectiveManager.GetOrder<AIObjectiveGoTo>();
|
||||
if (gotoObjective != null)
|
||||
{
|
||||
gotoObjective.ForceAct(deltaTime);
|
||||
if (!character.AnimController.InWater)
|
||||
{
|
||||
HumanAIController.FaceTarget(Enemy);
|
||||
ForceWalk = true;
|
||||
HumanAIController.AutoFaceMovement = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
Engage(deltaTime);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Retreat(deltaTime);
|
||||
}
|
||||
break;
|
||||
case CombatMode.Retreat:
|
||||
Retreat(deltaTime);
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
|
||||
void BackOff()
|
||||
{
|
||||
RemoveFollowTarget();
|
||||
isMoving = true;
|
||||
if (!IsEnemyClose(MeleeDistance))
|
||||
{
|
||||
ForceWalk = true;
|
||||
}
|
||||
HumanAIController.FaceTarget(Enemy);
|
||||
HumanAIController.AutoFaceMovement = false;
|
||||
character.ReleaseSecondaryItem();
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Engage(deltaTime);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,7 +494,8 @@ namespace Barotrauma
|
||||
bool isAllowedToSeekWeapons = character.IsHostileEscortee || character.IsPrisoner || // Prisoners and terrorists etc are always allowed to seek new weapons.
|
||||
(character.IsInFriendlySub // Other characters need to be on a friendly sub in order to "know" where the weapons are. This also prevents NPCs "stealing" player items.
|
||||
&& IsOffensiveOrArrest // = Defensive or retreating AI shouldn't seek new weapons.
|
||||
&& !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
|
||||
&& !character.IsInstigator // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
|
||||
&& objectiveManager.CurrentOrder is not AIObjectiveGoTo); // if ordered to wait/follow, shouldn't go seeking new weapons.
|
||||
if (checkWeaponsTimer < 0)
|
||||
{
|
||||
checkWeaponsTimer = CheckWeaponsInterval;
|
||||
@@ -434,7 +521,12 @@ namespace Barotrauma
|
||||
// All good, the weapon is loaded
|
||||
break;
|
||||
}
|
||||
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(CloseDistanceThreshold);
|
||||
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null;
|
||||
if (seekAmmo)
|
||||
{
|
||||
// Bots set to arrest the target are always allowed to seek (or spawn) more ammo, because otherwise they might not be able to stun the target and need to use lethal weapons.
|
||||
seekAmmo = Mode == CombatMode.Arrest || !IsEnemyClose(CloseDistance);
|
||||
}
|
||||
if (Reload(seekAmmo: seekAmmo))
|
||||
{
|
||||
// All good, we can use the weapon.
|
||||
@@ -480,7 +572,7 @@ namespace Barotrauma
|
||||
Mode = CombatMode.Retreat;
|
||||
}
|
||||
}
|
||||
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistanceThreshold))))
|
||||
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistance))))
|
||||
{
|
||||
// No weapon or only a poor weapon equipped -> try to find better.
|
||||
RemoveSubObjective(ref retreatObjective);
|
||||
@@ -489,7 +581,7 @@ namespace Barotrauma
|
||||
constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false)
|
||||
{
|
||||
AllowStealing = HumanAIController.IsMentallyUnstable,
|
||||
AbortCondition = obj => IsEnemyClose(200),
|
||||
AbortCondition = _ => IsEnemyClose(CloseDistance / 2),
|
||||
EvaluateCombatPriority = false, // Use a custom formula instead
|
||||
GetItemPriority = i =>
|
||||
{
|
||||
@@ -582,6 +674,12 @@ namespace Barotrauma
|
||||
|
||||
private void OperateWeapon(float deltaTime)
|
||||
{
|
||||
if (isMoving && character.IsClimbing)
|
||||
{
|
||||
// Don't climb and shoot at the same time, because it messes up the aiming.
|
||||
ClearInputs();
|
||||
return;
|
||||
}
|
||||
switch (Mode)
|
||||
{
|
||||
case CombatMode.Offensive:
|
||||
@@ -790,17 +888,22 @@ namespace Barotrauma
|
||||
return containers.None() || containers.Any(container =>
|
||||
(container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
|
||||
}
|
||||
|
||||
|
||||
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
|
||||
{
|
||||
hasValidRangedWeapon = false;
|
||||
weaponComponent = null;
|
||||
float bestPriority = 0;
|
||||
float lethalDmg = -1;
|
||||
bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character);
|
||||
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistanceThreshold);
|
||||
foreach (var weapon in weaponList)
|
||||
bool prioritizeMelee = IsEnemyClose(TooCloseToShoot) || EnemyAIController.IsLatchedTo(Enemy, character);
|
||||
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistance);
|
||||
foreach (ItemComponent weapon in weaponList)
|
||||
{
|
||||
float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg);
|
||||
if (priority >= GoodWeaponPriority && weapon is RangedWeapon or RepairTool)
|
||||
{
|
||||
hasValidRangedWeapon = true;
|
||||
}
|
||||
if (priority > bestPriority)
|
||||
{
|
||||
weaponComponent = weapon;
|
||||
@@ -898,23 +1001,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Unequip()
|
||||
|
||||
private void UnequipWeapon()
|
||||
{
|
||||
if (!character.LockHands && character.HeldItems.Contains(Weapon))
|
||||
{
|
||||
if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
if (Weapon.AllowedSlots.Contains(InvSlotType.Bag))
|
||||
{
|
||||
if (character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Bag }))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
Weapon.Drop(character);
|
||||
}
|
||||
}
|
||||
if (Weapon == null) { return; }
|
||||
if (character.LockHands) { return; }
|
||||
if (character.HeldItems.Contains(Weapon)) { return; }
|
||||
character.Unequip(Weapon);
|
||||
}
|
||||
|
||||
private bool Equip()
|
||||
@@ -929,7 +1022,15 @@ namespace Barotrauma
|
||||
ClearInputs();
|
||||
Weapon.TryInteract(character, forceSelectKey: true);
|
||||
var slots = Weapon.AllowedSlots.Where(CharacterInventory.IsHandSlotType);
|
||||
if (character.Inventory.TryPutItem(Weapon, character, slots))
|
||||
bool successfullyEquipped = character.TryPutItem(Weapon, slots);
|
||||
if (!successfullyEquipped && character.HasHandsFull(out (Item leftHandItem, Item rightHandItem) items))
|
||||
{
|
||||
// Unequip and try again.
|
||||
character.Unequip(items.leftHandItem);
|
||||
character.Unequip(items.rightHandItem);
|
||||
successfullyEquipped = character.TryPutItem(Weapon, slots);
|
||||
}
|
||||
if (successfullyEquipped)
|
||||
{
|
||||
SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
|
||||
SetReloadTime(WeaponComponent);
|
||||
@@ -950,6 +1051,7 @@ namespace Barotrauma
|
||||
|
||||
private void Retreat(float deltaTime)
|
||||
{
|
||||
isMoving = true;
|
||||
if (!Enemy.IsHuman && !character.IsInFriendlySub)
|
||||
{
|
||||
// Only relevant when we are retreating from monsters and are not inside a friendly sub.
|
||||
@@ -1047,6 +1149,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
|
||||
{
|
||||
isMoving = true;
|
||||
character.ReleaseSecondaryItem();
|
||||
// Swim towards the target
|
||||
SteeringManager.Reset();
|
||||
@@ -1097,7 +1200,7 @@ namespace Barotrauma
|
||||
}
|
||||
});
|
||||
if (followTargetObjective == null) { return; }
|
||||
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown && !arrestingRegistered)
|
||||
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDownOrRagdolled && !arrestingRegistered)
|
||||
{
|
||||
bool hasHandCuffs = HumanAIController.HasItem(character, Tags.HandLockerItem, out _);
|
||||
if (!hasHandCuffs && character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
@@ -1122,12 +1225,20 @@ namespace Barotrauma
|
||||
followTargetObjective.CloseEnough =
|
||||
WeaponComponent switch
|
||||
{
|
||||
RangedWeapon => 1000,
|
||||
RangedWeapon => isAimBlocked ? BlockedDistance : 1000,
|
||||
MeleeWeapon mw => mw.Range,
|
||||
RepairTool rt => rt.Range,
|
||||
_ => 50
|
||||
};
|
||||
}
|
||||
if (isAimBlocked)
|
||||
{
|
||||
ForceWalk = true;
|
||||
}
|
||||
if (!followTargetObjective.IsCloseEnough)
|
||||
{
|
||||
isMoving = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveFollowTarget()
|
||||
@@ -1145,19 +1256,23 @@ namespace Barotrauma
|
||||
|
||||
private void OnArrestTargetReached()
|
||||
{
|
||||
if (!Enemy.IsKnockedDown)
|
||||
if (!Enemy.IsKnockedDownOrRagdolled)
|
||||
{
|
||||
RemoveFollowTarget();
|
||||
return;
|
||||
}
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
// Confiscate stolen goods and all weapons
|
||||
foreach (var item in Enemy.Inventory.AllItemsMod)
|
||||
{
|
||||
// Ignore handcuffs already on the target.
|
||||
if (item.HasTag(Tags.HandLockerItem) && Enemy.HasEquippedItem(item)) { continue; }
|
||||
if (item.Illegitimate || item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 })
|
||||
AIObjectiveFindThieves.MarkTargetAsInspected(character);
|
||||
bool confiscateItem = AIObjectiveCheckStolenItems.IsItemIllegitimate(Enemy, item);
|
||||
if (!confiscateItem && Enemy.IsActingOffensively)
|
||||
{
|
||||
// Confiscate any weapons or items that can be used offensively.
|
||||
confiscateItem = item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 };
|
||||
}
|
||||
if (confiscateItem)
|
||||
{
|
||||
item.Drop(character);
|
||||
character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
|
||||
@@ -1172,7 +1287,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
if (matchingItems.Any() &&
|
||||
!Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !Enemy.LockHands)
|
||||
!Enemy.IsUnconscious && Enemy.IsKnockedDownOrRagdolled && character.CanInteractWith(Enemy) && !Enemy.LockHands)
|
||||
{
|
||||
var handCuffs = matchingItems.First();
|
||||
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true, wear: true))
|
||||
@@ -1205,7 +1320,8 @@ namespace Barotrauma
|
||||
RemoveFollowTarget();
|
||||
var itemContainer = Weapon.GetComponent<ItemContainer>();
|
||||
TryAddSubObjective(ref seekAmmunitionObjective,
|
||||
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager)
|
||||
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager,
|
||||
spawnItemIfNotFound: !character.IsOnPlayerTeam && character.AIController.HasInfiniteItemSpawns(ammunitionIdentifiers))
|
||||
{
|
||||
ItemCount = itemContainer.MainContainerCapacity * itemContainer.MaxStackSize,
|
||||
checkInventory = false,
|
||||
@@ -1227,10 +1343,9 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
private bool Reload(bool seekAmmo)
|
||||
{
|
||||
if (WeaponComponent == null) { return false; }
|
||||
if (WeaponComponent == null) { return false; }
|
||||
if (Weapon.OwnInventory == null) { return true; }
|
||||
// Eject empty ammo
|
||||
HumanAIController.UnequipEmptyItems(Weapon);
|
||||
HumanAIController.UnequipEmptyItems(Weapon, allowDestroying: !character.IsOnPlayerTeam);
|
||||
ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
|
||||
if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained))
|
||||
{
|
||||
@@ -1270,7 +1385,7 @@ namespace Barotrauma
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
|
||||
else if (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
|
||||
@@ -1280,6 +1395,20 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool isAimBlocked;
|
||||
private float _blockedDistance;
|
||||
private float BlockedDistance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_blockedDistance <= 0)
|
||||
{
|
||||
_blockedDistance = CloseDistance * Rand.Range(1.0f, 1.3f);
|
||||
}
|
||||
return _blockedDistance;
|
||||
}
|
||||
}
|
||||
public List<Vector2> BlockedPositions;
|
||||
private void Attack(float deltaTime)
|
||||
{
|
||||
character.CursorPosition = Enemy.WorldPosition;
|
||||
@@ -1322,8 +1451,6 @@ namespace Barotrauma
|
||||
aimTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
if (reloadTimer > 0) { return; }
|
||||
if (holdFireCondition != null && holdFireCondition()) { return; }
|
||||
sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
|
||||
distanceTimer = DistanceCheckInterval;
|
||||
if (WeaponComponent is MeleeWeapon meleeWeapon)
|
||||
@@ -1353,9 +1480,11 @@ namespace Barotrauma
|
||||
if (closeEnough && Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25)
|
||||
{
|
||||
// The target is probably knocked down? -> try to reach it by crouching.
|
||||
HumanAIController.AnimController.Crouching = true;
|
||||
HumanAIController.AnimController.Crouch();
|
||||
}
|
||||
}
|
||||
if (reloadTimer > 0) { return; }
|
||||
if (holdFireCondition != null && holdFireCondition()) { return; }
|
||||
if (closeEnough)
|
||||
{
|
||||
UseWeapon(deltaTime);
|
||||
@@ -1371,36 +1500,59 @@ namespace Barotrauma
|
||||
{
|
||||
if (WeaponComponent is RepairTool repairTool)
|
||||
{
|
||||
if (sqrDistance > repairTool.Range * repairTool.Range) { return; }
|
||||
float reach = AIObjectiveFixLeak.CalculateReach(repairTool, character);
|
||||
if (sqrDistance > reach * reach) { return; }
|
||||
}
|
||||
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
|
||||
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
|
||||
{
|
||||
myBodies ??= character.AnimController.Limbs.Select(l => l.body.FarseerBody);
|
||||
// Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay)
|
||||
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
|
||||
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy),
|
||||
ignoredBodies: character.AnimController.LimbBodies,
|
||||
Physics.CollisionCharacter);
|
||||
|
||||
foreach (var body in pickedBodies)
|
||||
{
|
||||
Character target = body.UserData switch
|
||||
if (body.UserData is Limb limb)
|
||||
{
|
||||
Character c => c,
|
||||
Limb limb => limb.character,
|
||||
_ => null
|
||||
};
|
||||
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
|
||||
{
|
||||
return;
|
||||
Character target = limb.character;
|
||||
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
|
||||
{
|
||||
// Blocked by a friendly target.
|
||||
isAimBlocked = true;
|
||||
if (HumanAIController.DebugAI)
|
||||
{
|
||||
BlockedPositions.Add(ConvertUnits.ToDisplayUnits(body.Position));
|
||||
}
|
||||
// Stand up, so that we might shoot past the friendlies that are crouching.
|
||||
allowCrouching = false;
|
||||
standUpTimer = StandUpCooldown;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
UseWeapon(deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool allowCrouching;
|
||||
private float standUpTimer;
|
||||
private const float StandUpCooldown = 5;
|
||||
private void UseWeapon(float deltaTime)
|
||||
{
|
||||
// Never allow friendly crew (bots) to attack with deadly weapons.
|
||||
if (Mode == CombatMode.Arrest && isLethalWeapon && character.IsOnPlayerTeam && Enemy.IsOnPlayerTeam) { return; }
|
||||
// Enable this to debug intentional friendly fire.
|
||||
// if (isLethalWeapon && character.TeamID == Enemy.TeamID && character.IsOnPlayerTeam)
|
||||
// {
|
||||
// // Never allow friendly crew (bots) to attack with deadly weapons (this check should be redundant)
|
||||
// Debugger.Break();
|
||||
// return;
|
||||
// }
|
||||
if (allowCrouching && !isMoving && !character.AnimController.InWater && WeaponComponent is not MeleeWeapon)
|
||||
{
|
||||
HumanAIController.AnimController.Crouch();
|
||||
}
|
||||
character.SetInput(InputType.Shoot, hit: false, held: true);
|
||||
Weapon.Use(deltaTime, user: character);
|
||||
SetReloadTime(WeaponComponent);
|
||||
@@ -1420,11 +1572,8 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
case MeleeWeapon mw:
|
||||
{
|
||||
if (character.AnimController is HumanoidAnimController { Crouching: false })
|
||||
{
|
||||
reloadTime = mw.Reload;
|
||||
}
|
||||
{
|
||||
reloadTime = mw.Reload;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1485,7 +1634,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (ShouldUnequipWeapon)
|
||||
{
|
||||
Unequip();
|
||||
UnequipWeapon();
|
||||
}
|
||||
SteeringManager?.Reset();
|
||||
}
|
||||
@@ -1495,7 +1644,7 @@ namespace Barotrauma
|
||||
base.OnAbandon();
|
||||
if (ShouldUnequipWeapon)
|
||||
{
|
||||
Unequip();
|
||||
UnequipWeapon();
|
||||
}
|
||||
SteeringManager?.Reset();
|
||||
}
|
||||
@@ -1516,6 +1665,7 @@ namespace Barotrauma
|
||||
hasAimed = false;
|
||||
holdFireTimer = 0;
|
||||
pathBackTimer = 0;
|
||||
standUpTimer = 0;
|
||||
isLethalWeapon = false;
|
||||
canSeeTarget = false;
|
||||
seekWeaponObjective = null;
|
||||
|
||||
+9
-5
@@ -48,6 +48,8 @@ namespace Barotrauma
|
||||
public int? RemoveMax { get; set; }
|
||||
|
||||
public bool MoveWholeStack { get; set; }
|
||||
|
||||
public bool AllowStealing { get; set; }
|
||||
|
||||
private int _itemCount = 1;
|
||||
public int ItemCount
|
||||
@@ -77,9 +79,8 @@ namespace Barotrauma
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
if (container?.Item == null || !container.Item.HasAccess(character))
|
||||
{
|
||||
Abandon = true;
|
||||
@@ -148,11 +149,11 @@ namespace Barotrauma
|
||||
|
||||
if (RemoveExisting || (RemoveExistingWhenNecessary && !CanBePut(container.Inventory, TargetSlot, ItemToContain)))
|
||||
{
|
||||
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax);
|
||||
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax, allowDestroying: spawnItemIfNotFound);
|
||||
}
|
||||
else if (RemoveEmpty)
|
||||
{
|
||||
HumanAIController.UnequipEmptyItems(container.Item);
|
||||
HumanAIController.UnequipEmptyItems(container.Item, allowDestroying: spawnItemIfNotFound);
|
||||
}
|
||||
Inventory originalInventory = ItemToContain.ParentInventory;
|
||||
var slots = originalInventory?.FindIndices(ItemToContain);
|
||||
@@ -196,6 +197,7 @@ namespace Barotrauma
|
||||
{
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
|
||||
{
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
|
||||
TargetName = container.Item.Name,
|
||||
AbortCondition = obj =>
|
||||
container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) ||
|
||||
@@ -232,7 +234,9 @@ namespace Barotrauma
|
||||
return (RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem)) && container.ShouldBeContained(potentialItem, out _);
|
||||
},
|
||||
ItemCount = ItemCount,
|
||||
TakeWholeStack = MoveWholeStack
|
||||
TakeWholeStack = MoveWholeStack,
|
||||
ContainTarget = container,
|
||||
AllowStealing = AllowStealing
|
||||
}, onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
|
||||
+35
-12
@@ -14,7 +14,8 @@ namespace Barotrauma
|
||||
|
||||
private Deconstructor deconstructor;
|
||||
|
||||
private AIObjectiveDecontainItem decontainObjective;
|
||||
private AIObjectiveMoveItem moveItemObjective;
|
||||
private AIObjectiveGoTo gotoObjective;
|
||||
|
||||
public AIObjectiveDeconstructItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
@@ -36,8 +37,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
TryAddSubObjective(ref decontainObjective,
|
||||
constructor: () => new AIObjectiveDecontainItem(character, Item, objectiveManager,
|
||||
TryAddSubObjective(ref moveItemObjective,
|
||||
constructor: () => new AIObjectiveMoveItem(character, Item, objectiveManager,
|
||||
sourceContainer: Item.Container?.GetComponent<ItemContainer>(), targetContainer: deconstructor.InputContainer, priorityModifier: PriorityModifier)
|
||||
{
|
||||
Equip = true,
|
||||
@@ -45,15 +46,25 @@ namespace Barotrauma
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
StartDeconstructor();
|
||||
//make sure the item gets moved to the main sub if the crew leaves while a bot is deconstructing something in the outpost
|
||||
if (deconstructor.Item.Submarine is { Info.IsOutpost: true })
|
||||
if (character.CanInteractWith(deconstructor.Item))
|
||||
{
|
||||
HumanAIController.HandleRelocation(Item);
|
||||
deconstructor.RelocateOutputToMainSub = true;
|
||||
StartDeconstruction();
|
||||
}
|
||||
IsCompleted = true;
|
||||
RemoveSubObjective(ref decontainObjective);
|
||||
else
|
||||
{
|
||||
TryAddSubObjective(ref gotoObjective,
|
||||
constructor: () => new AIObjectiveGoTo(Item, character, objectiveManager, priorityModifier: PriorityModifier),
|
||||
onCompleted: () =>
|
||||
{
|
||||
StartDeconstruction();
|
||||
RemoveSubObjective(ref gotoObjective);
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
});
|
||||
}
|
||||
RemoveSubObjective(ref moveItemObjective);
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
@@ -61,6 +72,18 @@ namespace Barotrauma
|
||||
});
|
||||
}
|
||||
|
||||
private void StartDeconstruction()
|
||||
{
|
||||
StartDeconstructor();
|
||||
//make sure the item gets moved to the main sub if the crew leaves while a bot is deconstructing something in the outpost
|
||||
if (deconstructor.Item.Submarine is { Info.IsOutpost: true })
|
||||
{
|
||||
HumanAIController.HandleRelocation(Item);
|
||||
deconstructor.RelocateOutputToMainSub = true;
|
||||
}
|
||||
IsCompleted = true;
|
||||
}
|
||||
|
||||
private Deconstructor FindDeconstructor()
|
||||
{
|
||||
Deconstructor closestDeconstructor = null;
|
||||
@@ -86,7 +109,7 @@ namespace Barotrauma
|
||||
deconstructor.SetActive(active: true, user: character, createNetworkEvent: true);
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (Item.IgnoreByAI(character))
|
||||
{
|
||||
@@ -102,7 +125,7 @@ namespace Barotrauma
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
decontainObjective = null;
|
||||
moveItemObjective = null;
|
||||
}
|
||||
|
||||
public void DropTarget()
|
||||
|
||||
+4
-2
@@ -59,13 +59,15 @@ namespace Barotrauma
|
||||
|
||||
protected override bool IsValidTarget(Item target)
|
||||
{
|
||||
if (target == null || target.Removed) { return false; }
|
||||
// If the target was selected as a valid target, we'll have to accept it so that the objective can be completed.
|
||||
// The validity changes when a character picks the item up.
|
||||
if (!IsValidTarget(target, character, checkInventory: true))
|
||||
{
|
||||
return Objectives.ContainsKey(target) && AIObjectiveCleanupItems.IsItemInsideValidSubmarine(target, character);
|
||||
}
|
||||
if (target.CurrentHull.FireSources.Count > 0) { return false; }
|
||||
//note that the item can be outside hulls and still be a valid target - it can be in the character's inventory
|
||||
if (target.CurrentHull != null && target.CurrentHull.FireSources.Count > 0) { return false; }
|
||||
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
@@ -96,7 +98,7 @@ namespace Barotrauma
|
||||
|
||||
private static bool IsValidTarget(Item item, Character character, bool checkInventory)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
if (item == null || item.Removed) { return false; }
|
||||
if (item.GetRootInventoryOwner() == character) { return true; }
|
||||
return AIObjectiveCleanupItems.IsValidTarget(
|
||||
item,
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public override bool CanBeCompleted => true;
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
|
||||
// escape timer is set to 60 by default to allow players to locate prisoners in time
|
||||
private float escapeTimer = 60f;
|
||||
|
||||
+40
-39
@@ -39,58 +39,57 @@ namespace Barotrauma
|
||||
// Don't go into rooms with any enemies, unless it's an order
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
else
|
||||
// Prioritize fires that currently damage the character.
|
||||
bool inDamageRange = targetHull.FireSources.Any(fs => fs.IsInDamageRange(character, fs.DamageRange));
|
||||
float severity = inDamageRange ? 1.0f : AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
|
||||
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
|
||||
float distanceFactor = targetHull == character.CurrentHull ? 1.0f
|
||||
: HumanAIController.VisibleHulls.Contains(targetHull) ? 0.75f : 0.0f;
|
||||
|
||||
if (distanceFactor <= 0.0f)
|
||||
{
|
||||
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
|
||||
|
||||
float distanceFactor = 1.0f;
|
||||
if (targetHull != character.CurrentHull &&
|
||||
!HumanAIController.VisibleHulls.Contains(targetHull))
|
||||
{
|
||||
distanceFactor =
|
||||
GetDistanceFactor(
|
||||
new Vector2(character.WorldPosition.Y, characterY),
|
||||
targetHull.WorldPosition,
|
||||
verticalDistanceMultiplier: 3,
|
||||
maxDistance: 5000,
|
||||
factorAtMaxDistance: 0.1f);
|
||||
}
|
||||
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
|
||||
if (severity > 0.75f && !isOrder &&
|
||||
targetHull.RoomName != null &&
|
||||
!targetHull.RoomName.Contains("reactor", StringComparison.OrdinalIgnoreCase) &&
|
||||
!targetHull.RoomName.Contains("engine", StringComparison.OrdinalIgnoreCase) &&
|
||||
!targetHull.RoomName.Contains("command", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Ignore severe fires to prevent casualities unless ordered to extinguish.
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
}
|
||||
distanceFactor =
|
||||
GetDistanceFactor(
|
||||
new Vector2(character.WorldPosition.Y, characterY),
|
||||
targetHull.WorldPosition,
|
||||
verticalDistanceMultiplier: 3,
|
||||
maxDistance: 5000,
|
||||
factorAtMaxDistance: 0.1f);
|
||||
}
|
||||
|
||||
if (!inDamageRange && severity > 0.75f && distanceFactor < 0.75f && !isOrder && character.IsOnPlayerTeam &&
|
||||
targetHull.RoomName != null &&
|
||||
!targetHull.RoomName.Contains("reactor", StringComparison.OrdinalIgnoreCase) &&
|
||||
!targetHull.RoomName.Contains("engine", StringComparison.OrdinalIgnoreCase) &&
|
||||
!targetHull.RoomName.Contains("command", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Bots in the player crew ignore severe fires that are not close to the target to prevent casualties unless ordered to extinguish.
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => targetHull.FireSources.None();
|
||||
protected override bool CheckObjectiveState() => targetHull.FireSources.None();
|
||||
|
||||
private float sinTime;
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
var extinguisherItem = character.Inventory.FindItemByTag("fireextinguisher".ToIdentifier());
|
||||
var extinguisherItem = character.Inventory.FindItemByTag(Tags.FireExtinguisher);
|
||||
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
|
||||
{
|
||||
TryAddSubObjective(ref getExtinguisherObjective, () =>
|
||||
{
|
||||
if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher".ToIdentifier(), allowBroken: false))
|
||||
if (character.IsOnPlayerTeam && !character.HasEquippedItem(Tags.FireExtinguisher, allowBroken: false))
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogFindExtinguisher").Value, null, 2.0f, "findextinguisher".ToIdentifier(), 30.0f);
|
||||
character.Speak(TextManager.Get("DialogFindExtinguisher").Value, null, 2.0f, Tags.FireExtinguisher, 30.0f);
|
||||
}
|
||||
var getItemObjective = new AIObjectiveGetItem(character, "fireextinguisher".ToIdentifier(), objectiveManager, equip: true)
|
||||
var getItemObjective = new AIObjectiveGetItem(character, Tags.FireExtinguisher, objectiveManager, equip: true)
|
||||
{
|
||||
AllowStealing = true,
|
||||
// If the item is inside an unsafe hull, decrease the priority
|
||||
@@ -124,7 +123,9 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X);
|
||||
float yDist = Math.Abs(character.CurrentHull.WorldPosition.Y - targetHull.WorldPosition.Y);
|
||||
// If fire source and the character are on the same level, it's better to ignore the y-axis (e.g. it doesn't matter if we stand or crouch), as the fire size is rectangular.
|
||||
// If we'd do this while climbing, the character would often get too close to the fire.
|
||||
float yDist = !character.IsClimbing && MathUtils.NearlyEqual(character.CurrentHull.WorldPosition.Y, targetHull.WorldPosition.Y) ? 0.0f : Math.Abs(character.CurrentHull.WorldPosition.Y - fs.WorldPosition.Y);
|
||||
float dist = xDist + yDist;
|
||||
bool inRange = dist < extinguisher.Range;
|
||||
bool isInDamageRange = fs.IsInDamageRange(character, fs.DamageRange) && character.CanSeeTarget(targetHull);
|
||||
@@ -153,8 +154,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range * 0.8f)
|
||||
{
|
||||
DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(),
|
||||
TargetName = fs.Hull.DisplayName,
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachFire,
|
||||
TargetName = fs.Hull.DisplayName
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
|
||||
|
||||
+2
-2
@@ -59,7 +59,7 @@ namespace Barotrauma
|
||||
public static bool IsValidTarget(Character target, Character character, bool targetCharactersInOtherSubs)
|
||||
{
|
||||
if (target == null || target.Removed) { return false; }
|
||||
if (target.IsDead) { return false; }
|
||||
if (target.IsDead || target.InDetectable) { return false; }
|
||||
if (target.IsUnconscious && target.Params.Health.ConstantHealthRegeneration <= 0.0f) { return false; }
|
||||
if (target == character) { return false; }
|
||||
if (target.Submarine == null) { return false; }
|
||||
@@ -75,7 +75,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
|
||||
if (target.IsHandcuffed && target.IsKnockedDown) { return false; }
|
||||
if (target.IsHandcuffed) { return false; }
|
||||
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
+79
-71
@@ -21,9 +21,9 @@ namespace Barotrauma
|
||||
private Item targetItem;
|
||||
private int? oxygenSourceSlotIndex;
|
||||
|
||||
public const float MIN_OXYGEN = 10;
|
||||
private const float MinOxygen = 10;
|
||||
|
||||
protected override bool CheckObjectiveSpecific() =>
|
||||
protected override bool CheckObjectiveState() =>
|
||||
targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head);
|
||||
|
||||
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
|
||||
@@ -39,83 +39,98 @@ namespace Barotrauma
|
||||
TrySetTargetItem(character.Inventory.FindItem(
|
||||
it => it.HasTag(Tags.HeavyDivingGear) && IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true));
|
||||
}
|
||||
if (targetItem == null ||
|
||||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) &&
|
||||
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
|
||||
|
||||
bool findDivingGear = targetItem == null ||
|
||||
(!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) && targetItem.ContainedItems.Any(IsSuitableContainedOxygenSource));
|
||||
|
||||
if (findDivingGear)
|
||||
{
|
||||
bool mustFindMorePressureProtection =
|
||||
!objectiveManager.FailedToFindDivingGearForDepth &&
|
||||
character.Inventory.FindItem(
|
||||
it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null;
|
||||
TryAddSubObjective(ref getDivingGear, () =>
|
||||
bool mustFindMorePressureProtection = !objectiveManager.FailedToFindDivingGearForDepth &&
|
||||
character.Inventory.FindItem(it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null;
|
||||
|
||||
if (gearTag == Tags.LightDivingGear)
|
||||
{
|
||||
if (targetItem == null && character.IsOnPlayerTeam)
|
||||
if (character.GetEquippedItem(Tags.HeavyDivingGear, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes) is Item divingSuit && divingSuit.ContainedItems.None(IsSuitableContainedOxygenSource))
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f);
|
||||
// A special case: we are already wearing a suit without enough oxygen, but seeking for a mask, because a suit is not really needed.
|
||||
// This would result into wearing boh the mask and the suit (because the suit shouldn't be unequipped in this situation), which is a bit weird and also suboptimal, because the mask uses the oxygen 2x faster.
|
||||
// So, let's target the diving suit and try to find oxygen instead.
|
||||
targetItem = divingSuit;
|
||||
findDivingGear = false;
|
||||
}
|
||||
var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
|
||||
}
|
||||
if (findDivingGear)
|
||||
{
|
||||
TryAddSubObjective(ref getDivingGear, () =>
|
||||
{
|
||||
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true,
|
||||
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
|
||||
Wear = true
|
||||
};
|
||||
if (gearTag == Tags.HeavyDivingGear)
|
||||
{
|
||||
if (mustFindMorePressureProtection)
|
||||
if (targetItem == null && character.IsOnPlayerTeam)
|
||||
{
|
||||
//if we're looking for a suit specifically because the current suit isn't enough,
|
||||
//let's ignore unsuitable suits altogether...
|
||||
getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character);
|
||||
character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f);
|
||||
}
|
||||
else
|
||||
var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
|
||||
{
|
||||
//...Otherwise it's fine to give a very small priority
|
||||
//to inadequate suits (a suit not adequate for the depth is better than no suit)
|
||||
getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f;
|
||||
}
|
||||
getItemObjective.GetItemPriority = it =>
|
||||
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true,
|
||||
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
|
||||
Wear = true
|
||||
};
|
||||
if (gearTag == Tags.HeavyDivingGear)
|
||||
{
|
||||
if (IsSuitablePressureProtection(it, gearTag, character))
|
||||
if (mustFindMorePressureProtection)
|
||||
{
|
||||
return 1000.0f;
|
||||
//if we're looking for a suit specifically because the current suit isn't enough,
|
||||
//let's ignore unsuitable suits altogether...
|
||||
getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character);
|
||||
}
|
||||
else
|
||||
{
|
||||
//if we're looking for a suit specifically because the current suit isn't enough,
|
||||
//let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority
|
||||
//...Otherwise it's fine to give a very small priority
|
||||
//to inadequate suits (a suit not adequate for the depth is better than no suit)
|
||||
return mustFindMorePressureProtection ? 0.0f : 1.0f;
|
||||
getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f;
|
||||
}
|
||||
};
|
||||
}
|
||||
return getItemObjective;
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; }
|
||||
Abandon = true;
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref getDivingGear);
|
||||
if (gearTag == Tags.HeavyDivingGear && HumanAIController.HasItem(character, Tags.LightDivingGear, out IEnumerable<Item> masks, requireEquipped: true))
|
||||
{
|
||||
foreach (Item mask in masks)
|
||||
{
|
||||
if (mask != targetItem)
|
||||
getItemObjective.GetItemPriority = it =>
|
||||
{
|
||||
character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot);
|
||||
if (IsSuitablePressureProtection(it, gearTag, character))
|
||||
{
|
||||
return 1000.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
//if we're looking for a suit specifically because the current suit isn't enough,
|
||||
//let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority
|
||||
//to inadequate suits (a suit not adequate for the depth is better than no suit)
|
||||
return mustFindMorePressureProtection ? 0.0f : 1.0f;
|
||||
}
|
||||
};
|
||||
}
|
||||
return getItemObjective;
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; }
|
||||
Abandon = true;
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref getDivingGear);
|
||||
if (gearTag == Tags.HeavyDivingGear && HumanAIController.HasItem(character, Tags.LightDivingGear, out IEnumerable<Item> masks, requireEquipped: true))
|
||||
{
|
||||
foreach (Item mask in masks)
|
||||
{
|
||||
if (mask != targetItem)
|
||||
{
|
||||
character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
if (!findDivingGear)
|
||||
{
|
||||
float min = GetMinOxygen(character);
|
||||
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => IsSuitableContainedOxygenSource(it)))
|
||||
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(IsSuitableContainedOxygenSource))
|
||||
{
|
||||
TryAddSubObjective(ref getOxygen, () =>
|
||||
{
|
||||
@@ -139,9 +154,10 @@ namespace Barotrauma
|
||||
{
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true,
|
||||
ConditionLevel = MIN_OXYGEN,
|
||||
ConditionLevel = MinOxygen,
|
||||
RemoveExistingWhenNecessary = true,
|
||||
TargetSlot = oxygenSourceSlotIndex
|
||||
TargetSlot = oxygenSourceSlotIndex,
|
||||
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _)
|
||||
};
|
||||
if (container.HasSubContainers)
|
||||
{
|
||||
@@ -184,7 +200,7 @@ namespace Barotrauma
|
||||
int ReportOxygenTankCount()
|
||||
{
|
||||
if (character.Submarine != Submarine.MainSub) { return 1; }
|
||||
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.OxygenSource) && i.Condition > 1);
|
||||
int remainingOxygenTanks = Submarine.MainSub?.GetItems(false).Count(i => i.HasTag(Tags.OxygenSource) && i.Condition > 1) ?? 0;
|
||||
if (remainingOxygenTanks == 0)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogOutOfOxygenTanks").Value, null, 0.0f, "outofoxygentanks".ToIdentifier(), 30.0f);
|
||||
@@ -212,7 +228,6 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private bool IsSuitableContainedOxygenSource(Item item)
|
||||
{
|
||||
return
|
||||
@@ -226,14 +241,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (targetItem == item) { return; }
|
||||
targetItem = item;
|
||||
if (targetItem != null)
|
||||
{
|
||||
oxygenSourceSlotIndex = targetItem.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(Tags.OxygenSource);
|
||||
}
|
||||
else
|
||||
{
|
||||
oxygenSourceSlotIndex = null;
|
||||
}
|
||||
oxygenSourceSlotIndex = targetItem?.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(Tags.OxygenSource);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
@@ -251,7 +259,7 @@ namespace Barotrauma
|
||||
// The margin helps us to survive, because we might need some oxygen before we can find more oxygen.
|
||||
// When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks.
|
||||
float min = 0.01f;
|
||||
float minOxygen = character.IsInFriendlySub ? MIN_OXYGEN : min;
|
||||
float minOxygen = character.IsInFriendlySub ? MinOxygen : min;
|
||||
if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag(Tags.OxygenSource) && i.ConditionPercentage >= minOxygen))
|
||||
{
|
||||
// There's a valid oxygen tank in the inventory -> no need to swap the tank too early.
|
||||
|
||||
+119
-33
@@ -3,6 +3,7 @@ using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -31,7 +32,7 @@ namespace Barotrauma
|
||||
|
||||
public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
|
||||
private bool resetPriority;
|
||||
@@ -41,9 +42,9 @@ namespace Barotrauma
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
Priority = (
|
||||
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
|
||||
objectiveManager.CurrentOrder is AIObjectiveGoTo ||
|
||||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
|
||||
objectiveManager.Objectives.Any(o => (o is AIObjectiveCombat || o is AIObjectiveReturn) && o.Priority > 0))
|
||||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat or AIObjectiveReturn && o.Priority > 0))
|
||||
&& ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10;
|
||||
}
|
||||
else
|
||||
@@ -70,6 +71,11 @@ namespace Barotrauma
|
||||
Priority = AIObjectiveManager.MaxObjectivePriority;
|
||||
}
|
||||
}
|
||||
else if (objectiveManager.CurrentOrder is AIObjectiveGoTo { IsFollowOrder: true })
|
||||
{
|
||||
// Ordered to follow -> Don't flee from the enemies/fires (doesn't get here if we need more oxygen).
|
||||
Priority = 0;
|
||||
}
|
||||
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
|
||||
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
|
||||
{
|
||||
@@ -82,7 +88,7 @@ namespace Barotrauma
|
||||
Priority = 0;
|
||||
}
|
||||
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
|
||||
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
|
||||
if (divingGearObjective is { IsCompleted: false, CanBeCompleted: true, Priority: > 0f })
|
||||
{
|
||||
// Boost the priority while seeking the diving gear
|
||||
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.EmergencyObjectivePriority - 1, AIObjectiveManager.MaxObjectivePriority));
|
||||
@@ -148,7 +154,13 @@ namespace Barotrauma
|
||||
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
|
||||
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
|
||||
{
|
||||
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
|
||||
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit, objectiveManager);
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine?.Info is { IsOutpost: true })
|
||||
{
|
||||
// In outposts, the NPCs don't try to use diving suits, because otherwise there's probably not enough for those trying to fix the leaks.
|
||||
// This is not a hard rule: the bots may still grab a suit, unless they find a diving mask.
|
||||
needsDivingSuit = false;
|
||||
}
|
||||
bool needsEquipment = shouldActOnSuffocation;
|
||||
if (needsDivingSuit)
|
||||
{
|
||||
@@ -306,10 +318,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (escapeVel != Vector2.Zero)
|
||||
if (escapeVel != Vector2.Zero && character.CurrentHull is Hull currentHull)
|
||||
{
|
||||
float left = character.CurrentHull.Rect.X + 50;
|
||||
float right = character.CurrentHull.Rect.Right - 50;
|
||||
float left = currentHull.Rect.X + 50;
|
||||
float right = currentHull.Rect.Right - 50;
|
||||
//only move if we haven't reached the edge of the room
|
||||
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
|
||||
{
|
||||
@@ -339,6 +351,10 @@ namespace Barotrauma
|
||||
float bestHullValue = 0;
|
||||
bool bestHullIsAirlock = false;
|
||||
Hull potentialBestHull;
|
||||
|
||||
#if DEBUG
|
||||
private readonly Stopwatch stopWatch = new Stopwatch();
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find the best (safe, nearby) hull the character can find a path to.
|
||||
@@ -353,6 +369,9 @@ namespace Barotrauma
|
||||
bestHullIsAirlock = false;
|
||||
hulls.Clear();
|
||||
var connectedSubs = character.Submarine?.GetConnectedSubs();
|
||||
#if DEBUG
|
||||
stopWatch.Restart();
|
||||
#endif
|
||||
foreach (Hull hull in Hull.HullList)
|
||||
{
|
||||
if (hull.Submarine == null) { continue; }
|
||||
@@ -363,25 +382,66 @@ namespace Barotrauma
|
||||
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
|
||||
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
|
||||
if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; }
|
||||
//sort the hulls based on distance and which sub they're in
|
||||
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
|
||||
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
|
||||
//path calculations, only to discard all of them when going through the hulls in the outpost)
|
||||
float hullSuitability = EstimateHullSuitability(character, hull);
|
||||
if (hulls.None())
|
||||
{
|
||||
hulls.Add(hull);
|
||||
}
|
||||
else
|
||||
{
|
||||
//sort the hulls first based on distance and a rough suitability estimation
|
||||
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
|
||||
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
|
||||
//path calculations, only to discard all of them when going through the hulls in the outpost)
|
||||
bool addLast = true;
|
||||
float hullSuitability = EstimateHullSuitability(hull);
|
||||
for (int i = 0; i < hulls.Count; i++)
|
||||
{
|
||||
if (hullSuitability > EstimateHullSuitability(character, hulls[i]))
|
||||
Hull otherHull = hulls[i];
|
||||
float otherHullSuitability = EstimateHullSuitability(otherHull);
|
||||
if (hullSuitability > otherHullSuitability)
|
||||
{
|
||||
hulls.Insert(i, hull);
|
||||
addLast = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (addLast)
|
||||
{
|
||||
hulls.Add(hull);
|
||||
}
|
||||
}
|
||||
|
||||
float EstimateHullSuitability(Hull h)
|
||||
{
|
||||
float distX = Math.Abs(h.WorldPosition.X - character.WorldPosition.X);
|
||||
float distY = Math.Abs(h.WorldPosition.Y - character.WorldPosition.Y);
|
||||
if (character.CurrentHull != null)
|
||||
{
|
||||
distY *= 3;
|
||||
}
|
||||
float dist = distX + distY;
|
||||
float suitability = -dist;
|
||||
const float suitabilityReduction = 10000.0f;
|
||||
if (h.Submarine != character.Submarine)
|
||||
{
|
||||
suitability -= suitabilityReduction;
|
||||
}
|
||||
if (character.CurrentHull != null)
|
||||
{
|
||||
if (h.AvoidStaying)
|
||||
{
|
||||
suitability -= suitabilityReduction;
|
||||
}
|
||||
if (HumanAIController.UnsafeHulls.Contains(h))
|
||||
{
|
||||
suitability -= suitabilityReduction;
|
||||
}
|
||||
if (HumanAIController.NeedsDivingGear(h, out _))
|
||||
{
|
||||
suitability -= suitabilityReduction;
|
||||
}
|
||||
}
|
||||
return suitability;
|
||||
}
|
||||
}
|
||||
if (hulls.None())
|
||||
@@ -390,19 +450,10 @@ namespace Barotrauma
|
||||
return HullSearchStatus.Finished;
|
||||
}
|
||||
hullSearchIndex = 0;
|
||||
}
|
||||
|
||||
static float EstimateHullSuitability(Character character, Hull hull)
|
||||
{
|
||||
float dist =
|
||||
Math.Abs(hull.WorldPosition.X - character.WorldPosition.X) +
|
||||
Math.Abs(hull.WorldPosition.Y - character.WorldPosition.Y) * 3;
|
||||
float suitability = -dist;
|
||||
if (hull.Submarine != character.Submarine)
|
||||
{
|
||||
suitability -= 10000.0f;
|
||||
}
|
||||
return suitability;
|
||||
#if DEBUG
|
||||
stopWatch.Stop();
|
||||
DebugConsole.Log($"({character.DisplayName}) Sorted hulls by suitability in {stopWatch.ElapsedMilliseconds} ms");
|
||||
#endif
|
||||
}
|
||||
|
||||
Hull potentialHull = hulls[hullSearchIndex];
|
||||
@@ -420,7 +471,7 @@ namespace Barotrauma
|
||||
if (hullSafety > bestHullValue)
|
||||
{
|
||||
//avoid airlock modules if not allowed to change the sub
|
||||
if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock"))
|
||||
if (allowChangingSubmarine || potentialHull.OutpostModuleTags.All(t => t != "airlock"))
|
||||
{
|
||||
// Don't allow to go outside if not already outside.
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(potentialHull), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
|
||||
@@ -431,12 +482,47 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// Each unsafe node reduces the hull safety value.
|
||||
// Ignore the current hull, because otherwise we couldn't find a path out.
|
||||
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
|
||||
hullSafety /= 1 + unsafeNodes;
|
||||
// Check the path safety. Each unsafe node reduces the hull safety value.
|
||||
Hull previousHull = null;
|
||||
foreach (WayPoint node in path.Nodes)
|
||||
{
|
||||
Hull hull = node.CurrentHull;
|
||||
if (hull == previousHull)
|
||||
{
|
||||
// Let's evaluate each hull only once. If we'd want to make this foolproof, we'd have to add the checked hulls to a list,
|
||||
// yet in practice there shouldn't be a case where the path would get back to a hull once it has exited it.
|
||||
continue;
|
||||
}
|
||||
previousHull = hull;
|
||||
if (hull == character.CurrentHull)
|
||||
{
|
||||
// Ignore the current hull, because otherwise we couldn't find a path out.
|
||||
continue;
|
||||
}
|
||||
if (HumanAIController.UnsafeHulls.Contains(hull))
|
||||
{
|
||||
// Compare safety of the node hull to the current hull safety.
|
||||
float nodeHullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character);
|
||||
if (nodeHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD && nodeHullSafety < HumanAIController.CurrentHullSafety)
|
||||
{
|
||||
// If the node hull is considered unsafe and less safe than the current hull, let's ignore the target.
|
||||
hullSafety = 0;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, each unsafe hull on the path reduces the safety of the target hull by 50% of their threat value.
|
||||
float hullThreat = 100 - nodeHullSafety;
|
||||
hullSafety -= hullThreat / 2;
|
||||
if (hullSafety <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, true))
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, includingConnectedSubs: true))
|
||||
{
|
||||
hullSafety /= 10;
|
||||
}
|
||||
|
||||
+29
-12
@@ -83,7 +83,7 @@ namespace Barotrauma
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!IsValidTarget(target, character)) { return false; }
|
||||
if (!CheckTarget(target)) { return false; }
|
||||
float inspectDist = target.IsCriminal ? CriminalInspectDistance : inspectDistance;
|
||||
if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > inspectDist * inspectDist) { return false; }
|
||||
if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime))
|
||||
@@ -145,26 +145,31 @@ namespace Barotrauma
|
||||
// Might be e.g. sitting on a chair.
|
||||
character.SelectedSecondaryItem = null;
|
||||
}
|
||||
foreach (var target in Character.CharacterList)
|
||||
if (HumanAIController.CurrentHullSafety >= HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
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.Illegitimate && target.HasEquippedItem(it)) &&
|
||||
character.CanSeeTarget(target, seeThroughWindows: true))
|
||||
foreach (var target in Character.CharacterList)
|
||||
{
|
||||
AIObjectiveCheckStolenItems? existingObjective =
|
||||
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.Target == target);
|
||||
if (existingObjective == null)
|
||||
if (!CheckTarget(target)) { 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 => target.HasEquippedItem(it) && AIObjectiveCheckStolenItems.IsItemIllegitimate(target, it)) && character.CanSeeTarget(target, seeThroughWindows: true))
|
||||
{
|
||||
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
|
||||
lastInspectionTimes[target] = Timing.TotalTime;
|
||||
if (HumanAIController.CalculateObjectiveHullSafety(target) >= HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
// Don't do inspections in unsafe hulls, because under a threat, bots are allowed to wear diving gear or hold fire extinguishers etc. Even if they are "stolen".
|
||||
AIObjectiveCheckStolenItems? existingObjective = objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.Target == target);
|
||||
if (existingObjective == null)
|
||||
{
|
||||
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
|
||||
lastInspectionTimes[target] = Timing.TotalTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
checkVisibleStolenItemsTimer = CheckVisibleStolenItemsInterval;
|
||||
}
|
||||
|
||||
private bool IsValidTarget(Character target, Character character)
|
||||
private bool CheckTarget(Character target)
|
||||
{
|
||||
if (target == null || target.Removed) { return false; }
|
||||
if (target.IsIncapacitated) { return false; }
|
||||
@@ -176,6 +181,8 @@ namespace Barotrauma
|
||||
//only player's crew can steal, ignore other teams
|
||||
if (!target.IsOnPlayerTeam) { return false; }
|
||||
if (target.IsHandcuffed) { return false; }
|
||||
//ignore thieves in the same team
|
||||
if (character.OriginalTeamID == target.TeamID || character.TeamID == target.TeamID) { return false; }
|
||||
// Ignore targets that are climbing, because might need to use ladders to get to them.
|
||||
if (target.IsClimbing) { return false; }
|
||||
if (HumanAIController.IsTrueForAnyBotInTheCrew(bot =>
|
||||
@@ -190,6 +197,16 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
|
||||
{
|
||||
MarkTargetAsInspected(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the targets as being inspected for stolen items (e.g. while arresting the character),
|
||||
/// meaning characters with this objective won't attempt to trigger an inspection in a while.
|
||||
/// </summary>
|
||||
/// <param name="target"></param>
|
||||
public static void MarkTargetAsInspected(Character target)
|
||||
{
|
||||
lastInspectionTimes[target] = Timing.TotalTime;
|
||||
}
|
||||
|
||||
+6
-6
@@ -31,7 +31,7 @@ namespace Barotrauma
|
||||
this.isPriority = isPriority;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => Leak.Open <= 0 || Leak.Removed;
|
||||
protected override bool CheckObjectiveState() => Leak.Open <= 0 || Leak.Removed;
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
@@ -166,7 +166,7 @@ namespace Barotrauma
|
||||
// TODO: use the collider size/reach?
|
||||
if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150)
|
||||
{
|
||||
HumanAIController.AnimController.Crouching = true;
|
||||
HumanAIController.AnimController.Crouch();
|
||||
}
|
||||
float reach = CalculateReach(repairTool, character);
|
||||
bool canOperate = toLeak.LengthSquared() < reach * reach;
|
||||
@@ -180,7 +180,7 @@ namespace Barotrauma
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (CheckObjectiveSpecific()) { IsCompleted = true; }
|
||||
if (CheckObjectiveState()) { IsCompleted = true; }
|
||||
else
|
||||
{
|
||||
// Failed to operate. Probably too far.
|
||||
@@ -194,7 +194,7 @@ namespace Barotrauma
|
||||
{
|
||||
UseDistanceRelativeToAimSourcePos = true,
|
||||
CloseEnough = reach,
|
||||
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak".ToIdentifier() : Identifier.Empty,
|
||||
DialogueIdentifier = Leak.FlowTargetHull != null ? AIObjectiveGoTo.DialogCannotReachLeak : Identifier.Empty,
|
||||
TargetName = Leak.FlowTargetHull?.DisplayName,
|
||||
requiredCondition = () =>
|
||||
Leak.Submarine == character.Submarine &&
|
||||
@@ -202,11 +202,11 @@ namespace Barotrauma
|
||||
endNodeFilter = IsSuitableEndNode,
|
||||
// The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue)
|
||||
// Only report about contextual targets.
|
||||
SpeakCannotReachCondition = () => isPriority && !CheckObjectiveSpecific()
|
||||
SpeakCannotReachCondition = () => isPriority && !CheckObjectiveState()
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
if (CheckObjectiveSpecific()) { IsCompleted = true; }
|
||||
if (CheckObjectiveState()) { IsCompleted = true; }
|
||||
else if ((Leak.WorldPosition - character.AnimController.AimSourceWorldPos).LengthSquared() > MathUtils.Pow(reach * 2, 2))
|
||||
{
|
||||
// Too far
|
||||
|
||||
+27
-3
@@ -31,6 +31,10 @@ namespace Barotrauma
|
||||
|
||||
private Item targetItem;
|
||||
private readonly Item originalTarget;
|
||||
/// <summary>
|
||||
/// ItemContainer the bot is trying to put the <see cref="TargetItem"/> into. Only set when the objective is a subobjective of a <see cref="AIObjectiveContainItem"/>.
|
||||
/// </summary>
|
||||
public ItemContainer ContainTarget;
|
||||
private ISpatialEntity moveToTarget;
|
||||
private bool isDoneSeeking;
|
||||
public Item TargetItem => targetItem;
|
||||
@@ -76,6 +80,12 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public InvSlotType? EquipSlotType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tags of items that bots are allowed to take from outposts, when needed. For example when there's not enough oxygen in the room, or if they need to extinguish a fire.
|
||||
/// The guards won't react if these items are taken by the bots.
|
||||
/// </summary>
|
||||
public static readonly Identifier[] AllowedItemsToTake = { Tags.OxygenSource, Tags.FireExtinguisher, Tags.LightDivingGear, Tags.HeavyDivingGear };
|
||||
|
||||
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
@@ -477,7 +487,17 @@ namespace Barotrauma
|
||||
//the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool)
|
||||
if (ownerItem != item.Container)
|
||||
{
|
||||
itemPriority *= 0.1f;
|
||||
if (ContainTarget != null && ContainTarget.Item.Prefab.Identifier == item.Container.Prefab.Identifier)
|
||||
{
|
||||
// The item is identical to the item we are trying to contain the item to (e.g. trying to find an oxygen source to a mask -> allow to take oxygen sources from other masks)
|
||||
// Reduce the priority just a tiny bit, so that we choose items that are not inside the items first.
|
||||
// TODO: Doesn't solve the issue for items that are not the same type but that should be treated the same. E.g. diving mask and clown diving mask.
|
||||
itemPriority = 0.95f;
|
||||
}
|
||||
else
|
||||
{
|
||||
itemPriority *= 0.1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -645,6 +665,11 @@ namespace Barotrauma
|
||||
if (prefab is not ItemPrefab itemPrefab) { continue; }
|
||||
if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id)))
|
||||
{
|
||||
if (character.AIController.HasInfiniteItemSpawns(prefab.Identifier))
|
||||
{
|
||||
// If an item with infinite spawns is defined, let's use it.
|
||||
return itemPrefab;
|
||||
}
|
||||
float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ?
|
||||
itemPrefab.DefaultPrice.Price :
|
||||
float.MaxValue;
|
||||
@@ -658,9 +683,8 @@ namespace Barotrauma
|
||||
return bestItem;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
if (targetItem == null)
|
||||
{
|
||||
// Not yet ready
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@ namespace Barotrauma
|
||||
ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToImmutableHashSet();
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => subObjectivesCreated && subObjectives.None();
|
||||
protected override bool CheckObjectiveState() => subObjectivesCreated && subObjectives.None();
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
@@ -56,7 +56,7 @@ namespace Barotrauma
|
||||
AIObjectiveGetItem? getItem = null;
|
||||
TryAddSubObjective(ref getItem, () =>
|
||||
{
|
||||
var getItem = new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
|
||||
getItem = new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
|
||||
{
|
||||
AllowVariants = AllowVariants,
|
||||
Wear = Wear,
|
||||
|
||||
+167
-44
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -94,7 +95,35 @@ namespace Barotrauma
|
||||
protected override bool AllowOutsideSubmarine => AllowGoingOutside;
|
||||
protected override bool AllowInAnySub => true;
|
||||
|
||||
public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier();
|
||||
/// <summary>
|
||||
/// NPC line for when the NPC fails to find a path to a target.
|
||||
/// Note that this line includes the tag [name], which needs to be replaced with the name of the target.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachTarget = "dialogcannotreachtarget".ToIdentifier();
|
||||
/// <summary>
|
||||
/// Generic NPC line for when the NPC fails to find a path to some place/target.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachPlace = "dialogcannotreachplace".ToIdentifier();
|
||||
/// <summary>
|
||||
/// NPC line for when the NPC fails to find a path to a patient they're trying to treat.
|
||||
/// Note that this line includes the tag [name], which needs to be replaced with the name of the target.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachPatient = "dialogcannotreachpatient".ToIdentifier();
|
||||
/// <summary>
|
||||
/// NPC line for when the NPC fails to find a path to a fire they're trying to extinguish.
|
||||
/// Note that this line includes the tag [name], which needs to be replaced with the name of the room the NPC is trying to get to.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachFire = "dialogcannotreachfire".ToIdentifier();
|
||||
/// <summary>
|
||||
/// NPC line for when the NPC fails to find a path to a leak they're trying to fix.
|
||||
/// Note that this line includes the tag [name], which needs to be replaced with the name of the room the NPC is trying to get to.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachLeak = "dialogcannotreachleak".ToIdentifier();
|
||||
|
||||
public Identifier DialogueIdentifier { get; set; } = DialogCannotReachPlace;
|
||||
private readonly Identifier ExoSuitRefuel = "dialog.exosuit.refuel".ToIdentifier();
|
||||
private readonly Identifier ExoSuitOutOfFuel = "dialog.exosuit.outoffuel".ToIdentifier();
|
||||
|
||||
public LocalizedString TargetName { get; set; }
|
||||
|
||||
public ISpatialEntity Target { get; private set; }
|
||||
@@ -112,12 +141,12 @@ namespace Barotrauma
|
||||
Abandon = !isOrder;
|
||||
return Priority;
|
||||
}
|
||||
if (Target == null || Target is Entity e && e.Removed)
|
||||
if (Target is null or Entity { Removed: true })
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = !isOrder;
|
||||
}
|
||||
if (IgnoreIfTargetDead && Target is Character character && character.IsDead)
|
||||
if (IgnoreIfTargetDead && Target is Character { IsDead: true })
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = !isOrder;
|
||||
@@ -178,6 +207,17 @@ namespace Barotrauma
|
||||
if (DialogueIdentifier == null) { return; }
|
||||
if (!SpeakIfFails) { return; }
|
||||
if (SpeakCannotReachCondition != null && !SpeakCannotReachCondition()) { return; }
|
||||
|
||||
if (TargetName == null && DialogueIdentifier == DialogCannotReachTarget)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError(
|
||||
$"Error in {nameof(SpeakCannotReach)}: "+
|
||||
$"attempted to use a dialog line that mentions the target (dialogue identifier: {DialogueIdentifier}), but the name of the target ({(Target?.ToString() ?? "null")}) isn't set.");
|
||||
#endif
|
||||
DialogueIdentifier = DialogCannotReachPlace;
|
||||
}
|
||||
|
||||
LocalizedString msg = TargetName == null ?
|
||||
TextManager.Get(DialogueIdentifier) :
|
||||
TextManager.GetWithVariable(DialogueIdentifier, "[name]".ToIdentifier(), TargetName, formatCapitals: Target is Character ? FormatCapitals.No : FormatCapitals.Yes);
|
||||
@@ -194,6 +234,43 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (checkExoSuitTimer <= 0)
|
||||
{
|
||||
checkExoSuitTimer = CheckExoSuitTime * Rand.Range(0.9f, 1.1f);
|
||||
if (character.GetEquippedItem(Tags.PoweredDivingSuit, InvSlotType.OuterClothes) is { OwnInventory: Inventory exoSuitInventory } exoSuit &&
|
||||
exoSuit.GetComponent<Powered>() is not { HasPower: true })
|
||||
{
|
||||
if (HumanAIController.HasItem(character, Tags.DivingSuitFuel, out IEnumerable<Item> fuelRods, conditionPercentage: 1, recursive: true))
|
||||
{
|
||||
// Try to switch the fuel sources
|
||||
if (character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.Get(ExoSuitRefuel).Value, minDurationBetweenSimilar: 10f, identifier: ExoSuitRefuel);
|
||||
}
|
||||
// Have to copy the list, because it's modified when we unequip the item.
|
||||
foreach (Item containedItem in exoSuit.ContainedItems.ToList())
|
||||
{
|
||||
if (containedItem.HasTag(Tags.DivingSuitFuel) && containedItem.Condition <= 0)
|
||||
{
|
||||
character.Unequip(containedItem);
|
||||
}
|
||||
}
|
||||
// Refuel
|
||||
// The information about the target slot is defined in a status effect. We could parse it, but let's keep it simple and just presume that the target slot is the second slot, as it the case with the vanilla exosuits.
|
||||
const int targetSlot = 1;
|
||||
Item fuelRod = fuelRods.MaxBy(b => b.Condition);
|
||||
exoSuitInventory.TryPutItem(fuelRod, targetSlot, allowSwapping: true, allowCombine: true, user: character);
|
||||
}
|
||||
else if (character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.Get(ExoSuitOutOfFuel).Value, minDurationBetweenSimilar: 30.0f, identifier: ExoSuitOutOfFuel);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
checkExoSuitTimer -= deltaTime;
|
||||
}
|
||||
if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy))
|
||||
{
|
||||
// Wait
|
||||
@@ -301,34 +378,43 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
if (Abandon) { return; }
|
||||
if (getDivingGearIfNeeded)
|
||||
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
|
||||
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
|
||||
bool tryToGetDivingSuit = needsDivingSuit;
|
||||
Character followTarget = Target as Character;
|
||||
if (Mimic && !character.IsImmuneToPressure)
|
||||
{
|
||||
Character followTarget = Target as Character;
|
||||
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
|
||||
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
|
||||
bool tryToGetDivingSuit = needsDivingSuit;
|
||||
if (Mimic && !character.IsImmuneToPressure)
|
||||
if (HumanAIController.HasDivingSuit(followTarget))
|
||||
{
|
||||
if (HumanAIController.HasDivingSuit(followTarget))
|
||||
{
|
||||
tryToGetDivingGear = true;
|
||||
tryToGetDivingSuit = true;
|
||||
}
|
||||
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
|
||||
{
|
||||
tryToGetDivingGear = true;
|
||||
}
|
||||
tryToGetDivingGear = true;
|
||||
tryToGetDivingSuit = true;
|
||||
}
|
||||
bool needsEquipment = false;
|
||||
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
|
||||
if (tryToGetDivingSuit)
|
||||
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
|
||||
tryToGetDivingGear = true;
|
||||
}
|
||||
else if (tryToGetDivingGear)
|
||||
}
|
||||
bool needsEquipment = false;
|
||||
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
|
||||
if (tryToGetDivingSuit)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
|
||||
}
|
||||
else if (tryToGetDivingGear)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
|
||||
}
|
||||
if (!getDivingGearIfNeeded)
|
||||
{
|
||||
if (needsEquipment)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
|
||||
// Don't try to reach the target without proper equipment.
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (character.LockHands)
|
||||
{
|
||||
cantFindDivingGear = true;
|
||||
@@ -353,9 +439,9 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try again without requiring the diving suit
|
||||
// Try again without requiring the diving suit (or mask)
|
||||
RemoveSubObjective(ref findDivingGear);
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: !tryToGetDivingSuit, objectiveManager),
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
|
||||
@@ -442,7 +528,7 @@ namespace Barotrauma
|
||||
if (checkScooterTimer <= 0)
|
||||
{
|
||||
useScooter = false;
|
||||
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
|
||||
checkScooterTimer = CheckScooterTime * Rand.Range(0.9f, 1.1f);
|
||||
Item scooter = null;
|
||||
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(Tags.Scooter, allowBroken: false);
|
||||
if (!shouldUseScooter)
|
||||
@@ -465,24 +551,25 @@ namespace Barotrauma
|
||||
}
|
||||
else if (shouldUseScooter)
|
||||
{
|
||||
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
|
||||
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
|
||||
bool handsFull =
|
||||
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem) && !character.Inventory.TryPutItem(leftHandItem, character, InvSlotType.Bag.ToEnumerable())) ||
|
||||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem) && !character.Inventory.TryPutItem(rightHandItem, character, InvSlotType.Bag.ToEnumerable()));
|
||||
if (!handsFull)
|
||||
bool hasHandsFull = character.HasHandsFull(out (Item leftHandItem, Item rightHandItem) items);
|
||||
if (hasHandsFull)
|
||||
{
|
||||
hasHandsFull = !character.TryPutItemInAnySlot(items.leftHandItem) &&
|
||||
!character.TryPutItemInAnySlot(items.rightHandItem) &&
|
||||
!character.TryPutItemInBag(items.leftHandItem) &&
|
||||
!character.TryPutItemInBag(items.rightHandItem);
|
||||
}
|
||||
if (!hasHandsFull)
|
||||
{
|
||||
bool hasBattery = false;
|
||||
if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScooters, containedTag: Tags.MobileBattery, conditionPercentage: 1, requireEquipped: false))
|
||||
if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScootersWithBattery, containedTag: Tags.MobileBattery, conditionPercentage: 1, requireEquipped: false))
|
||||
{
|
||||
// Non-equipped scooter with a battery
|
||||
scooter = nonEquippedScooters.FirstOrDefault();
|
||||
scooter = nonEquippedScootersWithBattery.FirstOrDefault();
|
||||
hasBattery = true;
|
||||
}
|
||||
else if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
|
||||
else if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScootersWithoutBattery, requireEquipped: false))
|
||||
{
|
||||
// Non-equipped scooter without a battery
|
||||
scooter = _nonEquippedScooters.FirstOrDefault();
|
||||
scooter = nonEquippedScootersWithoutBattery.FirstOrDefault();
|
||||
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
|
||||
hasBattery = HumanAIController.HasItem(character, Tags.MobileBattery, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
|
||||
}
|
||||
@@ -518,8 +605,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (!useScooter)
|
||||
{
|
||||
// Unequip
|
||||
character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
|
||||
character.TryPutItemInAnySlot(scooter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -663,7 +749,10 @@ namespace Barotrauma
|
||||
|
||||
private bool useScooter;
|
||||
private float checkScooterTimer;
|
||||
private readonly float checkScooterTime = 0.5f;
|
||||
private const float CheckScooterTime = 0.5f;
|
||||
|
||||
private float checkExoSuitTimer;
|
||||
private const float CheckExoSuitTime = 2.0f;
|
||||
|
||||
public Hull GetTargetHull() => GetTargetHull(Target);
|
||||
|
||||
@@ -750,6 +839,11 @@ namespace Barotrauma
|
||||
// Going through a hatch
|
||||
return false;
|
||||
}
|
||||
if (Target is Item targetItem && targetItem.GetComponent<Pickable>() == null)
|
||||
{
|
||||
// Targeting a static item, such as a reactor or a controller -> Don't complete, until we are no longer climbing.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!AlwaysUseEuclideanDistance && !character.AnimController.InWater)
|
||||
@@ -764,9 +858,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
// First check the distance and then if can interact (heaviest)
|
||||
if (Target == null)
|
||||
{
|
||||
@@ -850,5 +943,35 @@ namespace Barotrauma
|
||||
pathSteering.ResetPath();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldRun(bool run)
|
||||
{
|
||||
if (run && objectiveManager.ForcedOrder == this && IsWaitOrder && !character.IsOnPlayerTeam)
|
||||
{
|
||||
// NPCs with a wait order don't run.
|
||||
run = false;
|
||||
}
|
||||
else if (Target != null)
|
||||
{
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
run = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > 300 * 300;
|
||||
}
|
||||
else
|
||||
{
|
||||
float yDiff = Target.WorldPosition.Y - character.WorldPosition.Y;
|
||||
if (Math.Abs(yDiff) > 100)
|
||||
{
|
||||
run = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
float xDiff = Target.WorldPosition.X - character.WorldPosition.X;
|
||||
run = Math.Abs(xDiff) > 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
return run;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+167
-130
@@ -88,7 +88,7 @@ namespace Barotrauma
|
||||
CalculatePriority();
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
|
||||
public readonly HashSet<Identifier> PreferredOutpostModuleTypes = new HashSet<Identifier>();
|
||||
@@ -158,8 +158,17 @@ namespace Barotrauma
|
||||
{
|
||||
character.DeselectCharacter();
|
||||
}
|
||||
|
||||
character.SelectedItem = null;
|
||||
if (character.SelectedItem != null)
|
||||
{
|
||||
if (character.SelectedItem.Prefab.AllowDeselectWhenIdling)
|
||||
{
|
||||
character.SelectedItem = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!character.IsClimbing)
|
||||
{
|
||||
@@ -176,135 +185,162 @@ namespace Barotrauma
|
||||
IsForbidden(currentTarget) ||
|
||||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
|
||||
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull != null && !IsForbidden(TargetHull) && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull != null && !currentTargetIsInvalid && !IsForbidden(TargetHull))
|
||||
{
|
||||
currentTarget = TargetHull;
|
||||
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
|
||||
if (stayInHull)
|
||||
if (HumanAIController.UnsafeHulls.Contains(TargetHull))
|
||||
{
|
||||
Wander(deltaTime);
|
||||
}
|
||||
else if (currentTarget != null)
|
||||
{
|
||||
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
|
||||
// Ask to refresh, because otherwise we can't get back to the hull.
|
||||
HumanAIController.AskToRecalculateHullSafety(TargetHull);
|
||||
}
|
||||
else
|
||||
{
|
||||
PathSteering.ResetPath();
|
||||
PathSteering.Reset();
|
||||
currentTarget = TargetHull;
|
||||
NavigateTo(currentTarget);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (currentTarget != null && !currentTargetIsInvalid)
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
|
||||
{
|
||||
if (currentTarget.Submarine.TeamID != character.TeamID)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentTarget.Submarine != character.Submarine)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished())
|
||||
{
|
||||
if (newTargetTimer > timerMargin)
|
||||
{
|
||||
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
|
||||
//almost constantly when there's a small number of potential hulls to move to
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
}
|
||||
else if (character.IsClimbing)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
|
||||
{
|
||||
// Don't allow new targets when climbing straight up or down
|
||||
SetTargetTimerHigh();
|
||||
}
|
||||
}
|
||||
else if (character.AnimController.InWater)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
}
|
||||
if (newTargetTimer <= 0.0f)
|
||||
{
|
||||
if (!searchingNewHull)
|
||||
{
|
||||
//find all available hulls first
|
||||
searchingNewHull = true;
|
||||
FindTargetHulls();
|
||||
}
|
||||
else if (targetHulls.Any())
|
||||
{
|
||||
//choose a random available hull
|
||||
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
|
||||
bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
|
||||
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
|
||||
Vector2 targetPos = character.GetRelativeSimPosition(currentTarget);
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, targetPos, character.Submarine, nodeFilter: node =>
|
||||
{
|
||||
if (node.Waypoint.CurrentHull == null) { return false; }
|
||||
// Check that there is no unsafe hulls on the way to the target
|
||||
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
|
||||
return true;
|
||||
//don't stop at ladders when idling
|
||||
}, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
|
||||
if (path.Unreachable)
|
||||
{
|
||||
//can't go to this room, remove it from the list and try another room
|
||||
int index = targetHulls.IndexOf(currentTarget);
|
||||
targetHulls.RemoveAt(index);
|
||||
hullWeights.RemoveAt(index);
|
||||
PathSteering.Reset();
|
||||
currentTarget = null;
|
||||
SetTargetTimerLow();
|
||||
return;
|
||||
}
|
||||
character.AIController.SelectTarget(currentTarget.AiTarget);
|
||||
PathSteering.SetPath(targetPos, path);
|
||||
SetTargetTimerNormal();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Couldn't find a valid hull
|
||||
SetTargetTimerHigh();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
}
|
||||
newTargetTimer -= deltaTime;
|
||||
if (currentTarget == null || PathSteering.CurrentPath == null)
|
||||
{
|
||||
Wander(deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentTarget != null && !currentTargetIsInvalid)
|
||||
NavigateTo(currentTarget);
|
||||
}
|
||||
|
||||
void NavigateTo(Hull target)
|
||||
{
|
||||
bool isAtTarget = character.CurrentHull == target && IsSteeringFinished();
|
||||
if (isAtTarget)
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
|
||||
if (character.IsClimbing)
|
||||
{
|
||||
if (currentTarget.Submarine.TeamID != character.TeamID)
|
||||
StopMoving();
|
||||
if (character.AnimController.GetHeightFromFloor() < character.AnimController.ImpactTolerance / 2)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
character.StopClimbing();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentTarget.Submarine != character.Submarine)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
}
|
||||
Wander(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished())
|
||||
else if (target != null)
|
||||
{
|
||||
if (newTargetTimer > timerMargin)
|
||||
{
|
||||
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
|
||||
//almost constantly when there's a small number of potential hulls to move to
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
}
|
||||
else if (character.IsClimbing)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
|
||||
{
|
||||
// Don't allow new targets when climbing straight up or down
|
||||
SetTargetTimerHigh();
|
||||
}
|
||||
}
|
||||
else if (character.AnimController.InWater)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
}
|
||||
if (newTargetTimer <= 0.0f)
|
||||
{
|
||||
if (!searchingNewHull)
|
||||
{
|
||||
//find all available hulls first
|
||||
searchingNewHull = true;
|
||||
FindTargetHulls();
|
||||
}
|
||||
else if (targetHulls.Any())
|
||||
{
|
||||
//choose a random available hull
|
||||
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
|
||||
bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
|
||||
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
|
||||
Vector2 targetPos = character.GetRelativeSimPosition(currentTarget);
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, targetPos, character.Submarine, nodeFilter: node =>
|
||||
{
|
||||
if (node.Waypoint.CurrentHull == null) { return false; }
|
||||
// Check that there is no unsafe hulls on the way to the target
|
||||
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
|
||||
return true;
|
||||
//don't stop at ladders when idling
|
||||
}, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
|
||||
if (path.Unreachable)
|
||||
{
|
||||
//can't go to this room, remove it from the list and try another room
|
||||
int index = targetHulls.IndexOf(currentTarget);
|
||||
targetHulls.RemoveAt(index);
|
||||
hullWeights.RemoveAt(index);
|
||||
PathSteering.Reset();
|
||||
currentTarget = null;
|
||||
SetTargetTimerLow();
|
||||
return;
|
||||
}
|
||||
character.AIController.SelectTarget(currentTarget.AiTarget);
|
||||
PathSteering.SetPath(targetPos, path);
|
||||
SetTargetTimerNormal();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Couldn't find a valid hull
|
||||
SetTargetTimerHigh();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
}
|
||||
newTargetTimer -= deltaTime;
|
||||
if (!character.IsClimbing && (PathSteering == null || PathSteering.CurrentPath == null || IsSteeringFinished()))
|
||||
{
|
||||
Wander(deltaTime);
|
||||
}
|
||||
else if (currentTarget != null)
|
||||
{
|
||||
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1,
|
||||
nodeFilter: node => node.Waypoint.CurrentHull != null,
|
||||
endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null);
|
||||
PathTo(target);
|
||||
}
|
||||
else
|
||||
{
|
||||
PathSteering.ResetPath();
|
||||
PathSteering.Reset();
|
||||
StopMoving();
|
||||
}
|
||||
}
|
||||
|
||||
void StopMoving()
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
PathSteering.ResetPath();
|
||||
}
|
||||
|
||||
void PathTo(ISpatialEntity target)
|
||||
{
|
||||
PathSteering.SteeringSeek(character.GetRelativeSimPosition(target), weight: 1,
|
||||
nodeFilter: node => node.Waypoint.CurrentHull != null,
|
||||
endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null);
|
||||
}
|
||||
}
|
||||
|
||||
public void Wander(float deltaTime)
|
||||
@@ -378,14 +414,14 @@ namespace Barotrauma
|
||||
chairCheckTimer -= deltaTime;
|
||||
if (chairCheckTimer <= 0.0f && character.SelectedSecondaryItem == null)
|
||||
{
|
||||
foreach (Item item in Item.ItemList)
|
||||
foreach (Item chair in Item.ChairItems)
|
||||
{
|
||||
if (item.CurrentHull != currentHull || !item.HasTag(Tags.ChairItem)) { continue; }
|
||||
if (chair.CurrentHull != currentHull) { continue; }
|
||||
//not possible in vanilla game, but a mod might have holdable/attachable chairs
|
||||
if (item.ParentInventory != null || item.body is { Enabled: true }) { continue; }
|
||||
var controller = item.GetComponent<Controller>();
|
||||
if (chair.ParentInventory != null || chair.body is { Enabled: true }) { continue; }
|
||||
var controller = chair.GetComponent<Controller>();
|
||||
if (controller == null || controller.User != null) { continue; }
|
||||
item.TryInteract(character, forceSelectKey: true);
|
||||
chair.TryInteract(character, forceSelectKey: true);
|
||||
}
|
||||
chairCheckTimer = chairCheckInterval;
|
||||
}
|
||||
@@ -489,27 +525,26 @@ namespace Barotrauma
|
||||
if (checkItemsTimer <= 0)
|
||||
{
|
||||
checkItemsTimer = checkItemsInterval * Rand.Range(0.9f, 1.1f);
|
||||
var hull = character.CurrentHull;
|
||||
if (hull != null)
|
||||
if (character.Submarine is not Submarine sub) { return; }
|
||||
if (sub.TeamID != character.TeamID) { return; }
|
||||
if (character.CurrentHull is not Hull currentHull) { return; }
|
||||
itemsToClean.Clear();
|
||||
foreach (Item item in Item.CleanableItems)
|
||||
{
|
||||
itemsToClean.Clear();
|
||||
foreach (Item item in Item.CleanableItems)
|
||||
if (item.CurrentHull != currentHull) { continue; }
|
||||
if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item))
|
||||
{
|
||||
if (item.CurrentHull != hull) { continue; }
|
||||
if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item))
|
||||
{
|
||||
itemsToClean.Add(item);
|
||||
}
|
||||
itemsToClean.Add(item);
|
||||
}
|
||||
if (itemsToClean.Any())
|
||||
}
|
||||
if (itemsToClean.Any())
|
||||
{
|
||||
var targetItem = itemsToClean.MinBy(i => Math.Abs(character.WorldPosition.X - i.WorldPosition.X));
|
||||
if (targetItem != null)
|
||||
{
|
||||
var targetItem = itemsToClean.OrderBy(i => Math.Abs(character.WorldPosition.X - i.WorldPosition.X)).FirstOrDefault();
|
||||
if (targetItem != null)
|
||||
{
|
||||
var cleanupObjective = new AIObjectiveCleanupItem(targetItem, character, objectiveManager, PriorityModifier);
|
||||
cleanupObjective.Abandoned += () => ignoredItems.Add(targetItem);
|
||||
subObjectives.Add(cleanupObjective);
|
||||
}
|
||||
var cleanupObjective = new AIObjectiveCleanupItem(targetItem, character, objectiveManager, PriorityModifier);
|
||||
cleanupObjective.Abandoned += () => ignoredItems.Add(targetItem);
|
||||
subObjectives.Add(cleanupObjective);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,6 +569,8 @@ namespace Barotrauma
|
||||
itemsToClean.Clear();
|
||||
ignoredItems.Clear();
|
||||
autonomousObjectiveRetryTimer = 10;
|
||||
timerMargin = 0;
|
||||
newTargetTimer = 0;
|
||||
}
|
||||
|
||||
public override void OnDeselected()
|
||||
|
||||
+1
-2
@@ -120,7 +120,6 @@ namespace Barotrauma
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
}
|
||||
}
|
||||
|
||||
+12
-11
@@ -22,7 +22,7 @@ namespace Barotrauma
|
||||
private static Dictionary<ItemPrefab, ImmutableHashSet<Identifier>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<Identifier>>();
|
||||
|
||||
private int itemIndex;
|
||||
private AIObjectiveDecontainItem decontainObjective;
|
||||
private AIObjectiveMoveItem moveItemObjective;
|
||||
private readonly HashSet<Item> ignoredItems = new HashSet<Item>();
|
||||
private Item targetItem;
|
||||
private readonly string abandonGetItemDialogueIdentifier = "dialogcannotfindloadable";
|
||||
@@ -196,17 +196,17 @@ namespace Barotrauma
|
||||
float devotion = (CumulatedDevotion + (hasContainable ? 100 - MaxDevotion : 0)) / 100;
|
||||
float max = AIObjectiveManager.LowestOrderPriority - (hasContainable ? 1 : 2);
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
|
||||
if (decontainObjective != null && targetItem.Container != Container)
|
||||
if (moveItemObjective != null && targetItem.Container != Container)
|
||||
{
|
||||
if (!IsValidContainable(targetItem))
|
||||
{
|
||||
// Target is not valid anymore, abandon the objective
|
||||
decontainObjective.Abandon = true;
|
||||
moveItemObjective.Abandon = true;
|
||||
}
|
||||
else if (!ItemContainer.Inventory.CanBePut(targetItem) && ItemContainer.Inventory.AllItems.None(i => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition)))
|
||||
{
|
||||
// The container is full and there's no item that should be removed, abandon the objective
|
||||
decontainObjective.Abandon = true;
|
||||
moveItemObjective.Abandon = true;
|
||||
}
|
||||
}
|
||||
if (ItemContainer.Inventory.IsFull())
|
||||
@@ -257,26 +257,27 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if(decontainObjective == null && !IsValidContainable(targetItem))
|
||||
if(moveItemObjective == null && !IsValidContainable(targetItem))
|
||||
{
|
||||
IgnoreTargetItem();
|
||||
Reset();
|
||||
return;
|
||||
}
|
||||
TryAddSubObjective(ref decontainObjective,
|
||||
constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
|
||||
TryAddSubObjective(ref moveItemObjective,
|
||||
constructor: () => new AIObjectiveMoveItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
|
||||
{
|
||||
AbandonGetItemDialogueCondition = () => IsValidContainable(targetItem),
|
||||
AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier,
|
||||
Equip = true,
|
||||
RemoveExistingWhenNecessary = true,
|
||||
RemoveExistingPredicate = (i) => !ValidContainableItemIdentifiers.Contains(i.Prefab.Identifier) || AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition),
|
||||
RemoveExistingMax = 1
|
||||
RemoveExistingMax = 1,
|
||||
AllowToFindDivingGear = objectiveManager.HasOrder<AIObjectiveLoadItems>()
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
IsCompleted = true;
|
||||
RemoveSubObjective(ref decontainObjective);
|
||||
RemoveSubObjective(ref moveItemObjective);
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
@@ -318,13 +319,13 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
protected override bool CheckObjectiveState() => IsCompleted;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
// Don't reset the target item when resetting the objective because it affects priority calculations
|
||||
decontainObjective = null;
|
||||
moveItemObjective = null;
|
||||
itemIndex = 0;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ namespace Barotrauma
|
||||
if (item.IsClaimedByBallastFlora) { return false; }
|
||||
if (!item.HasAccess(character)) { return false; }
|
||||
// Ignore items that require power but don't have it
|
||||
if (item.GetComponent<Powered>() is Powered powered && powered.PowerConsumption > 0 && powered.Voltage < powered.MinVoltage) { return false; }
|
||||
if (item.GetComponent<Powered>() is { PowerConsumption: > 0, HasPower: false }) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ namespace Barotrauma
|
||||
: base(character, objectiveManager, priorityModifier, option) { }
|
||||
|
||||
protected override void Act(float deltaTime) { }
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
public override bool AbandonWhenCannotCompleteSubObjectives => false;
|
||||
public override bool AllowSubObjectiveSorting => true;
|
||||
|
||||
+16
-9
@@ -35,7 +35,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public const float HighestOrderPriority = 70;
|
||||
/// <summary>
|
||||
/// Maximum priority of an order given to the character (rightmost order in the crew list)
|
||||
/// Minimum priority of an order given to the character (rightmost order in the crew list)
|
||||
/// </summary>
|
||||
public const float LowestOrderPriority = 60;
|
||||
/// <summary>
|
||||
@@ -228,11 +228,7 @@ namespace Barotrauma
|
||||
coroutine = CoroutineManager.Invoke(() =>
|
||||
{
|
||||
//round ended before the coroutine finished
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession == null || Level.Loaded == null && !(GameMain.GameSession.GameMode is TestGameMode)) { return; }
|
||||
#else
|
||||
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
|
||||
#endif
|
||||
if (GameMain.GameSession == null || Level.Loaded == null && GameMain.GameSession.GameMode is not TestGameMode) { return; }
|
||||
DelayedObjectives.Remove(objective);
|
||||
AddObjective(objective);
|
||||
callback?.Invoke();
|
||||
@@ -480,7 +476,7 @@ namespace Barotrauma
|
||||
IgnoreIfTargetDead = true,
|
||||
IsFollowOrder = true,
|
||||
Mimic = character.IsOnPlayerTeam,
|
||||
DialogueIdentifier = "dialogcannotreachplace".ToIdentifier()
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPlace
|
||||
};
|
||||
break;
|
||||
case "wait":
|
||||
@@ -719,6 +715,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public bool HasObjectiveOrOrder<T>() where T : AIObjective => Objectives.Any(o => o is T) || HasOrder<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current objective or its currently active subobjective (first in chain), regadless of the type.
|
||||
/// Note: Not recursive, and thus doesn't work for deeper hierarchy!
|
||||
/// For seeking objectives of specific type and in a deep hierarchy, use <see cref="GetLastActiveObjective{T}"/> or with looping objectives <see cref="GetFirstActiveObjective{T}"/>
|
||||
/// </summary>
|
||||
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
|
||||
|
||||
/// <summary>
|
||||
@@ -735,7 +736,8 @@ namespace Barotrauma
|
||||
/// Returns the last active objective of the specified objective type.
|
||||
/// Should generally be used to get the active objective (or subobjective) of objectives that don't sort their subobjectives by priority (see <see cref="AIObjective.AllowSubObjectiveSorting"/>.
|
||||
/// </summary>
|
||||
/// <returns>The last active objective of the specified type if found.
|
||||
/// <returns>
|
||||
/// The last active objective of the specified type if found.
|
||||
/// </returns>
|
||||
public T GetLastActiveObjective<T>() where T : AIObjective
|
||||
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
|
||||
@@ -758,7 +760,12 @@ namespace Barotrauma
|
||||
if (CurrentObjective == null) { return Enumerable.Empty<T>(); }
|
||||
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).OfType<T>();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Is the current objective or any of its subobjectives of the given type?
|
||||
/// Useful for checking whether the bot has a certain type of objective active in the hierarchy.
|
||||
/// </summary>
|
||||
/// <returns>False for objectives and orders that are not currently active.</returns>
|
||||
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
|
||||
|
||||
public bool IsOrder(AIObjective objective)
|
||||
|
||||
+26
-17
@@ -6,9 +6,9 @@ using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveDecontainItem : AIObjective
|
||||
class AIObjectiveMoveItem : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier();
|
||||
public override Identifier Identifier { get; set; } = "move item".ToIdentifier();
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
@@ -47,8 +47,15 @@ namespace Barotrauma
|
||||
public int? RemoveExistingMax { get; set; }
|
||||
public string AbandonGetItemDialogueIdentifier { get; set; }
|
||||
public Func<bool> AbandonGetItemDialogueCondition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// By default, finding diving gear is not allowed here, because it can cause unexpected behavior in most use cases.
|
||||
/// E.g. bots equipping diving suits to clean up some items in flooded rooms.
|
||||
/// Sometimes, at least when used in orders, we might want to allow this. See <see cref="AIObjectiveLoadItem"/>.
|
||||
/// </summary>
|
||||
public bool AllowToFindDivingGear { get; set; }
|
||||
|
||||
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
public AIObjectiveMoveItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.targetItem = targetItem;
|
||||
@@ -56,10 +63,10 @@ namespace Barotrauma
|
||||
this.targetContainer = targetContainer;
|
||||
}
|
||||
|
||||
public AIObjectiveDecontainItem(Character character, Identifier itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
public AIObjectiveMoveItem(Character character, Identifier itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
: this(character, new Identifier[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { }
|
||||
|
||||
public AIObjectiveDecontainItem(Character character, Identifier[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
public AIObjectiveMoveItem(Character character, Identifier[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.itemIdentifiers = itemIdentifiers;
|
||||
@@ -71,20 +78,20 @@ namespace Barotrauma
|
||||
this.targetContainer = targetContainer;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
protected override bool CheckObjectiveState() => IsCompleted;
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
Item itemToDecontain =
|
||||
Item itemToMove =
|
||||
targetItem ??
|
||||
sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI(character)), recursive: false);
|
||||
|
||||
if (itemToDecontain == null)
|
||||
if (itemToMove == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (itemToDecontain.IgnoreByAI(character))
|
||||
if (itemToMove.IgnoreByAI(character))
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
@@ -96,19 +103,19 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (itemToDecontain.Container != sourceContainer.Item)
|
||||
if (itemToMove.Container != sourceContainer.Item)
|
||||
{
|
||||
itemToDecontain.Drop(character);
|
||||
itemToMove.Drop(character);
|
||||
IsCompleted = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (targetContainer.Inventory.Contains(itemToDecontain))
|
||||
else if (targetContainer.Inventory.Contains(itemToMove))
|
||||
{
|
||||
IsCompleted = true;
|
||||
return;
|
||||
}
|
||||
if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character))
|
||||
if (getItemObjective == null && !itemToMove.IsOwnedBy(character))
|
||||
{
|
||||
TryAddSubObjective(ref getItemObjective,
|
||||
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip)
|
||||
@@ -116,7 +123,8 @@ namespace Barotrauma
|
||||
CannotFindDialogueCondition = AbandonGetItemDialogueCondition,
|
||||
CannotFindDialogueIdentifierOverride = AbandonGetItemDialogueIdentifier,
|
||||
SpeakIfFails = AbandonGetItemDialogueIdentifier != null,
|
||||
TakeWholeStack = this.TakeWholeStack
|
||||
TakeWholeStack = TakeWholeStack,
|
||||
AllowToFindDivingGear = AllowToFindDivingGear
|
||||
},
|
||||
onAbandon: () => Abandon = true);
|
||||
return;
|
||||
@@ -124,7 +132,7 @@ namespace Barotrauma
|
||||
if (targetContainer != null)
|
||||
{
|
||||
TryAddSubObjective(ref containObjective,
|
||||
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager)
|
||||
constructor: () => new AIObjectiveContainItem(character, itemToMove, targetContainer, objectiveManager)
|
||||
{
|
||||
MoveWholeStack = TakeWholeStack,
|
||||
Equip = Equip,
|
||||
@@ -133,14 +141,15 @@ namespace Barotrauma
|
||||
RemoveExistingPredicate = RemoveExistingPredicate,
|
||||
RemoveMax = RemoveExistingMax,
|
||||
GetItemPriority = GetItemPriority,
|
||||
ignoredContainerIdentifiers = sourceContainer?.Item.Prefab.Identifier.ToEnumerable().ToImmutableHashSet()
|
||||
ignoredContainerIdentifiers = sourceContainer?.Item.Prefab.Identifier.ToEnumerable().ToImmutableHashSet(),
|
||||
AllowToFindDivingGear = AllowToFindDivingGear
|
||||
},
|
||||
onCompleted: () => IsCompleted = true,
|
||||
onAbandon: () => Abandon = true);
|
||||
}
|
||||
else
|
||||
{
|
||||
itemToDecontain.Drop(character);
|
||||
itemToMove.Drop(character);
|
||||
IsCompleted = true;
|
||||
}
|
||||
}
|
||||
+88
-14
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -15,7 +16,7 @@ namespace Barotrauma
|
||||
public override bool AllowMultipleInstances => true;
|
||||
protected override bool AllowInAnySub => true;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret);
|
||||
public override bool PrioritizeIfSubObjectivesActive => component is Reactor or Turret;
|
||||
|
||||
private readonly ItemComponent component, controller;
|
||||
private readonly Entity operateTarget;
|
||||
@@ -88,12 +89,23 @@ namespace Barotrauma
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
}
|
||||
var reactor = component?.Item.GetComponent<Reactor>();
|
||||
Hull targetHull = targetItem.CurrentHull;
|
||||
if (HumanAIController.UnsafeHulls.Contains(targetHull))
|
||||
{
|
||||
// Ignore the objective, if the target hull is dangerous.
|
||||
Priority = 0;
|
||||
if (isOrder && this == objectiveManager.CurrentObjective && character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("dialogoperatetargetroomisunsafe", "[item]", targetItem.Name).Value, delay: 1.0f, identifier: "dialogoperatetargetroomisunsafe".ToIdentifier(), minDurationBetweenSimilar: 5.0f);
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
var reactor = component.Item.GetComponent<Reactor>();
|
||||
if (reactor != null)
|
||||
{
|
||||
if (!isOrder)
|
||||
{
|
||||
if (reactor.LastUserWasPlayer && character.TeamID != CharacterTeamType.FriendlyNPC)
|
||||
if (reactor.LastUserWasPlayer && character.IsOnPlayerTeam)
|
||||
{
|
||||
// The reactor was previously operated by a player -> ignore.
|
||||
Priority = 0;
|
||||
@@ -126,7 +138,7 @@ namespace Barotrauma
|
||||
}
|
||||
else if (!isOrder)
|
||||
{
|
||||
var steering = component?.Item.GetComponent<Steering>();
|
||||
var steering = component.Item.GetComponent<Steering>();
|
||||
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsCaptain, onlyActive: true, onlyConnectedSubs: true)))
|
||||
{
|
||||
// Ignore if already set to autopilot or if there's a captain onboard
|
||||
@@ -136,10 +148,8 @@ namespace Barotrauma
|
||||
}
|
||||
if (targetItem.CurrentHull == null ||
|
||||
targetItem.Submarine != character.Submarine && !isOrder ||
|
||||
targetItem.CurrentHull.FireSources.Any() ||
|
||||
HumanAIController.IsItemOperatedByAnother(target, out _) ||
|
||||
Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))
|
||||
|| component.Item.IgnoreByAI(character) || useController && controller.Item.IgnoreByAI(character))
|
||||
IsItemOperatedByAnother(target) ||
|
||||
component.Item.IgnoreByAI(character) || useController && controller.Item.IgnoreByAI(character))
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
@@ -154,8 +164,8 @@ namespace Barotrauma
|
||||
else if (!OverridePriority.HasValue)
|
||||
{
|
||||
float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier);
|
||||
float max = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && reactor.AutoTemp && Option == "powerup")
|
||||
const float max = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
if (reactor is { PowerOn: true, FissionRate: > 1, AutoTemp: true } && Option == "powerup")
|
||||
{
|
||||
// Already on, no need to operate.
|
||||
value = 0;
|
||||
@@ -171,12 +181,12 @@ namespace Barotrauma
|
||||
Entity operateTarget = null, bool useController = false, ItemComponent controller = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier, option)
|
||||
{
|
||||
component = item ?? throw new ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target.");
|
||||
component = item ?? throw new ArgumentNullException(nameof(item), "Attempted to create an AIObjectiveOperateItem with a null target.");
|
||||
this.requireEquip = requireEquip;
|
||||
this.operateTarget = operateTarget;
|
||||
this.useController = useController;
|
||||
if (useController) { this.controller = controller ?? component?.Item?.FindController(); }
|
||||
var target = GetTarget();
|
||||
if (useController) { this.controller = controller ?? component.Item?.FindController(); }
|
||||
ItemComponent target = GetTarget();
|
||||
if (target == null)
|
||||
{
|
||||
Abandon = true;
|
||||
@@ -245,6 +255,7 @@ namespace Barotrauma
|
||||
{
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
|
||||
{
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
|
||||
TargetName = target.Item.Name,
|
||||
endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item)
|
||||
},
|
||||
@@ -312,7 +323,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => isDoneOperating && !Repeat;
|
||||
protected override bool CheckObjectiveState() => isDoneOperating && !Repeat;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
@@ -320,5 +331,68 @@ namespace Barotrauma
|
||||
goToObjective = null;
|
||||
getItemObjective = null;
|
||||
}
|
||||
|
||||
private bool IsItemOperatedByAnother(ItemComponent target)
|
||||
{
|
||||
if (target?.Item == null) { return false; }
|
||||
bool isOrdered = IsOrderedToOperateTarget(HumanAIController);
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!HumanAIController.IsActive(c)) { continue; }
|
||||
if (c == character) { continue; }
|
||||
if (c.TeamID != character.TeamID) { continue; }
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
if (c.SelectedItem == target.Item)
|
||||
{
|
||||
// If the other character is player, don't try to operate
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (c.AIController is HumanAIController otherAI)
|
||||
{
|
||||
if (otherAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item))
|
||||
{
|
||||
// Not targeting the same item.
|
||||
continue;
|
||||
}
|
||||
bool isOtherCharacterOrdered = IsOrderedToOperateTarget(otherAI);
|
||||
switch (isOrdered)
|
||||
{
|
||||
case false when isOtherCharacterOrdered:
|
||||
// We are not ordered and the target is ordered -> let the other character operate the target item.
|
||||
return true;
|
||||
case true when !isOtherCharacterOrdered:
|
||||
// We are ordered and the other character is not -> allow to us to operate the target item.
|
||||
continue;
|
||||
default:
|
||||
{
|
||||
// Neither or both are ordered to operate this item.
|
||||
if (!IsOperatingTarget(otherAI))
|
||||
{
|
||||
// The other bot is doing something else -> stick to the target.
|
||||
continue;
|
||||
}
|
||||
if (target is Steering)
|
||||
{
|
||||
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
|
||||
if (character.GetSkillLevel(Tags.HelmSkill) <= c.GetSkillLevel(Tags.HelmSkill))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
bool IsOrderedToOperateTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item;
|
||||
bool IsOperatingTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentObjective is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
protected override bool CheckObjectiveState() => IsCompleted;
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
|
||||
+3
-2
@@ -50,7 +50,7 @@ namespace Barotrauma
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
if (HumanAIController.IsItemRepairedByAnother(Item, out _))
|
||||
if (AIObjectiveRepairItems.IsItemRepairedByAnother(character, Item))
|
||||
{
|
||||
Priority = 0;
|
||||
IsCompleted = true;
|
||||
@@ -91,7 +91,7 @@ namespace Barotrauma
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
IsCompleted = Item.IsFullCondition;
|
||||
if (character.IsOnPlayerTeam && IsCompleted && IsRepairing())
|
||||
@@ -234,6 +234,7 @@ namespace Barotrauma
|
||||
{
|
||||
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
|
||||
{
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
|
||||
TargetName = Item.Name,
|
||||
SpeakCannotReachCondition = () => isPriority
|
||||
};
|
||||
|
||||
+53
-1
@@ -76,7 +76,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (item.Repairables.None(r => r.RequiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; }
|
||||
}
|
||||
return !HumanAIController.IsItemRepairedByAnother(item, out _);
|
||||
return !IsItemRepairedByAnother(character, item);
|
||||
}
|
||||
|
||||
public static bool ViableForRepair(Item item, Character character, HumanAIController humanAIController)
|
||||
@@ -161,5 +161,57 @@ namespace Barotrauma
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsItemRepairedByAnother(Character character, Item target)
|
||||
{
|
||||
if (target == null) { return false; }
|
||||
bool isOrder = IsOrderedToPrioritizeTarget(character.AIController as HumanAIController);
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!HumanAIController.IsActive(c)) { continue; }
|
||||
if (c == character) { continue; }
|
||||
if (c.TeamID != character.TeamID) { continue; }
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
if (target.Repairables.Any(r => r.CurrentFixer == c))
|
||||
{
|
||||
// If the other character is player, don't try to repair
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (c.AIController is HumanAIController otherAI)
|
||||
{
|
||||
var repairItemsObjective = otherAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
|
||||
if (repairItemsObjective == null) { continue; }
|
||||
if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target)
|
||||
{
|
||||
// Not targeting the same item.
|
||||
continue;
|
||||
}
|
||||
bool isTargetOrdered = IsOrderedToPrioritizeTarget(otherAI);
|
||||
switch (isOrder)
|
||||
{
|
||||
case false when isTargetOrdered:
|
||||
// We are not ordered and the target is ordered -> let the other character repair the target.
|
||||
return true;
|
||||
case true when !isTargetOrdered:
|
||||
// We are ordered and the target is not -> allow us to repair the target.
|
||||
continue;
|
||||
default:
|
||||
{
|
||||
// Neither or both are ordered to repair this item.
|
||||
if (otherAI.ObjectiveManager.CurrentObjective is not AIObjectiveRepairItems)
|
||||
{
|
||||
// The other bot is doing something else -> stick to the target.
|
||||
continue;
|
||||
}
|
||||
return target.Repairables.Max(r => r.DegreeOfSuccess(character)) <= target.Repairables.Max(r => r.DegreeOfSuccess(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
bool IsOrderedToPrioritizeTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairOrder && repairOrder.PrioritizedItem == target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-20
@@ -70,7 +70,7 @@ namespace Barotrauma
|
||||
if (otherRescuer != null && otherRescuer != character)
|
||||
{
|
||||
// Someone else is rescuing/holding the target.
|
||||
Abandon = otherRescuer.IsPlayer || character.GetSkillLevel("medical") < otherRescuer.GetSkillLevel("medical");
|
||||
Abandon = otherRescuer.IsPlayer || character.GetSkillLevel(Tags.MedicalSkill) < otherRescuer.GetSkillLevel(Tags.MedicalSkill);
|
||||
return;
|
||||
}
|
||||
if (Target != character)
|
||||
@@ -149,7 +149,7 @@ namespace Barotrauma
|
||||
if (HumanAIController.VisibleHulls.Contains(Target.CurrentHull) && Target.CurrentHull.DisplayName != null)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget",
|
||||
("[targetname]", Target.Name, FormatCapitals.No),
|
||||
("[targetname]", Target.DisplayName, FormatCapitals.No),
|
||||
("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
|
||||
null, 1.0f, $"foundunconscioustarget{Target.Name}".ToIdentifier(), 60.0f);
|
||||
}
|
||||
@@ -161,7 +161,7 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
|
||||
{
|
||||
CloseEnough = CloseEnoughToTreat,
|
||||
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPatient,
|
||||
TargetName = Target.DisplayName
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
@@ -197,13 +197,16 @@ namespace Barotrauma
|
||||
{
|
||||
RemoveSubObjective(ref replaceOxygenObjective);
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
safeHull = character.CurrentHull;
|
||||
});
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager)
|
||||
{
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPlace
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
safeHull = character.CurrentHull;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,7 +224,7 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
|
||||
{
|
||||
CloseEnough = CloseEnoughToTreat,
|
||||
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPatient,
|
||||
TargetName = Target.DisplayName
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
@@ -239,7 +242,7 @@ namespace Barotrauma
|
||||
if (Target.CurrentHull?.DisplayName != null)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget",
|
||||
("[targetname]", Target.Name, FormatCapitals.No),
|
||||
("[targetname]", Target.DisplayName, FormatCapitals.No),
|
||||
("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
|
||||
null, 1.0f, $"foundwoundedtarget{Target.Name}".ToIdentifier(), 60.0f);
|
||||
}
|
||||
@@ -287,6 +290,8 @@ namespace Barotrauma
|
||||
currentTreatmentSuitabilities,
|
||||
limb: Target.CharacterHealth.GetAfflictionLimb(affliction),
|
||||
user: character,
|
||||
checkTreatmentThreshold: true,
|
||||
checkTreatmentSuggestionThreshold: false,
|
||||
predictFutureDuration: 10.0f);
|
||||
|
||||
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
|
||||
@@ -330,7 +335,10 @@ namespace Barotrauma
|
||||
{
|
||||
//get "overall" suitability for no specific limb at this point
|
||||
Target.CharacterHealth.GetSuitableTreatments(
|
||||
currentTreatmentSuitabilities, user: character, predictFutureDuration: 10.0f);
|
||||
currentTreatmentSuitabilities, user: character,
|
||||
checkTreatmentThreshold: true,
|
||||
checkTreatmentSuggestionThreshold: false,
|
||||
predictFutureDuration: 10.0f);
|
||||
//didn't have any suitable treatments available, try to find some medical items
|
||||
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
|
||||
{
|
||||
@@ -387,13 +395,22 @@ namespace Barotrauma
|
||||
if (Target != character && character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments",
|
||||
("[targetname]", Target.Name, FormatCapitals.No),
|
||||
("[targetname]", Target.DisplayName, FormatCapitals.No),
|
||||
("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value,
|
||||
null, 2.0f, $"listrequiredtreatments{Target.Name}".ToIdentifier(), 60.0f);
|
||||
}
|
||||
|
||||
var itemsToFind = currentTreatmentSuitabilities
|
||||
//items that have a positive effect and that the bot doesn't yet have
|
||||
.Where(kvp => kvp.Value > 0.0f && character.Inventory.AllItems.None(it => it.Prefab.Identifier == kvp.Key))
|
||||
.Select(kvp => kvp.Key);
|
||||
|
||||
RemoveSubObjective(ref getItemObjective);
|
||||
TryAddSubObjective(ref getItemObjective,
|
||||
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
|
||||
constructor: () => new AIObjectiveGetItem(character, itemsToFind, objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
GetItemPriority = it => currentTreatmentSuitabilities.GetValueOrDefault(it.Prefab.Identifier)
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref getItemObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
@@ -468,16 +485,16 @@ namespace Barotrauma
|
||||
item.ApplyTreatment(character, Target, Target.CharacterHealth.GetAfflictionLimb(affliction));
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(Target) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, Target);
|
||||
if (isCompleted && Target != character && character.IsOnPlayerTeam)
|
||||
IsCompleted = AIObjectiveRescueAll.GetVitalityFactor(Target) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, Target);
|
||||
if (IsCompleted && Target != character && character.IsOnPlayerTeam)
|
||||
{
|
||||
string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed";
|
||||
string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.Name)?.Value;
|
||||
string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.DisplayName)?.Value;
|
||||
character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
|
||||
}
|
||||
return isCompleted;
|
||||
return IsCompleted;
|
||||
}
|
||||
|
||||
protected override float GetPriority()
|
||||
|
||||
+15
-8
@@ -47,9 +47,9 @@ namespace Barotrauma
|
||||
if (objectiveManager.GetFirstActiveObjective<AIObjectiveRescue>() == null)
|
||||
{
|
||||
charactersWithMinorInjuries.Add(target);
|
||||
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value,
|
||||
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.DisplayName).Value,
|
||||
delay: 1.0f,
|
||||
identifier: $"notreatableafflictions{target.Name}".ToIdentifier(),
|
||||
identifier: $"notreatableafflictions{target.DisplayName}".ToIdentifier(),
|
||||
minDurationBetweenSimilar: 10.0f);
|
||||
}
|
||||
}
|
||||
@@ -96,13 +96,20 @@ namespace Barotrauma
|
||||
{
|
||||
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
|
||||
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
|
||||
if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
|
||||
if (affliction.Strength > affliction.Prefab.TreatmentThreshold)
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
}
|
||||
else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType)
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
}
|
||||
else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType)
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
}
|
||||
else if (affliction.Prefab == AfflictionPrefab.HuskInfection)
|
||||
{
|
||||
vitality -= affliction.Strength;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Math.Clamp(vitality, 0, 100);
|
||||
|
||||
+16
-17
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Barotrauma
|
||||
class AIObjectiveReturn : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "return".ToIdentifier();
|
||||
public Submarine ReturnTarget { get; }
|
||||
public Submarine Target { get; }
|
||||
|
||||
private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective;
|
||||
private bool usingEscapeBehavior, isSteeringThroughGap;
|
||||
@@ -17,10 +17,13 @@ namespace Barotrauma
|
||||
|
||||
public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
|
||||
if (ReturnTarget == null)
|
||||
Target = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
|
||||
if (Target == null)
|
||||
{
|
||||
DebugConsole.AddSafeError("Error with a Return objective: no suitable return target found");
|
||||
if (GameMain.GameSession?.GameMode is not TestGameMode)
|
||||
{
|
||||
DebugConsole.AddWarning($"({character.DisplayName}) No suitable return target found. Cannot return back to the main sub.");
|
||||
}
|
||||
Abandon = true;
|
||||
}
|
||||
|
||||
@@ -54,7 +57,7 @@ namespace Barotrauma
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (ReturnTarget == null)
|
||||
if (Target == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
@@ -62,7 +65,7 @@ namespace Barotrauma
|
||||
bool shouldUseEscapeBehavior = false;
|
||||
if (character.CurrentHull != null || isSteeringThroughGap)
|
||||
{
|
||||
if (character.Submarine == null || !character.Submarine.IsConnectedTo(ReturnTarget))
|
||||
if (character.Submarine == null || !character.Submarine.IsConnectedTo(Target))
|
||||
{
|
||||
// Character is on another sub that is not connected to the target sub, use the escape behavior to get them out
|
||||
shouldUseEscapeBehavior = true;
|
||||
@@ -76,13 +79,13 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
else if (character.Submarine != ReturnTarget)
|
||||
else if (character.Submarine != Target)
|
||||
{
|
||||
// Character is on another sub that is connected to the target sub, create a Go To objective to reach the target sub
|
||||
if (moveInsideObjective == null)
|
||||
{
|
||||
Hull targetHull = null;
|
||||
foreach (var d in ReturnTarget.ConnectedDockingPorts.Values)
|
||||
foreach (var d in Target.ConnectedDockingPorts.Values)
|
||||
{
|
||||
if (!d.Docked) { continue; }
|
||||
if (d.DockingTarget == null) { continue; }
|
||||
@@ -143,7 +146,7 @@ namespace Barotrauma
|
||||
Hull targetHull = null;
|
||||
float targetDistanceSquared = float.MaxValue;
|
||||
bool targetIsAirlock = false;
|
||||
foreach (var hull in ReturnTarget.GetHulls(false))
|
||||
foreach (var hull in Target.GetHulls(false))
|
||||
{
|
||||
bool hullIsAirlock = hull.IsAirlock;
|
||||
if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character)))
|
||||
@@ -178,18 +181,14 @@ namespace Barotrauma
|
||||
usingEscapeBehavior = shouldUseEscapeBehavior;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (IsCompleted)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (ReturnTarget == null)
|
||||
if (Target == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
if (character.Submarine == ReturnTarget)
|
||||
if (character.Submarine == Target)
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
|
||||
@@ -204,12 +204,8 @@ namespace Barotrauma
|
||||
var allTargetItems = new List<Identifier>();
|
||||
for (int i = 0; i < AllOptions.Length; i++)
|
||||
{
|
||||
Identifier[] optionTargetItemsSplit = i < splitTargetItems.Length ? splitTargetItems[i].Split(',', ',').ToIdentifiers() : Array.Empty<Identifier>();
|
||||
for (int j = 0; j < optionTargetItemsSplit.Length; j++)
|
||||
{
|
||||
optionTargetItemsSplit[j] = optionTargetItemsSplit[j].Value.Trim().ToIdentifier();
|
||||
allTargetItems.Add(optionTargetItemsSplit[j]);
|
||||
}
|
||||
Identifier[] optionTargetItemsSplit = i < splitTargetItems.Length ? splitTargetItems[i].ToIdentifiers().ToArray() : Array.Empty<Identifier>();
|
||||
allTargetItems.AddRange(optionTargetItemsSplit);
|
||||
optionTargetItems.Add(AllOptions[i], optionTargetItemsSplit.ToImmutableArray());
|
||||
}
|
||||
TargetItems = allTargetItems.ToImmutableArray();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
@@ -45,7 +45,8 @@ namespace Barotrauma
|
||||
public float HappyThreshold { get; set; }
|
||||
|
||||
public float MaxHappiness { get; set; }
|
||||
|
||||
|
||||
public bool HideStatusIndicators { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// At which point is the pet considered "hungry" (playing unhappy sounds and showing the icon)
|
||||
@@ -59,6 +60,14 @@ namespace Barotrauma
|
||||
public float PlayForce { get; set; }
|
||||
|
||||
public float PlayTimer { get; set; }
|
||||
|
||||
public float PlayCooldown { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should the pet lose ownership (and stop following) when the same character interacts with it twice? Unlike with other pets, if another character interacts with the pet, they will become the owner.
|
||||
/// </summary>
|
||||
public bool ToggleOwner { get; set; }
|
||||
|
||||
private float? UnstunY { get; set; }
|
||||
|
||||
public EnemyAIController AIController { get; private set; } = null;
|
||||
@@ -151,7 +160,8 @@ namespace Barotrauma
|
||||
aggregate += Items[i].Commonness;
|
||||
if (aggregate >= r && Items[i].Prefab != null)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
|
||||
//disabled to reduce the amount of data we collect through GA
|
||||
//GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
|
||||
Entity.Spawner?.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition);
|
||||
break;
|
||||
}
|
||||
@@ -162,7 +172,7 @@ namespace Barotrauma
|
||||
|
||||
private class Food
|
||||
{
|
||||
public string Tag;
|
||||
public Identifier Tag;
|
||||
public Vector2 HungerRange;
|
||||
public float Hunger;
|
||||
public float Happiness;
|
||||
@@ -182,6 +192,7 @@ namespace Barotrauma
|
||||
MaxHappiness = element.GetAttributeFloat(nameof(MaxHappiness), 100.0f);
|
||||
UnhappyThreshold = element.GetAttributeFloat(nameof(UnhappyThreshold), MaxHappiness * 0.25f);
|
||||
HappyThreshold = element.GetAttributeFloat(nameof(HappyThreshold), MaxHappiness * 0.8f);
|
||||
HideStatusIndicators = element.GetAttributeBool(nameof(HideStatusIndicators), false);
|
||||
|
||||
MaxHunger = element.GetAttributeFloat(nameof(MaxHunger), 100.0f);
|
||||
HungryThreshold = element.GetAttributeFloat(nameof(HungryThreshold), MaxHunger * 0.5f);
|
||||
@@ -192,7 +203,9 @@ namespace Barotrauma
|
||||
HappinessDecreaseRate = element.GetAttributeFloat(nameof(HappinessDecreaseRate), 0.1f);
|
||||
HungerIncreaseRate = element.GetAttributeFloat(nameof(HungerIncreaseRate), 0.25f);
|
||||
|
||||
PlayForce = element.GetAttributeFloat("playforce", 15.0f);
|
||||
PlayForce = element.GetAttributeFloat(nameof(PlayForce), 15.0f);
|
||||
PlayCooldown = element.GetAttributeFloat(nameof(PlayCooldown), 5.0f);
|
||||
ToggleOwner = element.GetAttributeBool(nameof(ToggleOwner), false);
|
||||
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
@@ -204,7 +217,7 @@ namespace Barotrauma
|
||||
case "eat":
|
||||
Food food = new Food
|
||||
{
|
||||
Tag = subElement.GetAttributeString("tag", ""),
|
||||
Tag = subElement.GetAttributeIdentifier("tag", Identifier.Empty),
|
||||
Hunger = subElement.GetAttributeFloat("hunger", -1),
|
||||
Happiness = subElement.GetAttributeFloat("happiness", 1),
|
||||
Priority = subElement.GetAttributeFloat("priority", 100),
|
||||
@@ -227,6 +240,7 @@ namespace Barotrauma
|
||||
|
||||
public StatusIndicatorType GetCurrentStatusIndicatorType()
|
||||
{
|
||||
if (HideStatusIndicators) { return StatusIndicatorType.None; }
|
||||
if (Hunger > HungryThreshold) { return StatusIndicatorType.Hungry; }
|
||||
if (Happiness > HappyThreshold) { return StatusIndicatorType.Happy; }
|
||||
if (Happiness < UnhappyThreshold) { return StatusIndicatorType.Sad; }
|
||||
@@ -280,17 +294,30 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CanPlayWith(Character player)
|
||||
{
|
||||
return AIController.Character.IsOnFriendlyTeam(player);
|
||||
}
|
||||
|
||||
public void Play(Character player)
|
||||
{
|
||||
if (PlayTimer > 0.0f) { return; }
|
||||
Owner ??= player;
|
||||
PlayTimer = 5.0f;
|
||||
if (!CanPlayWith(player)) { return; }
|
||||
if (ToggleOwner)
|
||||
{
|
||||
Owner = Owner == player ? null : player;
|
||||
}
|
||||
else
|
||||
{
|
||||
Owner ??= player;
|
||||
}
|
||||
PlayTimer = PlayCooldown;
|
||||
AIController.Character.IsRagdolled = true;
|
||||
Happiness += 10.0f;
|
||||
AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce);
|
||||
UnstunY = AIController.Character.SimPosition.Y;
|
||||
#if CLIENT
|
||||
AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f);
|
||||
AIController.Character.PlaySound(Owner == null ? CharacterSound.SoundType.Unhappy : CharacterSound.SoundType.Happy);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -318,7 +345,7 @@ namespace Barotrauma
|
||||
|
||||
if (UnstunY.HasValue)
|
||||
{
|
||||
if (PlayTimer > 4.0f)
|
||||
if (PlayTimer > PlayCooldown - 1.0f)
|
||||
{
|
||||
float extent = character.AnimController.MainLimb.body.GetMaxExtent();
|
||||
if (character.SimPosition.Y < (UnstunY.Value + extent * 3.0f) &&
|
||||
@@ -354,9 +381,12 @@ namespace Barotrauma
|
||||
{
|
||||
if (food.TargetParams == null)
|
||||
{
|
||||
if (AIController.AIParams.TryGetTarget(food.Tag, out TargetParams target))
|
||||
if (AIController.AIParams.TryGetTargets(food.Tag, out IEnumerable<TargetParams> existingTargetParams))
|
||||
{
|
||||
food.TargetParams = target;
|
||||
foreach (var targetParams in existingTargetParams)
|
||||
{
|
||||
food.TargetParams = targetParams;
|
||||
}
|
||||
}
|
||||
else if (AIController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out TargetParams targetParams))
|
||||
{
|
||||
@@ -444,11 +474,15 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
WayPoint spawnPoint = null;
|
||||
//try to find a spawnpoint in the main sub
|
||||
var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandomUnsynced();
|
||||
if (Submarine.MainSub != null)
|
||||
{
|
||||
spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandomUnsynced();
|
||||
}
|
||||
//if not found, try any player sub (shuttle/drone etc)
|
||||
spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced();
|
||||
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
|
||||
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub?.WorldPosition ?? Vector2.Zero;
|
||||
}
|
||||
|
||||
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName.ToIdentifier());
|
||||
|
||||
+11
-3
@@ -50,9 +50,12 @@ namespace Barotrauma
|
||||
if (OrderedCharacter.AIController is HumanAIController humanAI &&
|
||||
humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option) && o.TargetEntity == TargetItem))
|
||||
{
|
||||
if (orderedCharacter != CommandingCharacter)
|
||||
bool orderGivenByDifferentCharacter = orderedCharacter != CommandingCharacter;
|
||||
if (orderGivenByDifferentCharacter)
|
||||
{
|
||||
CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false), minDurationBetweenSimilar: 5);
|
||||
CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false),
|
||||
minDurationBetweenSimilar: 5,
|
||||
identifier: ("GiveOrder." + SuggestedOrder.Prefab.Identifier).ToIdentifier());
|
||||
}
|
||||
CurrentOrder = SuggestedOrder
|
||||
.WithOption(Option)
|
||||
@@ -60,7 +63,12 @@ namespace Barotrauma
|
||||
.WithOrderGiver(CommandingCharacter)
|
||||
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
|
||||
OrderedCharacter.SetOrder(CurrentOrder, CommandingCharacter != OrderedCharacter);
|
||||
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f, minDurationBetweenSimilar: 5);
|
||||
if (orderGivenByDifferentCharacter)
|
||||
{
|
||||
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f,
|
||||
minDurationBetweenSimilar: 5,
|
||||
identifier: ("ReceiveOrder." + SuggestedOrder.Prefab.Identifier).ToIdentifier());
|
||||
}
|
||||
}
|
||||
TimeSinceLastAttempt = 0f;
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ namespace Barotrauma
|
||||
public override void CalculateImportanceSpecific()
|
||||
{
|
||||
if (shipCommandManager.NavigationState == ShipCommandManager.NavigationStates.Inactive) { return; }
|
||||
if (TargetItemComponent is Powered powered && powered.Voltage <= powered.MinVoltage) { return; }
|
||||
if (TargetItemComponent is Powered { HasPower: false }) { return; }
|
||||
if (TargetItem.Condition <= 0f) { return; }
|
||||
|
||||
Importance = 70f;
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using Barotrauma.Networking;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -89,12 +90,12 @@ namespace Barotrauma
|
||||
{
|
||||
public bool IsAlive { get; private set; }
|
||||
|
||||
private readonly List<Item> allItems;
|
||||
private readonly List<Item> thalamusItems;
|
||||
private readonly List<Structure> thalamusStructures;
|
||||
private readonly List<WayPoint> wayPoints = new List<WayPoint>();
|
||||
private readonly List<Hull> hulls = new List<Hull>();
|
||||
private readonly List<Item> spawnOrgans = new List<Item>();
|
||||
private readonly List<Door> jammedDoors = new List<Door>();
|
||||
private readonly Item brain;
|
||||
|
||||
private bool initialCellsSpawned;
|
||||
@@ -105,7 +106,7 @@ namespace Barotrauma
|
||||
|
||||
private bool IsThalamus(MapEntityPrefab entityPrefab) => IsThalamus(entityPrefab, Config.Entity);
|
||||
|
||||
private static IEnumerable<T> GetThalamusEntities<T>(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T);
|
||||
private static IEnumerable<T> GetThalamusEntities<T>(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).OfType<T>();
|
||||
|
||||
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.MapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag));
|
||||
|
||||
@@ -122,93 +123,52 @@ namespace Barotrauma
|
||||
{
|
||||
GetConfig();
|
||||
if (Config == null) { return; }
|
||||
var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p));
|
||||
var thalamusPrefabs = ItemPrefab.Prefabs.Where(IsThalamus);
|
||||
var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.ServerAndClient);
|
||||
if (brainPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI.");
|
||||
DebugConsole.ThrowError($"WreckAI {wreck.Info.Name}: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI.", contentPackage: Config.ContentPackage);
|
||||
return;
|
||||
}
|
||||
allItems = wreck.GetItems(false);
|
||||
thalamusItems = allItems.FindAll(i => IsThalamus(((MapEntity)i).Prefab));
|
||||
hulls.AddRange(wreck.GetHulls(false));
|
||||
var potentialBrainHulls = new List<(Hull hull, float weight)>();
|
||||
thalamusItems = GetThalamusEntities<Item>(wreck, Config.Entity).ToList();
|
||||
hulls.AddRange(wreck.GetHulls(alsoFromConnectedSubs: false));
|
||||
brain = new Item(brainPrefab, Vector2.Zero, wreck);
|
||||
thalamusItems.Add(brain);
|
||||
Point minSize = brain.Rect.Size.Multiply(brain.Scale);
|
||||
// Bigger hulls are allowed, but not preferred more than what's sufficent.
|
||||
Vector2 sufficentSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f);
|
||||
// Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls.
|
||||
Rectangle shrinkedBounds = ToolBox.GetWorldBounds(wreck.WorldPosition.ToPoint(), new Point(wreck.Borders.Width - 500, wreck.Borders.Height));
|
||||
foreach (Hull hull in hulls)
|
||||
{
|
||||
float distanceFromCenter = Vector2.Distance(wreck.WorldPosition, hull.WorldPosition);
|
||||
float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(shrinkedBounds.Width, shrinkedBounds.Height) / 2, distanceFromCenter));
|
||||
float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficentSize.X, hull.Rect.Width));
|
||||
float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficentSize.Y, hull.Rect.Height));
|
||||
float weight = verticalSizeFactor * horizontalSizeFactor * distanceFactor;
|
||||
if (hull.GetLinkedEntities<Hull>().Any())
|
||||
{
|
||||
// Ignore hulls that have any linked hulls to keep the calculations simple.
|
||||
continue;
|
||||
}
|
||||
else if (hull.ConnectedGaps.Any(g => g.Open > 0 && (!g.IsRoomToRoom || g.Position.Y < hull.Position.Y)))
|
||||
{
|
||||
// Ignore hulls that have open gaps to outside or below the center point, because we'll want the room to be full of water and not be accessible without breaking the wall.
|
||||
continue;
|
||||
}
|
||||
else if (thalamusItems.Any(i => i.CurrentHull == hull))
|
||||
{
|
||||
// Don't create the brain in a room that already has thalamus items inside it.
|
||||
continue;
|
||||
}
|
||||
else if (hull.Rect.Width < minSize.X || hull.Rect.Height < minSize.Y)
|
||||
{
|
||||
// Don't select too small rooms.
|
||||
continue;
|
||||
}
|
||||
if (weight > 0)
|
||||
{
|
||||
potentialBrainHulls.Add((hull, weight));
|
||||
}
|
||||
}
|
||||
var potentialBrainHulls = GetPotentialBrainRooms(wreck, Config, minSize, thalamusItems);
|
||||
Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Select(pbh => pbh.hull).ToList(), potentialBrainHulls.Select(pbh => pbh.weight).ToList(), Rand.RandSync.ServerAndClient);
|
||||
var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(IsThalamus);
|
||||
if (brainHull == null)
|
||||
{
|
||||
DebugConsole.AddWarning("Wreck AI: Cannot find a proper room for the brain. Using a random room.");
|
||||
DebugConsole.ThrowError($"Wreck AI {wreck.Info.Name}: Cannot find a suitable room for the Thalamus brain. Using a random room. " +
|
||||
$"The wreck should be fixed so that there's at least one room where the following conditions are met: No linked hulls, no open gaps in the floor or to outside the sub, and no other Thalamus items present in the hull.",
|
||||
contentPackage: Config.ContentPackage);
|
||||
|
||||
brainHull = hulls.GetRandom(Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
if (brainHull == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Wreck AI: Cannot find any room for the brain! Failed to create the Thalamus.");
|
||||
DebugConsole.ThrowError($"Wreck AI {wreck.Info.Name}: Cannot find any room for the brain! Failed to create the Thalamus.", contentPackage: Config.ContentPackage);
|
||||
return;
|
||||
}
|
||||
Debug.WriteLine($"Wreck AI {wreck.Info.Name}: Selected brain room: {brainHull.DisplayName}");
|
||||
brainHull.WaterVolume = brainHull.Volume;
|
||||
brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false);
|
||||
brain.CurrentHull = brainHull;
|
||||
|
||||
// Jam the doors, mainly to prevent any mechanisms from opening them. Also makes it a little bit more difficult for the player to breach into the brain room, because they now have to break the door.
|
||||
foreach (Door door in brainHull.ConnectedGaps.Select(g => g.ConnectedDoor))
|
||||
{
|
||||
if (door == null) { continue; }
|
||||
door.IsJammed = true;
|
||||
jammedDoors.Add(door);
|
||||
}
|
||||
|
||||
var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.ServerAndClient);
|
||||
if (backgroundPrefab != null)
|
||||
{
|
||||
new Structure(brainHull.Rect, backgroundPrefab, wreck);
|
||||
}
|
||||
var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.ServerAndClient);
|
||||
if (horizontalWallPrefab != null)
|
||||
{
|
||||
int height = (int)horizontalWallPrefab.Size.Y;
|
||||
int halfHeight = height / 2;
|
||||
int quarterHeight = halfHeight / 2;
|
||||
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck);
|
||||
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck);
|
||||
}
|
||||
var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.ServerAndClient);
|
||||
if (verticalWallPrefab != null)
|
||||
{
|
||||
int width = (int)verticalWallPrefab.Size.X;
|
||||
int halfWidth = width / 2;
|
||||
int quarterWidth = halfWidth / 2;
|
||||
new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck);
|
||||
new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck);
|
||||
var background = new Structure(brainHull.Rect, backgroundPrefab, wreck);
|
||||
background.SpriteDepth -= 0.01f;
|
||||
}
|
||||
foreach (Item item in thalamusItems)
|
||||
{
|
||||
@@ -360,6 +320,7 @@ namespace Barotrauma
|
||||
|
||||
public void Kill()
|
||||
{
|
||||
jammedDoors.ForEach(d => d.IsJammed = false);
|
||||
thalamusItems.ForEach(i => i.Condition = 0);
|
||||
foreach (var turret in turrets)
|
||||
{
|
||||
@@ -376,27 +337,24 @@ namespace Barotrauma
|
||||
protectiveCells.ForEach(c => c.OnDeath -= OnCellDeath);
|
||||
if (!IsClient)
|
||||
{
|
||||
if (Config != null)
|
||||
if (Config is { KillAgentsWhenEntityDies: true })
|
||||
{
|
||||
if (Config.KillAgentsWhenEntityDies)
|
||||
protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null));
|
||||
if (!string.IsNullOrWhiteSpace(Config.OffensiveAgent))
|
||||
{
|
||||
protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null));
|
||||
if (!string.IsNullOrWhiteSpace(Config.OffensiveAgent))
|
||||
foreach (var character in Character.CharacterList)
|
||||
{
|
||||
foreach (var character in Character.CharacterList)
|
||||
// Kills ALL offensive agents that are near the thalamus. Not the ideal solution,
|
||||
// but as long as spawning is handled via status effects, I don't know if there is any better way.
|
||||
// In practice there shouldn't be terminal cells from different thalamus organisms at the same time.
|
||||
// And if there was, the distance check should prevent killing the agents of a different organism.
|
||||
if (character.SpeciesName == Config.OffensiveAgent)
|
||||
{
|
||||
// Kills ALL offensive agents that are near the thalamus. Not the ideal solution,
|
||||
// but as long as spawning is handled via status effects, I don't know if there is any better way.
|
||||
// In practice there shouldn't be terminal cells from different thalamus organisms at the same time.
|
||||
// And if there was, the distance check should prevent killing the agents of a different organism.
|
||||
if (character.SpeciesName == Config.OffensiveAgent)
|
||||
// Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this.
|
||||
float maxDistance = Sonar.DefaultSonarRange;
|
||||
if (Vector2.DistanceSquared(character.WorldPosition, Submarine.WorldPosition) < maxDistance * maxDistance)
|
||||
{
|
||||
// Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this.
|
||||
float maxDistance = Sonar.DefaultSonarRange;
|
||||
if (Vector2.DistanceSquared(character.WorldPosition, Submarine.WorldPosition) < maxDistance * maxDistance)
|
||||
{
|
||||
character.Kill(CauseOfDeathType.Unknown, null);
|
||||
}
|
||||
character.Kill(CauseOfDeathType.Unknown, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,5 +473,62 @@ namespace Barotrauma
|
||||
msg.WriteBoolean(IsAlive);
|
||||
}
|
||||
#endif
|
||||
|
||||
public static List<(Hull hull, float weight)> GetPotentialBrainRooms(Submarine wreck, WreckAIConfig wreckAI, Point minSize, IEnumerable<Item> thalamusItems = null)
|
||||
{
|
||||
var potentialBrainHulls = new List<(Hull hull, float weight)>();
|
||||
// Bigger hulls are allowed, but not preferred more than what's sufficient.
|
||||
Vector2 sufficientSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f);
|
||||
Rectangle worldBounds = ToolBox.GetWorldBounds(wreck.WorldPosition.ToPoint(), new Point(wreck.Borders.Width, wreck.Borders.Height));
|
||||
thalamusItems ??= GetThalamusEntities<Item>(wreck, wreckAI.Entity);
|
||||
foreach (Hull hull in wreck.GetHulls(alsoFromConnectedSubs: false))
|
||||
{
|
||||
if (hull.GetLinkedEntities<Hull>().Any())
|
||||
{
|
||||
// Ignore hulls that have any linked hulls to keep the calculations simple.
|
||||
continue;
|
||||
}
|
||||
else if (hull.ConnectedGaps.Any(g => (g.Open > 0 || g.ConnectedDoor?.Item.Condition <= 0) && (!g.IsRoomToRoom || g.Position.Y < hull.Position.Y)))
|
||||
{
|
||||
// Ignore hulls that have open gaps to outside or below the center point, because we'll want the room to be full of water and not be accessible without breaking the wall.
|
||||
// Gaps in the broken doors are not yet open at this stage. Also Door.IsBroken is not yet up-to-date, so we'll have to check the item condition.
|
||||
continue;
|
||||
}
|
||||
else if (thalamusItems.Any(i => i.CurrentHull == hull && !i.HasTag(Tags.WireItem)))
|
||||
{
|
||||
// Don't create the brain in a room that already has thalamus items inside it.
|
||||
continue;
|
||||
}
|
||||
else if (hull.Rect.Width < minSize.X || hull.Rect.Height < minSize.Y)
|
||||
{
|
||||
// Don't select too small rooms.
|
||||
continue;
|
||||
}
|
||||
float weight = 0;
|
||||
if (hull.IsAirlock)
|
||||
{
|
||||
// Prefer something else than airlocks
|
||||
weight = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
float distanceFromCenter = Vector2.Distance(wreck.WorldPosition, hull.WorldPosition);
|
||||
float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(worldBounds.Width, worldBounds.Height) / 2f, distanceFromCenter));
|
||||
float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficientSize.X, hull.Rect.Width));
|
||||
float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficientSize.Y, hull.Rect.Height));
|
||||
weight = verticalSizeFactor * horizontalSizeFactor * distanceFactor;
|
||||
}
|
||||
if (weight > 0 || potentialBrainHulls.None())
|
||||
{
|
||||
potentialBrainHulls.Add((hull, weight));
|
||||
}
|
||||
}
|
||||
Debug.WriteLine($"Wreck AI {wreck.Info.Name}: Potential brain rooms: {potentialBrainHulls.Count}");
|
||||
foreach ((Hull hull, float weight) in potentialBrainHulls)
|
||||
{
|
||||
Debug.WriteLine($"Wreck AI: Potential brain room: {hull.DisplayName}, {weight.FormatSingleDecimal()}");
|
||||
}
|
||||
return potentialBrainHulls;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,21 +38,7 @@ namespace Barotrauma
|
||||
public readonly AnimationType AnimationType;
|
||||
public readonly AnimationParams TemporaryAnimation;
|
||||
public readonly float Priority;
|
||||
public bool IsActive
|
||||
{
|
||||
get { return _isActive; }
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
expirationTimer = expirationTime;
|
||||
}
|
||||
_isActive = value;
|
||||
}
|
||||
}
|
||||
private bool _isActive;
|
||||
private float expirationTimer;
|
||||
private const float expirationTime = 0.1f;
|
||||
public bool IsActive;
|
||||
|
||||
public AnimSwap(AnimationParams temporaryAnimation, float priority)
|
||||
{
|
||||
@@ -61,15 +47,6 @@ namespace Barotrauma
|
||||
Priority = priority;
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
expirationTimer -= deltaTime;
|
||||
if (expirationTimer <= 0)
|
||||
{
|
||||
IsActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly Dictionary<AnimationType, AnimSwap> tempAnimations = new Dictionary<AnimationType, AnimSwap>();
|
||||
@@ -151,7 +128,8 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
return Math.Abs(TargetMovement.X) > (WalkParams.MovementSpeed + RunParams.MovementSpeed) / 2.0f;
|
||||
float movementSpeed = IsClimbing ? TargetMovement.Y : TargetMovement.X;
|
||||
return Math.Abs(movementSpeed) > (WalkParams.MovementSpeed + RunParams.MovementSpeed) / 2.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,7 +204,7 @@ namespace Barotrauma
|
||||
|
||||
public void UpdateAnimations(float deltaTime)
|
||||
{
|
||||
UpdateTemporaryAnimations(deltaTime);
|
||||
UpdateTemporaryAnimations();
|
||||
UpdateAnim(deltaTime);
|
||||
}
|
||||
|
||||
@@ -338,6 +316,31 @@ namespace Barotrauma
|
||||
{
|
||||
FlipLockTime = (float)Timing.TotalTime + time;
|
||||
}
|
||||
|
||||
protected void UpdateConstantTorque(float deltaTime)
|
||||
{
|
||||
foreach (var limb in Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (Math.Abs(limb.Params.ConstantTorque) > 0)
|
||||
{
|
||||
// TODO: not sure if this works on ground
|
||||
float movementFactor = Math.Max(character.AnimController.Collider.LinearVelocity.Length() * 0.5f, 1);
|
||||
limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque * movementFactor, wrapAngle: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void UpdateBlink(float deltaTime)
|
||||
{
|
||||
foreach (var limb in Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (limb.Params.BlinkFrequency <= 0) { continue; }
|
||||
if (!limb.InWater && limb.Params.OnlyBlinkInWater) { continue; }
|
||||
limb.UpdateBlink(deltaTime, MainLimb.Rotation);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos)
|
||||
{
|
||||
@@ -408,9 +411,9 @@ namespace Barotrauma
|
||||
character.WorldPosition.Y - handWorldPos.Y > ConvertUnits.ToDisplayUnits(CurrentGroundedParams.TorsoPosition) / 4 &&
|
||||
this is HumanoidAnimController humanoidAnimController)
|
||||
{
|
||||
humanoidAnimController.Crouching = true;
|
||||
humanoidAnimController.Crouch();
|
||||
// TODO: is this redundant/required?
|
||||
humanoidAnimController.ForceSelectAnimationType = AnimationType.Crouch;
|
||||
character.SetInput(InputType.Crouch, hit: false, held: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,7 +608,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!character.Inventory.IsInLimbSlot(item, i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand)) { continue; }
|
||||
#if DEBUG
|
||||
if (handlePos[i].LengthSquared() > ArmLength)
|
||||
if (ArmLength > 0 && handlePos[i].LengthSquared() > ArmLength)
|
||||
{
|
||||
DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)",
|
||||
item.Prefab.ContentPackage);
|
||||
@@ -696,6 +699,290 @@ namespace Barotrauma
|
||||
hand.body.SmoothRotate(handAngle, 10.0f * handTorque * hand.Mass, wrapAngle: false);
|
||||
}
|
||||
}
|
||||
|
||||
private float prevFootPos;
|
||||
protected void UpdateClimbing()
|
||||
{
|
||||
var ladder = character.SelectedSecondaryItem?.GetComponent<Ladder>();
|
||||
if (character.IsIncapacitated)
|
||||
{
|
||||
Anim = Animation.None;
|
||||
return;
|
||||
}
|
||||
else if (ladder == null)
|
||||
{
|
||||
StopClimbing();
|
||||
return;
|
||||
}
|
||||
|
||||
onGround = false;
|
||||
IgnorePlatforms = true;
|
||||
|
||||
bool climbFast = !character.Params.ForceSlowClimbing && character.AnimController.IsMovingFast;
|
||||
var animParams = climbFast ? RunParams : WalkParams;
|
||||
// Don't slide if we can climb faster than slide.
|
||||
bool slide = animParams.SlideSpeed > animParams.ClimbSpeed && targetMovement.Y < -0.1f && climbFast;
|
||||
float maxClimbingSpeed = climbFast && !character.Params.ForceSlowClimbing ? RunParams.ClimbSpeed : WalkParams.ClimbSpeed;
|
||||
Vector2 tempTargetMovement = TargetMovement;
|
||||
tempTargetMovement.Y = Math.Clamp(tempTargetMovement.Y, slide ? -animParams.SlideSpeed : -maxClimbingSpeed, maxClimbingSpeed);
|
||||
|
||||
movement = MathUtils.SmoothStep(movement, tempTargetMovement, 0.3f);
|
||||
|
||||
Limb leftFoot = GetClimbingLimb(LimbType.LeftFoot);
|
||||
Limb rightFoot = GetClimbingLimb(LimbType.RightFoot);
|
||||
Limb head = GetClimbingLimb(LimbType.Head);
|
||||
Limb torso = GetClimbingLimb(LimbType.Torso);
|
||||
|
||||
Limb leftHand = GetClimbingLimb(LimbType.LeftHand);
|
||||
Limb rightHand = GetClimbingLimb(LimbType.RightHand);
|
||||
|
||||
Vector2 ladderSimPos = ConvertUnits.ToSimUnits(
|
||||
ladder.Item.Rect.X + ladder.Item.Rect.Width / 2.0f,
|
||||
ladder.Item.Rect.Y);
|
||||
|
||||
Vector2 ladderSimSize = ConvertUnits.ToSimUnits(ladder.Item.Rect.Size.ToVector2());
|
||||
|
||||
var lowestNearbyLadder = GetLowestNearbyLadder(ladder);
|
||||
if (lowestNearbyLadder != null && lowestNearbyLadder != ladder)
|
||||
{
|
||||
ladderSimSize.Y = ConvertUnits.ToSimUnits(ladder.Item.WorldRect.Y - (lowestNearbyLadder.Item.WorldRect.Y - lowestNearbyLadder.Item.Rect.Size.Y));
|
||||
}
|
||||
|
||||
float stepHeight = ConvertUnits.ToSimUnits(animParams.ClimbStepHeight);
|
||||
|
||||
if (currentHull == null && ladder.Item.Submarine != null)
|
||||
{
|
||||
ladderSimPos += ladder.Item.Submarine.SimPosition;
|
||||
}
|
||||
else if (currentHull?.Submarine != null && currentHull.Submarine != ladder.Item.Submarine && ladder.Item.Submarine != null)
|
||||
{
|
||||
ladderSimPos += ladder.Item.Submarine.SimPosition - currentHull.Submarine.SimPosition;
|
||||
}
|
||||
else if (currentHull?.Submarine != null && ladder.Item.Submarine == null)
|
||||
{
|
||||
ladderSimPos -= currentHull.Submarine.SimPosition;
|
||||
}
|
||||
|
||||
float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.Radius - Collider.Height / 2.0f;
|
||||
float torsoPos = TorsoPosition ?? 0;
|
||||
float bodyMoveForce = animParams.ClimbBodyMoveForce;
|
||||
if (torso != null)
|
||||
{
|
||||
MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + torsoPos), bodyMoveForce);
|
||||
}
|
||||
if (head != null)
|
||||
{
|
||||
float headPos = HeadPosition ?? 0;
|
||||
MoveLimb(head, new Vector2(ladderSimPos.X - 0.2f * Dir, bottomPos + headPos), bodyMoveForce);
|
||||
}
|
||||
|
||||
Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.1f * Dir, Collider.SimPosition.Y), bodyMoveForce);
|
||||
|
||||
Vector2 handPos = new Vector2(
|
||||
ladderSimPos.X,
|
||||
bottomPos + torsoPos + movement.Y * 0.1f - ladderSimPos.Y);
|
||||
if (climbFast) { handPos.Y -= stepHeight; }
|
||||
|
||||
float handMoveForce = animParams.ClimbHandMoveForce;
|
||||
|
||||
//prevent the hands from going above the top of the ladders
|
||||
handPos.Y = Math.Min(-0.5f, handPos.Y);
|
||||
if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
|
||||
{
|
||||
if (rightHand != null)
|
||||
{
|
||||
MoveLimb(rightHand,
|
||||
new Vector2(slide ? handPos.X + ladderSimSize.X * 0.75f : handPos.X,
|
||||
(slide ? handPos.Y + stepHeight : MathUtils.Round(handPos.Y, stepHeight * 2.0f)) + ladderSimPos.Y),
|
||||
handMoveForce);
|
||||
rightHand.body.ApplyTorque(Dir * 2.0f);
|
||||
}
|
||||
}
|
||||
if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
|
||||
{
|
||||
if (leftHand != null)
|
||||
{
|
||||
MoveLimb(leftHand,
|
||||
new Vector2(handPos.X - ladderSimSize.X * (slide ? 1.0f : 0.5f),
|
||||
(slide ? handPos.Y + stepHeight : MathUtils.Round(handPos.Y - stepHeight, stepHeight * 2.0f) + stepHeight) + ladderSimPos.Y),
|
||||
handMoveForce); ;
|
||||
leftHand.body.ApplyTorque(Dir * 2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
float stepHeightAdjustment = stepHeight * 2.7f;
|
||||
Vector2 footPos = new Vector2(
|
||||
handPos.X - Dir * 0.05f,
|
||||
bottomPos + ColliderHeightFromFloor - stepHeightAdjustment - ladderSimPos.Y);
|
||||
if (climbFast) { footPos.Y += stepHeight; }
|
||||
|
||||
//apply torque to the legs to make the knees bend
|
||||
Limb leftLeg = GetClimbingLimb(LimbType.LeftLeg);
|
||||
Limb rightLeg = GetClimbingLimb(LimbType.RightLeg);
|
||||
|
||||
//only move the feet if they're above the bottom of the ladders
|
||||
//(if not, they'll just dangle in air, and the character holds itself up with its arms)
|
||||
if (footPos.Y > -ladderSimSize.Y - 0.2f && leftFoot != null && rightFoot != null && leftLeg != null && rightLeg != null)
|
||||
{
|
||||
Limb refLimb = GetClimbingLimb(LimbType.Waist) ?? GetClimbingLimb(LimbType.Torso) ?? MainLimb;
|
||||
bool leftLegBackwards = Math.Abs(leftLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi;
|
||||
bool rightLegBackwards = Math.Abs(rightLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi;
|
||||
float footMoveForce = animParams.ClimbFootMoveForce;
|
||||
if (slide)
|
||||
{
|
||||
if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X - ladderSimSize.X * 0.5f, footPos.Y + ladderSimPos.Y), footMoveForce, pullFromCenter: true); }
|
||||
if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), footMoveForce, pullFromCenter: true); }
|
||||
}
|
||||
else
|
||||
{
|
||||
float leftFootPos = MathUtils.Round(footPos.Y + stepHeight, stepHeight * 2.0f) - stepHeight;
|
||||
float prevLeftFootPos = MathUtils.Round(prevFootPos + stepHeight, stepHeight * 2.0f) - stepHeight;
|
||||
if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X, leftFootPos + ladderSimPos.Y), footMoveForce, pullFromCenter: true); }
|
||||
|
||||
float rightFootPos = MathUtils.Round(footPos.Y, stepHeight * 2.0f);
|
||||
float prevRightFootPos = MathUtils.Round(prevFootPos, stepHeight * 2.0f);
|
||||
if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, rightFootPos + ladderSimPos.Y), footMoveForce, pullFromCenter: true); }
|
||||
#if CLIENT
|
||||
if (Math.Abs(leftFootPos - prevLeftFootPos) > stepHeight && leftFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval)
|
||||
{
|
||||
SoundPlayer.PlaySound("footstep_armor_heavy", leftFoot.WorldPosition, hullGuess: currentHull);
|
||||
leftFoot.LastImpactSoundTime = (float)Timing.TotalTime;
|
||||
}
|
||||
if (Math.Abs(rightFootPos - prevRightFootPos) > stepHeight && rightFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval)
|
||||
{
|
||||
SoundPlayer.PlaySound("footstep_armor_heavy", rightFoot.WorldPosition, hullGuess: currentHull);
|
||||
rightFoot.LastImpactSoundTime = (float)Timing.TotalTime;
|
||||
}
|
||||
#endif
|
||||
prevFootPos = footPos.Y;
|
||||
}
|
||||
|
||||
if (!leftLegBackwards) { leftLeg.body.ApplyTorque(Dir * -8.0f); } // TODO: expose?
|
||||
if (!rightLegBackwards) { rightLeg.body.ApplyTorque(Dir * -8.0f); }
|
||||
}
|
||||
|
||||
float movementFactor = (handPos.Y / stepHeight) * (float)Math.PI;
|
||||
movementFactor = 0.8f + (float)Math.Abs(Math.Sin(movementFactor));
|
||||
|
||||
Vector2 subSpeed = currentHull != null || ladder.Item.Submarine == null
|
||||
? Vector2.Zero : ladder.Item.Submarine.Velocity;
|
||||
|
||||
//reached the top of the ladders -> can't go further up
|
||||
Vector2 climbForce = new Vector2(0.0f, movement.Y) * movementFactor;
|
||||
|
||||
if (!InWater) { climbForce.Y += 0.3f * movementFactor; }
|
||||
|
||||
if (character.SimPosition.Y > ladderSimPos.Y) { climbForce.Y = Math.Min(0.0f, climbForce.Y); }
|
||||
//reached the bottom -> can't go further down
|
||||
float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.Height;
|
||||
if (floorFixture != null &&
|
||||
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionStairs) &&
|
||||
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionPlatform) &&
|
||||
character.SimPosition.Y < standOnFloorY + minHeightFromFloor)
|
||||
{
|
||||
climbForce.Y = MathHelper.Clamp((standOnFloorY + minHeightFromFloor - character.SimPosition.Y) * 5.0f, climbForce.Y, 1.0f);
|
||||
}
|
||||
|
||||
//apply forces to the collider to move the Character up/down
|
||||
Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass);
|
||||
// Don't rotate the head on non-humanoids, because it can cause issues with some ragdolls.
|
||||
// E.g. the head might not actually be head, or it's not where we expect it to be.
|
||||
if (head != null && character.IsHumanoid)
|
||||
{
|
||||
if (Aiming)
|
||||
{
|
||||
RotateHead(head);
|
||||
}
|
||||
else if (Anim == Animation.UsingItemWhileClimbing && character.SelectedItem is { } selectedItem)
|
||||
{
|
||||
Vector2 diff = (selectedItem.WorldPosition - head.WorldPosition) * Dir;
|
||||
float targetRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver4 * Dir);
|
||||
head.body.SmoothRotate(targetRotation, force: animParams.HeadTorque);
|
||||
}
|
||||
else
|
||||
{
|
||||
float movementMultiplier = targetMovement.Y < 0 ? 0 : 1;
|
||||
head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, force: animParams.HeadTorque);
|
||||
}
|
||||
}
|
||||
|
||||
if (ladder.Item.Prefab.Triggers.None())
|
||||
{
|
||||
character.ReleaseSecondaryItem();
|
||||
return;
|
||||
}
|
||||
|
||||
Rectangle trigger = ladder.Item.Prefab.Triggers.FirstOrDefault();
|
||||
trigger = ladder.Item.TransformTrigger(trigger);
|
||||
|
||||
bool isRemote = false;
|
||||
bool isClimbing = true;
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
isRemote = character.IsRemotelyControlled;
|
||||
}
|
||||
//if the character is remotely controlled,
|
||||
//let the server decide when to deselect the ladder and stop climbing
|
||||
if (!isRemote)
|
||||
{
|
||||
if ((character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right)) &&
|
||||
(!character.IsKeyDown(InputType.Up) && !character.IsKeyDown(InputType.Down)))
|
||||
{
|
||||
isClimbing = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isClimbing)
|
||||
{
|
||||
character.StopClimbing();
|
||||
IgnorePlatforms = false;
|
||||
}
|
||||
|
||||
Ladder GetLowestNearbyLadder(Ladder currentLadder, float threshold = 16.0f)
|
||||
{
|
||||
foreach (Ladder ladder in Ladder.List)
|
||||
{
|
||||
if (ladder == currentLadder || !ladder.Item.IsInteractable(character)) { continue; }
|
||||
if (Math.Abs(ladder.Item.WorldPosition.X - currentLadder.Item.WorldPosition.X) > threshold) { continue; }
|
||||
if (ladder.Item.WorldPosition.Y > currentLadder.Item.WorldPosition.Y) { continue; }
|
||||
if ((currentLadder.Item.WorldRect.Y - currentLadder.Item.Rect.Height) - ladder.Item.WorldRect.Y > threshold) { continue; }
|
||||
return ladder;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Limb GetClimbingLimb(LimbType limbType)
|
||||
{
|
||||
if (HasMultipleLimbsOfSameType)
|
||||
{
|
||||
// First try to find a match using the secondary type, if that fails, use the primary type and exclude all the limbs with the secondary type.
|
||||
// Secondary limbs are first excluded and then targeted, because some feet are meant to be used as hands in this context, which means we don't want to get them when seeking the feet.
|
||||
return GetLimb(limbType, useSecondaryType: true) ?? GetLimb(limbType, excludeLimbsWithSecondaryType: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
return GetLimb(limbType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void RotateHead(Limb head)
|
||||
{
|
||||
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
|
||||
Vector2 dir = (mousePos - head.SimPosition) * Dir;
|
||||
float rot = MathUtils.VectorToAngle(dir);
|
||||
var neckJoint = GetJointBetweenLimbs(LimbType.Head, LimbType.Torso);
|
||||
if (neckJoint != null)
|
||||
{
|
||||
float offset = MathUtils.WrapAnglePi(GetLimb(LimbType.Torso).body.Rotation);
|
||||
float lowerLimit = neckJoint.LowerLimit + offset;
|
||||
float upperLimit = neckJoint.UpperLimit + offset;
|
||||
float min = Math.Min(lowerLimit, upperLimit);
|
||||
float max = Math.Max(lowerLimit, upperLimit);
|
||||
rot = Math.Clamp(rot, min, max);
|
||||
}
|
||||
head.body.SmoothRotate(rot, CurrentAnimationParams.HeadTorque);
|
||||
}
|
||||
|
||||
public void ApplyPose(Vector2 leftHandPos, Vector2 rightHandPos, Vector2 leftFootPos, Vector2 rightFootPos, float footMoveForce = 10)
|
||||
{
|
||||
@@ -754,8 +1041,16 @@ namespace Barotrauma
|
||||
Limb rightHand = GetLimb(LimbType.RightHand);
|
||||
if (rightHand == null) { return; }
|
||||
|
||||
rightShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm) ?? GetJointBetweenLimbs(LimbType.Head, LimbType.RightArm) ?? GetJoint(LimbType.RightArm, new LimbType[] { LimbType.RightHand, LimbType.RightForearm });
|
||||
leftShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftArm) ?? GetJointBetweenLimbs(LimbType.Head, LimbType.LeftArm) ?? GetJoint(LimbType.LeftArm, new LimbType[] { LimbType.LeftHand, LimbType.LeftForearm });
|
||||
rightShoulder =
|
||||
GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm) ??
|
||||
GetJointBetweenLimbs(LimbType.Head, LimbType.RightArm) ??
|
||||
GetJoint(LimbType.RightArm, new LimbType[] { LimbType.RightHand, LimbType.RightForearm }) ??
|
||||
GetJointBetweenLimbs(LimbType.Torso, LimbType.RightHand);
|
||||
leftShoulder =
|
||||
GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftArm) ??
|
||||
GetJointBetweenLimbs(LimbType.Head, LimbType.LeftArm) ??
|
||||
GetJoint(LimbType.LeftArm, new LimbType[] { LimbType.LeftHand, LimbType.LeftForearm }) ??
|
||||
GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftHand);
|
||||
|
||||
Vector2 localAnchorShoulder = Vector2.Zero;
|
||||
Vector2 localAnchorElbow = Vector2.Zero;
|
||||
@@ -818,6 +1113,13 @@ namespace Barotrauma
|
||||
CalculateArmLengths();
|
||||
}
|
||||
}
|
||||
|
||||
public void RecreateAndRespawn(RagdollParams ragdollParams = null)
|
||||
{
|
||||
Vector2 pos = character.WorldPosition;
|
||||
Recreate(ragdollParams);
|
||||
character.TeleportTo(pos);
|
||||
}
|
||||
|
||||
private void StartAnimation(Animation animation)
|
||||
{
|
||||
@@ -906,7 +1208,7 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateTemporaryAnimations(float deltaTime)
|
||||
private void UpdateTemporaryAnimations()
|
||||
{
|
||||
if (tempAnimations.None()) { return; }
|
||||
foreach ((AnimationType animationType, AnimSwap animSwap) in tempAnimations)
|
||||
@@ -932,7 +1234,8 @@ namespace Barotrauma
|
||||
expiredAnimations.Clear();
|
||||
foreach (AnimSwap animSwap in tempAnimations.Values)
|
||||
{
|
||||
animSwap.Update(deltaTime);
|
||||
// Will be removed on the next frame, unless something keeps it alive.
|
||||
animSwap.IsActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+59
-101
@@ -139,26 +139,15 @@ namespace Barotrauma
|
||||
ResetState();
|
||||
return;
|
||||
}
|
||||
UpdateConstantTorque(deltaTime);
|
||||
UpdateBlink(deltaTime);
|
||||
var mainLimb = MainLimb;
|
||||
|
||||
levitatingCollider = !IsHangingWithRope;
|
||||
levitatingCollider = !IsHangingWithRope && !IsClimbing;
|
||||
|
||||
if (!character.CanMove)
|
||||
{
|
||||
levitatingCollider = false;
|
||||
Collider.FarseerBody.FixedRotation = false;
|
||||
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
Collider.Enabled = false;
|
||||
Collider.LinearVelocity = mainLimb.LinearVelocity;
|
||||
Collider.SetTransformIgnoreContacts(mainLimb.SimPosition, mainLimb.Rotation);
|
||||
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
|
||||
//(except when dragging, then we need the pull joints)
|
||||
if (!Draggable || character.SelectedBy == null)
|
||||
{
|
||||
ResetPullJoints();
|
||||
}
|
||||
}
|
||||
UpdateRagdollControlsMovement();
|
||||
if (character.IsDead && deathAnimTimer < deathAnimDuration)
|
||||
{
|
||||
deathAnimTimer += deltaTime;
|
||||
@@ -184,11 +173,11 @@ namespace Barotrauma
|
||||
|
||||
if (InWater)
|
||||
{
|
||||
Collider.SetTransform(new Vector2(Collider.SimPosition.X, MainLimb.SimPosition.Y), 0.0f);
|
||||
Collider.SetTransformIgnoreContacts(new Vector2(Collider.SimPosition.X, MainLimb.SimPosition.Y), 0.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
Collider.SetTransform(new Vector2(
|
||||
Collider.SetTransformIgnoreContacts(new Vector2(
|
||||
Collider.SimPosition.X,
|
||||
Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)),
|
||||
0.0f);
|
||||
@@ -208,6 +197,11 @@ namespace Barotrauma
|
||||
{
|
||||
TargetMovement = TargetMovement.ClampLength(2);
|
||||
}
|
||||
|
||||
if (IsClimbing)
|
||||
{
|
||||
UpdateClimbing();
|
||||
}
|
||||
|
||||
if (inWater && !forceStanding)
|
||||
{
|
||||
@@ -336,7 +330,6 @@ namespace Barotrauma
|
||||
if (target == null) { return; }
|
||||
Limb mouthLimb = GetLimb(LimbType.Head);
|
||||
if (mouthLimb == null) { return; }
|
||||
|
||||
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
//stop dragging if there's something between the pull limb and the target
|
||||
@@ -357,23 +350,23 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
float dmg = character.Params.EatingSpeed;
|
||||
float eatSpeed = dmg / ((float)Math.Sqrt(Math.Max(target.Mass, 1)) * 10);
|
||||
eatTimer += deltaTime * eatSpeed;
|
||||
|
||||
Vector2 mouthPos = SimplePhysicsEnabled ? character.SimPosition : GetMouthPosition().Value;
|
||||
Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition;
|
||||
|
||||
Vector2 limbDiff = attackSimPosition - mouthPos;
|
||||
float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 1);
|
||||
bool tooFar = character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent;
|
||||
if (tooFar)
|
||||
{
|
||||
character.SelectedCharacter = null;
|
||||
}
|
||||
else
|
||||
if (Character.CanEat)
|
||||
{
|
||||
Vector2 mouthPos = SimplePhysicsEnabled ? character.SimPosition : GetMouthPosition() ?? Vector2.Zero;
|
||||
Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition;
|
||||
Vector2 limbDiff = attackSimPosition - mouthPos;
|
||||
float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 1);
|
||||
bool tooFar = character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent;
|
||||
if (tooFar)
|
||||
{
|
||||
character.DeselectCharacter();
|
||||
return;
|
||||
}
|
||||
|
||||
float dmg = character.Params.EatingSpeed;
|
||||
float eatSpeed = dmg / ((float)Math.Sqrt(Math.Max(target.Mass, 1)) * 10);
|
||||
eatTimer += deltaTime * eatSpeed;
|
||||
|
||||
//pull the target character to the position of the mouth
|
||||
//(+ make the force fluctuate to waggle the character a bit)
|
||||
float dragForce = MathHelper.Clamp(eatSpeed * 10, 0, 40);
|
||||
@@ -405,20 +398,19 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
float force = (float)Math.Sin(eatTimer * 100) * mouthLimb.Mass;
|
||||
mouthLimb.body.ApplyLinearImpulse(Vector2.UnitY * force * 2, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
mouthLimb.body.ApplyTorque(-force * 50);
|
||||
mouthLimb.body.ApplyLinearImpulse(Vector2.UnitY * force * mouthLimb.Params.EatImpulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
mouthLimb.body.ApplyTorque(-force * mouthLimb.Params.EatTorque);
|
||||
}
|
||||
|
||||
if (Character.CanEat && target.IsDead)
|
||||
|
||||
var jaw = GetLimb(LimbType.Jaw);
|
||||
if (jaw != null)
|
||||
{
|
||||
jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25);
|
||||
}
|
||||
character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
|
||||
|
||||
if (target.IsDead)
|
||||
{
|
||||
var jaw = GetLimb(LimbType.Jaw);
|
||||
if (jaw != null)
|
||||
{
|
||||
jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25);
|
||||
}
|
||||
|
||||
character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
|
||||
|
||||
float particleFrequency = MathHelper.Clamp(eatSpeed / 2, 0.02f, 0.5f);
|
||||
if (Rand.Value() < particleFrequency / 6)
|
||||
{
|
||||
@@ -430,7 +422,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f)
|
||||
{
|
||||
static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered;
|
||||
static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA is { IsSevered: false } && j.LimbB is { IsSevered: false };
|
||||
//keep severing joints until there is only one limb left
|
||||
var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered);
|
||||
if (nonSeveredJoints.None())
|
||||
@@ -440,16 +432,13 @@ namespace Barotrauma
|
||||
{
|
||||
target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null));
|
||||
}
|
||||
|
||||
//only one limb left, the character is now full eaten
|
||||
Entity.Spawner?.AddEntityToRemoveQueue(target);
|
||||
|
||||
if (Character.AIController is EnemyAIController enemyAi)
|
||||
{
|
||||
enemyAi.PetBehavior?.OnEat(target);
|
||||
}
|
||||
|
||||
character.SelectedCharacter = null;
|
||||
character.DeselectCharacter();
|
||||
}
|
||||
else //sever a random joint
|
||||
{
|
||||
@@ -460,7 +449,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool reverse;
|
||||
public bool Reverse;
|
||||
|
||||
void UpdateSineAnim(float deltaTime)
|
||||
{
|
||||
@@ -510,7 +499,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector2 transformedMovement = reverse ? -movement : movement;
|
||||
Vector2 transformedMovement = Reverse ? -movement : movement;
|
||||
float movementAngle = MathUtils.VectorToAngle(transformedMovement) - MathHelper.PiOver2;
|
||||
float mainLimbAngle = 0;
|
||||
if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
|
||||
@@ -555,7 +544,6 @@ namespace Barotrauma
|
||||
foreach (var limb in Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (limb.type != LimbType.Tail) { continue; }
|
||||
if (!limb.Params.ApplyTailAngle) { continue; }
|
||||
RotateTail(limb);
|
||||
isAngleApplied = true;
|
||||
@@ -592,7 +580,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2;
|
||||
if (reverse)
|
||||
if (Reverse)
|
||||
{
|
||||
movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi);
|
||||
}
|
||||
@@ -651,29 +639,21 @@ namespace Barotrauma
|
||||
foreach (var limb in Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
switch (limb.type)
|
||||
if (limb.type is LimbType.LeftFoot or LimbType.RightFoot)
|
||||
{
|
||||
case LimbType.LeftFoot:
|
||||
case LimbType.RightFoot:
|
||||
if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
|
||||
{
|
||||
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque);
|
||||
}
|
||||
break;
|
||||
case LimbType.Tail:
|
||||
if (waveLength > 0 && waveAmplitude > 0)
|
||||
{
|
||||
float waveRotation = (float)Math.Sin(WalkPos * limb.Params.SineFrequencyMultiplier);
|
||||
limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude * limb.Params.SineAmplitudeMultiplier);
|
||||
}
|
||||
break;
|
||||
if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
|
||||
{
|
||||
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque);
|
||||
}
|
||||
}
|
||||
if (limb.type == LimbType.Tail || limb.Params.ApplySineMovement)
|
||||
{
|
||||
if (waveLength > 0 && waveAmplitude > 0)
|
||||
{
|
||||
float waveRotation = (float)Math.Sin(WalkPos * limb.Params.SineFrequencyMultiplier);
|
||||
limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude * limb.Params.SineAmplitudeMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < Limbs.Length; i++)
|
||||
{
|
||||
var limb = Limbs[i];
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (limb.SteerForce <= 0.0f) { continue; }
|
||||
if (!Collider.PhysEnabled) { continue; }
|
||||
Vector2 pullPos = limb.PullJointWorldAnchorA;
|
||||
@@ -698,20 +678,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var limb in Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (Math.Abs(limb.Params.ConstantTorque) > 0)
|
||||
{
|
||||
float movementFactor = Math.Max(character.AnimController.Collider.LinearVelocity.Length() * 0.5f, 1);
|
||||
limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque * movementFactor, wrapAngle: true);
|
||||
}
|
||||
if (limb.Params.BlinkFrequency > 0)
|
||||
{
|
||||
limb.UpdateBlink(deltaTime, MainLimb.Rotation);
|
||||
}
|
||||
}
|
||||
|
||||
floorY = Limbs[0].SimPosition.Y;
|
||||
}
|
||||
|
||||
@@ -744,9 +710,9 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
float offset = MathHelper.Pi * CurrentGroundedParams.StepLiftOffset;
|
||||
if (CurrentGroundedParams.MultiplyByDir)
|
||||
if (character.AnimController.Dir < 0)
|
||||
{
|
||||
offset *= Dir;
|
||||
offset += MathHelper.Pi * CurrentGroundedParams.StepLiftFrequency;
|
||||
}
|
||||
float stepLift = TargetMovement.X == 0.0f ? 0 :
|
||||
(float)Math.Sin(WalkPos * Dir * CurrentGroundedParams.StepLiftFrequency + offset) * (CurrentGroundedParams.StepLiftAmount / 100);
|
||||
@@ -847,14 +813,6 @@ namespace Barotrauma
|
||||
foreach (Limb limb in Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (Math.Abs(limb.Params.ConstantTorque) > 0)
|
||||
{
|
||||
limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque, wrapAngle: true);
|
||||
}
|
||||
if (limb.Params.BlinkFrequency > 0 && !limb.Params.OnlyBlinkInWater)
|
||||
{
|
||||
limb.UpdateBlink(deltaTime, MainLimb.Rotation);
|
||||
}
|
||||
switch (limb.type)
|
||||
{
|
||||
case LimbType.LeftFoot:
|
||||
@@ -1024,7 +982,7 @@ namespace Barotrauma
|
||||
if (RagdollParams.IsSpritesheetOrientationHorizontal)
|
||||
{
|
||||
//horizontally aligned limbs need to be flipped 180 degrees
|
||||
l.body.SetTransform(l.SimPosition, l.body.Rotation + MathHelper.Pi * Dir);
|
||||
l.body.SetTransformIgnoreContacts(l.SimPosition, l.body.Rotation + MathHelper.Pi * Dir);
|
||||
}
|
||||
//no need to do anything when flipping vertically oriented limbs
|
||||
//the sprite gets flipped horizontally, which does the job
|
||||
|
||||
+23
-290
@@ -13,10 +13,8 @@ namespace Barotrauma
|
||||
private const float SteepestWalkableSlopeAngleDegrees = 55f;
|
||||
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 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;
|
||||
@@ -254,7 +252,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (Frozen) { return; }
|
||||
if (MainLimb == null) { return; }
|
||||
|
||||
UpdateConstantTorque(deltaTime);
|
||||
UpdateBlink(deltaTime);
|
||||
levitatingCollider = !IsHangingWithRope;
|
||||
if (onGround && character.CanMove)
|
||||
{
|
||||
@@ -297,25 +296,7 @@ namespace Barotrauma
|
||||
fallingProneAnimTimer += deltaTime;
|
||||
UpdateFallingProne(1.0f);
|
||||
}
|
||||
levitatingCollider = false;
|
||||
Collider.FarseerBody.FixedRotation = false;
|
||||
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
if (Collider.Enabled)
|
||||
{
|
||||
//deactivating the collider -> make the main limb inherit the collider's velocity because it'll control the movement now
|
||||
MainLimb.body.LinearVelocity = Collider.LinearVelocity;
|
||||
Collider.Enabled = false;
|
||||
}
|
||||
Collider.LinearVelocity = MainLimb.LinearVelocity;
|
||||
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
|
||||
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
|
||||
//(except when dragging, then we need the pull joints)
|
||||
if (!Draggable || character.SelectedBy == null)
|
||||
{
|
||||
ResetPullJoints();
|
||||
}
|
||||
}
|
||||
UpdateRagdollControlsMovement();
|
||||
return;
|
||||
}
|
||||
fallingProneAnimTimer = 0.0f;
|
||||
@@ -325,7 +306,7 @@ namespace Barotrauma
|
||||
{
|
||||
var lowestLimb = FindLowestLimb();
|
||||
|
||||
Collider.SetTransform(new Vector2(
|
||||
Collider.SetTransformIgnoreContacts(new Vector2(
|
||||
Collider.SimPosition.X,
|
||||
Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)),
|
||||
Collider.Rotation);
|
||||
@@ -357,7 +338,7 @@ namespace Barotrauma
|
||||
float angleDiff = MathUtils.GetShortestAngle(Collider.Rotation, 0.0f);
|
||||
if (Math.Abs(angleDiff) > 0.001f)
|
||||
{
|
||||
Collider.SetTransform(Collider.SimPosition, Collider.Rotation + angleDiff);
|
||||
Collider.SetTransformIgnoreContacts(Collider.SimPosition, Collider.Rotation + angleDiff);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,9 +563,7 @@ namespace Barotrauma
|
||||
footMid += (Math.Max(Math.Abs(walkPosX) * limpAmount, 0.0f) * Math.Min(Math.Abs(TargetMovement.X), 0.3f)) * Dir;
|
||||
}
|
||||
|
||||
movement = overrideTargetMovement == Vector2.Zero ?
|
||||
MathUtils.SmoothStep(movement, TargetMovement, movementLerp) :
|
||||
overrideTargetMovement;
|
||||
movement = overrideTargetMovement ?? MathUtils.SmoothStep(movement, TargetMovement, movementLerp);
|
||||
|
||||
if (Math.Abs(movement.X) < 0.005f)
|
||||
{
|
||||
@@ -652,9 +631,14 @@ namespace Barotrauma
|
||||
{
|
||||
movement = Vector2.Zero;
|
||||
}
|
||||
|
||||
|
||||
float offset = MathHelper.Pi * currentGroundedParams.StepLiftOffset;
|
||||
if (character.AnimController.Dir < 0)
|
||||
{
|
||||
offset += MathHelper.Pi * currentGroundedParams.StepLiftFrequency;
|
||||
}
|
||||
float stepLift = TargetMovement.X == 0.0f ? 0 :
|
||||
(float)Math.Sin(WalkPos * currentGroundedParams.StepLiftFrequency + MathHelper.Pi * currentGroundedParams.StepLiftOffset) * (currentGroundedParams.StepLiftAmount / 100);
|
||||
(float)Math.Sin(WalkPos * Dir * currentGroundedParams.StepLiftFrequency + offset) * (currentGroundedParams.StepLiftAmount / 100);
|
||||
|
||||
float y = colliderPos.Y + stepLift;
|
||||
|
||||
@@ -987,7 +971,7 @@ namespace Barotrauma
|
||||
{
|
||||
head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.HeadTorque);
|
||||
}
|
||||
else
|
||||
else if (character.FollowCursor)
|
||||
{
|
||||
RotateHead(head);
|
||||
}
|
||||
@@ -1145,245 +1129,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private float prevFootPos;
|
||||
|
||||
void UpdateClimbing()
|
||||
{
|
||||
var ladder = character.SelectedSecondaryItem?.GetComponent<Ladder>();
|
||||
if (character.IsIncapacitated)
|
||||
{
|
||||
Anim = Animation.None;
|
||||
return;
|
||||
}
|
||||
else if (ladder == null)
|
||||
{
|
||||
StopClimbing();
|
||||
return;
|
||||
}
|
||||
|
||||
onGround = false;
|
||||
IgnorePlatforms = true;
|
||||
|
||||
bool climbFast = targetMovement.Y > 3.0f;
|
||||
bool slide = targetMovement.Y < -1.1f;
|
||||
Vector2 tempTargetMovement = TargetMovement;
|
||||
tempTargetMovement.Y = climbFast ?
|
||||
Math.Min(tempTargetMovement.Y, 2.0f) :
|
||||
Math.Min(tempTargetMovement.Y, 1.0f);
|
||||
|
||||
movement = MathUtils.SmoothStep(movement, tempTargetMovement, 0.3f);
|
||||
|
||||
Limb leftFoot = GetLimb(LimbType.LeftFoot);
|
||||
Limb rightFoot = GetLimb(LimbType.RightFoot);
|
||||
Limb head = GetLimb(LimbType.Head);
|
||||
Limb torso = GetLimb(LimbType.Torso);
|
||||
|
||||
Limb leftHand = GetLimb(LimbType.LeftHand);
|
||||
Limb rightHand = GetLimb(LimbType.RightHand);
|
||||
|
||||
if (leftHand == null || rightHand == null || head == null || torso == null) { return; }
|
||||
|
||||
Vector2 ladderSimPos = ConvertUnits.ToSimUnits(
|
||||
ladder.Item.Rect.X + ladder.Item.Rect.Width / 2.0f,
|
||||
ladder.Item.Rect.Y);
|
||||
|
||||
Vector2 ladderSimSize = ConvertUnits.ToSimUnits(ladder.Item.Rect.Size.ToVector2());
|
||||
|
||||
float lowestLadderSimPos = ladderSimPos.Y - ladderSimPos.Y;
|
||||
var lowestNearbyLadder = GetLowestNearbyLadder(ladder);
|
||||
if (lowestNearbyLadder != null && lowestNearbyLadder != ladder)
|
||||
{
|
||||
ladderSimSize.Y = ConvertUnits.ToSimUnits(ladder.Item.WorldRect.Y - (lowestNearbyLadder.Item.WorldRect.Y - lowestNearbyLadder.Item.Rect.Size.Y));
|
||||
}
|
||||
|
||||
float stepHeight = ConvertUnits.ToSimUnits(30.0f);
|
||||
if (climbFast) { stepHeight *= 2; }
|
||||
|
||||
if (currentHull == null && ladder.Item.Submarine != null)
|
||||
{
|
||||
ladderSimPos += ladder.Item.Submarine.SimPosition;
|
||||
}
|
||||
else if (currentHull?.Submarine != null && currentHull.Submarine != ladder.Item.Submarine && ladder.Item.Submarine != null)
|
||||
{
|
||||
ladderSimPos += ladder.Item.Submarine.SimPosition - currentHull.Submarine.SimPosition;
|
||||
}
|
||||
else if (currentHull?.Submarine != null && ladder.Item.Submarine == null)
|
||||
{
|
||||
ladderSimPos -= currentHull.Submarine.SimPosition;
|
||||
}
|
||||
|
||||
float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.Radius - Collider.Height / 2.0f;
|
||||
float torsoPos = TorsoPosition ?? 0;
|
||||
MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + torsoPos), 10.5f);
|
||||
float headPos = HeadPosition ?? 0;
|
||||
MoveLimb(head, new Vector2(ladderSimPos.X - 0.2f * Dir, bottomPos + headPos), 10.5f);
|
||||
|
||||
Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.1f * Dir, Collider.SimPosition.Y), 10.5f);
|
||||
|
||||
Vector2 handPos = new Vector2(
|
||||
ladderSimPos.X,
|
||||
bottomPos + torsoPos + movement.Y * 0.1f - ladderSimPos.Y);
|
||||
if (climbFast) { handPos.Y -= stepHeight; }
|
||||
|
||||
//prevent the hands from going above the top of the ladders
|
||||
handPos.Y = Math.Min(-0.5f, handPos.Y);
|
||||
if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
|
||||
{
|
||||
MoveLimb(rightHand,
|
||||
new Vector2(slide ? handPos.X + ladderSimSize.X * 0.5f : handPos.X,
|
||||
(slide ? handPos.Y : MathUtils.Round(handPos.Y, stepHeight * 2.0f)) + ladderSimPos.Y),
|
||||
5.2f);
|
||||
rightHand.body.ApplyTorque(Dir * 2.0f);
|
||||
}
|
||||
if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
|
||||
{
|
||||
MoveLimb(leftHand,
|
||||
new Vector2(handPos.X - ladderSimSize.X * 0.5f,
|
||||
(slide ? handPos.Y : MathUtils.Round(handPos.Y - stepHeight, stepHeight * 2.0f) + stepHeight) + ladderSimPos.Y),
|
||||
5.2f); ;
|
||||
leftHand.body.ApplyTorque(Dir * 2.0f);
|
||||
}
|
||||
|
||||
Vector2 footPos = new Vector2(
|
||||
handPos.X - Dir * 0.05f,
|
||||
bottomPos + ColliderHeightFromFloor - stepHeight * 2.7f - ladderSimPos.Y);
|
||||
if (climbFast) { footPos.Y += stepHeight; }
|
||||
|
||||
//apply torque to the legs to make the knees bend
|
||||
Limb leftLeg = GetLimb(LimbType.LeftLeg);
|
||||
Limb rightLeg = GetLimb(LimbType.RightLeg);
|
||||
|
||||
//only move the feet if they're above the bottom of the ladders
|
||||
//(if not, they'll just dangle in air, and the character holds itself up with it's arms)
|
||||
if (footPos.Y > -ladderSimSize.Y - 0.2f && leftFoot != null && rightFoot != null)
|
||||
{
|
||||
Limb refLimb = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso);
|
||||
bool leftLegBackwards = Math.Abs(leftLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi;
|
||||
bool rightLegBackwards = Math.Abs(rightLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi;
|
||||
|
||||
if (slide)
|
||||
{
|
||||
if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X - ladderSimSize.X * 0.5f, footPos.Y + ladderSimPos.Y), 15.5f, true); }
|
||||
if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true); }
|
||||
}
|
||||
else
|
||||
{
|
||||
float leftFootPos = MathUtils.Round(footPos.Y + stepHeight, stepHeight * 2.0f) - stepHeight;
|
||||
float prevLeftFootPos = MathUtils.Round(prevFootPos + stepHeight, stepHeight * 2.0f) - stepHeight;
|
||||
if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X, leftFootPos + ladderSimPos.Y), 15.5f, true); }
|
||||
|
||||
float rightFootPos = MathUtils.Round(footPos.Y, stepHeight * 2.0f);
|
||||
float prevRightFootPos = MathUtils.Round(prevFootPos, stepHeight * 2.0f);
|
||||
if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, rightFootPos + ladderSimPos.Y), 15.5f, true); }
|
||||
#if CLIENT
|
||||
if (Math.Abs(leftFootPos - prevLeftFootPos) > stepHeight && leftFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval)
|
||||
{
|
||||
SoundPlayer.PlaySound("footstep_armor_heavy", leftFoot.WorldPosition, hullGuess: currentHull);
|
||||
leftFoot.LastImpactSoundTime = (float)Timing.TotalTime;
|
||||
}
|
||||
if (Math.Abs(rightFootPos - prevRightFootPos) > stepHeight && rightFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval)
|
||||
{
|
||||
SoundPlayer.PlaySound("footstep_armor_heavy", rightFoot.WorldPosition, hullGuess: currentHull);
|
||||
rightFoot.LastImpactSoundTime = (float)Timing.TotalTime;
|
||||
}
|
||||
#endif
|
||||
prevFootPos = footPos.Y;
|
||||
}
|
||||
|
||||
if (!leftLegBackwards) { leftLeg.body.ApplyTorque(Dir * -8.0f); }
|
||||
if (!rightLegBackwards) { rightLeg.body.ApplyTorque(Dir * -8.0f); }
|
||||
}
|
||||
|
||||
float movementFactor = (handPos.Y / stepHeight) * (float)Math.PI;
|
||||
movementFactor = 0.8f + (float)Math.Abs(Math.Sin(movementFactor));
|
||||
|
||||
Vector2 subSpeed = currentHull != null || ladder.Item.Submarine == null
|
||||
? Vector2.Zero : ladder.Item.Submarine.Velocity;
|
||||
|
||||
//reached the top of the ladders -> can't go further up
|
||||
Vector2 climbForce = new Vector2(0.0f, movement.Y) * movementFactor;
|
||||
|
||||
if (!InWater) { climbForce.Y += 0.3f * movementFactor; }
|
||||
|
||||
if (character.SimPosition.Y > ladderSimPos.Y) { climbForce.Y = Math.Min(0.0f, climbForce.Y); }
|
||||
//reached the bottom -> can't go further down
|
||||
float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.Height;
|
||||
if (floorFixture != null &&
|
||||
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionStairs) &&
|
||||
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionPlatform) &&
|
||||
character.SimPosition.Y < standOnFloorY + minHeightFromFloor)
|
||||
{
|
||||
climbForce.Y = MathHelper.Clamp((standOnFloorY + minHeightFromFloor - character.SimPosition.Y) * 5.0f, climbForce.Y, 1.0f);
|
||||
}
|
||||
|
||||
//apply forces to the collider to move the Character up/down
|
||||
Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass);
|
||||
if (Aiming)
|
||||
{
|
||||
RotateHead(head);
|
||||
}
|
||||
else if (Anim == Animation.UsingItemWhileClimbing && character.SelectedItem is { } selectedItem)
|
||||
{
|
||||
Vector2 diff = (selectedItem.WorldPosition - head.WorldPosition) * Dir;
|
||||
float targetRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver4 * Dir);
|
||||
head.body.SmoothRotate(targetRotation, force: WalkParams.HeadTorque);
|
||||
}
|
||||
else
|
||||
{
|
||||
float movementMultiplier = targetMovement.Y < 0 ? 0 : 1;
|
||||
head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, force: WalkParams.HeadTorque);
|
||||
}
|
||||
|
||||
if (ladder.Item.Prefab.Triggers.None())
|
||||
{
|
||||
character.ReleaseSecondaryItem();
|
||||
return;
|
||||
}
|
||||
|
||||
Rectangle trigger = ladder.Item.Prefab.Triggers.FirstOrDefault();
|
||||
trigger = ladder.Item.TransformTrigger(trigger);
|
||||
|
||||
bool isRemote = false;
|
||||
bool isClimbing = true;
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
isRemote = character.IsRemotelyControlled;
|
||||
}
|
||||
if (isRemote)
|
||||
{
|
||||
if (Math.Abs(targetMovement.X) > 0.05f ||
|
||||
(TargetMovement.Y < 0.0f && ConvertUnits.ToSimUnits(trigger.Height) + handPos.Y < HeadPosition) ||
|
||||
(TargetMovement.Y > 0.0f && handPos.Y > 0.1f))
|
||||
{
|
||||
isClimbing = false;
|
||||
}
|
||||
}
|
||||
else if ((character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right)) &&
|
||||
(!character.IsKeyDown(InputType.Up) && !character.IsKeyDown(InputType.Down)))
|
||||
{
|
||||
isClimbing = false;
|
||||
}
|
||||
|
||||
if (!isClimbing)
|
||||
{
|
||||
character.StopClimbing();
|
||||
IgnorePlatforms = false;
|
||||
}
|
||||
|
||||
Ladder GetLowestNearbyLadder(Ladder currentLadder, float threshold = 16.0f)
|
||||
{
|
||||
foreach (Ladder ladder in Ladder.List)
|
||||
{
|
||||
if (ladder == currentLadder || !ladder.Item.IsInteractable(character)) { continue; }
|
||||
if (Math.Abs(ladder.Item.WorldPosition.X - currentLadder.Item.WorldPosition.X) > threshold) { continue; }
|
||||
if (ladder.Item.WorldPosition.Y > currentLadder.Item.WorldPosition.Y) { continue; }
|
||||
if ((currentLadder.Item.WorldRect.Y - currentLadder.Item.Rect.Height) - ladder.Item.WorldRect.Y > threshold) { continue; }
|
||||
return ladder;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateFallingProne(float strength, bool moveHands = true, bool moveTorso = true, bool moveLegs = true)
|
||||
{
|
||||
if (strength <= 0.0f) { return; }
|
||||
@@ -1518,7 +1263,7 @@ namespace Barotrauma
|
||||
|
||||
float cprBoost = character.GetStatValue(StatTypes.CPRBoost);
|
||||
|
||||
int skill = (int)character.GetSkillLevel("medical");
|
||||
int skill = (int)character.GetSkillLevel(Tags.MedicalSkill);
|
||||
|
||||
if (GameMain.NetworkMember is not { IsClient: true })
|
||||
{
|
||||
@@ -1595,7 +1340,7 @@ namespace Barotrauma
|
||||
//otherwise it's easy to abuse the system by repeatedly reviving in a low-oxygen room
|
||||
if (!target.IsDead)
|
||||
{
|
||||
target.CharacterHealth.CalculateVitality();
|
||||
target.CharacterHealth.RecalculateVitality();
|
||||
if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f)
|
||||
{
|
||||
character.Info?.ApplySkillGain(Tags.MedicalSkill, SkillSettings.Current.SkillIncreasePerCprRevive);
|
||||
@@ -1811,7 +1556,7 @@ namespace Barotrauma
|
||||
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);
|
||||
GameAnalyticsManager.AddErrorEventOnce("DragCharacter:PullJointTooFar", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg);
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
#endif
|
||||
@@ -1876,23 +1621,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RotateHead(Limb head)
|
||||
|
||||
public void Crouch()
|
||||
{
|
||||
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
|
||||
Vector2 dir = (mousePos - head.SimPosition) * Dir;
|
||||
float rot = MathUtils.VectorToAngle(dir);
|
||||
var neckJoint = GetJointBetweenLimbs(LimbType.Head, LimbType.Torso);
|
||||
if (neckJoint != null)
|
||||
{
|
||||
float offset = MathUtils.WrapAnglePi(GetLimb(LimbType.Torso).body.Rotation);
|
||||
float lowerLimit = neckJoint.LowerLimit + offset;
|
||||
float upperLimit = neckJoint.UpperLimit + offset;
|
||||
float min = Math.Min(lowerLimit, upperLimit);
|
||||
float max = Math.Max(lowerLimit, upperLimit);
|
||||
rot = Math.Clamp(rot, min, max);
|
||||
}
|
||||
head.body.SmoothRotate(rot, CurrentAnimationParams.HeadTorque);
|
||||
Crouching = true;
|
||||
character.SetInput(InputType.Crouch, hit: false, held: true);
|
||||
}
|
||||
|
||||
private void FootIK(Limb foot, Vector2 pos, float legTorque, float footTorque, float footAngle)
|
||||
|
||||
@@ -64,6 +64,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<Body> limbBodies = new List<Body>();
|
||||
public IEnumerable<Body> LimbBodies => limbBodies;
|
||||
|
||||
public bool HasMultipleLimbsOfSameType => limbs != null && limbs.Length > limbDictionary.Count;
|
||||
|
||||
private bool frozen;
|
||||
@@ -91,7 +94,7 @@ namespace Barotrauma
|
||||
private bool simplePhysicsEnabled;
|
||||
|
||||
public Character Character => character;
|
||||
protected Character character;
|
||||
protected readonly Character character;
|
||||
|
||||
protected float strongestImpact;
|
||||
|
||||
@@ -108,7 +111,7 @@ namespace Barotrauma
|
||||
|
||||
//a movement vector that overrides targetmovement if trying to steer
|
||||
//a Character to the position sent by server in multiplayer mode
|
||||
protected Vector2 overrideTargetMovement;
|
||||
protected Vector2? overrideTargetMovement;
|
||||
|
||||
protected float floorY, standOnFloorY;
|
||||
protected Fixture floorFixture;
|
||||
@@ -138,6 +141,12 @@ namespace Barotrauma
|
||||
|
||||
private Category prevCollisionCategory = Category.None;
|
||||
|
||||
/// <summary>
|
||||
/// When the character is alive/conscious, the collider drives the character's movement and is used to sync the character's position in MP.
|
||||
/// When unconscious, the ragdoll controls the movement and the collider just sticks to the main limb.
|
||||
/// </summary>
|
||||
public bool ColliderControlsMovement => character.CanMove;
|
||||
|
||||
public bool IsStuck => Limbs.Any(l => l.IsStuck);
|
||||
|
||||
public PhysicsBody Collider
|
||||
@@ -185,7 +194,7 @@ namespace Barotrauma
|
||||
Vector2 pos = collider[colliderIndex].SimPosition;
|
||||
pos.Y -= collider[colliderIndex].Height * 0.5f;
|
||||
pos.Y += collider[value].Height * 0.5f;
|
||||
collider[value].SetTransform(pos, collider[colliderIndex].Rotation);
|
||||
collider[value].SetTransformIgnoreContacts(pos, collider[colliderIndex].Rotation);
|
||||
|
||||
collider[value].LinearVelocity = collider[colliderIndex].LinearVelocity;
|
||||
collider[value].AngularVelocity = collider[colliderIndex].AngularVelocity;
|
||||
@@ -282,7 +291,7 @@ namespace Barotrauma
|
||||
foreach (Limb limb in Limbs)
|
||||
{
|
||||
if (limb.IsSevered || !limb.body.PhysEnabled) { continue; }
|
||||
limb.body.SetTransform(Collider.SimPosition, Collider.Rotation);
|
||||
limb.body.SetTransformIgnoreContacts(Collider.SimPosition, Collider.Rotation);
|
||||
//reset pull joints (they may be somewhere far away if the character has moved from the position where animations were last updated)
|
||||
limb.PullJointEnabled = false;
|
||||
limb.PullJointWorldAnchorB = limb.SimPosition;
|
||||
@@ -297,11 +306,11 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
return (overrideTargetMovement == Vector2.Zero) ? targetMovement : overrideTargetMovement;
|
||||
return overrideTargetMovement ?? targetMovement;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value)) return;
|
||||
if (!MathUtils.IsValid(value)) { return; }
|
||||
targetMovement.X = MathHelper.Clamp(value.X, -MAX_SPEED, MAX_SPEED);
|
||||
targetMovement.Y = MathHelper.Clamp(value.Y, -MAX_SPEED, MAX_SPEED);
|
||||
}
|
||||
@@ -385,15 +394,16 @@ namespace Barotrauma
|
||||
if (ragdollParams != null)
|
||||
{
|
||||
RagdollParams = ragdollParams;
|
||||
if (!character.VariantOf.IsEmpty)
|
||||
{
|
||||
RagdollParams.TryApplyVariantScale(character.Params.VariantFile);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only re-equip items if the ragdoll doesn't change, because re-equiping items might throw exceptions if the limbs have changed.
|
||||
items = limbs?.ToDictionary(l => l.Params, l => l.WearingItems);
|
||||
}
|
||||
if (character.Params.VariantFile is XDocument variantFile)
|
||||
{
|
||||
RagdollParams.TryApplyVariantScale(variantFile);
|
||||
}
|
||||
foreach (var limbParams in RagdollParams.Limbs)
|
||||
{
|
||||
if (!PhysicsBody.IsValidShape(limbParams.Radius, limbParams.Height, limbParams.Width))
|
||||
@@ -430,18 +440,13 @@ namespace Barotrauma
|
||||
|
||||
if (character.IsHusk && character.Params.UseHuskAppendage)
|
||||
{
|
||||
bool inEditor = false;
|
||||
#if CLIENT
|
||||
inEditor = Screen.Selected == GameMain.CharacterEditorScreen;
|
||||
#endif
|
||||
|
||||
var characterPrefab = CharacterPrefab.FindByFilePath(character.ConfigPath);
|
||||
if (characterPrefab?.ConfigElement != null)
|
||||
{
|
||||
var mainElement = characterPrefab.ConfigElement;
|
||||
foreach (var huskAppendage in mainElement.GetChildElements("huskappendage"))
|
||||
{
|
||||
if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; }
|
||||
if (huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; }
|
||||
|
||||
Identifier afflictionIdentifier = huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty);
|
||||
if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab affliction) ||
|
||||
@@ -452,7 +457,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskAppendage, ragdoll: this);
|
||||
AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskedSpeciesName: character.SpeciesName, huskAppendage, ragdoll: this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,7 +483,7 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError("Invalid collider dimensions: " + cParams.Name);
|
||||
break; ;
|
||||
}
|
||||
var body = new PhysicsBody(cParams);
|
||||
var body = new PhysicsBody(cParams, findNewContacts: false);
|
||||
collider.Add(body);
|
||||
body.UserData = character;
|
||||
body.FarseerBody.OnCollision += OnLimbCollision;
|
||||
@@ -520,7 +525,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (joint == null) { continue; }
|
||||
float angle = (joint.LowerLimit + joint.UpperLimit) / 2.0f;
|
||||
joint.LimbB?.body?.SetTransform(
|
||||
joint.LimbB?.body?.SetTransformIgnoreContacts(
|
||||
(joint.WorldAnchorA - MathUtils.RotatePointAroundTarget(joint.LocalAnchorB, Vector2.Zero, joint.BodyA.Rotation + angle, true)),
|
||||
joint.BodyA.Rotation + angle);
|
||||
}
|
||||
@@ -528,11 +533,13 @@ namespace Barotrauma
|
||||
|
||||
protected void CreateLimbs()
|
||||
{
|
||||
limbBodies.Clear();
|
||||
limbs?.ForEach(l => l.Remove());
|
||||
Mass = 0;
|
||||
DebugConsole.Log($"Creating limbs from {RagdollParams.Name}.");
|
||||
limbDictionary = new Dictionary<LimbType, Limb>();
|
||||
limbs = new Limb[RagdollParams.Limbs.Count];
|
||||
RagdollParams.Limbs.ForEach(l => AddLimb(l));
|
||||
RagdollParams.Limbs.ForEach(AddLimb);
|
||||
if (limbs.Contains(null)) { return; }
|
||||
SetupDrawOrder();
|
||||
}
|
||||
@@ -549,11 +556,11 @@ namespace Barotrauma
|
||||
|
||||
/// <summary>
|
||||
/// Resets the serializable data to the currently selected ragdoll params.
|
||||
/// Force reloading always loads the xml stored on the disk.
|
||||
/// Always loads the xml stored on the disk.
|
||||
/// </summary>
|
||||
public void ResetRagdoll(bool forceReload = false)
|
||||
public void ResetRagdoll()
|
||||
{
|
||||
RagdollParams.Reset(forceReload);
|
||||
RagdollParams.Reset(forceReload: true);
|
||||
ResetJoints();
|
||||
ResetLimbs();
|
||||
}
|
||||
@@ -577,7 +584,7 @@ namespace Barotrauma
|
||||
|
||||
public void AddJoint(JointParams jointParams)
|
||||
{
|
||||
if (!checkLimbIndex(jointParams.Limb2, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2"))
|
||||
if (!checkLimbIndex(jointParams.Limb1, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -621,6 +628,7 @@ namespace Barotrauma
|
||||
{
|
||||
throw new Exception($"Failed to add a limb to the character \"{Character?.ConfigPath ?? "null"}\" (limb index {ID} out of bounds). The ragdoll file may be configured incorrectly.");
|
||||
}
|
||||
limbBodies.Add(limb.body.FarseerBody);
|
||||
Limbs[ID] = limb;
|
||||
Mass += limb.Mass;
|
||||
if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); }
|
||||
@@ -632,6 +640,7 @@ namespace Barotrauma
|
||||
limb.body.FarseerBody.OnCollision += OnLimbCollision;
|
||||
Array.Resize(ref limbs, Limbs.Length + 1);
|
||||
Limbs[Limbs.Length - 1] = limb;
|
||||
limbBodies.Add(limb.body.FarseerBody);
|
||||
Mass += limb.Mass;
|
||||
if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); }
|
||||
SetupDrawOrder();
|
||||
@@ -639,14 +648,14 @@ namespace Barotrauma
|
||||
|
||||
public void RemoveLimb(Limb limb)
|
||||
{
|
||||
if (!Limbs.Contains(limb)) return;
|
||||
if (!Limbs.Contains(limb)) { return; }
|
||||
|
||||
Limb[] newLimbs = new Limb[Limbs.Length - 1];
|
||||
|
||||
int i = 0;
|
||||
foreach (Limb existingLimb in Limbs)
|
||||
{
|
||||
if (existingLimb == limb) continue;
|
||||
if (existingLimb == limb) { continue; }
|
||||
newLimbs[i] = existingLimb;
|
||||
i++;
|
||||
}
|
||||
@@ -684,8 +693,10 @@ namespace Barotrauma
|
||||
LimbJoints = newJoints;
|
||||
}
|
||||
|
||||
SubtractMass(limb);
|
||||
limbBodies.Remove(limb.body.FarseerBody);
|
||||
limb.Remove();
|
||||
System.Diagnostics.Debug.Assert(!limbs.Contains(limb));
|
||||
System.Diagnostics.Debug.Assert(limbs.None(l => l.Removed));
|
||||
foreach (LimbJoint limbJoint in attachedJoints)
|
||||
{
|
||||
GameMain.World.Remove(limbJoint.Joint);
|
||||
@@ -819,9 +830,10 @@ namespace Barotrauma
|
||||
{
|
||||
if (character.DisableImpactDamageTimer > 0.0f) { return; }
|
||||
|
||||
if (f2.Body?.UserData is Item)
|
||||
if (f2.Body?.UserData is Item &&
|
||||
f2.Body.BodyType != BodyType.Static)
|
||||
{
|
||||
//no impact damage from items
|
||||
//no impact damage from items with a non-static body
|
||||
//items that can impact characters (melee weapons, projectiles) should handle the damage themselves
|
||||
return;
|
||||
}
|
||||
@@ -1073,7 +1085,7 @@ namespace Barotrauma
|
||||
Vector2 moveDir = hullDiff.LengthSquared() < 0.001f ? Vector2.UnitY : Vector2.Normalize(hullDiff);
|
||||
|
||||
//find a position 32 units away from the hull
|
||||
if (MathUtils.GetLineRectangleIntersection(
|
||||
if (MathUtils.GetLineWorldRectangleIntersection(
|
||||
newHull.WorldPosition,
|
||||
newHull.WorldPosition + moveDir * Math.Max(newHull.Rect.Width, newHull.Rect.Height),
|
||||
new Rectangle(newHull.WorldRect.X - 32, newHull.WorldRect.Y + 32, newHull.WorldRect.Width + 64, newHull.Rect.Height + 64),
|
||||
@@ -1294,6 +1306,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
float MaxVel = NetConfig.MaxPhysicsBodyVelocity;
|
||||
Collider.LinearVelocity = new Vector2(
|
||||
NetConfig.Quantize(Collider.LinearVelocity.X, -MaxVel, MaxVel, 12),
|
||||
NetConfig.Quantize(Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12));
|
||||
|
||||
if (forceStanding)
|
||||
{
|
||||
inWater = false;
|
||||
@@ -1400,7 +1417,12 @@ namespace Barotrauma
|
||||
limb.Update(deltaTime);
|
||||
}
|
||||
|
||||
if (!inWater && character.AllowInput && levitatingCollider)
|
||||
bool isAttachedToController =
|
||||
character.SelectedItem?.GetComponent<Items.Components.Controller>() is { } controller &&
|
||||
controller.User == character &&
|
||||
controller.IsAttachedUser(controller.User);
|
||||
|
||||
if (!inWater && character.AllowInput && levitatingCollider && !isAttachedToController)
|
||||
{
|
||||
if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance)
|
||||
{
|
||||
@@ -1433,7 +1455,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
// Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck.
|
||||
if (Collider.LinearVelocity == Vector2.Zero)
|
||||
if (Collider.LinearVelocity == Vector2.Zero && !character.IsRemotePlayer)
|
||||
{
|
||||
character.IsRagdolled = true;
|
||||
if (character.IsBot)
|
||||
@@ -1448,6 +1470,30 @@ namespace Barotrauma
|
||||
forceNotStanding = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the logic that needs to run when the ragdoll is what controls the character's movement instead of the collider <see cref="ColliderControlsMovement"/>
|
||||
/// (making the collider stick to the ragdoll's main limb).
|
||||
/// </summary>
|
||||
protected void UpdateRagdollControlsMovement()
|
||||
{
|
||||
levitatingCollider = false;
|
||||
Collider.FarseerBody.FixedRotation = false;
|
||||
if (Collider.Enabled)
|
||||
{
|
||||
//deactivating the collider -> make the main limb inherit the collider's velocity because it'll control the movement now
|
||||
MainLimb.body.LinearVelocity = Collider.LinearVelocity;
|
||||
Collider.Enabled = false;
|
||||
}
|
||||
Collider.LinearVelocity = MainLimb.LinearVelocity;
|
||||
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
|
||||
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
|
||||
//(except when dragging, then we need the pull joints)
|
||||
if (!Draggable || character.SelectedBy == null)
|
||||
{
|
||||
ResetPullJoints();
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckBodyInRest(float deltaTime)
|
||||
{
|
||||
if (SimplePhysicsEnabled) { return; }
|
||||
@@ -1910,7 +1956,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
Collider.SetTransform(simPosition, Collider.Rotation);
|
||||
Collider.SetTransformIgnoreContacts(simPosition, Collider.Rotation);
|
||||
}
|
||||
|
||||
if (!MathUtils.NearlyEqual(limbMoveAmount, Vector2.Zero))
|
||||
@@ -2009,7 +2055,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
limb.body.SetTransform(movePos, rotation);
|
||||
limb.body.SetTransformIgnoreContacts(movePos, rotation);
|
||||
limb.PullJointWorldAnchorB = limb.PullJointWorldAnchorA;
|
||||
limb.PullJointEnabled = false;
|
||||
}
|
||||
@@ -2084,7 +2130,7 @@ namespace Barotrauma
|
||||
partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos);
|
||||
private void UpdateNetPlayerPosition(float deltaTime)
|
||||
{
|
||||
if (GameMain.NetworkMember == null) return;
|
||||
if (GameMain.NetworkMember == null) { return; }
|
||||
|
||||
float lowestSubPos = float.MaxValue;
|
||||
if (Submarine.Loaded.Any())
|
||||
@@ -2113,26 +2159,48 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Note that if there are multiple limbs of the same type, only the first (valid) limb is returned.
|
||||
/// </summary>
|
||||
public Limb GetLimb(LimbType limbType, bool excludeSevered = true)
|
||||
/// <param name="limbType"></param>
|
||||
/// <param name="excludeSevered">Should we filter out severed limbs?</param>
|
||||
/// <param name="useSecondaryType">Should we target limbs with secondary type instead of (primary) type?</param>
|
||||
/// <param name="excludeLimbsWithSecondaryType">Should we filter out all limbs with a secondary type something else than "None"?</param>
|
||||
/// <returns></returns>
|
||||
public Limb GetLimb(LimbType limbType, bool excludeSevered = true, bool excludeLimbsWithSecondaryType = false, bool useSecondaryType = false)
|
||||
{
|
||||
if (limbDictionary.TryGetValue(limbType, out Limb limb))
|
||||
Limb limb = null;
|
||||
if (!HasMultipleLimbsOfSameType && !useSecondaryType && !excludeLimbsWithSecondaryType)
|
||||
{
|
||||
if (excludeSevered && limb.IsSevered)
|
||||
// Faster method, but doesn't work when there's multiple limbs of the same type or if we want to seek/exclude limbs with different conditions.
|
||||
if (limbDictionary.TryGetValue(limbType, out limb))
|
||||
{
|
||||
limb = null;
|
||||
}
|
||||
if (limb.Removed)
|
||||
{
|
||||
limb = null;
|
||||
}
|
||||
if (excludeSevered && limb is { IsSevered: true } )
|
||||
{
|
||||
limb = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (limb == null && HasMultipleLimbsOfSameType)
|
||||
if (limb == null)
|
||||
{
|
||||
// Didn't find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs.
|
||||
// Didn't seek or find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs.
|
||||
foreach (var l in limbs)
|
||||
{
|
||||
if (l.type != limbType) { continue; }
|
||||
if (!excludeSevered || !l.IsSevered)
|
||||
if (l.Removed) { continue; }
|
||||
if (useSecondaryType)
|
||||
{
|
||||
limb = l;
|
||||
break;
|
||||
if (l.Params.SecondaryType != limbType) { continue; }
|
||||
}
|
||||
else if (l.type != limbType)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (excludeSevered && l.IsSevered) { continue; }
|
||||
if (excludeLimbsWithSecondaryType && l.Params.SecondaryType != LimbType.None) { continue; }
|
||||
// Found a valid and match
|
||||
limb = l;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return limb;
|
||||
@@ -2220,6 +2288,7 @@ namespace Barotrauma
|
||||
}
|
||||
limbs = null;
|
||||
}
|
||||
limbBodies.Clear();
|
||||
|
||||
if (collider != null)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -24,11 +25,22 @@ namespace Barotrauma
|
||||
NotDefined
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum AttackTarget
|
||||
{
|
||||
Any,
|
||||
Character,
|
||||
Structure // Including hulls etc. Evaluated as anything but a character.
|
||||
Any = 0,
|
||||
/// <summary>
|
||||
/// Characters only
|
||||
/// </summary>
|
||||
Character = 1,
|
||||
/// <summary>
|
||||
/// Structures and hulls, but also items (for backwards support)!
|
||||
/// </summary>
|
||||
Structure = 2,
|
||||
/// <summary>
|
||||
/// Items only
|
||||
/// </summary>
|
||||
Item = 4
|
||||
}
|
||||
|
||||
public enum AIBehaviorAfterAttack
|
||||
@@ -185,7 +197,22 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Used for multiplying all the damage.
|
||||
/// </summary>
|
||||
public float DamageMultiplier { get; set; } = 1;
|
||||
public float DamageMultiplier
|
||||
{
|
||||
get => _damageMultiplier ?? initialDamageMultiplier;
|
||||
set
|
||||
{
|
||||
if (!_damageMultiplier.HasValue)
|
||||
{
|
||||
SetInitialDamageMultiplier(value);
|
||||
}
|
||||
_damageMultiplier = value;
|
||||
}
|
||||
}
|
||||
private float? _damageMultiplier;
|
||||
private float initialDamageMultiplier = 1.0f;
|
||||
public void ResetDamageMultiplier() => _damageMultiplier = initialDamageMultiplier;
|
||||
public void SetInitialDamageMultiplier(float value) => initialDamageMultiplier = value;
|
||||
|
||||
/// <summary>
|
||||
/// Used for multiplying all the ranges.
|
||||
@@ -198,7 +225,12 @@ namespace Barotrauma
|
||||
public float ImpactMultiplier { get; set; } = 1;
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to level walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
|
||||
public float LevelWallDamage { get; set; }
|
||||
public float LevelWallDamage
|
||||
{
|
||||
get => _levelWallDamage * DamageMultiplier;
|
||||
set => _levelWallDamage = value;
|
||||
}
|
||||
private float _levelWallDamage;
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Sets whether or not the attack is ranged or not."), Editable]
|
||||
public bool Ranged { get; set; }
|
||||
@@ -263,6 +295,8 @@ namespace Barotrauma
|
||||
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
|
||||
public Vector2 RootForceWorldEnd { get; private set; }
|
||||
|
||||
public bool HasRootForce => RootForceWorldStart != Vector2.Zero || RootForceWorldMiddle != Vector2.Zero || RootForceWorldEnd != Vector2.Zero;
|
||||
|
||||
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:"Applied to the main limb. The transition smoothing of the applied force."), Editable]
|
||||
public TransitionMode RootTransitionEasing { get; private set; }
|
||||
|
||||
@@ -410,10 +444,9 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
//if level wall damage is not defined, default to the structure damage
|
||||
if (element.GetAttribute("LevelWallDamage") == null &&
|
||||
element.GetAttribute("levelwalldamage") == null)
|
||||
if (element.GetAttribute("LevelWallDamage") == null)
|
||||
{
|
||||
LevelWallDamage = StructureDamage;
|
||||
LevelWallDamage = _structureDamage;
|
||||
}
|
||||
|
||||
InitProjSpecific(element);
|
||||
@@ -816,15 +849,28 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType;
|
||||
public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType.HasAnyFlag(targetType);
|
||||
|
||||
public bool IsValidTarget(Entity target)
|
||||
{
|
||||
return TargetType switch
|
||||
{
|
||||
AttackTarget.Character => target is Character,
|
||||
AttackTarget.Structure => !(target is Character),
|
||||
_ => true,
|
||||
AttackTarget.Structure => target is Structure or Hull or Item, // Items are intentionally included for backwards-support.
|
||||
AttackTarget.Item => target is Item,
|
||||
_ => IsValidTarget(GetAttackTargetTypeFromEntity(target))
|
||||
};
|
||||
}
|
||||
|
||||
private static AttackTarget GetAttackTargetTypeFromEntity(Entity entity)
|
||||
{
|
||||
return entity switch
|
||||
{
|
||||
Character => AttackTarget.Character,
|
||||
Item => AttackTarget.Item,
|
||||
Structure => AttackTarget.Structure,
|
||||
Hull => AttackTarget.Structure,
|
||||
_ => AttackTarget.Any
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,11 +29,13 @@ namespace Barotrauma
|
||||
UpdatePermanentStats = 14,
|
||||
RemoveFromCrew = 15,
|
||||
LatchOntoTarget = 16,
|
||||
|
||||
UpdateTalentRefundPoints = 17,
|
||||
ConfirmTalentRefund = 18,
|
||||
|
||||
MinValue = 0,
|
||||
MaxValue = 16
|
||||
MaxValue = 18
|
||||
}
|
||||
|
||||
|
||||
private interface IEventData : NetEntityEvent.IData
|
||||
{
|
||||
public EventType EventType { get; }
|
||||
@@ -230,9 +232,18 @@ namespace Barotrauma
|
||||
|
||||
public struct UpdateSkillsEventData : IEventData
|
||||
{
|
||||
public EventType EventType => EventType.UpdateSkills;
|
||||
public readonly EventType EventType => EventType.UpdateSkills;
|
||||
|
||||
public readonly bool ForceNotification;
|
||||
public readonly Identifier SkillIdentifier;
|
||||
|
||||
public UpdateSkillsEventData(Identifier skillIdentifier, bool forceNotification)
|
||||
{
|
||||
SkillIdentifier = skillIdentifier;
|
||||
ForceNotification = forceNotification;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private struct UpdateMoneyEventData : IEventData
|
||||
{
|
||||
public EventType EventType => EventType.UpdateMoney;
|
||||
@@ -242,11 +253,21 @@ namespace Barotrauma
|
||||
{
|
||||
public EventType EventType => EventType.UpdatePermanentStats;
|
||||
public readonly StatTypes StatType;
|
||||
|
||||
|
||||
public UpdatePermanentStatsEventData(StatTypes statType)
|
||||
{
|
||||
StatType = statType;
|
||||
}
|
||||
}
|
||||
|
||||
public struct UpdateRefundPointsEventData : IEventData
|
||||
{
|
||||
public EventType EventType => EventType.UpdateTalentRefundPoints;
|
||||
}
|
||||
|
||||
public struct ConfirmRefundEventData : IEventData
|
||||
{
|
||||
public EventType EventType => EventType.ConfirmTalentRefund;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +301,25 @@ namespace Barotrauma
|
||||
public bool PermanentlyDead;
|
||||
public bool RenamingEnabled = false;
|
||||
|
||||
private BotStatus botStatus = BotStatus.ActiveService;
|
||||
|
||||
public BotStatus BotStatus
|
||||
{
|
||||
get => botStatus;
|
||||
set
|
||||
{
|
||||
botStatus = value;
|
||||
if (botStatus == BotStatus.ActiveService && character == null)
|
||||
{
|
||||
//no character yet -> spawn is pending
|
||||
PendingSpawnToActiveService = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsOnReserveBench => BotStatus == BotStatus.ReserveBench;
|
||||
public bool PendingSpawnToActiveService;
|
||||
|
||||
private static ushort idCounter = 1;
|
||||
private const string disguiseName = "???";
|
||||
|
||||
@@ -312,7 +331,18 @@ namespace Barotrauma
|
||||
public LocalizedString Title;
|
||||
|
||||
public (Identifier NpcSetIdentifier, Identifier NpcIdentifier) HumanPrefabIds;
|
||||
|
||||
|
||||
private HumanPrefab _humanPrefab;
|
||||
public HumanPrefab HumanPrefab
|
||||
{
|
||||
get
|
||||
{
|
||||
if (HumanPrefabIds == default) { return null; }
|
||||
_humanPrefab ??= NPCSet.Get(HumanPrefabIds.NpcSetIdentifier, HumanPrefabIds.NpcIdentifier);
|
||||
return _humanPrefab;
|
||||
}
|
||||
}
|
||||
|
||||
public string DisplayName
|
||||
{
|
||||
get
|
||||
@@ -340,19 +370,55 @@ namespace Barotrauma
|
||||
|
||||
public Identifier SpeciesName { get; }
|
||||
|
||||
private Character character;
|
||||
/// <summary>
|
||||
/// Note: Can be null.
|
||||
/// </summary>
|
||||
public Character Character;
|
||||
|
||||
public Character Character
|
||||
{
|
||||
get => character;
|
||||
set
|
||||
{
|
||||
character = value;
|
||||
if (character != null)
|
||||
{
|
||||
//character spawned -> spawn no longer pending
|
||||
PendingSpawnToActiveService = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Job Job;
|
||||
|
||||
|
||||
public int Salary;
|
||||
|
||||
public int ExperiencePoints { get; private set; }
|
||||
|
||||
private int talentRefundPoints;
|
||||
|
||||
/// <summary>
|
||||
/// How many times the player is eligible to refund talents
|
||||
/// </summary>
|
||||
public int TalentRefundPoints
|
||||
{
|
||||
get => talentRefundPoints;
|
||||
set => talentRefundPoints = MathHelper.Max(value, 0);
|
||||
}
|
||||
|
||||
public HashSet<Identifier> UnlockedTalents { get; private set; } = new HashSet<Identifier>();
|
||||
|
||||
private int talentResetCount;
|
||||
|
||||
/// <summary>
|
||||
/// How many times have the characters' talents been reset?
|
||||
/// </summary>
|
||||
public int TalentResetCount
|
||||
{
|
||||
get => talentResetCount;
|
||||
set => talentResetCount = MathHelper.Max(value, 0);
|
||||
}
|
||||
|
||||
public (Identifier factionId, float reputation) MinReputationToHire;
|
||||
|
||||
/// <summary>
|
||||
@@ -708,7 +774,9 @@ namespace Barotrauma
|
||||
SetAttachments(randSync);
|
||||
SetColors(randSync);
|
||||
|
||||
Job = job ?? ((jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant));
|
||||
Job = job ?? ((jobPrefab == null) ?
|
||||
Job.Random(isPvP: false, Rand.RandSync.Unsynced) :
|
||||
new Job(jobPrefab, isPvP: false, randSync, variant));
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
@@ -725,6 +793,8 @@ namespace Barotrauma
|
||||
}
|
||||
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
|
||||
|
||||
TalentRefundPoints = CharacterConfigElement.GetAttributeInt("refundpoints", 0);
|
||||
|
||||
int loadedLastRewardDistribution = CharacterConfigElement.GetAttributeInt("lastrewarddistribution", -1);
|
||||
if (loadedLastRewardDistribution >= 0)
|
||||
{
|
||||
@@ -742,6 +812,13 @@ namespace Barotrauma
|
||||
return name;
|
||||
}
|
||||
|
||||
public void SetNameBasedOnJob()
|
||||
{
|
||||
if (Job == null) { return; }
|
||||
Name = Job.Name.Value;
|
||||
OriginalName = Name;
|
||||
}
|
||||
|
||||
public static Color SelectRandomColor(in ImmutableArray<(Color Color, float Commonness)> array, Rand.RandSync randSync)
|
||||
=> ToolBox.SelectWeightedRandom(array, array.Select(p => p.Commonness).ToArray(), randSync)
|
||||
.Color;
|
||||
@@ -799,10 +876,12 @@ namespace Barotrauma
|
||||
Salary = infoElement.GetAttributeInt("salary", 1000);
|
||||
ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0);
|
||||
AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0);
|
||||
TalentResetCount = infoElement.GetAttributeInt(nameof(talentResetCount), 0);
|
||||
HashSet<Identifier> tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
|
||||
LoadTagsBackwardsCompatibility(infoElement, tags);
|
||||
SpeciesName = infoElement.GetAttributeIdentifier("speciesname", "");
|
||||
PermanentlyDead = infoElement.GetAttributeBool("permanentlydead", false);
|
||||
BotStatus = infoElement.GetAttributeBool(nameof(IsOnReserveBench), false) ? BotStatus.ReserveBench : BotStatus.ActiveService;
|
||||
RenamingEnabled = infoElement.GetAttributeBool("renamingenabled", false);
|
||||
ContentXElement element;
|
||||
if (!SpeciesName.IsEmpty)
|
||||
@@ -1047,6 +1126,7 @@ namespace Barotrauma
|
||||
|
||||
public string ReplaceVars(string str)
|
||||
{
|
||||
if (Head == null) { return str; }
|
||||
return Prefab.ReplaceVars(str, Head.Preset);
|
||||
}
|
||||
|
||||
@@ -1254,7 +1334,7 @@ namespace Barotrauma
|
||||
|
||||
partial void LoadAttachmentSprites();
|
||||
|
||||
public int CalculateSalary()
|
||||
public int CalculateSalary(int baseSalary = 0, float salaryMultiplier = 1.0f)
|
||||
{
|
||||
if (Name == null || Job == null) { return 0; }
|
||||
|
||||
@@ -1264,25 +1344,25 @@ namespace Barotrauma
|
||||
salary += (int)(skill.Level * skill.PriceMultiplier);
|
||||
}
|
||||
|
||||
return (int)(salary * Job.Prefab.PriceMultiplier);
|
||||
return (int)(baseSalary + (salary * Job.Prefab.PriceMultiplier * salaryMultiplier));
|
||||
}
|
||||
|
||||
/// <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)
|
||||
public void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility = false, float maxGain = 2f, bool forceNotification = false)
|
||||
{
|
||||
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);
|
||||
IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility, forceNotification);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false, bool forceNotification = false)
|
||||
{
|
||||
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
|
||||
|
||||
@@ -1312,14 +1392,14 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
OnSkillChanged(skillIdentifier, prevLevel, newLevel);
|
||||
OnSkillChanged(skillIdentifier, prevLevel, newLevel, forceNotification);
|
||||
}
|
||||
|
||||
private static readonly ImmutableDictionary<Identifier, StatTypes> skillGainStatValues = new Dictionary<Identifier, StatTypes>
|
||||
{
|
||||
{ new("helm"), StatTypes.HelmSkillGainSpeed },
|
||||
{ new("medical"), StatTypes.WeaponsSkillGainSpeed },
|
||||
{ new("weapons"), StatTypes.MedicalSkillGainSpeed },
|
||||
{ new("weapons"), StatTypes.WeaponsSkillGainSpeed },
|
||||
{ new("medical"), StatTypes.MedicalSkillGainSpeed },
|
||||
{ new("electrical"), StatTypes.ElectricalSkillGainSpeed },
|
||||
{ new("mechanical"), StatTypes.MechanicalSkillGainSpeed }
|
||||
}.ToImmutableDictionary();
|
||||
@@ -1334,7 +1414,7 @@ namespace Barotrauma
|
||||
return increase;
|
||||
}
|
||||
|
||||
public void SetSkillLevel(Identifier skillIdentifier, float level)
|
||||
public void SetSkillLevel(Identifier skillIdentifier, float level, bool forceNotification = false)
|
||||
{
|
||||
if (Job == null) { return; }
|
||||
|
||||
@@ -1342,17 +1422,17 @@ namespace Barotrauma
|
||||
if (skill == null)
|
||||
{
|
||||
Job.IncreaseSkillLevel(skillIdentifier, level, increasePastMax: false);
|
||||
OnSkillChanged(skillIdentifier, 0.0f, level);
|
||||
OnSkillChanged(skillIdentifier, 0.0f, level, forceNotification);
|
||||
}
|
||||
else
|
||||
{
|
||||
float prevLevel = skill.Level;
|
||||
skill.Level = level;
|
||||
OnSkillChanged(skillIdentifier, prevLevel, skill.Level);
|
||||
OnSkillChanged(skillIdentifier, prevLevel, skill.Level, forceNotification);
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel);
|
||||
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel, bool forceNotification);
|
||||
|
||||
public void GiveExperience(int amount)
|
||||
{
|
||||
@@ -1437,10 +1517,11 @@ namespace Barotrauma
|
||||
experienceRequired += ExperienceRequiredPerLevel(level);
|
||||
level++;
|
||||
}
|
||||
return level;
|
||||
|
||||
return Math.Max(level, 0);
|
||||
}
|
||||
|
||||
private static int ExperienceRequiredPerLevel(int level)
|
||||
public static int ExperienceRequiredPerLevel(int level)
|
||||
{
|
||||
return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level;
|
||||
}
|
||||
@@ -1449,6 +1530,43 @@ namespace Barotrauma
|
||||
|
||||
partial void OnPermanentStatChanged(StatTypes statType);
|
||||
|
||||
public void RefundTalents()
|
||||
{
|
||||
if (TalentRefundPoints <= 0) { return; }
|
||||
|
||||
//e.g. talents from endocrine booster or extra talents some special NPC has
|
||||
var talentsFromOutsideTree = GetUnlockedTalentsOutsideTree().ToList();
|
||||
|
||||
UnlockedTalents.Clear();
|
||||
SavedStatValues.Clear();
|
||||
Character?.ResetTalents(talentPointReduction: talentResetCount);
|
||||
TalentRefundPoints--;
|
||||
talentResetCount++;
|
||||
|
||||
//it's simpler to just remove everything first and then reapply the "extra" talents than to
|
||||
//try determining which talent the resistances, ability flags etc came from and only remove specific ones
|
||||
if (Character == null)
|
||||
{
|
||||
talentsFromOutsideTree.ForEach(talentId => UnlockedTalents.Add(talentId));
|
||||
}
|
||||
else
|
||||
{
|
||||
talentsFromOutsideTree.ForEach(talentId => Character.GiveTalent(talentId, addingFirstTime: true));
|
||||
}
|
||||
|
||||
GameMain.NetworkMember?.CreateEntityEvent(Character, new Character.ConfirmRefundEventData());
|
||||
}
|
||||
|
||||
public void AddRefundPoints(int newRefundPoints)
|
||||
{
|
||||
TalentRefundPoints += newRefundPoints;
|
||||
#if SERVER
|
||||
GameMain.NetworkMember?.CreateEntityEvent(Character, new Character.UpdateRefundPointsEventData());
|
||||
#elif CLIENT
|
||||
ShowTalentResetPopupOnOpen = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Rename(string newName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(newName)) { return; }
|
||||
@@ -1491,6 +1609,7 @@ namespace Barotrauma
|
||||
new XAttribute("salary", Salary),
|
||||
new XAttribute("experiencepoints", ExperiencePoints),
|
||||
new XAttribute("additionaltalentpoints", AdditionalTalentPoints),
|
||||
new XAttribute(nameof(talentResetCount), TalentResetCount),
|
||||
new XAttribute("hairindex", Head.HairIndex),
|
||||
new XAttribute("beardindex", Head.BeardIndex),
|
||||
new XAttribute("moustacheindex", Head.MoustacheIndex),
|
||||
@@ -1500,8 +1619,10 @@ namespace Barotrauma
|
||||
new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)),
|
||||
new XAttribute("startitemsgiven", StartItemsGiven),
|
||||
new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty),
|
||||
new XAttribute("refundpoints", TalentRefundPoints),
|
||||
new XAttribute("lastrewarddistribution", LastRewardDistribution.Match(some: value => value, none: () => -1).ToString()),
|
||||
new XAttribute("permanentlydead", PermanentlyDead),
|
||||
new XAttribute(nameof(IsOnReserveBench), IsOnReserveBench),
|
||||
new XAttribute("renamingenabled", RenamingEnabled)
|
||||
);
|
||||
|
||||
@@ -1601,7 +1722,7 @@ namespace Barotrauma
|
||||
targetAvailableInNextLevel =
|
||||
!isOutside &&
|
||||
GameMain.GameSession?.Campaign is not { SwitchedSubsThisRound: true } &&
|
||||
(isOnConnectedLinkedSub || entitySub == Submarine.MainSub);
|
||||
(isOnConnectedLinkedSub || (Submarine.MainSub != null && entitySub == Submarine.MainSub));
|
||||
if (!targetAvailableInNextLevel)
|
||||
{
|
||||
if (!order.Prefab.CanBeGeneralized)
|
||||
@@ -1636,7 +1757,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (order.TargetSpatialEntity?.Submarine is Submarine targetSub)
|
||||
{
|
||||
if (targetSub == Submarine.MainSub)
|
||||
if (Submarine.MainSub != null && targetSub == Submarine.MainSub)
|
||||
{
|
||||
orderElement.Add(new XAttribute("onmainsub", true));
|
||||
}
|
||||
|
||||
@@ -14,24 +14,29 @@ namespace Barotrauma
|
||||
|
||||
public readonly AnimController.Animation Animation;
|
||||
|
||||
public CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, AnimController.Animation animation = AnimController.Animation.None)
|
||||
: this(pos, rotation, velocity, angularVelocity, 0, time, dir, selectedCharacter, selectedItem, selectedSecondaryItem, animation)
|
||||
public bool IgnorePlatforms;
|
||||
|
||||
public readonly Vector2 TargetMovement;
|
||||
|
||||
public CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, Vector2 targetMovement, AnimController.Animation animation = AnimController.Animation.None, bool ignorePlatforms = false)
|
||||
: this(pos, rotation, velocity, angularVelocity, 0, time, dir, selectedCharacter, selectedItem, selectedSecondaryItem, targetMovement, animation, ignorePlatforms)
|
||||
{
|
||||
}
|
||||
|
||||
public CharacterStateInfo(Vector2 pos, float? rotation, UInt16 ID, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, AnimController.Animation animation = AnimController.Animation.None)
|
||||
: this(pos, rotation, Vector2.Zero, 0.0f, ID, 0.0f, dir, selectedCharacter, selectedItem, selectedSecondaryItem, animation)
|
||||
public CharacterStateInfo(Vector2 pos, float? rotation, UInt16 ID, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, Vector2 targetMovement, AnimController.Animation animation = AnimController.Animation.None, bool ignorePlatforms = false)
|
||||
: this(pos, rotation, Vector2.Zero, 0.0f, ID, 0.0f, dir, selectedCharacter, selectedItem, selectedSecondaryItem, targetMovement, animation, ignorePlatforms)
|
||||
{
|
||||
}
|
||||
|
||||
protected CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, UInt16 ID, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, AnimController.Animation animation = AnimController.Animation.None)
|
||||
protected CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, UInt16 ID, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, Vector2 targetMovement, AnimController.Animation animation = AnimController.Animation.None, bool ignorePlatforms = false)
|
||||
: base(pos, rotation, velocity, angularVelocity, ID, time)
|
||||
{
|
||||
Direction = dir;
|
||||
SelectedCharacter = selectedCharacter;
|
||||
SelectedItem = selectedItem;
|
||||
SelectedSecondaryItem = selectedSecondaryItem;
|
||||
|
||||
IgnorePlatforms = ignorePlatforms;
|
||||
TargetMovement = targetMovement;
|
||||
Animation = animation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public bool HasCharacterInfo { get; private set; }
|
||||
|
||||
public Identifier Group { get; private set; }
|
||||
|
||||
public bool MatchesSpeciesNameOrGroup(Identifier speciesNameOrGroup) => Identifier == speciesNameOrGroup || Group == speciesNameOrGroup;
|
||||
|
||||
public void InheritFrom(CharacterPrefab parent)
|
||||
{
|
||||
@@ -52,9 +56,10 @@ namespace Barotrauma
|
||||
{
|
||||
CharacterInfoPrefab = new CharacterInfoPrefab(this, headsElement, varsElement, menuCategoryElement, pronounsElement);
|
||||
}
|
||||
Group = ConfigElement.GetAttributeIdentifier(nameof(Group), Identifier.Empty);
|
||||
}
|
||||
|
||||
private readonly XElement originalElement;
|
||||
private readonly ContentXElement originalElement;
|
||||
public ContentXElement ConfigElement { get; private set; }
|
||||
|
||||
public CharacterInfoPrefab CharacterInfoPrefab { get; private set; }
|
||||
|
||||
+34
-9
@@ -54,6 +54,11 @@ namespace Barotrauma
|
||||
activeEffectDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Armor penetration for status effects. Normally defined per attack, but that doesn't work on status effects.
|
||||
/// </summary>
|
||||
public float Penetration { get; set; }
|
||||
|
||||
private float _nonClampedStrength = -1;
|
||||
public float NonClampedStrength => _nonClampedStrength > 0 ? _nonClampedStrength : _strength;
|
||||
@@ -64,7 +69,7 @@ namespace Barotrauma
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)]
|
||||
public float Probability { get; set; } = 1.0f;
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Explosion damage is applied per each affected limb. Should this affliction damage be divided by the count of affected limbs (1-15) or applied in full? Default: true. Only affects explosions."), Editable]
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Explosion damage is applied per each affected limb. Should this affliction damage be divided by the count of affected limbs (1-15) or applied in full? Default: true. Only affects status effects and explosions."), Editable]
|
||||
public bool DivideByLimbCount { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Is the damage relative to the max vitality (percentage) or absolute (normal)"), Editable]
|
||||
@@ -120,6 +125,7 @@ namespace Barotrauma
|
||||
Probability = source.Probability;
|
||||
DivideByLimbCount = source.DivideByLimbCount;
|
||||
MultiplyByMaxVitality = source.MultiplyByMaxVitality;
|
||||
Penetration = source.Penetration;
|
||||
}
|
||||
|
||||
public void Serialize(XElement element)
|
||||
@@ -337,18 +343,24 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public float GetResistance(Identifier afflictionId)
|
||||
/// <summary>
|
||||
/// How much resistance to the specified affliction does this affliction currently give?
|
||||
/// </summary>
|
||||
public float GetResistance(Identifier afflictionId, LimbType limbType)
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
||||
var affliction = AfflictionPrefab.Prefabs[afflictionId];
|
||||
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
||||
if (currentEffect == null) { return 0.0f; }
|
||||
if (!currentEffect.ResistanceFor.Any(r =>
|
||||
r == affliction.Identifier ||
|
||||
r == affliction.AfflictionType))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
bool hasResistanceForAffliction = currentEffect.ResistanceFor.Any(identifier =>
|
||||
identifier == affliction.Identifier ||
|
||||
identifier == affliction.AfflictionType);
|
||||
if (!hasResistanceForAffliction) { return 0.0f; }
|
||||
|
||||
bool hasResistanceForLimb = limbType == LimbType.None || currentEffect.ResistanceLimbs.None() || currentEffect.ResistanceLimbs.Contains(limbType);
|
||||
if (!hasResistanceForLimb) { return 0.0f; }
|
||||
|
||||
return MathHelper.Lerp(
|
||||
currentEffect.MinResistance,
|
||||
currentEffect.MaxResistance,
|
||||
@@ -402,6 +414,8 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
//force an update when a periodic effect triggers to get it to trigger client-side
|
||||
characterHealth.Character.healthUpdateTimer = 0.0f;
|
||||
foreach (StatusEffect statusEffect in periodicEffect.StatusEffects)
|
||||
{
|
||||
ApplyStatusEffect(ActionType.OnActive, statusEffect, 1.0f, characterHealth, targetLimb);
|
||||
@@ -430,7 +444,7 @@ namespace Barotrauma
|
||||
}
|
||||
else if (currentEffect.StrengthChange > 0) // Reduce strengthening of afflictions if resistant
|
||||
{
|
||||
_strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab));
|
||||
_strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab, targetLimb?.type ?? LimbType.None));
|
||||
}
|
||||
// Don't use the property, because it's virtual and some afflictions like husk overload it for external use.
|
||||
_strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength);
|
||||
@@ -441,6 +455,17 @@ namespace Barotrauma
|
||||
ApplyStatusEffect(ActionType.OnActive, statusEffect, deltaTime, characterHealth, targetLimb);
|
||||
}
|
||||
|
||||
if (currentEffect.ConvulseAmount > 0f)
|
||||
{
|
||||
foreach (Limb limb in characterHealth.Character.AnimController.Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (limb.Hidden) { continue; }
|
||||
float force = Rand.Value() * limb.Mass * currentEffect.ConvulseAmount;
|
||||
limb.body.ApplyLinearImpulse(Rand.Vector(force), maxVelocity: Networking.NetConfig.MaxPhysicsBodyVelocity * 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
float amount = deltaTime;
|
||||
if (Prefab.GrainBurst > 0)
|
||||
{
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
|
||||
{
|
||||
base.Update(characterHealth, targetLimb, deltaTime);
|
||||
float bloodlossResistance = GetResistance(characterHealth.BloodlossAffliction.Identifier);
|
||||
float bloodlossResistance = characterHealth.GetResistance(characterHealth.BloodlossAffliction.Prefab, targetLimb?.type ?? LimbType.None);
|
||||
characterHealth.BloodlossAmount += Strength * (1.0f - bloodlossResistance) / 60.0f * deltaTime;
|
||||
if (Source != null)
|
||||
{
|
||||
|
||||
+74
-46
@@ -28,8 +28,6 @@ namespace Barotrauma
|
||||
|
||||
private bool stun = false;
|
||||
|
||||
private readonly List<Affliction> huskInfection = new List<Affliction>();
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes), Editable]
|
||||
public override float Strength
|
||||
{
|
||||
@@ -62,7 +60,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private readonly AfflictionPrefabHusk HuskPrefab;
|
||||
public readonly AfflictionPrefabHusk HuskPrefab;
|
||||
|
||||
private float DormantThreshold => HuskPrefab.DormantThreshold;
|
||||
private float ActiveThreshold => HuskPrefab.ActiveThreshold;
|
||||
@@ -129,7 +127,7 @@ namespace Barotrauma
|
||||
{
|
||||
State = InfectionState.Final;
|
||||
ActivateHusk();
|
||||
ApplyDamage(deltaTime, applyForce: true);
|
||||
ApplyDamage(deltaTime);
|
||||
character.SetStun(5);
|
||||
}
|
||||
}
|
||||
@@ -192,27 +190,41 @@ namespace Barotrauma
|
||||
prevDisplayedMessage = State;
|
||||
}
|
||||
|
||||
private void ApplyDamage(float deltaTime, bool applyForce)
|
||||
private const float DamageCooldown = 0.1f;
|
||||
private float damageCooldownTimer;
|
||||
private void ApplyDamage(float deltaTime)
|
||||
{
|
||||
int limbCount = character.AnimController.Limbs.Count(l => !l.IgnoreCollisions && !l.IsSevered && !l.Hidden);
|
||||
if (damageCooldownTimer > 0)
|
||||
{
|
||||
damageCooldownTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
damageCooldownTimer = DamageCooldown;
|
||||
int limbCount = character.AnimController.Limbs.Count(IsValidLimb);
|
||||
foreach (Limb limb in character.AnimController.Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (limb.Hidden) { continue; }
|
||||
if (!IsValidLimb(limb)) { continue; }
|
||||
float random = Rand.Value();
|
||||
huskInfection.Clear();
|
||||
huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount));
|
||||
if (random == 0) { continue; }
|
||||
const float damageRate = 2;
|
||||
float dmg = random / limbCount * damageRate;
|
||||
character.LastDamageSource = null;
|
||||
float force = applyForce ? random * 0.5f * limb.Mass : 0;
|
||||
character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, Rand.Vector(force));
|
||||
var afflictions = AfflictionPrefab.InternalDamage.Instantiate(dmg).ToEnumerable();
|
||||
const float forceMultiplier = 5;
|
||||
float force = dmg * limb.Mass * forceMultiplier;
|
||||
character.DamageLimb(limb.WorldPosition, limb, afflictions, stun: 0, playSound: false, Rand.Vector(force), ignoreDamageOverlay: true, recalculateVitality: false);
|
||||
}
|
||||
character.CharacterHealth.RecalculateVitality();
|
||||
|
||||
static bool IsValidLimb(Limb limb) => !limb.IgnoreCollisions && !limb.IsSevered && !limb.Hidden;
|
||||
}
|
||||
|
||||
public void ActivateHusk()
|
||||
{
|
||||
if (huskAppendage == null && character.Params.UseHuskAppendage)
|
||||
{
|
||||
huskAppendage = AttachHuskAppendage(character, Prefab as AfflictionPrefabHusk);
|
||||
var huskAffliction = Prefab as AfflictionPrefabHusk;
|
||||
huskAppendage = AttachHuskAppendage(character, huskAffliction, GetHuskedSpeciesName(character.Params, huskAffliction));
|
||||
}
|
||||
|
||||
if (Prefab is AfflictionPrefabHusk { NeedsAir: false })
|
||||
@@ -287,7 +299,7 @@ namespace Barotrauma
|
||||
Entity.Spawner.AddEntityToRemoveQueue(character);
|
||||
UnsubscribeFromDeathEvent();
|
||||
|
||||
Identifier huskedSpeciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk);
|
||||
Identifier huskedSpeciesName = GetHuskedSpeciesName(character.Params, Prefab as AfflictionPrefabHusk);
|
||||
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
|
||||
|
||||
if (prefab == null)
|
||||
@@ -309,11 +321,16 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var husk = Character.Create(huskedSpeciesName, character.WorldPosition, ToolBox.RandomSeed(8), huskCharacterInfo, isRemotePlayer: false, hasAi: true);
|
||||
if (character.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
|
||||
{
|
||||
husk.AddAbilityFlag(AbilityFlags.IgnoredByEnemyAI);
|
||||
}
|
||||
if (husk.Info != null)
|
||||
{
|
||||
husk.Info.Character = husk;
|
||||
husk.Info.TeamID = CharacterTeamType.None;
|
||||
}
|
||||
husk.AllowPlayDead = character.AllowPlayDead;
|
||||
|
||||
if (Prefab is AfflictionPrefabHusk huskPrefab)
|
||||
{
|
||||
@@ -379,17 +396,15 @@ namespace Barotrauma
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
|
||||
public static List<Limb> AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null)
|
||||
public static List<Limb> AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, Identifier huskedSpeciesName, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null)
|
||||
{
|
||||
var appendage = new List<Limb>();
|
||||
Identifier nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction);
|
||||
Identifier huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction);
|
||||
var appendageLimbs = new List<Limb>();
|
||||
CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
|
||||
if (huskPrefab?.ConfigElement == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!",
|
||||
contentPackage: matchingAffliction.ContentPackage);
|
||||
return appendage;
|
||||
return appendageLimbs;
|
||||
}
|
||||
var mainElement = huskPrefab.ConfigElement;
|
||||
var element = appendageDefinition;
|
||||
@@ -401,28 +416,25 @@ namespace Barotrauma
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{matchingAffliction.Identifier}'!",
|
||||
contentPackage: matchingAffliction.ContentPackage);
|
||||
return appendage;
|
||||
return appendageLimbs;
|
||||
}
|
||||
ContentPath pathToAppendage = element.GetAttributeContentPath("path") ?? ContentPath.Empty;
|
||||
XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage);
|
||||
if (doc == null) { return appendage; }
|
||||
if (ragdoll == null)
|
||||
{
|
||||
ragdoll = character.AnimController;
|
||||
}
|
||||
if (doc == null) { return appendageLimbs; }
|
||||
ragdoll ??= character.AnimController;
|
||||
if (ragdoll.Dir < 1.0f)
|
||||
{
|
||||
ragdoll.Flip();
|
||||
}
|
||||
|
||||
var root = doc.Root.FromPackage(pathToAppendage.ContentPackage);
|
||||
var limbElements = root.GetChildElements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e);
|
||||
var limbElements = root.GetChildElements("limb").ToDictionary(e => e.GetAttributeInt("id", -1), e => e);
|
||||
//the IDs may need to be offset if the character has other extra appendages (e.g. from gene splicing)
|
||||
//that take up the IDs of this appendage
|
||||
int idOffset = 0;
|
||||
int? idOffset = null;
|
||||
foreach (var jointElement in root.GetChildElements("joint"))
|
||||
{
|
||||
if (!limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement)) { continue; }
|
||||
if (!limbElements.TryGetValue(jointElement.GetAttributeInt("limb2", -1), out ContentXElement limbElement)) { continue; }
|
||||
|
||||
var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams);
|
||||
Limb attachLimb = null;
|
||||
@@ -444,38 +456,54 @@ namespace Barotrauma
|
||||
}
|
||||
if (attachLimb != null)
|
||||
{
|
||||
jointParams.Limb1 = attachLimb.Params.ID;
|
||||
//the joint attaches to a limb outside the character's normal limb count = to another part of the appendage
|
||||
// -> if the appendage's IDs have been offset, we need to take that into account to attach to the correct limb
|
||||
if (jointParams.Limb1 >= ragdoll.RagdollParams.Limbs.Count)
|
||||
{
|
||||
jointParams.Limb1 += idOffset;
|
||||
}
|
||||
var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams);
|
||||
if (idOffset == 0)
|
||||
idOffset ??= ragdoll.Limbs.Length - appendageLimbParams.ID;
|
||||
jointParams.Limb1 = attachLimb.Params.ID;
|
||||
//the joint attaches to one of the limbs we're creating = to another part of the appendage
|
||||
// -> if the appendage's IDs have been offset, we need to take that into account to attach to the correct limb
|
||||
if (limbElements.ContainsKey(jointParams.Limb1))
|
||||
{
|
||||
idOffset = ragdoll.Limbs.Length - appendageLimbParams.ID;
|
||||
jointParams.Limb1 += idOffset.Value;
|
||||
}
|
||||
jointParams.Limb2 = appendageLimbParams.ID = ragdoll.Limbs.Length;
|
||||
Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams);
|
||||
if (limbElements.ContainsKey(jointParams.Limb2))
|
||||
{
|
||||
jointParams.Limb2 += idOffset.Value;
|
||||
}
|
||||
Limb huskAppendage =
|
||||
//check if this joint is supposed to attach to a limb we already created
|
||||
appendageLimbs.Find(limb => limb.Params.ID == appendageLimbParams.ID) ??
|
||||
//if not, create a new limb
|
||||
new Limb(ragdoll, character, appendageLimbParams);
|
||||
huskAppendage.body.Submarine = character.Submarine;
|
||||
huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation);
|
||||
ragdoll.AddLimb(huskAppendage);
|
||||
ragdoll.AddJoint(jointParams);
|
||||
appendage.Add(huskAppendage);
|
||||
}
|
||||
appendageLimbs.Add(huskAppendage);
|
||||
}
|
||||
}
|
||||
return appendage;
|
||||
return appendageLimbs;
|
||||
}
|
||||
|
||||
public static Identifier GetHuskedSpeciesName(Identifier speciesName, AfflictionPrefabHusk prefab)
|
||||
public static Identifier GetHuskedSpeciesName(CharacterParams character, AfflictionPrefabHusk prefab)
|
||||
{
|
||||
return new Identifier(speciesName.Value + prefab.HuskedSpeciesName.Value);
|
||||
Identifier huskedSpecies = character.HuskedSpecies;
|
||||
if (huskedSpecies.IsEmpty)
|
||||
{
|
||||
// Default pattern: Crawler -> Crawlerhusk, Human -> Humanhusk
|
||||
return new Identifier(character.SpeciesName.Value + prefab.HuskedSpeciesName.Value);
|
||||
}
|
||||
return huskedSpecies;
|
||||
}
|
||||
|
||||
public static Identifier GetNonHuskedSpeciesName(Identifier huskedSpeciesName, AfflictionPrefabHusk prefab)
|
||||
public static Identifier GetNonHuskedSpeciesName(CharacterParams character, AfflictionPrefabHusk prefab)
|
||||
{
|
||||
return huskedSpeciesName.Remove(prefab.HuskedSpeciesName);
|
||||
Identifier nonHuskedSpecies = character.NonHuskedSpecies;
|
||||
if (nonHuskedSpecies.IsEmpty)
|
||||
{
|
||||
// Default pattern: Crawlerhusk -> Crawler, Humanhusk -> Human
|
||||
return character.SpeciesName.Remove(prefab.HuskedSpeciesName);
|
||||
}
|
||||
return nonHuskedSpecies;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+48
-4
@@ -329,6 +329,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public readonly ImmutableArray<Identifier> ResistanceFor;
|
||||
|
||||
/// <summary>
|
||||
/// List of limb types that the resistance applies to. If empty, the resistance applies to the whole body.
|
||||
/// </summary>
|
||||
public readonly ImmutableArray<LimbType> ResistanceLimbs;
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.No,
|
||||
description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's lowest strength.")]
|
||||
public float MinResistance { get; private set; }
|
||||
@@ -359,6 +364,18 @@ namespace Barotrauma
|
||||
description: "Color to tint the affected character's entire body with at this effect's highest strength. The alpha channel is used to determine how much to tint the character.")]
|
||||
public Color MaxBodyTint { get; private set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.No,
|
||||
description: "Range of the \"thermal goggles overlay\" enabled by the affliction.")]
|
||||
public float ThermalOverlayRange { get; private set; }
|
||||
|
||||
[Serialize("255,0,0,255", IsPropertySaveable.No,
|
||||
description: $"Color of the \"thermal goggles overlay\" enabled by the affliction. Only has an effect if {nameof(ThermalOverlayRange)} is larger than 0.")]
|
||||
public Color ThermalOverlayColor { get; private set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.No,
|
||||
description: "Multiplier for the convulsion/seizure effect on the character's ragdoll when this effect is active.")]
|
||||
public float ConvulseAmount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength.
|
||||
/// </summary>
|
||||
@@ -385,7 +402,7 @@ namespace Barotrauma
|
||||
public readonly float MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum value to apply
|
||||
/// Maximum value to apply
|
||||
/// </summary>
|
||||
public readonly float MaxValue;
|
||||
|
||||
@@ -423,6 +440,8 @@ namespace Barotrauma
|
||||
SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
ResistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty<Identifier>())!.ToImmutableArray();
|
||||
ResistanceLimbs = element.GetAttributeEnumArray<LimbType>("resistancelimbs", Array.Empty<LimbType>()).ToImmutableArray();
|
||||
|
||||
BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty<Identifier>())!.ToImmutableArray();
|
||||
|
||||
var afflictionStatValues = new Dictionary<StatTypes, AppliedStatValue>();
|
||||
@@ -594,7 +613,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
MinInterval = Math.Max(element.GetAttributeFloat(nameof(MinInterval), 1.0f), 1.0f);
|
||||
MinInterval = Math.Max(element.GetAttributeFloat(nameof(MinInterval), 1.0f), 0.1f);
|
||||
MaxInterval = Math.Max(element.GetAttributeFloat(nameof(MaxInterval), 1.0f), MinInterval);
|
||||
MinStrength = Math.Max(element.GetAttributeFloat(nameof(MinStrength), 0f), 0f);
|
||||
MaxStrength = Math.Max(element.GetAttributeFloat(nameof(MaxStrength), MinStrength), MinStrength);
|
||||
@@ -612,6 +631,7 @@ namespace Barotrauma
|
||||
public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier();
|
||||
public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier();
|
||||
public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier();
|
||||
public static readonly Identifier DisguisedAsHuskType = "disguiseashusk".ToIdentifier();
|
||||
|
||||
public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"];
|
||||
public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"];
|
||||
@@ -624,7 +644,8 @@ namespace Barotrauma
|
||||
public static AfflictionPrefab OrganDamage => Prefabs["organdamage"];
|
||||
public static AfflictionPrefab Stun => Prefabs[StunType];
|
||||
public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"];
|
||||
|
||||
public static AfflictionPrefab HuskInfection => Prefabs["huskinfection"];
|
||||
public static AfflictionPrefab JovianRadiation => Prefabs["jovianradiation"];
|
||||
|
||||
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
|
||||
|
||||
@@ -641,6 +662,11 @@ namespace Barotrauma
|
||||
private readonly LocalizedString defaultDescription;
|
||||
public readonly ImmutableList<Description> Descriptions;
|
||||
|
||||
/// <summary>
|
||||
/// Should the affliction's description be included in the tooltips on the affliction icons above the health bar?
|
||||
/// </summary>
|
||||
public readonly bool ShowDescriptionInTooltip;
|
||||
|
||||
/// <summary>
|
||||
/// Arbitrary string that is used to identify the type of the affliction.
|
||||
/// </summary>
|
||||
@@ -748,6 +774,12 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public readonly float TreatmentThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// How strong the affliction needs to be for treatment suggestions to be shown in the health interface.
|
||||
/// Defaults to <see cref="TreatmentThreshold"/>.
|
||||
/// </summary>
|
||||
public readonly float TreatmentSuggestionThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// Bots will not try to treat the affliction if the character has any of these afflictions
|
||||
/// </summary>
|
||||
@@ -880,6 +912,8 @@ namespace Barotrauma
|
||||
{
|
||||
defaultDescription = defaultDescription.Fallback(fallbackDescription);
|
||||
}
|
||||
ShowDescriptionInTooltip = element.GetAttributeBool(nameof(ShowDescriptionInTooltip), true);
|
||||
|
||||
IsBuff = element.GetAttributeBool(nameof(IsBuff), false);
|
||||
AffectMachines = element.GetAttributeBool(nameof(AffectMachines), true);
|
||||
|
||||
@@ -898,7 +932,10 @@ namespace Barotrauma
|
||||
|
||||
if (element.GetAttribute("nameidentifier") != null)
|
||||
{
|
||||
Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty)).Fallback(Name);
|
||||
string nameIdentifier = element.GetAttributeString("nameidentifier", string.Empty);
|
||||
Name = TextManager.Get(nameIdentifier)
|
||||
.Fallback(TextManager.Get($"AfflictionName.{nameIdentifier}"))
|
||||
.Fallback(Name);
|
||||
}
|
||||
|
||||
LimbSpecific = element.GetAttributeBool("limbspecific", false);
|
||||
@@ -914,6 +951,12 @@ namespace Barotrauma
|
||||
HideIconAfterDelay = element.GetAttributeBool(nameof(HideIconAfterDelay), false);
|
||||
|
||||
ActivationThreshold = element.GetAttributeFloat(nameof(ActivationThreshold), 0.0f);
|
||||
if (Identifier == StunType && ActivationThreshold > 0.0f)
|
||||
{
|
||||
ActivationThreshold = 0.0f;
|
||||
DebugConsole.AddWarning($"Error in affliction prefab {Identifier}: activation threshold of the stun affliction must be 0, because the strength of the affliction represents the length of the stun and any amount of stun has an effect.");
|
||||
}
|
||||
|
||||
ShowIconThreshold = element.GetAttributeFloat(nameof(ShowIconThreshold), Math.Max(ActivationThreshold, 0.05f));
|
||||
ShowIconToOthersThreshold = element.GetAttributeFloat(nameof(ShowIconToOthersThreshold), ShowIconThreshold);
|
||||
MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f);
|
||||
@@ -922,6 +965,7 @@ namespace Barotrauma
|
||||
ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold),
|
||||
Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold));
|
||||
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 10.0f));
|
||||
TreatmentSuggestionThreshold = element.GetAttributeFloat(nameof(TreatmentSuggestionThreshold), TreatmentThreshold);
|
||||
|
||||
DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f);
|
||||
BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f);
|
||||
|
||||
@@ -248,6 +248,12 @@ namespace Barotrauma
|
||||
/// Was the character in full health at the beginning of the frame?
|
||||
/// </summary>
|
||||
public bool WasInFullHealth { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show the blood overlay screen space effect when the character takes damage.
|
||||
/// Enabled normally, but can be disabled for some special cases.
|
||||
/// </summary>
|
||||
public bool ShowDamageOverlay = true;
|
||||
|
||||
public Affliction PressureAffliction
|
||||
{
|
||||
@@ -442,7 +448,7 @@ namespace Barotrauma
|
||||
return strength;
|
||||
}
|
||||
|
||||
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false)
|
||||
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false, bool recalculateVitality = true)
|
||||
{
|
||||
if (Character.GodMode) { return; }
|
||||
if (!ignoreUnkillability)
|
||||
@@ -456,12 +462,12 @@ namespace Barotrauma
|
||||
//if a limb-specific affliction is applied to no specific limb, apply to all limbs
|
||||
foreach (LimbHealth limbHealth in limbHealths)
|
||||
{
|
||||
AddLimbAffliction(limbHealth, affliction, allowStacking: allowStacking);
|
||||
AddLimbAffliction(limbHealth, limb: null, affliction, allowStacking: allowStacking, recalculateVitality: recalculateVitality);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddLimbAffliction(targetLimb, affliction, allowStacking: allowStacking);
|
||||
AddLimbAffliction(targetLimb, affliction, allowStacking: allowStacking, recalculateVitality: recalculateVitality);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -470,14 +476,17 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public float GetResistance(AfflictionPrefab afflictionPrefab)
|
||||
/// <summary>
|
||||
/// How much resistance all the afflictions the character has give to the specified affliction?
|
||||
/// </summary>
|
||||
public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
|
||||
{
|
||||
// This is a % resistance (0 to 1.0)
|
||||
float resistance = 0.0f;
|
||||
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
||||
{
|
||||
var affliction = kvp.Key;
|
||||
resistance += affliction.GetResistance(afflictionPrefab.Identifier);
|
||||
resistance += affliction.GetResistance(afflictionPrefab.Identifier, limbType);
|
||||
}
|
||||
// This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance
|
||||
float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab);
|
||||
@@ -610,7 +619,11 @@ namespace Barotrauma
|
||||
CalculateVitality();
|
||||
}
|
||||
|
||||
public void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking = true)
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="recalculateVitality">Set false only as an optimization when you manually call <see cref="RecalculateVitality"/>. Only applies to limb specific afflictions.</param>
|
||||
public void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking = true, bool recalculateVitality = true)
|
||||
{
|
||||
if (Unkillable || Character.GodMode) { return; }
|
||||
if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count)
|
||||
@@ -619,18 +632,19 @@ namespace Barotrauma
|
||||
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + hitLimb.type + " is targeting index " + hitLimb.HealthIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
foreach (Affliction newAffliction in attackResult.Afflictions)
|
||||
{
|
||||
if (newAffliction.Prefab.LimbSpecific)
|
||||
{
|
||||
AddLimbAffliction(hitLimb, newAffliction, allowStacking);
|
||||
AddLimbAffliction(hitLimb, newAffliction, allowStacking, recalculateVitality: recalculateVitality);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Always recalculate vitality for non-limb specific afflictions.
|
||||
AddAffliction(newAffliction, allowStacking);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void KillIfOutOfVitality()
|
||||
@@ -664,9 +678,8 @@ namespace Barotrauma
|
||||
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
|
||||
if (burnDamageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
|
||||
}
|
||||
|
||||
CalculateVitality();
|
||||
KillIfOutOfVitality();
|
||||
|
||||
RecalculateVitality();
|
||||
}
|
||||
|
||||
public float GetLimbDamage(Limb limb, Identifier afflictionType)
|
||||
@@ -697,12 +710,25 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAfflictions(Func<Affliction, bool> predicate)
|
||||
{
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
|
||||
foreach (var affliction in afflictionsToRemove)
|
||||
{
|
||||
afflictions.Remove(affliction);
|
||||
}
|
||||
CalculateVitality();
|
||||
}
|
||||
|
||||
public void RemoveAllAfflictions()
|
||||
{
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.Contains(a)));
|
||||
foreach (var affliction in afflictionsToRemove)
|
||||
{
|
||||
//set strength to 0 in case the affliction needs to react to becoming inactive
|
||||
affliction.Strength = 0.0f;
|
||||
afflictions.Remove(affliction);
|
||||
}
|
||||
foreach (Affliction affliction in irremovableAfflictions)
|
||||
@@ -731,7 +757,11 @@ namespace Barotrauma
|
||||
CalculateVitality();
|
||||
}
|
||||
|
||||
private void AddLimbAffliction(Limb limb, Affliction newAffliction, bool allowStacking = true)
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="recalculateVitality">Set false only as an optimization when you manually call <see cref="RecalculateVitality"/></param>
|
||||
private void AddLimbAffliction(Limb limb, Affliction newAffliction, bool allowStacking = true, bool recalculateVitality = true)
|
||||
{
|
||||
if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; }
|
||||
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
|
||||
@@ -740,11 +770,16 @@ namespace Barotrauma
|
||||
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
|
||||
return;
|
||||
}
|
||||
AddLimbAffliction(limbHealths[limb.HealthIndex], newAffliction, allowStacking);
|
||||
AddLimbAffliction(limbHealths[limb.HealthIndex], limb, newAffliction, allowStacking, recalculateVitality);
|
||||
}
|
||||
|
||||
private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction, bool allowStacking = true)
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="recalculateVitality">Set false only as an optimization when you manually call <see cref="RecalculateVitality"/></param>
|
||||
private void AddLimbAffliction(LimbHealth limbHealth, Limb limb, Affliction newAffliction, bool allowStacking = true, bool recalculateVitality = true)
|
||||
{
|
||||
LimbType limbType = limb?.type ?? LimbType.None;
|
||||
if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; }
|
||||
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
|
||||
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
|
||||
@@ -778,26 +813,29 @@ namespace Barotrauma
|
||||
|
||||
if (existingAffliction != null)
|
||||
{
|
||||
float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab));
|
||||
float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab, limbType));
|
||||
if (allowStacking)
|
||||
{
|
||||
// Add the existing strength
|
||||
newStrength += existingAffliction.Strength;
|
||||
}
|
||||
newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength);
|
||||
if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, true, true); }
|
||||
existingAffliction.Strength = newStrength;
|
||||
//set stun after setting the strength, because stun multipliers might want to set the strength to something else
|
||||
if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, allowStunDecrease: true, isNetworkMessage: true); }
|
||||
existingAffliction.Duration = existingAffliction.Prefab.Duration;
|
||||
if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
|
||||
CalculateVitality();
|
||||
KillIfOutOfVitality();
|
||||
if (recalculateVitality)
|
||||
{
|
||||
RecalculateVitality();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//create a new instance of the affliction to make sure we don't use the same instance for multiple characters
|
||||
//or modify the affliction instance of an Attack or a StatusEffect
|
||||
var copyAffliction = newAffliction.Prefab.Instantiate(
|
||||
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))),
|
||||
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType))),
|
||||
newAffliction.Source);
|
||||
afflictions.Add(copyAffliction, limbHealth);
|
||||
AchievementManager.OnAfflictionReceived(copyAffliction, Character);
|
||||
@@ -805,8 +843,10 @@ namespace Barotrauma
|
||||
|
||||
Character.HealthUpdateInterval = 0.0f;
|
||||
|
||||
CalculateVitality();
|
||||
KillIfOutOfVitality();
|
||||
if (recalculateVitality)
|
||||
{
|
||||
RecalculateVitality();
|
||||
}
|
||||
#if CLIENT
|
||||
if (OpenHealthWindow != this && limbHealth != null)
|
||||
{
|
||||
@@ -817,7 +857,7 @@ namespace Barotrauma
|
||||
|
||||
private void AddAffliction(Affliction newAffliction, bool allowStacking = true)
|
||||
{
|
||||
AddLimbAffliction(limbHealth: null, newAffliction, allowStacking);
|
||||
AddLimbAffliction(limbHealth: null, limb: null, newAffliction, allowStacking);
|
||||
}
|
||||
|
||||
partial void UpdateSkinTint();
|
||||
@@ -850,6 +890,8 @@ namespace Barotrauma
|
||||
affliction.Duration -= deltaTime;
|
||||
if (affliction.Duration <= 0.0f)
|
||||
{
|
||||
//set strength to 0 in case the affliction needs to react to becoming inactive
|
||||
affliction.Strength = 0.0f;
|
||||
afflictionsToRemove.Add(affliction);
|
||||
continue;
|
||||
}
|
||||
@@ -902,14 +944,15 @@ namespace Barotrauma
|
||||
if (!Character.GodMode)
|
||||
{
|
||||
#if CLIENT
|
||||
if (Character.IsVisible)
|
||||
updateVisualsTimer -= deltaTime;
|
||||
if (Character.IsVisible && updateVisualsTimer <= 0.0f)
|
||||
{
|
||||
UpdateLimbAfflictionOverlays();
|
||||
UpdateSkinTint();
|
||||
updateVisualsTimer = UpdateVisualsInterval;
|
||||
}
|
||||
#endif
|
||||
CalculateVitality();
|
||||
KillIfOutOfVitality();
|
||||
RecalculateVitality();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -941,7 +984,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// 0-1.
|
||||
/// </summary>
|
||||
public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab);
|
||||
public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab, LimbType.None);
|
||||
|
||||
private void UpdateOxygen(float deltaTime)
|
||||
{
|
||||
@@ -951,7 +994,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
|
||||
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab, LimbType.None);
|
||||
float prevOxygen = OxygenAmount;
|
||||
if (IsUnconscious)
|
||||
{
|
||||
@@ -991,13 +1034,13 @@ namespace Barotrauma
|
||||
CalculateVitality();
|
||||
}
|
||||
|
||||
public void CalculateVitality()
|
||||
private void CalculateVitality()
|
||||
{
|
||||
vitality = MaxVitality;
|
||||
IsParalyzed = false;
|
||||
if (Unkillable || Character.GodMode) { return; }
|
||||
|
||||
foreach (var (affliction, limbHealth) in afflictions)
|
||||
foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions)
|
||||
{
|
||||
float vitalityDecrease = affliction.GetVitalityDecrease(this);
|
||||
if (limbHealth != null)
|
||||
@@ -1020,6 +1063,12 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public void RecalculateVitality()
|
||||
{
|
||||
CalculateVitality();
|
||||
KillIfOutOfVitality();
|
||||
}
|
||||
|
||||
private static float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
|
||||
{
|
||||
@@ -1139,7 +1188,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
/// <param name="treatmentSuitability">A dictionary where the key is the identifier of the item and the value the suitability</param>
|
||||
/// <param name="predictFutureDuration">If above 0, the method will take into account how much currently active status effects while affect the afflictions in the next x seconds.</param>
|
||||
public void GetSuitableTreatments(Dictionary<Identifier, float> treatmentSuitability, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f)
|
||||
/// <param name="checkTreatmentThreshold">Should the method check whether the afflictions are above <see cref="AfflictionPrefab.TreatmentThreshold"/> (whether they're severe enough for AI to treat)?</param>
|
||||
/// <param name="checkTreatmentSuggestionThreshold">Should the method check whether the afflictions are above <see cref="AfflictionPrefab.TreatmentSuggestionThreshold"/> (whether treatment suggestions are shown in the health interface)?</param>
|
||||
public void GetSuitableTreatments(Dictionary<Identifier, float> treatmentSuitability, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false,
|
||||
bool checkTreatmentThreshold = true, bool checkTreatmentSuggestionThreshold = true,
|
||||
float predictFutureDuration = 0.0f)
|
||||
{
|
||||
//key = item identifier
|
||||
//float = suitability
|
||||
@@ -1190,7 +1243,14 @@ namespace Barotrauma
|
||||
//if this a suitable treatment, ignore it if the affliction isn't severe enough to treat
|
||||
//if the suitability is negative though, we need to take it into account!
|
||||
//otherwise we may end up e.g. giving too much opiates to someone already close to overdosing
|
||||
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
|
||||
if (checkTreatmentThreshold)
|
||||
{
|
||||
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
|
||||
}
|
||||
if (checkTreatmentSuggestionThreshold)
|
||||
{
|
||||
if (totalAfflictionStrength < affliction.Prefab.TreatmentSuggestionThreshold) { continue; }
|
||||
}
|
||||
}
|
||||
if (treatment.Value > strength)
|
||||
{
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Barotrauma
|
||||
private set
|
||||
{
|
||||
rawAfflictionIdentifierString = value;
|
||||
ParseAfflictionIdentifiers();
|
||||
parsedAfflictionIdentifiers = rawAfflictionIdentifierString.ToIdentifiers().ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Barotrauma
|
||||
private set
|
||||
{
|
||||
rawAfflictionTypeString = value;
|
||||
ParseAfflictionTypes();
|
||||
parsedAfflictionTypes = rawAfflictionTypeString.ToIdentifiers().ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,30 +119,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseAfflictionTypes()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawAfflictionTypeString))
|
||||
{
|
||||
parsedAfflictionTypes = Enumerable.Empty<Identifier>().ToImmutableArray();
|
||||
return;
|
||||
}
|
||||
|
||||
parsedAfflictionTypes = rawAfflictionTypeString.Split(',', ',')
|
||||
.Select(s => s.Trim()).ToIdentifiers().ToImmutableArray();
|
||||
}
|
||||
|
||||
private void ParseAfflictionIdentifiers()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawAfflictionIdentifierString))
|
||||
{
|
||||
parsedAfflictionIdentifiers = Enumerable.Empty<Identifier>().ToImmutableArray();
|
||||
return;
|
||||
}
|
||||
|
||||
parsedAfflictionIdentifiers = rawAfflictionIdentifierString.Split(',', ',')
|
||||
.Select(s => s.Trim()).ToIdentifiers().ToImmutableArray();
|
||||
}
|
||||
|
||||
public bool MatchesAfflictionIdentifier(string identifier) =>
|
||||
MatchesAfflictionIdentifier(identifier.ToIdentifier());
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -33,6 +34,12 @@ namespace Barotrauma
|
||||
[Serialize(0, IsPropertySaveable.No)]
|
||||
public int ExperiencePoints { get; private set; }
|
||||
|
||||
[Serialize(0, IsPropertySaveable.No)]
|
||||
public int BaseSalary { get; private set; }
|
||||
|
||||
[Serialize(1f, IsPropertySaveable.No)]
|
||||
public float SalaryMultiplier { get; private set; }
|
||||
|
||||
private readonly HashSet<Identifier> tags = new HashSet<Identifier>();
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
@@ -94,6 +101,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "If enabled, the NPC will not spawn if the specified spawn point tags can't be found.")]
|
||||
public bool RequireSpawnPointTag { get; protected set; }
|
||||
|
||||
[Serialize(CampaignMode.InteractionType.None, IsPropertySaveable.No)]
|
||||
public CampaignMode.InteractionType CampaignInteractionType { get; protected set; }
|
||||
|
||||
@@ -242,14 +252,20 @@ namespace Barotrauma
|
||||
foreach (var skill in characterInfo.Job.GetSkills())
|
||||
{
|
||||
float newSkill = skill.Level * SkillMultiplier;
|
||||
skill.IncreaseSkill(newSkill - skill.Level, increasePastMax: false);
|
||||
skill.IncreaseSkill(newSkill - skill.Level, canIncreasePastDefaultMaximumSkill: false);
|
||||
}
|
||||
characterInfo.Salary = characterInfo.CalculateSalary();
|
||||
}
|
||||
characterInfo.Salary = characterInfo.CalculateSalary(BaseSalary, SalaryMultiplier);
|
||||
characterInfo.HumanPrefabIds = (NpcSetIdentifier, Identifier);
|
||||
characterInfo.GiveExperience(ExperiencePoints);
|
||||
return characterInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Items marked to be spawned infinitely (by NPCs).
|
||||
/// </summary>
|
||||
private readonly Dictionary<Identifier, ItemPrefab> infiniteItems = new();
|
||||
public IReadOnlyCollection<ItemPrefab> InfiniteItems => infiniteItems.Values;
|
||||
|
||||
public static void InitializeItem(Character character, ContentXElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true)
|
||||
{
|
||||
@@ -314,9 +330,13 @@ namespace Barotrauma
|
||||
wifiComponent.TeamID = character.TeamID;
|
||||
}
|
||||
parentItem?.Combine(item, user: null);
|
||||
if (itemElement.GetAttributeBool(nameof(JobPrefab.JobItem.Infinite), false))
|
||||
{
|
||||
humanPrefab.infiniteItems.TryAdd(itemPrefab.Identifier, itemPrefab);
|
||||
}
|
||||
foreach (ContentXElement childItemElement in itemElement.Elements())
|
||||
{
|
||||
int amount = childItemElement.GetAttributeInt("amount", 1);
|
||||
int amount = childItemElement.GetAttributeInt(nameof(JobPrefab.JobItem.Amount), 1);
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
InitializeItem(character, childItemElement, submarine, humanPrefab, spawnPoint, item, createNetworkEvents);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using System;
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
@@ -21,9 +22,9 @@ namespace Barotrauma
|
||||
|
||||
public Skill PrimarySkill { get; private set; }
|
||||
|
||||
public Job(JobPrefab jobPrefab) : this(jobPrefab, randSync: Rand.RandSync.Unsynced, variant: 0) { }
|
||||
public Job(JobPrefab jobPrefab, bool isPvP) : this(jobPrefab, isPvP, randSync: Rand.RandSync.Unsynced, variant: 0) { }
|
||||
|
||||
public Job(JobPrefab jobPrefab, Rand.RandSync randSync, int variant, params Skill[] s)
|
||||
public Job(JobPrefab jobPrefab, bool isPvP, Rand.RandSync randSync, int variant, params Skill[] s)
|
||||
{
|
||||
prefab = jobPrefab;
|
||||
Variant = variant;
|
||||
@@ -40,7 +41,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
skill = new Skill(skillPrefab, randSync);
|
||||
skill = new Skill(skillPrefab, isPvP, randSync);
|
||||
skills.Add(skillPrefab.Identifier, skill);
|
||||
}
|
||||
if (skillPrefab.IsPrimarySkill) { PrimarySkill = skill; }
|
||||
@@ -74,11 +75,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static Job Random(Rand.RandSync randSync)
|
||||
public static Job Random(bool isPvP, Rand.RandSync randSync)
|
||||
{
|
||||
var prefab = JobPrefab.Random(randSync);
|
||||
var variant = Rand.Range(0, prefab.Variants, randSync);
|
||||
return new Job(prefab, randSync, variant);
|
||||
int variant = Rand.Range(0, prefab.Variants, randSync);
|
||||
return new Job(prefab, isPvP, randSync, variant);
|
||||
}
|
||||
|
||||
public IEnumerable<Skill> GetSkills()
|
||||
@@ -127,14 +128,24 @@ namespace Barotrauma
|
||||
new Skill(skillIdentifier, increase));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note: Does not automatically filter items by team or by game mode. See <see cref="JobItem.GetItemIdentifier(CharacterTeamType?, JobItem.GameModeType)"/>
|
||||
/// </summary>
|
||||
public bool HasJobItem(Func<JobPrefab.JobItem, bool> predicate) => prefab.HasJobItem(Variant, predicate);
|
||||
|
||||
public void GiveJobItems(Character character, WayPoint spawnPoint = null)
|
||||
public void GiveJobItems(Character character, bool isPvPMode, WayPoint spawnPoint = null)
|
||||
{
|
||||
if (!prefab.ItemSets.TryGetValue(Variant, out var spawnItems)) { return; }
|
||||
if (!prefab.JobItems.TryGetValue(Variant, out var spawnItems)) { return; }
|
||||
|
||||
foreach (XElement itemElement in spawnItems.GetChildElements("Item"))
|
||||
foreach (JobPrefab.JobItem jobItem in spawnItems)
|
||||
{
|
||||
InitializeJobItem(character, itemElement, spawnPoint);
|
||||
//spawn the "root items" here, InitializeJobItem goes through the children recursively
|
||||
if (jobItem.ParentItem != null) { continue; }
|
||||
for (int i = 0; i < jobItem.Amount; i++)
|
||||
{
|
||||
InitializeJobItem(character, isPvPMode, jobItem, spawnItems, spawnPoint);
|
||||
}
|
||||
}
|
||||
|
||||
if (GameMain.GameSession is { TraitorsEnabled: true } && character.IsSecurity)
|
||||
@@ -144,29 +155,14 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeJobItem(Character character, XElement itemElement, WayPoint spawnPoint = null, Item parentItem = null)
|
||||
private void InitializeJobItem(Character character, bool isPvPMode, JobPrefab.JobItem jobItem, IEnumerable<JobPrefab.JobItem> allJobItems, WayPoint spawnPoint = null, Item parentItem = null)
|
||||
{
|
||||
ItemPrefab itemPrefab;
|
||||
if (itemElement.Attribute("name") != null)
|
||||
Identifier itemIdentifier = jobItem.GetItemIdentifier(character.TeamID, isPvPMode);
|
||||
if (itemIdentifier.IsEmpty) { return; }
|
||||
if ((MapEntityPrefab.FindByIdentifier(itemIdentifier) ?? MapEntityPrefab.FindByName(itemIdentifier.Value)) is not ItemPrefab itemPrefab)
|
||||
{
|
||||
string itemName = itemElement.Attribute("name").Value;
|
||||
DebugConsole.ThrowErrorLocalized("Error in Job config (" + Name + ") - use item identifiers instead of names to configure the items.");
|
||||
itemPrefab = MapEntityPrefab.FindByName(itemName) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowErrorLocalized("Tried to spawn \"" + Name + "\" with the item \"" + itemName + "\". Matching item prefab not found.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
|
||||
itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowErrorLocalized("Tried to spawn \"" + Name + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
|
||||
return;
|
||||
}
|
||||
DebugConsole.ThrowErrorLocalized($"Tried to spawn \"{Name}\" with the item \"{itemIdentifier}\". Matching item prefab not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
Item item = new Item(itemPrefab, character.Position, null);
|
||||
@@ -187,7 +183,7 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
|
||||
if (itemElement.GetAttributeBool("equip", false))
|
||||
if (jobItem.Equip)
|
||||
{
|
||||
//if the item is both pickable and wearable, try to wear it instead of picking it up
|
||||
List<InvSlotType> allowedSlots =
|
||||
@@ -229,12 +225,18 @@ namespace Barotrauma
|
||||
wifiComponent.TeamID = character.TeamID;
|
||||
}
|
||||
|
||||
if (parentItem != null) { parentItem.Combine(item, user: null); }
|
||||
parentItem?.Combine(item, user: null);
|
||||
|
||||
foreach (XElement childItemElement in itemElement.Elements())
|
||||
foreach (JobPrefab.JobItem childItem in allJobItems)
|
||||
{
|
||||
InitializeJobItem(character, childItemElement, spawnPoint, item);
|
||||
}
|
||||
if (childItem.ParentItem == jobItem)
|
||||
{
|
||||
for (int i = 0; i < childItem.Amount; i++)
|
||||
{
|
||||
InitializeJobItem(character, isPvPMode, childItem, allJobItems, spawnPoint, parentItem: item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public XElement Save(XElement parentElement)
|
||||
|
||||
@@ -94,21 +94,73 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note: Does not automatically filter items by team or by game mode. See <see cref="JobItem.GetItemIdentifier(CharacterTeamType, bool)"/>
|
||||
/// </summary>
|
||||
public IEnumerable<JobItem> GetJobItems(int jobVariant, Func<JobItem, bool> predicate)
|
||||
=> JobItems.TryGetValue(jobVariant, out ImmutableArray<JobItem> items) ? items.Where(predicate) : Enumerable.Empty<JobItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Note: Does not automatically filter items by team or by game mode. See <see cref="JobItem.GetItemIdentifier(CharacterTeamType, bool)"/>
|
||||
/// </summary>
|
||||
public bool HasJobItem(int jobVariant, Func<JobItem, bool> predicate)
|
||||
=> JobItems.TryGetValue(jobVariant, out ImmutableArray<JobItem> items) && items.Any(predicate);
|
||||
|
||||
public class PreviewItem
|
||||
public class JobItem
|
||||
{
|
||||
public readonly Identifier ItemIdentifier;
|
||||
public readonly bool ShowPreview;
|
||||
|
||||
public PreviewItem(Identifier itemIdentifier, bool showPreview)
|
||||
public enum GameModeType
|
||||
{
|
||||
ItemIdentifier = itemIdentifier;
|
||||
ShowPreview = showPreview;
|
||||
Any, PvP, PvE
|
||||
}
|
||||
|
||||
public readonly Identifier ItemIdentifier;
|
||||
public readonly Identifier ItemIdentifierTeam2;
|
||||
public readonly bool ShowPreview;
|
||||
public readonly bool Equip;
|
||||
public readonly bool Outfit;
|
||||
public readonly int Amount;
|
||||
public readonly bool Infinite;
|
||||
|
||||
public readonly JobItem ParentItem;
|
||||
|
||||
public readonly GameModeType GameMode;
|
||||
|
||||
public JobItem(ContentXElement element, JobItem parentItem)
|
||||
{
|
||||
ItemIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
|
||||
ItemIdentifierTeam2 = element.GetAttributeIdentifier("identifierteam2", Identifier.Empty);
|
||||
ShowPreview = element.GetAttributeBool(nameof(ShowPreview), true);
|
||||
GameMode = element.GetAttributeEnum(nameof(GameMode), parentItem?.GameMode ?? GameModeType.Any);
|
||||
Amount = element.GetAttributeInt(nameof(Amount), 1);
|
||||
Equip = element.GetAttributeBool(nameof(Equip), false);
|
||||
Outfit = element.GetAttributeBool(nameof(Outfit), false);
|
||||
Infinite = element.GetAttributeBool(nameof(Infinite), false);
|
||||
ParentItem = parentItem;
|
||||
}
|
||||
|
||||
public Identifier GetItemIdentifier(CharacterTeamType team, bool isPvPMode)
|
||||
{
|
||||
switch (GameMode)
|
||||
{
|
||||
case GameModeType.PvP:
|
||||
if (!isPvPMode) { return Identifier.Empty; }
|
||||
break;
|
||||
case GameModeType.PvE:
|
||||
if (isPvPMode) { return Identifier.Empty; }
|
||||
break;
|
||||
}
|
||||
return
|
||||
team == CharacterTeamType.Team2 && !ItemIdentifierTeam2.IsEmpty ?
|
||||
ItemIdentifierTeam2 :
|
||||
ItemIdentifier;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly Dictionary<int, ContentXElement> ItemSets = new Dictionary<int, ContentXElement>();
|
||||
public readonly ImmutableDictionary<int, ImmutableArray<PreviewItem>> PreviewItems;
|
||||
/// <summary>
|
||||
/// The items the character can get when spawning. The key is the index of the job variant.
|
||||
/// </summary>
|
||||
public readonly ImmutableDictionary<int, ImmutableArray<JobItem>> JobItems;
|
||||
public readonly List<SkillPrefab> Skills = new List<SkillPrefab>();
|
||||
public readonly List<AutonomousObjective> AutonomousObjectives = new List<AutonomousObjective>();
|
||||
public readonly List<Identifier> AppropriateOrders = new List<Identifier>();
|
||||
@@ -145,6 +197,13 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(10, IsPropertySaveable.No, description: "Determines the order of the characters in the campaign setup ui.")]
|
||||
public int CampaignSetupUIOrder
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "If set to true, a client that has chosen this as their preferred job will get it regardless of the maximum number or the amount of spawnpoints in the sub.")]
|
||||
public bool AllowAlways
|
||||
{
|
||||
@@ -211,7 +270,7 @@ namespace Barotrauma
|
||||
Description = TextManager.Get("JobDescription." + Identifier);
|
||||
Element = element;
|
||||
|
||||
var previewItems = new Dictionary<int, List<PreviewItem>>();
|
||||
var jobItems = new Dictionary<int, List<JobItem>>();
|
||||
|
||||
int variant = 0;
|
||||
foreach (var subElement in element.Elements())
|
||||
@@ -219,9 +278,8 @@ namespace Barotrauma
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "itemset":
|
||||
ItemSets.Add(variant, subElement);
|
||||
previewItems[variant] = new List<PreviewItem>();
|
||||
loadItemIdentifiers(subElement, variant);
|
||||
jobItems[variant] = new List<JobItem>();
|
||||
loadJobItems(subElement, variant, parentItem: null);
|
||||
variant++;
|
||||
break;
|
||||
case "skills":
|
||||
@@ -246,35 +304,39 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
void loadItemIdentifiers(XElement parentElement, int variant)
|
||||
void loadJobItems(ContentXElement parentElement, int variant, JobItem parentItem)
|
||||
{
|
||||
foreach (XElement itemElement in parentElement.GetChildElements("Item"))
|
||||
foreach (ContentXElement itemElement in parentElement.GetChildElements("Item"))
|
||||
{
|
||||
if (itemElement.Element("name") != null)
|
||||
if (itemElement.GetAttribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items.");
|
||||
DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items.",
|
||||
contentPackage: parentElement.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
|
||||
Identifier itemIdentifier = itemElement.GetAttributeIdentifier("identifier", Identifier.Empty);
|
||||
JobItem jobItem = null;
|
||||
if (itemIdentifier.IsEmpty)
|
||||
{
|
||||
DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - item with no identifier.");
|
||||
DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - item with no identifier.",
|
||||
contentPackage: parentElement.ContentPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
previewItems[variant].Add(new PreviewItem(itemIdentifier, itemElement.GetAttributeBool("showpreview", true)));
|
||||
jobItem = new JobItem(itemElement, parentItem);
|
||||
jobItems[variant].Add(jobItem);
|
||||
}
|
||||
loadItemIdentifiers(itemElement, variant);
|
||||
loadJobItems(itemElement, variant, parentItem: jobItem);
|
||||
}
|
||||
}
|
||||
|
||||
PreviewItems = previewItems.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray()))
|
||||
JobItems = jobItems.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray()))
|
||||
.ToImmutableDictionary();
|
||||
|
||||
Variants = variant;
|
||||
|
||||
Skills.Sort((x,y) => y.LevelRange.Start.CompareTo(x.LevelRange.Start));
|
||||
Skills.Sort((x,y) => y.GetLevelRange(isPvP: false).Start.CompareTo(x.GetLevelRange(isPvP: false).Start));
|
||||
}
|
||||
|
||||
public static JobPrefab Random(Rand.RandSync sync, Func<JobPrefab, bool> predicate = null) => Prefabs.GetRandom(p => !p.HiddenJob && (predicate == null || predicate(p)), sync);
|
||||
|
||||
@@ -6,7 +6,10 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly Identifier Identifier;
|
||||
|
||||
public const float MaximumSkill = 100.0f;
|
||||
/// <summary>
|
||||
/// The "normal" maximum skill level. It's possible to go above this with certain talents, see <see cref="SkillSettings.MaximumSkillWithTalents"/>.
|
||||
/// </summary>
|
||||
public const float DefaultMaximumSkill = 100.0f;
|
||||
|
||||
private float level;
|
||||
|
||||
@@ -27,9 +30,21 @@ namespace Barotrauma
|
||||
|
||||
public LocalizedString DisplayName { get; private set; }
|
||||
|
||||
public void IncreaseSkill(float value, bool increasePastMax)
|
||||
/// <summary>
|
||||
/// Increase the skill level by a value. Handles clamping the level above the maximum.
|
||||
/// Note that if the skill level is already above maximum (if it for example has been set by console commands), it's allowed to stay at that level, but not to increase further.
|
||||
/// </summary>
|
||||
/// <param name="value">How much to increase the skill.</param>
|
||||
/// <param name="canIncreasePastDefaultMaximumSkill">Can the skill level increase above <see cref="DefaultMaximumSkill"/>, or can it go all the way to <see cref="SkillSettings.MaximumSkillWithTalents"/>?</param>
|
||||
public void IncreaseSkill(float value, bool canIncreasePastDefaultMaximumSkill)
|
||||
{
|
||||
Level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill);
|
||||
float currentMaximum = canIncreasePastDefaultMaximumSkill ? SkillSettings.Current.MaximumSkillWithTalents : DefaultMaximumSkill;
|
||||
if (Level > currentMaximum && value > 0)
|
||||
{
|
||||
//level above max already (set with console commands?), don't allow increasing it further and don't clamp it below max either
|
||||
return;
|
||||
}
|
||||
Level = MathHelper.Clamp(level + value, 0.0f, currentMaximum);
|
||||
}
|
||||
|
||||
private readonly Identifier iconJobId;
|
||||
@@ -40,10 +55,12 @@ namespace Barotrauma
|
||||
|
||||
public readonly float PriceMultiplier = 1.0f;
|
||||
|
||||
public Skill(SkillPrefab prefab, Rand.RandSync randSync)
|
||||
public Skill(SkillPrefab prefab, bool isPvP, Rand.RandSync randSync)
|
||||
{
|
||||
Identifier = prefab.Identifier;
|
||||
Level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync);
|
||||
|
||||
var levelRange = prefab.GetLevelRange(isPvP);
|
||||
Level = Rand.Range(levelRange.Start, levelRange.End, randSync);
|
||||
iconJobId = GetIconJobId();
|
||||
PriceMultiplier = prefab.PriceMultiplier;
|
||||
DisplayName = TextManager.Get("SkillName." + Identifier);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Xml.Linq;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -7,7 +6,8 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly Identifier Identifier;
|
||||
|
||||
public Range<float> LevelRange { get; private set; }
|
||||
private readonly Range<float> levelRange;
|
||||
private readonly Range<float> levelRangePvP;
|
||||
|
||||
/// <summary>
|
||||
/// How much this skill affects characters' hiring cost
|
||||
@@ -19,20 +19,33 @@ namespace Barotrauma
|
||||
public SkillPrefab(ContentXElement element)
|
||||
{
|
||||
Identifier = element.GetAttributeIdentifier("identifier", "");
|
||||
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 25.0f);
|
||||
var levelString = element.GetAttributeString("level", "");
|
||||
if (levelString.Contains(","))
|
||||
{
|
||||
var rangeVector2 = XMLExtensions.ParseVector2(levelString, false);
|
||||
LevelRange = new Range<float>(rangeVector2.X, rangeVector2.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
float skillLevel = float.Parse(levelString, System.Globalization.CultureInfo.InvariantCulture);
|
||||
LevelRange = new Range<float>(skillLevel, skillLevel);
|
||||
}
|
||||
|
||||
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 15.0f);
|
||||
levelRange = GetSkillRange("level", element, defaultValue: new Range<float>(0, 0));
|
||||
levelRangePvP = GetSkillRange("pvplevel", element, defaultValue: levelRange);
|
||||
IsPrimarySkill = element.GetAttributeBool("primary", false);
|
||||
|
||||
static Range<float> GetSkillRange(string attributeName, ContentXElement element, Range<float> defaultValue)
|
||||
{
|
||||
string levelString = element.GetAttributeString(attributeName, string.Empty);
|
||||
if (levelString.Contains(','))
|
||||
{
|
||||
var rangeVector2 = XMLExtensions.ParseVector2(levelString, false);
|
||||
return new Range<float>(rangeVector2.X, rangeVector2.Y);
|
||||
}
|
||||
else if (float.TryParse(levelString, NumberStyles.Any, CultureInfo.InvariantCulture, out float skillLevel))
|
||||
{
|
||||
return new Range<float>(skillLevel, skillLevel);
|
||||
}
|
||||
else
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Range<float> GetLevelRange(bool isPvP)
|
||||
{
|
||||
return isPvP ? levelRangePvP : levelRange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +213,9 @@ namespace Barotrauma
|
||||
public readonly Ragdoll ragdoll;
|
||||
public readonly LimbParams Params;
|
||||
|
||||
//the physics body of the limb
|
||||
/// <summary>
|
||||
/// The physics body of the limb
|
||||
/// </summary>
|
||||
public PhysicsBody body;
|
||||
|
||||
public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale;
|
||||
@@ -258,10 +260,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!mouthPos.HasValue)
|
||||
{
|
||||
mouthPos = Params.MouthPos;
|
||||
}
|
||||
mouthPos ??= Params.MouthPos;
|
||||
return mouthPos.Value;
|
||||
}
|
||||
set
|
||||
@@ -366,7 +365,7 @@ namespace Barotrauma
|
||||
if (isSevered)
|
||||
{
|
||||
ragdoll.SubtractMass(this);
|
||||
if (type == LimbType.Head)
|
||||
if (type == LimbType.Head && character.Params.Health.DieFromBeheading)
|
||||
{
|
||||
character.Kill(CauseOfDeathType.Unknown, null);
|
||||
}
|
||||
@@ -386,10 +385,18 @@ namespace Barotrauma
|
||||
|
||||
public Submarine Submarine => character?.Submarine;
|
||||
|
||||
private bool _hidden;
|
||||
public bool Hidden
|
||||
{
|
||||
get => Params.Hide;
|
||||
set => Params.Hide = value;
|
||||
get => _hidden || Params.Hide;
|
||||
set => _hidden = value;
|
||||
}
|
||||
|
||||
// Just a wrapper for Hidden, but both can be used via status effects, so it's not safe to remove it.
|
||||
public bool Hide
|
||||
{
|
||||
get => Hidden;
|
||||
set => Hidden = value;
|
||||
}
|
||||
|
||||
public Vector2 WorldPosition
|
||||
@@ -523,6 +530,9 @@ namespace Barotrauma
|
||||
|
||||
public readonly List<WearableSprite> WearingItems = new List<WearableSprite>();
|
||||
|
||||
/// <summary>
|
||||
/// Other wearables attached to the head. I.e. husk sprite, hair, beard, moustache, and face attachments.
|
||||
/// </summary>
|
||||
public readonly List<WearableSprite> OtherWearables = new List<WearableSprite>();
|
||||
|
||||
public bool PullJointEnabled
|
||||
@@ -636,7 +646,7 @@ namespace Barotrauma
|
||||
//if (character.Params.CanInteract) { return false; }
|
||||
if (this == character.AnimController.MainLimb) { return false; }
|
||||
bool canBeSevered = Params.CanBeSeveredAlive;
|
||||
if (character.AnimController.CanWalk)
|
||||
if (character.AnimController.CanWalk && !character.Params.Health.AllowSeveringLegs)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
@@ -671,7 +681,7 @@ namespace Barotrauma
|
||||
this.character = character;
|
||||
this.Params = limbParams;
|
||||
dir = Direction.Right;
|
||||
body = new PhysicsBody(limbParams);
|
||||
body = new PhysicsBody(limbParams, findNewContacts: false);
|
||||
type = limbParams.Type;
|
||||
IgnoreCollisions = limbParams.IgnoreCollisions;
|
||||
body.UserData = this;
|
||||
@@ -716,7 +726,7 @@ namespace Barotrauma
|
||||
var attackElement = character.Params.VariantFile.GetRootExcludingOverride().GetChildElement("attack");
|
||||
if (attackElement != null)
|
||||
{
|
||||
attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f);
|
||||
attack.SetInitialDamageMultiplier(attackElement.GetAttributeFloat("damagemultiplier", 1f));
|
||||
attack.RangeMultiplier = attackElement.GetAttributeFloat("rangemultiplier", 1f);
|
||||
attack.ImpactMultiplier = attackElement.GetAttributeFloat("impactmultiplier", 1f);
|
||||
}
|
||||
@@ -937,7 +947,7 @@ namespace Barotrauma
|
||||
severedFadeOutTimer = SeveredFadeOutTime;
|
||||
}
|
||||
}
|
||||
else if (!IsDead)
|
||||
else if (!IsDead && (character.IsPlayer || character.AIState is not AIState.PlayDead))
|
||||
{
|
||||
if (Params.BlinkFrequency > 0)
|
||||
{
|
||||
@@ -989,6 +999,7 @@ namespace Barotrauma
|
||||
public void ReEnable()
|
||||
{
|
||||
if (!temporarilyDisabled) { return; }
|
||||
temporarilyDisabled = false;
|
||||
Hidden = false;
|
||||
Disabled = false;
|
||||
IgnoreCollisions = originalIgnoreCollisions;
|
||||
@@ -1008,6 +1019,7 @@ namespace Barotrauma
|
||||
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
|
||||
bool wasRunning = attack.IsRunning;
|
||||
attack.UpdateAttackTimer(deltaTime, character);
|
||||
|
||||
if (attack.Blink)
|
||||
{
|
||||
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Any())
|
||||
@@ -1156,7 +1168,7 @@ namespace Barotrauma
|
||||
// Set the main collider where the body lands after the attack
|
||||
if (Vector2.DistanceSquared(character.AnimController.Collider.SimPosition, character.AnimController.MainLimb.body.SimPosition) > 0.1f * 0.1f)
|
||||
{
|
||||
character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation);
|
||||
character.AnimController.Collider.SetTransformIgnoreContacts(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation);
|
||||
}
|
||||
}
|
||||
return wasHit;
|
||||
@@ -1172,9 +1184,11 @@ namespace Barotrauma
|
||||
LastAttackSoundTime = SoundInterval;
|
||||
}
|
||||
#endif
|
||||
if (damageTarget is Character targetCharacter && targetLimb != null)
|
||||
{
|
||||
attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound, body, this);
|
||||
attack.ResetDamageMultiplier();
|
||||
attack.DamageMultiplier *= 1.0f + character.GetStatValue(attack.Ranged ? StatTypes.NaturalRangedAttackMultiplier : StatTypes.NaturalMeleeAttackMultiplier);
|
||||
if (damageTarget is Character && targetLimb != null)
|
||||
{
|
||||
attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, deltaTime: 1.0f, playSound, body, sourceLimb: this);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1184,7 +1198,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound, body, this);
|
||||
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, deltaTime: 1.0f, playSound, body, sourceLimb: this);
|
||||
}
|
||||
}
|
||||
/*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient))
|
||||
@@ -1434,6 +1448,7 @@ namespace Barotrauma
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
ragdoll.SubtractMass(this);
|
||||
body?.Remove();
|
||||
body = null;
|
||||
if (pullJoint != null)
|
||||
|
||||
+53
-12
@@ -44,9 +44,6 @@ namespace Barotrauma
|
||||
[Serialize(0f, IsPropertySaveable.Yes, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)]
|
||||
public float StepLiftAmount { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes), Editable]
|
||||
public bool MultiplyByDir { get; set; }
|
||||
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)]
|
||||
public float StepLiftOffset { get; set; }
|
||||
|
||||
@@ -56,6 +53,48 @@ namespace Barotrauma
|
||||
[Header("Movement")]
|
||||
[Serialize(0.75f, IsPropertySaveable.Yes, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)]
|
||||
public float BackwardsMovementMultiplier { get; set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Adjusts the maximum speed while climbing. The actual speed is affected by the MovementSpeed."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10f, DecimalCount = 2)]
|
||||
public float ClimbSpeed { get; set; }
|
||||
|
||||
[Serialize(2.0f, IsPropertySaveable.Yes, description: "Used instead of ClimbSpeed when descending ladders while moving fast (running). Not used if lower than ClimbSpeed."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10f, DecimalCount = 2)]
|
||||
public float SlideSpeed { get; set; }
|
||||
|
||||
[Serialize(10.5f, IsPropertySaveable.Yes, description: "Force applied to the main collider, torso and head, when climbing ladders."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
|
||||
public float ClimbBodyMoveForce { get; set; }
|
||||
|
||||
[Serialize(5.2f, IsPropertySaveable.Yes, description: "Force applied to the hands when climbing ladders."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
|
||||
public float ClimbHandMoveForce { get; set; }
|
||||
|
||||
[Serialize(10.0f, IsPropertySaveable.Yes, description: "Force applied to the feet when climbing ladders."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
|
||||
public float ClimbFootMoveForce { get; set; }
|
||||
|
||||
[Serialize(30.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
|
||||
public float ClimbStepHeight { get; set; }
|
||||
|
||||
protected override bool Deserialize(XElement element = null)
|
||||
{
|
||||
if (element.GetAttributeEnum(nameof(AnimationType), AnimationType.NotDefined) is AnimationType.Run)
|
||||
{
|
||||
// These values were previously hard-coded when running, so we need to set different default values for the run animations, when they are not defined.
|
||||
const string climbSpeedName = nameof(ClimbSpeed);
|
||||
if (element.GetAttribute(climbSpeedName) == null)
|
||||
{
|
||||
element.SetAttribute(climbSpeedName, 2.0f);
|
||||
}
|
||||
const string climbStepName = nameof(ClimbStepHeight);
|
||||
if (element.GetAttribute(climbStepName) == null)
|
||||
{
|
||||
element.SetAttribute(climbStepName, 60.0f);
|
||||
}
|
||||
const string slideSpeedName = nameof(SlideSpeed);
|
||||
if (element.GetAttribute(slideSpeedName) == null)
|
||||
{
|
||||
element.SetAttribute(slideSpeedName, 4.0f);
|
||||
}
|
||||
}
|
||||
return base.Deserialize(element);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SwimParams : AnimationParams
|
||||
@@ -92,7 +131,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// In degrees.
|
||||
/// </summary>
|
||||
[Header("Standing")]
|
||||
[Header("Orientation")]
|
||||
[Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)]
|
||||
public float HeadAngle
|
||||
{
|
||||
@@ -143,14 +182,14 @@ namespace Barotrauma
|
||||
public float HandIKStrength { get; set; }
|
||||
|
||||
public static string GetDefaultFileName(Identifier speciesName, AnimationType animType) => $"{speciesName.Value.CapitaliseFirstInvariant()}{animType}";
|
||||
public static string GetDefaultFile(Identifier speciesName, AnimationType animType) => Barotrauma.IO.Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml");
|
||||
public static string GetDefaultFilePath(Identifier speciesName, AnimationType animType) => Barotrauma.IO.Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml");
|
||||
|
||||
public static string GetFolder(Identifier speciesName)
|
||||
{
|
||||
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
||||
if (prefab?.ConfigElement == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'");
|
||||
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", contentPackage: prefab?.ContentPackage);
|
||||
return string.Empty;
|
||||
}
|
||||
return GetFolder(prefab.ConfigElement, prefab.FilePath.Value);
|
||||
@@ -275,25 +314,27 @@ namespace Barotrauma
|
||||
else if (string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
// Files found, but none specified -> Get a matching animation from the specified folder.
|
||||
// First try to find a file that matches the default file name. If that fails, just take any file.
|
||||
// First try to find a file that matches the default file name. If that fails, just take any file of the matching type.
|
||||
string defaultFileName = GetDefaultFileName(animSpecies, animType);
|
||||
selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, defaultFileName)) ?? filteredFiles.First();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try to get the specified file. If that fails, just take any file of the matching type.
|
||||
selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, fileName));
|
||||
if (selectedFile == null)
|
||||
{
|
||||
errorMessages.Add($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
|
||||
errorMessages.Add($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the first file of the matching type.");
|
||||
selectedFile = filteredFiles.First();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessages.Add($"[AnimationParams] Invalid directory: {folder}. Using the default animation.");
|
||||
}
|
||||
selectedFile ??= GetDefaultFile(fallbackSpecies, animType);
|
||||
selectedFile ??= GetDefaultFilePath(fallbackSpecies, animType);
|
||||
Debug.Assert(selectedFile != null);
|
||||
if (errorMessages.None())
|
||||
{
|
||||
@@ -375,7 +416,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (animationType == AnimationType.NotDefined)
|
||||
{
|
||||
throw new Exception("Cannot create an animation file of type " + animationType.ToString());
|
||||
throw new Exception("Cannot create an animation file of type " + animationType);
|
||||
}
|
||||
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
|
||||
{
|
||||
@@ -504,7 +545,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (doc == null)
|
||||
{
|
||||
DebugConsole.ThrowError("[AnimationParams] The source XML Document is null!");
|
||||
DebugConsole.ThrowError("[AnimationParams] The source XML Document is null!", contentPackage: Path.ContentPackage);
|
||||
return;
|
||||
}
|
||||
Serialize();
|
||||
|
||||
@@ -19,6 +19,16 @@ namespace Barotrauma
|
||||
{
|
||||
[Serialize("", IsPropertySaveable.Yes), Editable]
|
||||
public Identifier SpeciesName { get; private set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes), Editable]
|
||||
public string Tags
|
||||
{
|
||||
get => tags.ConvertToString();
|
||||
set => tags = value.ToIdentifiers().ToHashSet();
|
||||
}
|
||||
private HashSet<Identifier> tags = new HashSet<Identifier>();
|
||||
|
||||
public bool HasTag(Identifier tag) => tags.Contains(tag);
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "References to another species. Define only if the creature is a variant that needs to use a pre-existing translation."), Editable]
|
||||
public Identifier SpeciesTranslationOverride { get; private set; }
|
||||
@@ -37,9 +47,21 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Can the creature interact with items?"), Editable]
|
||||
public bool CanInteract { get; private set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Can the creature use ladders? Doesn't have an effect, if CanInteract is false."), Editable]
|
||||
public bool CanClimb { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "If set true, this character only uses the climbing parameters defined in the walk parameters (not run)."), Editable]
|
||||
public bool ForceSlowClimbing { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should this character be treated as a husk?"), Editable]
|
||||
public bool Husk { get; private set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "If this character can turn into a husk, which character it turns to? If not defined, uses the default pattern (e.g. Crawler -> Crawlerhusk, Human -> Humanhusk)."), Editable]
|
||||
public Identifier HuskedSpecies { get; private set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "If this character is a husk, from what species it can be turned into? If not defined, uses the default pattern (e.g. Crawlerhusk -> Crawler, Humanhusk -> Human)."), Editable]
|
||||
public Identifier NonHuskedSpecies { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description:"Should this character use a special husk appendage, attached to the ragdoll, when it turns into a husk?"), Editable]
|
||||
public bool UseHuskAppendage { get; private set; }
|
||||
@@ -125,7 +147,17 @@ namespace Barotrauma
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Identifier or tag of the item the character's items are placed inside when the character despawns."), Editable]
|
||||
public Identifier DespawnContainer { get; private set; }
|
||||
|
||||
[Serialize("monster", IsPropertySaveable.Yes, description: "If changed, this character will try to play a custom music track with the specified identifier when encountered."), Editable]
|
||||
public Identifier MusicType { get; private set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The commonness of this character's music when a random track will be chosen."), Editable]
|
||||
public float MusicCommonness { get; private set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The multiplier of the minimum distance required between this character and the player/submarine before the music starts playing. The default distance is twice the length of the submarine, or a minimum of 50 meters."), Editable]
|
||||
public float MusicRangeMultiplier { get; private set; }
|
||||
|
||||
public readonly CharacterFile File;
|
||||
public bool IsPet => AI?.IsPet ?? false;
|
||||
|
||||
public XDocument VariantFile { get; private set; }
|
||||
|
||||
@@ -161,7 +193,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static XElement CreateVariantXml(XElement variantXML, XElement baseXML)
|
||||
public static XElement CreateVariantXml(ContentXElement variantXML, ContentXElement baseXML)
|
||||
{
|
||||
XElement newXml = variantXML.CreateVariantXML(baseXML);
|
||||
XElement variantAi = variantXML.GetChildElement("ai");
|
||||
@@ -171,25 +203,32 @@ namespace Barotrauma
|
||||
{
|
||||
return newXml;
|
||||
}
|
||||
// CreateVariantXML seems to merge the ai targets so that in the new xml we have both the old and the new target definitions.
|
||||
|
||||
// CreateVariantXML does not understand anything about targeting tags, it just replaces the <target> elements in the order they're defined in.
|
||||
// We can do better here by replacing the target with a matching tag, so let's clear the element and do that.
|
||||
var finalAiElement = newXml.GetChildElement("ai");
|
||||
var processedTags = new HashSet<string>();
|
||||
foreach (var aiTarget in finalAiElement.Elements().ToArray())
|
||||
finalAiElement.Elements().Remove();
|
||||
|
||||
//add all the targets from the base character
|
||||
baseAi.Elements().ForEach(e => finalAiElement.Add(e));
|
||||
|
||||
var processedTags = new List<Identifier>();
|
||||
foreach (var variantTargetElement in variantAi.Elements())
|
||||
{
|
||||
string tag = aiTarget.GetAttributeString("tag", null);
|
||||
if (tag == null) { continue; }
|
||||
if (processedTags.Contains(tag))
|
||||
Identifier tag = variantTargetElement.GetAttributeIdentifier("tag", Identifier.Empty);
|
||||
var matchingElements = finalAiElement.Elements().Where(e => e.GetAttributeIdentifier("tag", Identifier.Empty) == tag);
|
||||
int alreadyProcessed = processedTags.Count(t => t == tag);
|
||||
if (matchingElements.Count() > alreadyProcessed)
|
||||
{
|
||||
aiTarget.Remove();
|
||||
continue;
|
||||
//more matching elements found, replace the first one that hasn't been processed yet
|
||||
matchingElements.Skip(alreadyProcessed).First().ReplaceWith(variantTargetElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
//no more matching elements in the base XML, this must be a new target
|
||||
finalAiElement.Add(variantTargetElement);
|
||||
}
|
||||
processedTags.Add(tag);
|
||||
var matchInSelf = variantAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag);
|
||||
var matchInParent = baseAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag);
|
||||
if (matchInSelf != null && matchInParent != null)
|
||||
{
|
||||
aiTarget.ReplaceWith(new XElement(matchInSelf));
|
||||
}
|
||||
}
|
||||
return newXml;
|
||||
}
|
||||
@@ -433,17 +472,11 @@ namespace Barotrauma
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Which tags are required for this sound to play?"), Editable()]
|
||||
public string Tags
|
||||
{
|
||||
get { return string.Join(',', TagSet); }
|
||||
private set
|
||||
{
|
||||
TagSet = value.Split(',')
|
||||
.ToIdentifiers()
|
||||
.Where(id => !id.IsEmpty)
|
||||
.ToImmutableHashSet();
|
||||
}
|
||||
get => TagSet.ConvertToString();
|
||||
private set => TagSet = value.ToIdentifiers().ToImmutableHashSet();
|
||||
}
|
||||
|
||||
public ImmutableHashSet<Identifier> TagSet { get; private set; }
|
||||
public ImmutableHashSet<Identifier> TagSet { get; private set; } = ImmutableHashSet<Identifier>.Empty;
|
||||
|
||||
public SoundParams(ContentXElement element, CharacterParams character) : base(element, character)
|
||||
{
|
||||
@@ -549,6 +582,15 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes), Editable]
|
||||
public float EmpVulnerability { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Apply movement penalties when legs or tail limbs get damaged. Enabled by default."), Editable]
|
||||
public bool ApplyMovementPenalties { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Normally characters die when they don't have a head. But maybe not all of them?"), Editable]
|
||||
public bool DieFromBeheading { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Severing legs doesn't work with most characters, because we'd need to take that into account with the walking animations and the standing position of the main collider etc. But there might be cases where you'll want to override this default."), Editable]
|
||||
public bool AllowSeveringLegs { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable]
|
||||
public bool ApplyAfflictionColors { get; private set; }
|
||||
@@ -719,34 +761,50 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: "Defines the method of checking whether there's a blocking (submarine) wall."), Editable]
|
||||
public WallTargetingMethod WallTargetingMethod { get; private set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes, "How likely it is that the creature plays dead (= ragdolls) while idling? Only allowed inside a sub (not in the open waters). Evaluated once, when the creature spawns."), Editable]
|
||||
public float PlayDeadProbability { get; set; }
|
||||
|
||||
public readonly bool IsPet;
|
||||
|
||||
public IEnumerable<TargetParams> Targets => targets;
|
||||
protected readonly List<TargetParams> targets = new List<TargetParams>();
|
||||
private readonly List<TargetParams> targets = new List<TargetParams>();
|
||||
|
||||
public AIParams(ContentXElement element, CharacterParams character) : base(element, character)
|
||||
{
|
||||
if (element == null) { return; }
|
||||
element.GetChildElements("target").ForEach(t => TryAddTarget(t, out _));
|
||||
element.GetChildElements("targetpriority").ForEach(t => TryAddTarget(t, out _));
|
||||
element.GetChildElements("target").ForEach(t => AddTarget(t));
|
||||
element.GetChildElements("targetpriority").ForEach(t => AddTarget(t));
|
||||
IsPet = element.GetChildElement("petbehavior") != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a target but checks for duplicates first. Doesn't allow adding multiple targets with the same tag (see <see cref="AddTarget"/>).
|
||||
/// </summary>
|
||||
private bool TryAddTarget(ContentXElement targetElement, out TargetParams target)
|
||||
{
|
||||
string tag = targetElement.GetAttributeString("tag", null);
|
||||
if (HasTag(tag))
|
||||
{
|
||||
target = null;
|
||||
DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!",
|
||||
targetElement.ContentPackage);
|
||||
return false;
|
||||
DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!", targetElement.ContentPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
target = new TargetParams(targetElement, Character);
|
||||
targets.Add(target);
|
||||
SubParams.Add(target);
|
||||
return true;
|
||||
target = AddTarget(targetElement);
|
||||
}
|
||||
return target != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method allows adding multiple targets with the same tag.
|
||||
/// </summary>
|
||||
private TargetParams AddTarget(ContentXElement targetElement)
|
||||
{
|
||||
var target = new TargetParams(targetElement, Character);
|
||||
targets.Add(target);
|
||||
SubParams.Add(target);
|
||||
return target;
|
||||
}
|
||||
|
||||
public bool TryAddEmptyTarget(out TargetParams targetParams) => TryAddNewTarget("newtarget" + targets.Count, AIState.Attack, 0f, out targetParams);
|
||||
@@ -782,26 +840,40 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public bool RemoveTarget(TargetParams target) => RemoveSubParam(target, targets);
|
||||
|
||||
public bool TryGetTarget(string targetTag, out TargetParams target)
|
||||
=> TryGetTarget(targetTag.ToIdentifier(), out target);
|
||||
|
||||
public bool TryGetTarget(Identifier targetTag, out TargetParams target)
|
||||
public IEnumerable<TargetParams> GetMatchingTargets(Func<TargetParams, bool> predicate) => targets.Where(predicate);
|
||||
public IEnumerable<TargetParams> GetTargets(Identifier target) => GetMatchingTargets(t => t.Tag == target);
|
||||
public IEnumerable<TargetParams> GetTargets(Character target) => GetMatchingTargets(t => t.Tag == target.SpeciesName || t.Tag == target.Params.Group || target.Params.HasTag(t.Tag));
|
||||
public TargetParams GetHighestPriorityTarget(Identifier target) => GetHighestPriorityTarget(GetTargets(target));
|
||||
public TargetParams GetHighestPriorityTarget(Character target) => GetHighestPriorityTarget(GetTargets(target));
|
||||
|
||||
private static TargetParams GetHighestPriorityTarget(IEnumerable<TargetParams> targetParams) => targetParams.MaxBy(static t => t.Priority);
|
||||
|
||||
public bool TryGetTargets(Identifier target, out IEnumerable<TargetParams> targetParams)
|
||||
{
|
||||
target = targets.FirstOrDefault(t => t.Tag == targetTag);
|
||||
return target != null;
|
||||
targetParams = GetTargets(target);
|
||||
return targetParams.Any();
|
||||
}
|
||||
|
||||
public bool TryGetTargets(Character target, out IEnumerable<TargetParams> targetParams)
|
||||
{
|
||||
targetParams = GetTargets(target);
|
||||
return targetParams.Any();
|
||||
}
|
||||
|
||||
public bool TryGetHighestPriorityTarget(Identifier target, out TargetParams targetParams)
|
||||
{
|
||||
targetParams = GetHighestPriorityTarget(target);
|
||||
return targetParams != null;
|
||||
}
|
||||
|
||||
public bool TryGetHighestPriorityTarget(Character target, out TargetParams targetParams)
|
||||
{
|
||||
targetParams = GetHighestPriorityTarget(target);
|
||||
return targetParams != null;
|
||||
}
|
||||
|
||||
public bool TryGetTarget(Character targetCharacter, out TargetParams target)
|
||||
{
|
||||
if (!TryGetTarget(targetCharacter.SpeciesName, out target))
|
||||
{
|
||||
target = targets.FirstOrDefault(t => t.Tag == targetCharacter.Params.Group);
|
||||
}
|
||||
return target != null;
|
||||
}
|
||||
|
||||
public bool TryGetTarget(IEnumerable<Identifier> tags, out TargetParams target)
|
||||
public bool TryGetHighestPriorityTarget(IEnumerable<Identifier> tags, out TargetParams target)
|
||||
{
|
||||
target = null;
|
||||
if (tags == null || tags.None()) { return false; }
|
||||
@@ -819,22 +891,6 @@ namespace Barotrauma
|
||||
}
|
||||
return target != null;
|
||||
}
|
||||
|
||||
public TargetParams GetTarget(string targetTag, bool throwError = true)
|
||||
=> GetTarget(targetTag.ToIdentifier(), throwError);
|
||||
|
||||
public TargetParams GetTarget(Identifier targetTag, bool throwError = true)
|
||||
{
|
||||
if (targetTag.IsEmpty) { return null; }
|
||||
if (!TryGetTarget(targetTag, out TargetParams target))
|
||||
{
|
||||
if (throwError)
|
||||
{
|
||||
DebugConsole.ThrowError($"Cannot find a target with the tag {targetTag}!");
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
public class TargetParams : SubParam
|
||||
@@ -889,7 +945,7 @@ namespace Barotrauma
|
||||
[Serialize(-1f, IsPropertySaveable.Yes, description: "A generic max threshold. Not used if set to negative."), Editable]
|
||||
public float ThresholdMax { get; private set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Can be used to make the monster perceive the target further than it normally can."), Editable]
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Can be used to make the monster perceive the target further or closer than it normally can."), Editable]
|
||||
public float PerceptionDistanceMultiplier { get; private set; }
|
||||
|
||||
[Serialize(-1.0f, IsPropertySaveable.Yes, description: "Maximum distance at which the monster can perceive the target, regardless of the sight/hearing or how visible or how much noise the target is making. Not used if set to negative."), Editable]
|
||||
|
||||
+51
-15
@@ -113,7 +113,7 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes), Editable]
|
||||
public bool CanWalk { get; set; }
|
||||
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Can the character be dragged around by other creatures?"), Editable()]
|
||||
public bool Draggable { get; set; }
|
||||
|
||||
@@ -137,15 +137,14 @@ namespace Barotrauma
|
||||
.Concat(Joints);
|
||||
|
||||
public static string GetDefaultFileName(Identifier speciesName) => $"{speciesName.Value.CapitaliseFirstInvariant()}DefaultRagdoll";
|
||||
public static string GetDefaultFile(Identifier speciesName, ContentPackage contentPackage = null)
|
||||
=> IO.Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName)}.xml");
|
||||
|
||||
public static string GetFolder(Identifier speciesName, ContentPackage contentPackage = null)
|
||||
public static string GetDefaultFile(Identifier speciesName) => IO.Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName)}.xml");
|
||||
|
||||
public static string GetFolder(Identifier speciesName)
|
||||
{
|
||||
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier == speciesName && (contentPackage == null || p.ContentFile.ContentPackage == contentPackage));
|
||||
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
||||
if (prefab?.ConfigElement == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", contentPackage: contentPackage);
|
||||
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'");
|
||||
return string.Empty;
|
||||
}
|
||||
return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value);
|
||||
@@ -199,10 +198,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!variantOf.IsEmpty && CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab prefab)
|
||||
else if (!variantOf.IsEmpty && CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab parentPrefab)
|
||||
{
|
||||
// Ragdoll element not defined -> use the ragdoll defined in the base definition file.
|
||||
ragdollSpecies = prefab.GetBaseCharacterSpeciesName(variantOf);
|
||||
//get the params from the parent prefab if this one doesn't re-define them
|
||||
return GetDefaultRagdollParams<T>(variantOf, parentPrefab.ConfigElement, parentPrefab.ContentPackage);
|
||||
}
|
||||
// Using a null file definition means we use the default animations found in the Ragdolls folder.
|
||||
return GetRagdollParams<T>(speciesName, ragdollSpecies, file: null, contentPackage);
|
||||
@@ -245,7 +244,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {ragdollInstance} from {contentPath.Value} for the character {speciesName}. Using the default ragdoll.", contentPackage: contentPackage);
|
||||
DebugConsole.ThrowError($"[RagdollParams] Failed to load a ragdoll {ragdollInstance} from {contentPath.Value} for the character {speciesName}. Using the default ragdoll.", contentPackage: contentPackage);
|
||||
}
|
||||
}
|
||||
// Seek the default ragdoll from the character's ragdoll folder.
|
||||
@@ -294,8 +293,30 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harder to debug. It's better to fail early.
|
||||
throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}.");
|
||||
string error = $"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}.";
|
||||
if (contentPackage == GameMain.VanillaContent)
|
||||
{
|
||||
// Check if the base character content package is vanilla too.
|
||||
CharacterPrefab characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
||||
if (characterPrefab?.ParentPrefab == null || characterPrefab.ParentPrefab.ContentPackage == GameMain.VanillaContent)
|
||||
{
|
||||
// If the error is in the vanilla content, it's just better to crash early.
|
||||
// If dodging with the solution below fails, we'll also get here.
|
||||
throw new Exception(error);
|
||||
}
|
||||
}
|
||||
// Try to dodge crashing on modded content.
|
||||
DebugConsole.ThrowError(error, contentPackage: contentPackage);
|
||||
if (typeof(T) == typeof(HumanRagdollParams))
|
||||
{
|
||||
Identifier fallbackSpecies = CharacterPrefab.HumanSpeciesName;
|
||||
r = GetRagdollParams<T>(fallbackSpecies, fallbackSpecies, file: ContentPath.FromRaw(contentPackage, "Content/Characters/Human/Ragdolls/HumanDefaultRagdoll.xml"), contentPackage: GameMain.VanillaContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
Identifier fallbackSpecies = "crawler".ToIdentifier();
|
||||
r = GetRagdollParams<T>(fallbackSpecies, fallbackSpecies, file: ContentPath.FromRaw(contentPackage, "Content/Characters/Crawler/Ragdolls/CrawlerDefaultRagdoll.xml"), contentPackage: GameMain.VanillaContent);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
@@ -654,7 +675,7 @@ namespace Barotrauma
|
||||
[Serialize(0.25f, IsPropertySaveable.Yes), Editable]
|
||||
public float Stiffness { get; set; }
|
||||
|
||||
[Serialize(1f, IsPropertySaveable.Yes, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable]
|
||||
[Serialize(1f, IsPropertySaveable.Yes, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable(DecimalCount = 2)]
|
||||
public float Scale { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No), Editable(ReadOnly = true)]
|
||||
@@ -705,6 +726,9 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(LimbType.None, IsPropertySaveable.Yes, description: "The limb type affects many things, like the animations. Torso or Head are considered as the main limbs. Every character should have at least one Torso or Head."), Editable()]
|
||||
public LimbType Type { get; set; }
|
||||
|
||||
[Serialize(LimbType.None, IsPropertySaveable.Yes, description: "Secondary limb type to be used for generic purposes. Currently only used in climbing animations."), Editable()]
|
||||
public LimbType SecondaryType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The orientation of the sprite as drawn on the sprite sheet (in radians).
|
||||
@@ -775,6 +799,12 @@ namespace Barotrauma
|
||||
|
||||
[Serialize("0, 0", IsPropertySaveable.Yes, description: "Relative offset for the mouth position (starting from the center). Only applicable for LimbType.Head. Used for eating."), Editable(DecimalCount = 2, MinValueFloat = -10f, MaxValueFloat = 10f)]
|
||||
public Vector2 MouthPos { get; set; }
|
||||
|
||||
[Serialize(50f, IsPropertySaveable.Yes, description: "How much torque is applied on the head while updating the eating animations?"), Editable]
|
||||
public float EatTorque { get; set; }
|
||||
|
||||
[Serialize(2f, IsPropertySaveable.Yes, description: "How strong a linear impulse is applied on the head while updating the eating animations?"), Editable]
|
||||
public float EatImpulse { get; set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes), Editable]
|
||||
public float ConstantTorque { get; set; }
|
||||
@@ -795,8 +825,11 @@ namespace Barotrauma
|
||||
[Serialize(10f, IsPropertySaveable.Yes, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)]
|
||||
public float SeveredFadeOutTime { get; set; } = 10.0f;
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Only applied when the limb is of type Tail. If none of the tails have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable]
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should the tail angle be applied on this limb? If none of the limbs have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable]
|
||||
public bool ApplyTailAngle { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should this limb be moved like a tail when swimming? Always true for tail limbs. On tails, disable by setting SineFrequencyMultiplier to 0."), Editable]
|
||||
public bool ApplySineMovement { get; set; }
|
||||
|
||||
[Serialize(1f, IsPropertySaveable.Yes), Editable(ValueStep = 0.1f, DecimalCount = 2)]
|
||||
public float SineFrequencyMultiplier { get; set; }
|
||||
@@ -857,6 +890,9 @@ namespace Barotrauma
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Can the limb enter submarines? Only valid if the ragdoll's CanEnterSubmarine is set to Partial, otherwise the limb can enter if the ragdoll can."), Editable]
|
||||
public bool CanEnterSubmarine { get; private set; }
|
||||
|
||||
[Serialize(LimbType.None, IsPropertySaveable.Yes, description: "When set to something else than None, this limb will be hidden if the limb of the specified type is hidden."), Editable]
|
||||
public LimbType InheritHiding { get; set; }
|
||||
|
||||
public LimbParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll)
|
||||
{
|
||||
var spriteElement = element.GetChildElement("sprite");
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(200.0f, IsPropertySaveable.Yes)]
|
||||
[Serialize(200.0f, IsPropertySaveable.Yes, description: "The \"absolute\" maximum skill level with talents that increase the default maximum.")]
|
||||
public float MaximumSkillWithTalents
|
||||
{
|
||||
get;
|
||||
|
||||
+3
-4
@@ -69,21 +69,20 @@ namespace Barotrauma.Abilities
|
||||
switch (targetType)
|
||||
{
|
||||
case TargetType.Enemy:
|
||||
return !HumanAIController.IsFriendly(character, targetCharacter);
|
||||
return !HumanAIController.IsFriendly(character, targetCharacter, onlySameTeam: false);
|
||||
case TargetType.Ally:
|
||||
return HumanAIController.IsFriendly(character, targetCharacter);
|
||||
return HumanAIController.IsFriendly(character, targetCharacter, onlySameTeam: true);
|
||||
case TargetType.NotSelf:
|
||||
return targetCharacter != character;
|
||||
case TargetType.Alive:
|
||||
return !targetCharacter.IsDead;
|
||||
case TargetType.Monster:
|
||||
return !targetCharacter.IsHuman;
|
||||
return !targetCharacter.IsHuman && !targetCharacter.IsPet;
|
||||
case TargetType.InFriendlySubmarine:
|
||||
return targetCharacter.Submarine != null && targetCharacter.Submarine.TeamID == character.TeamID;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+2
-21
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
@@ -7,31 +6,13 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
class AbilityConditionMission : AbilityConditionData
|
||||
{
|
||||
private readonly ImmutableHashSet<MissionType> missionType;
|
||||
private readonly ImmutableHashSet<Identifier> missionType;
|
||||
private readonly bool isAffiliated;
|
||||
|
||||
public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
|
||||
{
|
||||
string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!;
|
||||
HashSet<MissionType> missionTypes = new HashSet<MissionType>();
|
||||
missionType = conditionElement.GetAttributeIdentifierImmutableHashSet("missiontype", ImmutableHashSet<Identifier>.Empty)!;
|
||||
isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false);
|
||||
|
||||
foreach (string missionTypeString in missionTypeStrings)
|
||||
{
|
||||
if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None)
|
||||
{
|
||||
if (!isAffiliated)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.",
|
||||
contentPackage: conditionElement.ContentPackage);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
missionTypes.Add(parsedMission);
|
||||
}
|
||||
|
||||
missionType = missionTypes.ToImmutableHashSet();
|
||||
}
|
||||
|
||||
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ namespace Barotrauma.Abilities
|
||||
|
||||
protected override bool MatchesConditionSpecific()
|
||||
{
|
||||
foreach (Character c in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
foreach (Character c in Character.GetFriendlyCrew(character))
|
||||
{
|
||||
if (!c.IsDead && c.IsUnconscious)
|
||||
{
|
||||
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
@@ -14,8 +13,8 @@ namespace Barotrauma.Abilities
|
||||
|
||||
protected override bool MatchesConditionSpecific()
|
||||
{
|
||||
IEnumerable<Character> crewmembers = Character.GetFriendlyCrew(character);
|
||||
int differentCrewAmount = crewmembers.Select(c => c.Info?.Job?.Prefab.Identifier).Distinct().Count();
|
||||
IEnumerable<Character> crewMembers = Character.GetFriendlyCrew(character);
|
||||
int differentCrewAmount = crewMembers.Select(c => c.Info.Job?.Prefab.Identifier).Distinct().Count();
|
||||
return differentCrewAmount >= amount;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-8
@@ -1,18 +1,13 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
class AbilityConditionHasSkill : AbilityConditionDataless
|
||||
{
|
||||
private readonly string skillIdentifier;
|
||||
private readonly Identifier skillIdentifier;
|
||||
private readonly float minValue;
|
||||
|
||||
public AbilityConditionHasSkill(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
|
||||
{
|
||||
skillIdentifier = conditionElement.GetAttributeString("skillidentifier", string.Empty);
|
||||
skillIdentifier = conditionElement.GetAttributeIdentifier("skillidentifier", Identifier.Empty);
|
||||
minValue = conditionElement.GetAttributeFloat("minvalue", 0f);
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -1,6 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
@@ -14,7 +12,7 @@ namespace Barotrauma.Abilities
|
||||
|
||||
protected override bool MatchesConditionSpecific()
|
||||
{
|
||||
return Character.GetFriendlyCrew(character).Where(c => c.Info != null && (c.Info.GetCurrentLevel() - character.Info.GetCurrentLevel() >= levelsBehind)).Any();
|
||||
return Character.GetFriendlyCrew(character).Any(c => c.Info.GetCurrentLevel() - character.Info.GetCurrentLevel() >= levelsBehind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ namespace Barotrauma.Abilities
|
||||
protected override bool MatchesCharacter(Character character)
|
||||
{
|
||||
int ownLevel = character.Info.GetCurrentLevel();
|
||||
foreach (Character otherCharacter in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
foreach (Character otherCharacter in Character.GetFriendlyCrew(character))
|
||||
{
|
||||
if (otherCharacter == character) { continue; }
|
||||
if (otherCharacter.Info.GetCurrentLevel() < ownLevel) { return false; }
|
||||
|
||||
+5
-3
@@ -13,13 +13,15 @@
|
||||
{
|
||||
if (GameMain.GameSession == null) { return false; }
|
||||
|
||||
foreach (Character character in GameMain.GameSession.Casualties)
|
||||
foreach (Character deadCharacter in GameMain.GameSession.Casualties)
|
||||
{
|
||||
if (assistantsDontCount && character.Info?.Job?.Prefab.Identifier == "assistant")
|
||||
if (deadCharacter.TeamID != character.TeamID) { continue; }
|
||||
|
||||
if (assistantsDontCount && deadCharacter.Info?.Job?.Prefab.Identifier == "assistant")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (character.CauseOfDeath != null && character.CauseOfDeath.Type != CauseOfDeathType.Disconnected)
|
||||
if (deadCharacter.CauseOfDeath != null && deadCharacter.CauseOfDeath.Type != CauseOfDeathType.Disconnected)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
+2
-1
@@ -14,7 +14,8 @@
|
||||
float waterVolume = 0.0f, totalVolume = 0.0f;
|
||||
foreach (Hull hull in Hull.HullList)
|
||||
{
|
||||
if (hull.Submarine != character.Submarine) { continue; }
|
||||
if (hull.Submarine is not { } hullSubmarine) { continue; }
|
||||
if (hullSubmarine != character.Submarine || hullSubmarine.TeamID != character.TeamID) { continue; }
|
||||
waterVolume += hull.WaterVolume;
|
||||
totalVolume += hull.Volume;
|
||||
}
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ namespace Barotrauma.Abilities
|
||||
string type = abilityElement.Name.ToString().ToLowerInvariant();
|
||||
try
|
||||
{
|
||||
abilityType = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.Abilities", type, false, true);
|
||||
abilityType = ReflectionUtils.GetTypeWithBackwardsCompatibility(ToolBox.BarotraumaAssembly, "Barotrauma.Abilities", type, false, true);
|
||||
if (abilityType == null)
|
||||
{
|
||||
if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")",
|
||||
|
||||
+14
-6
@@ -19,6 +19,13 @@ namespace Barotrauma.Abilities
|
||||
|
||||
private bool effectBeingApplied;
|
||||
|
||||
/// <summary>
|
||||
/// Should the character who has the ability be marked as the "user" of the status effect?
|
||||
/// Means that e.g. enemies will consider damage from the effect to be coming from the character with the ability, and that the character will gain skills if the effect e.g. heals someone.
|
||||
/// </summary>
|
||||
|
||||
private readonly bool setUser;
|
||||
|
||||
public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects"));
|
||||
@@ -27,6 +34,7 @@ namespace Barotrauma.Abilities
|
||||
nearbyCharactersAppliesToSelf = abilityElement.GetAttributeBool("nearbycharactersappliestoself", true);
|
||||
nearbyCharactersAppliesToAllies = abilityElement.GetAttributeBool("nearbycharactersappliestoallies", true);
|
||||
nearbyCharactersAppliesToEnemies = abilityElement.GetAttributeBool("nearbycharactersappliestoenemies", true);
|
||||
setUser = abilityElement.GetAttributeBool("setuser", true);
|
||||
}
|
||||
|
||||
protected void ApplyEffectSpecific(Character targetCharacter, Limb targetLimb = null)
|
||||
@@ -44,7 +52,7 @@ namespace Barotrauma.Abilities
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
{
|
||||
// currently used to spawn items on the targeted character
|
||||
statusEffect.SetUser(targetCharacter);
|
||||
if (setUser) { statusEffect.SetUser(targetCharacter); }
|
||||
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter);
|
||||
}
|
||||
else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
@@ -57,28 +65,28 @@ namespace Barotrauma.Abilities
|
||||
}
|
||||
if (!nearbyCharactersAppliesToAllies)
|
||||
{
|
||||
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character));
|
||||
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character, onlySameTeam: true));
|
||||
}
|
||||
if (!nearbyCharactersAppliesToEnemies)
|
||||
{
|
||||
targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character));
|
||||
}
|
||||
statusEffect.SetUser(Character);
|
||||
if (setUser) { statusEffect.SetUser(Character); }
|
||||
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
|
||||
}
|
||||
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb) && targetLimb != null)
|
||||
{
|
||||
statusEffect.SetUser(Character);
|
||||
if (setUser) { statusEffect.SetUser(Character); }
|
||||
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetLimb);
|
||||
}
|
||||
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
statusEffect.SetUser(Character);
|
||||
if (setUser) { statusEffect.SetUser(Character); }
|
||||
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter);
|
||||
}
|
||||
else
|
||||
{
|
||||
statusEffect.SetUser(Character);
|
||||
if (setUser) { statusEffect.SetUser(Character); }
|
||||
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -26,9 +26,9 @@ namespace Barotrauma.Abilities
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
foreach (Character character in Character.GetFriendlyCrew(Character))
|
||||
{
|
||||
JobPrefab? characterJob = character.Info?.Job?.Prefab;
|
||||
JobPrefab? characterJob = character.Info.Job?.Prefab;
|
||||
if (characterJob is null) { continue; }
|
||||
|
||||
switch (characterJob.Identifier == apprenticeJob.Identifier)
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
foreach (Character otherCharacter in Character.GetFriendlyCrew(Character))
|
||||
{
|
||||
if (otherCharacter == Character) { continue; }
|
||||
otherCharacter.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true);
|
||||
otherCharacter.Info.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
+3
-3
@@ -4,14 +4,14 @@
|
||||
{
|
||||
private readonly Identifier afflictionId;
|
||||
private readonly float strength;
|
||||
private readonly string multiplyStrengthBySkill;
|
||||
private readonly Identifier multiplyStrengthBySkill;
|
||||
private readonly bool setValue;
|
||||
|
||||
public CharacterAbilityGiveAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
afflictionId = abilityElement.GetAttributeIdentifier("afflictionid", abilityElement.GetAttributeIdentifier("affliction", Identifier.Empty));
|
||||
strength = abilityElement.GetAttributeFloat("strength", 0f);
|
||||
multiplyStrengthBySkill = abilityElement.GetAttributeString("multiplystrengthbyskill", string.Empty);
|
||||
multiplyStrengthBySkill = abilityElement.GetAttributeIdentifier("multiplystrengthbyskill", Identifier.Empty);
|
||||
setValue = abilityElement.GetAttributeBool("setvalue", false);
|
||||
|
||||
if (afflictionId.IsEmpty)
|
||||
@@ -52,7 +52,7 @@
|
||||
return;
|
||||
}
|
||||
float strength = this.strength;
|
||||
if (!string.IsNullOrEmpty(multiplyStrengthBySkill))
|
||||
if (!multiplyStrengthBySkill.IsEmpty)
|
||||
{
|
||||
strength *= Character.GetSkillLevel(multiplyStrengthBySkill);
|
||||
}
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
foreach (Character c in Character.GetFriendlyCrew(Character))
|
||||
{
|
||||
c?.Info?.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
|
||||
c.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
+1
-2
@@ -20,9 +20,8 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
if (!addingFirstTime) { return; }
|
||||
|
||||
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
foreach (Character character in Character.GetFriendlyCrew(Character))
|
||||
{
|
||||
if (character.Info is null) { return; }
|
||||
character.Info.AdditionalTalentPoints += amount;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
private readonly StatTypes statType;
|
||||
private readonly float maxValue;
|
||||
private readonly string skillIdentifier;
|
||||
private readonly Identifier skillIdentifier;
|
||||
private readonly bool useAll;
|
||||
private float lastValue = 0f;
|
||||
public override bool AllowClientSimulation => true;
|
||||
@@ -15,7 +15,7 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier);
|
||||
maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f);
|
||||
skillIdentifier = abilityElement.GetAttributeString("skillidentifier", string.Empty);
|
||||
skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", Identifier.Empty);
|
||||
useAll = skillIdentifier == "all";
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -5,6 +5,8 @@
|
||||
private readonly float addedValue;
|
||||
private readonly float multiplyValue;
|
||||
|
||||
public override bool AllowClientSimulation => true;
|
||||
|
||||
public CharacterAbilityModifyValue(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f);
|
||||
|
||||
+23
-3
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
@@ -19,10 +19,30 @@ namespace Barotrauma.Abilities
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ApplyEffect()
|
||||
{
|
||||
ApplyEffectToCharacter(Character);
|
||||
}
|
||||
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
{
|
||||
if (abilityObject is not IAbilityCharacter character) { return; }
|
||||
character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount, attacker: Character);
|
||||
if (abilityObject is IAbilityCharacter characterData)
|
||||
{
|
||||
ApplyEffectToCharacter(characterData.Character);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyEffectToCharacter(Character character)
|
||||
{
|
||||
character?.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount, attacker: Character);
|
||||
}
|
||||
|
||||
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
|
||||
{
|
||||
if (conditionsMatched)
|
||||
{
|
||||
ApplyEffect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -6,6 +6,7 @@ internal class CharacterAbilityUpgradeSubmarine : CharacterAbility
|
||||
private readonly UpgradePrefab? upgradePrefab;
|
||||
private readonly UpgradeCategory? upgradeCategory;
|
||||
public readonly int level;
|
||||
private readonly bool giveOnAddingFirstTime;
|
||||
|
||||
public override bool AllowClientSimulation => true;
|
||||
|
||||
@@ -13,7 +14,8 @@ internal class CharacterAbilityUpgradeSubmarine : CharacterAbility
|
||||
{
|
||||
var prefabIdentifier = abilityElement.GetAttributeIdentifier(nameof(upgradePrefab), Identifier.Empty);
|
||||
var categoryIdentifier = abilityElement.GetAttributeIdentifier(nameof(upgradeCategory), Identifier.Empty);
|
||||
|
||||
giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None);
|
||||
|
||||
if (UpgradePrefab.Find(prefabIdentifier) is not { } foundUpgradePrefab)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityUpgradeSubmarine)} - {nameof(upgradePrefab)} not found.",
|
||||
@@ -47,6 +49,14 @@ internal class CharacterAbilityUpgradeSubmarine : CharacterAbility
|
||||
ApplyEffectSpecific();
|
||||
}
|
||||
|
||||
public override void InitializeAbility(bool addingFirstTime)
|
||||
{
|
||||
if (addingFirstTime && giveOnAddingFirstTime)
|
||||
{
|
||||
ApplyEffectSpecific();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyEffectSpecific()
|
||||
{
|
||||
if (upgradePrefab == null || upgradeCategory == null) { return; }
|
||||
|
||||
+6
-2
@@ -24,14 +24,18 @@ namespace Barotrauma.Abilities
|
||||
foreach (Character enemyCharacter in enemyCharacters)
|
||||
{
|
||||
if (!enemyCharacter.IsHuman) { continue; }
|
||||
if (enemyCharacter.Submarine == null || enemyCharacter.Submarine != Submarine.MainSub) { continue; }
|
||||
if (enemyCharacter.Submarine == null ||
|
||||
(Submarine.MainSub != null && enemyCharacter.Submarine != Submarine.MainSub))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (enemyCharacter.IsDead) { continue; }
|
||||
if (!enemyCharacter.LockHands) { continue; }
|
||||
Character.GiveMoney(moneyAmount);
|
||||
GameAnalyticsManager.AddMoneyGainedEvent(moneyAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier.Value);
|
||||
foreach (Character character in Character.GetFriendlyCrew(Character))
|
||||
{
|
||||
character.Info?.GiveExperience(experienceAmount);
|
||||
character.Info.GiveExperience(experienceAmount);
|
||||
}
|
||||
timesGiven++;
|
||||
if (max > 0 && timesGiven >= max) { break; }
|
||||
|
||||
+9
-8
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using Barotrauma.Extensions;
|
||||
@@ -31,7 +31,7 @@ namespace Barotrauma.Abilities
|
||||
|
||||
if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; }
|
||||
|
||||
ImmutableHashSet<Character> characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
var characters = Character.GetFriendlyCrew(Character);
|
||||
|
||||
HashSet<ImmutableHashSet<Identifier>> talentsTrees = new HashSet<ImmutableHashSet<Identifier>>();
|
||||
foreach (TalentSubTree subTree in talentTree.TalentSubTrees)
|
||||
@@ -60,13 +60,14 @@ namespace Barotrauma.Abilities
|
||||
talentsTrees.Add(identifiers.ToImmutableHashSet());
|
||||
}
|
||||
|
||||
ImmutableHashSet<Identifier> selectedTalentTree = talentsTrees.GetRandomUnsynced();
|
||||
|
||||
foreach (Identifier identifier in selectedTalentTree)
|
||||
ImmutableHashSet<Identifier>? selectedTalentTree = talentsTrees.GetRandomUnsynced();
|
||||
if (selectedTalentTree != null)
|
||||
{
|
||||
if (Character.HasTalent(identifier)) { continue; }
|
||||
|
||||
Character.GiveTalent(identifier);
|
||||
foreach (Identifier identifier in selectedTalentTree)
|
||||
{
|
||||
if (Character.HasTalent(identifier)) { continue; }
|
||||
Character.GiveTalent(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsShowCaseTalent(Identifier identifier, TalentOption option)
|
||||
|
||||
+19
-8
@@ -12,14 +12,20 @@
|
||||
internal class CharacterAbilityWarStories : CharacterAbility
|
||||
{
|
||||
private readonly Identifier targetStat;
|
||||
private readonly float minCondition;
|
||||
private readonly float normalQualityThreshold;
|
||||
private readonly float goodQualityThreshold;
|
||||
private readonly float excellentQualityThreshold;
|
||||
private readonly float masterworkQualityThreshold;
|
||||
|
||||
private readonly ItemPrefab prefab;
|
||||
|
||||
public CharacterAbilityWarStories(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
targetStat = abilityElement.GetAttributeIdentifier("target", Identifier.Empty);
|
||||
minCondition = abilityElement.GetAttributeFloat("mincondition", 1);
|
||||
normalQualityThreshold = abilityElement.GetAttributeFloat("normalqualitythreshold", 4);
|
||||
goodQualityThreshold = abilityElement.GetAttributeFloat("goodqualitythreshold", 10);
|
||||
excellentQualityThreshold = abilityElement.GetAttributeFloat("excellentqualitythreshold", 20);
|
||||
masterworkQualityThreshold = abilityElement.GetAttributeFloat("masterworkqualitythreshold", 30);
|
||||
|
||||
if (targetStat.IsEmpty)
|
||||
{
|
||||
@@ -37,23 +43,28 @@ internal class CharacterAbilityWarStories : CharacterAbility
|
||||
{
|
||||
if (prefab is null || Character is null) { return; }
|
||||
|
||||
float condition = Character.Info?.GetSavedStatValue(StatTypes.None, targetStat) ?? 0;
|
||||
if (condition < minCondition) { return; }
|
||||
float statValue = Character.Info?.GetSavedStatValue(StatTypes.None, targetStat) ?? 0;
|
||||
|
||||
if (statValue < normalQualityThreshold) { return; }
|
||||
|
||||
int quality = 0;
|
||||
if (statValue >= masterworkQualityThreshold) { quality = 3; }
|
||||
else if (statValue >= excellentQualityThreshold) { quality = 2; }
|
||||
else if (statValue >= goodQualityThreshold) { quality = 1; }
|
||||
|
||||
if (GameMain.GameSession?.RoundEnding ?? true)
|
||||
{
|
||||
Item item = new(prefab, Character.WorldPosition, Character.Submarine)
|
||||
{
|
||||
Condition = condition,
|
||||
HealthMultiplier = condition
|
||||
Quality = quality,
|
||||
};
|
||||
Character.Inventory.TryPutItem(item, Character, item.AllowedSlots);
|
||||
}
|
||||
else
|
||||
{
|
||||
Entity.Spawner?.AddItemToSpawnQueue(prefab, Character.Inventory, condition: condition, onSpawned: item =>
|
||||
Entity.Spawner?.AddItemToSpawnQueue(prefab, Character.Inventory, quality: quality, onSpawned: item =>
|
||||
{
|
||||
item.HealthMultiplier = condition;
|
||||
item.Quality = quality;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ namespace Barotrauma.Abilities
|
||||
string type = conditionElement.Name.ToString().ToLowerInvariant();
|
||||
try
|
||||
{
|
||||
conditionType = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.Abilities", type, false, true);
|
||||
conditionType = ReflectionUtils.GetTypeWithBackwardsCompatibility(ToolBox.BarotraumaAssembly, "Barotrauma.Abilities", type, false, true);
|
||||
if (conditionType == null)
|
||||
{
|
||||
if (errorMessages)
|
||||
|
||||
@@ -22,6 +22,11 @@ namespace Barotrauma
|
||||
|
||||
public readonly Sprite Icon;
|
||||
|
||||
/// <summary>
|
||||
/// When set to true, this talent will not be visible in the "Extra Talents" panel if it is not part of the character's job talent tree.
|
||||
/// </summary>
|
||||
public readonly bool IsHiddenExtraTalent;
|
||||
|
||||
/// <summary>
|
||||
/// When set to a value the talent tooltip will display a text showing the current value of the stat and the max value.
|
||||
/// For example "Progress: 37/100".
|
||||
@@ -62,6 +67,8 @@ namespace Barotrauma
|
||||
DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value);
|
||||
}
|
||||
|
||||
IsHiddenExtraTalent = element.GetAttributeBool("ishiddenextratalent", false);
|
||||
|
||||
Description = string.Empty;
|
||||
|
||||
#if CLIENT
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace Barotrauma
|
||||
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
|
||||
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
|
||||
|
||||
if (IsTalentLocked(talentIdentifier)) { return false; }
|
||||
if (IsTalentLocked(talentIdentifier, Character.GetFriendlyCrew(character))) { return false; }
|
||||
|
||||
if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier))
|
||||
{
|
||||
@@ -163,10 +163,8 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsTalentLocked(Identifier talentIdentifier, ImmutableHashSet<Character> characterList = null)
|
||||
public static bool IsTalentLocked(Identifier talentIdentifier, IEnumerable<Character> characterList)
|
||||
{
|
||||
characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
|
||||
foreach (Character c in characterList)
|
||||
{
|
||||
if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Items.Components;
|
||||
@@ -182,7 +182,10 @@ namespace Barotrauma
|
||||
To.Connection.DisconnectWire(wire);
|
||||
}
|
||||
// if EntitySpawner is not available
|
||||
wireItem.Remove();
|
||||
if (!wireItem.Removed)
|
||||
{
|
||||
wireItem.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
public static ItemPrefab DefaultWirePrefab => ItemPrefab.Prefabs[Tags.RedWire];
|
||||
|
||||
+5
-6
@@ -1,11 +1,9 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -55,18 +53,19 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (AfflictionPrefab.Prefabs.ContainsKey(identifier))
|
||||
if (AfflictionPrefab.Prefabs.TryGet(identifier, out var existingAffliction))
|
||||
{
|
||||
if (overriding)
|
||||
{
|
||||
DebugConsole.NewMessage(
|
||||
$"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{Path}'",
|
||||
$"Overriding an affliction or a buff with the identifier '{identifier}' using the version in '{element.ContentPackage.Name}'",
|
||||
Color.MediumPurple);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
$"Duplicate affliction: '{identifier}' defined in {elementName} of '{Path}'",
|
||||
$"Duplicate affliction: '{identifier}' defined in {element.ContentPackage.Name} is already defined in the previously loaded content package {existingAffliction.ContentPackage.Name}."+
|
||||
$" You may need to adjust the mod load order to make sure {element.ContentPackage.Name} is loaded first.",
|
||||
contentPackage: element?.ContentPackage);
|
||||
return;
|
||||
}
|
||||
|
||||
+22
-3
@@ -1,9 +1,28 @@
|
||||
namespace Barotrauma
|
||||
namespace Barotrauma
|
||||
{
|
||||
sealed class BackgroundCreaturePrefabsFile : OtherFile
|
||||
#if CLIENT
|
||||
[NotSyncedInMultiplayer]
|
||||
sealed class BackgroundCreaturePrefabsFile : GenericPrefabFile<BackgroundCreaturePrefab>
|
||||
{
|
||||
public BackgroundCreaturePrefabsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
|
||||
|
||||
//this content type only comes into play when a level is generated, so LoadFile and UnloadFile don't have anything to do
|
||||
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
|
||||
protected override bool MatchesPlural(Identifier identifier) => identifier == "backgroundcreatures";
|
||||
protected override PrefabCollection<BackgroundCreaturePrefab> Prefabs => BackgroundCreaturePrefab.Prefabs;
|
||||
protected override BackgroundCreaturePrefab CreatePrefab(ContentXElement element)
|
||||
{
|
||||
return new BackgroundCreaturePrefab(element, this);
|
||||
}
|
||||
|
||||
public sealed override Md5Hash CalculateHash() => Md5Hash.Blank;
|
||||
}
|
||||
#else
|
||||
[NotSyncedInMultiplayer]
|
||||
sealed class BackgroundCreaturePrefabsFile : OtherFile
|
||||
{
|
||||
public BackgroundCreaturePrefabsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path)
|
||||
{
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user