v1.6.17.0 (Unto the Breach update)
This commit is contained in:
@@ -42,7 +42,7 @@ namespace Barotrauma
|
||||
private static PathFinder pathFinder;
|
||||
private static readonly Dictionary<Character, CachedDistance> cachedDistances = new Dictionary<Character, CachedDistance>();
|
||||
|
||||
public static void OnStartRound()
|
||||
public static void OnStartRound(Biome biome = null)
|
||||
{
|
||||
roundData = new RoundData();
|
||||
foreach (Item item in Item.ItemList)
|
||||
@@ -53,12 +53,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 +93,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 +241,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 +250,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 +258,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 +289,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 +298,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 +308,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 +345,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 +363,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 +396,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.Server?.ServerSettings?.RespawnMode == RespawnMode.Permadeath)
|
||||
{
|
||||
UnlockAchievement(character, "abyssbeckons".ToIdentifier());
|
||||
}
|
||||
|
||||
if (GameMain.Server?.TraitorManager != null)
|
||||
{
|
||||
if (GameMain.Server.TraitorManager.IsTraitor(character))
|
||||
@@ -359,6 +416,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 +458,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 +487,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 +517,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,7 +580,7 @@ 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; }
|
||||
@@ -527,16 +590,17 @@ namespace Barotrauma
|
||||
#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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -801,36 +804,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 decontain weapons, because it could be that we are holding a weapon that cannot be placed on back (if we have a toolbelt) nor in the 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 decontain those weapons 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 decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.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(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Drop(Character);
|
||||
HandleRelocation(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -842,7 +850,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 +877,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)
|
||||
{
|
||||
@@ -1295,7 +1304,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 +1322,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;
|
||||
@@ -2168,15 +2183,15 @@ namespace Barotrauma
|
||||
float fireFactor = 1;
|
||||
if (!ignoreFire)
|
||||
{
|
||||
static float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
|
||||
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 = visibleHulls?.Sum(CalculateFire) ?? CalculateFire(hull);
|
||||
fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
|
||||
}
|
||||
float enemyFactor = 1;
|
||||
if (!ignoreEnemies)
|
||||
{
|
||||
int enemyCount = 0;
|
||||
int enemyCount = 0;
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (visibleHulls == null)
|
||||
@@ -2476,7 +2491,7 @@ namespace Barotrauma
|
||||
{
|
||||
other = null;
|
||||
if (target?.Item == null) { return false; }
|
||||
bool isOrder = IsOrderedToOperateThis(Character.AIController);
|
||||
bool isOrder = IsOrderedToOperateTarget(this);
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!IsActive(c)) { continue; }
|
||||
@@ -2491,14 +2506,14 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (c.AIController is HumanAIController operatingAI)
|
||||
else if (c.AIController is HumanAIController otherAI)
|
||||
{
|
||||
if (operatingAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item))
|
||||
if (otherAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item))
|
||||
{
|
||||
// Not targeting the same item.
|
||||
continue;
|
||||
}
|
||||
bool isTargetOrdered = IsOrderedToOperateThis(c.AIController);
|
||||
bool isTargetOrdered = IsOrderedToOperateTarget(otherAI);
|
||||
if (!isOrder && isTargetOrdered)
|
||||
{
|
||||
// If the other bot is ordered to operate the item, let him do it, unless we are ordered too
|
||||
@@ -2514,15 +2529,15 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
|
||||
if (!IsOperatingTarget(otherAI))
|
||||
{
|
||||
// The other bot is ordered to do something else
|
||||
// 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("helm") <= c.GetSkillLevel("helm"))
|
||||
if (Character.GetSkillLevel(Tags.HelmSkill) <= c.GetSkillLevel(Tags.HelmSkill))
|
||||
{
|
||||
other = c;
|
||||
break;
|
||||
@@ -2538,7 +2553,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
return other != null;
|
||||
bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item;
|
||||
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;
|
||||
}
|
||||
|
||||
public bool IsItemRepairedByAnother(Item target, out Character other)
|
||||
|
||||
@@ -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,41 @@ 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.
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,10 +810,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 +860,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 +871,19 @@ namespace Barotrauma
|
||||
{
|
||||
if (!CanAccessDoor(door, button =>
|
||||
{
|
||||
// Ignore buttons that are on the wrong side of the door
|
||||
// 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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ namespace Barotrauma
|
||||
InitTimers();
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
|
||||
+1
-1
@@ -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))
|
||||
{
|
||||
|
||||
+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; }
|
||||
|
||||
+25
-29
@@ -257,7 +257,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine)
|
||||
{
|
||||
@@ -898,23 +898,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 +919,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);
|
||||
@@ -1322,8 +1320,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 +1349,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,7 +1369,8 @@ 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)
|
||||
@@ -1420,11 +1419,8 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
case MeleeWeapon mw:
|
||||
{
|
||||
if (character.AnimController is HumanoidAnimController { Crouching: false })
|
||||
{
|
||||
reloadTime = mw.Reload;
|
||||
}
|
||||
{
|
||||
reloadTime = mw.Reload;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1485,7 +1481,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (ShouldUnequipWeapon)
|
||||
{
|
||||
Unequip();
|
||||
UnequipWeapon();
|
||||
}
|
||||
SteeringManager?.Reset();
|
||||
}
|
||||
@@ -1495,7 +1491,7 @@ namespace Barotrauma
|
||||
base.OnAbandon();
|
||||
if (ShouldUnequipWeapon)
|
||||
{
|
||||
Unequip();
|
||||
UnequipWeapon();
|
||||
}
|
||||
SteeringManager?.Reset();
|
||||
}
|
||||
|
||||
+1
-2
@@ -77,9 +77,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;
|
||||
|
||||
+30
-7
@@ -15,6 +15,7 @@ namespace Barotrauma
|
||||
private Deconstructor deconstructor;
|
||||
|
||||
private AIObjectiveDecontainItem decontainObjective;
|
||||
private AIObjectiveGoTo gotoObjective;
|
||||
|
||||
public AIObjectiveDeconstructItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
@@ -45,14 +46,24 @@ 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();
|
||||
}
|
||||
else
|
||||
{
|
||||
TryAddSubObjective(ref gotoObjective,
|
||||
constructor: () => new AIObjectiveGoTo(Item, character, objectiveManager, priorityModifier: PriorityModifier),
|
||||
onCompleted: () =>
|
||||
{
|
||||
StartDeconstruction();
|
||||
RemoveSubObjective(ref gotoObjective);
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
});
|
||||
}
|
||||
IsCompleted = true;
|
||||
RemoveSubObjective(ref decontainObjective);
|
||||
},
|
||||
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))
|
||||
{
|
||||
|
||||
+3
-1
@@ -59,12 +59,14 @@ 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);
|
||||
}
|
||||
//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
@@ -71,7 +71,7 @@ namespace Barotrauma
|
||||
this.targetContainer = targetContainer;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
protected override bool CheckObjectiveState() => IsCompleted;
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
|
||||
+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;
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ namespace Barotrauma
|
||||
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)
|
||||
|
||||
+73
-65
@@ -23,7 +23,7 @@ namespace Barotrauma
|
||||
|
||||
public const float MIN_OXYGEN = 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, () =>
|
||||
{
|
||||
@@ -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()
|
||||
|
||||
+61
-21
@@ -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;
|
||||
@@ -339,6 +340,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 +358,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 +371,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 +439,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.NewMessage($"({character.DisplayName}) Sorted hulls by suitability in {stopWatch.ElapsedMilliseconds} ms", debugOnly: true);
|
||||
#endif
|
||||
}
|
||||
|
||||
Hull potentialHull = hulls[hullSearchIndex];
|
||||
@@ -420,7 +460,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);
|
||||
|
||||
+2
@@ -176,6 +176,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 =>
|
||||
|
||||
+5
-5
@@ -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.
|
||||
@@ -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
|
||||
|
||||
+1
-2
@@ -658,9 +658,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,
|
||||
|
||||
+63
-20
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -95,6 +96,9 @@ namespace Barotrauma
|
||||
protected override bool AllowInAnySub => true;
|
||||
|
||||
public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier();
|
||||
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; }
|
||||
@@ -194,6 +198,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
|
||||
@@ -353,9 +394,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 +483,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 +506,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 +560,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (!useScooter)
|
||||
{
|
||||
// Unequip
|
||||
character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
|
||||
character.TryPutItemInAnySlot(scooter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -663,7 +704,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);
|
||||
|
||||
@@ -764,9 +808,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)
|
||||
{
|
||||
|
||||
+30
-20
@@ -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)
|
||||
{
|
||||
@@ -489,27 +498,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 +542,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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -318,7 +318,7 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
protected override bool CheckObjectiveState() => IsCompleted;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
|
||||
+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;
|
||||
|
||||
+1
-5
@@ -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();
|
||||
|
||||
+1
-1
@@ -312,7 +312,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => isDoneOperating && !Repeat;
|
||||
protected override bool CheckObjectiveState() => isDoneOperating && !Repeat;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
protected override bool CheckObjectiveState() => IsCompleted;
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ namespace Barotrauma
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
IsCompleted = Item.IsFullCondition;
|
||||
if (character.IsOnPlayerTeam && IsCompleted && IsRepairing())
|
||||
|
||||
+15
-6
@@ -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)
|
||||
@@ -391,9 +391,18 @@ namespace Barotrauma
|
||||
("[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 +477,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;
|
||||
character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
|
||||
}
|
||||
return isCompleted;
|
||||
return IsCompleted;
|
||||
}
|
||||
|
||||
protected override float GetPriority()
|
||||
|
||||
+13
-6
@@ -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);
|
||||
|
||||
+15
-16
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -162,7 +171,7 @@ namespace Barotrauma
|
||||
|
||||
private class Food
|
||||
{
|
||||
public string Tag;
|
||||
public Identifier Tag;
|
||||
public Vector2 HungerRange;
|
||||
public float Hunger;
|
||||
public float Happiness;
|
||||
@@ -182,6 +191,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 +202,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 +216,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 +239,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; }
|
||||
@@ -283,14 +296,22 @@ namespace Barotrauma
|
||||
public void Play(Character player)
|
||||
{
|
||||
if (PlayTimer > 0.0f) { return; }
|
||||
Owner ??= player;
|
||||
PlayTimer = 5.0f;
|
||||
if (!AIController.Character.IsFriendly(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 +339,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 +375,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 +468,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());
|
||||
|
||||
+6
-2
@@ -52,7 +52,9 @@ namespace Barotrauma
|
||||
{
|
||||
if (orderedCharacter != CommandingCharacter)
|
||||
{
|
||||
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 +62,9 @@ namespace Barotrauma
|
||||
.WithOrderGiver(CommandingCharacter)
|
||||
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
|
||||
OrderedCharacter.SetOrder(CurrentOrder, CommandingCharacter != OrderedCharacter);
|
||||
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f, minDurationBetweenSimilar: 5);
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,6 +699,294 @@ 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 (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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -818,6 +1109,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 +1204,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 +1230,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+55
-84
@@ -139,9 +139,11 @@ namespace Barotrauma
|
||||
ResetState();
|
||||
return;
|
||||
}
|
||||
UpdateConstantTorque(deltaTime);
|
||||
UpdateBlink(deltaTime);
|
||||
var mainLimb = MainLimb;
|
||||
|
||||
levitatingCollider = !IsHangingWithRope;
|
||||
levitatingCollider = !IsHangingWithRope && !IsClimbing;
|
||||
|
||||
if (!character.CanMove)
|
||||
{
|
||||
@@ -208,6 +210,11 @@ namespace Barotrauma
|
||||
{
|
||||
TargetMovement = TargetMovement.ClampLength(2);
|
||||
}
|
||||
|
||||
if (IsClimbing)
|
||||
{
|
||||
UpdateClimbing();
|
||||
}
|
||||
|
||||
if (inWater && !forceStanding)
|
||||
{
|
||||
@@ -336,7 +343,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 +363,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 +411,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 +435,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 +445,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 +462,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool reverse;
|
||||
public bool Reverse;
|
||||
|
||||
void UpdateSineAnim(float deltaTime)
|
||||
{
|
||||
@@ -510,7 +512,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 +557,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 +593,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2;
|
||||
if (reverse)
|
||||
if (Reverse)
|
||||
{
|
||||
movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi);
|
||||
}
|
||||
@@ -651,29 +652,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 +691,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 +723,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 +826,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:
|
||||
|
||||
+18
-265
@@ -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)
|
||||
{
|
||||
@@ -652,9 +651,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 +991,7 @@ namespace Barotrauma
|
||||
{
|
||||
head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.HeadTorque);
|
||||
}
|
||||
else
|
||||
else if (character.FollowCursor)
|
||||
{
|
||||
RotateHead(head);
|
||||
}
|
||||
@@ -1145,245 +1149,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 +1283,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 +1360,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);
|
||||
@@ -1876,23 +1641,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)
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace Barotrauma
|
||||
private bool simplePhysicsEnabled;
|
||||
|
||||
public Character Character => character;
|
||||
protected Character character;
|
||||
protected readonly Character character;
|
||||
|
||||
protected float strongestImpact;
|
||||
|
||||
@@ -385,15 +385,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 +431,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 +448,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskAppendage, ragdoll: this);
|
||||
AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskedSpeciesName: character.SpeciesName, huskAppendage, ragdoll: this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,7 +474,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 +516,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);
|
||||
}
|
||||
@@ -529,10 +525,11 @@ namespace Barotrauma
|
||||
protected void CreateLimbs()
|
||||
{
|
||||
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 +546,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 +574,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;
|
||||
}
|
||||
@@ -683,8 +680,7 @@ namespace Barotrauma
|
||||
}
|
||||
LimbJoints = newJoints;
|
||||
}
|
||||
|
||||
SubtractMass(limb);
|
||||
|
||||
limb.Remove();
|
||||
foreach (LimbJoint limbJoint in attachedJoints)
|
||||
{
|
||||
@@ -1400,7 +1396,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)
|
||||
{
|
||||
@@ -1910,7 +1911,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
Collider.SetTransform(simPosition, Collider.Rotation);
|
||||
Collider.SetTransformIgnoreContacts(simPosition, Collider.Rotation);
|
||||
}
|
||||
|
||||
if (!MathUtils.NearlyEqual(limbMoveAmount, Vector2.Zero))
|
||||
@@ -2009,7 +2010,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
limb.body.SetTransform(movePos, rotation);
|
||||
limb.body.SetTransformIgnoreContacts(movePos, rotation);
|
||||
limb.PullJointWorldAnchorB = limb.PullJointWorldAnchorA;
|
||||
limb.PullJointEnabled = false;
|
||||
}
|
||||
@@ -2113,26 +2114,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;
|
||||
|
||||
@@ -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
|
||||
@@ -816,15 +828,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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -213,7 +213,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private CharacterTeamType? originalTeamID;
|
||||
public CharacterTeamType OriginalTeamID
|
||||
{
|
||||
@@ -242,6 +241,26 @@ namespace Barotrauma
|
||||
protected readonly Dictionary<string, ActiveTeamChange> activeTeamChanges = new Dictionary<string, ActiveTeamChange>();
|
||||
protected ActiveTeamChange currentTeamChange;
|
||||
private const string OriginalChangeTeamIdentifier = "original";
|
||||
|
||||
public bool AllowPlayDead { get; set; }
|
||||
|
||||
public void EvaluatePlayDeadProbability(float? probability = null)
|
||||
{
|
||||
if (Params.AI is CharacterParams.AIParams aiParams)
|
||||
{
|
||||
if (probability.HasValue)
|
||||
{
|
||||
// Override so that can't revert back to the old value.
|
||||
aiParams.PlayDeadProbability = probability.Value;
|
||||
}
|
||||
AllowPlayDead = Rand.Value() <= aiParams.PlayDeadProbability;
|
||||
}
|
||||
else if (probability.HasValue)
|
||||
{
|
||||
AllowPlayDead = Rand.Value() <= probability.Value;
|
||||
}
|
||||
// Do nothing, if no value is defined and no AI Params were found.
|
||||
}
|
||||
|
||||
private void ThrowIfAccessingWalletsInSingleplayer()
|
||||
{
|
||||
@@ -254,26 +273,37 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void SetOriginalTeam(CharacterTeamType newTeam)
|
||||
/// <summary>
|
||||
/// Saves the character's original team (which affects e.g. whether the character considers the sub/outpost they're in to be their own or a "foreign" one),
|
||||
/// and adds a new team change to be processed on the next update.
|
||||
/// </summary>
|
||||
/// <param name="processImmediately">Should the team change be processed right now, or along with any other pending team changes in the next Update?</param>
|
||||
public void SetOriginalTeamAndChangeTeam(CharacterTeamType newTeam, bool processImmediately = false)
|
||||
{
|
||||
TryRemoveTeamChange(OriginalChangeTeamIdentifier);
|
||||
currentTeamChange = new ActiveTeamChange(newTeam, ActiveTeamChange.TeamChangePriorities.Base);
|
||||
TryAddNewTeamChange(OriginalChangeTeamIdentifier, currentTeamChange);
|
||||
if (processImmediately)
|
||||
{
|
||||
UpdateTeam();
|
||||
}
|
||||
}
|
||||
|
||||
private void ChangeTeam(CharacterTeamType newTeam)
|
||||
{
|
||||
if (newTeam == teamID) { return; }
|
||||
if (originalTeamID == null) { originalTeamID = teamID; }
|
||||
originalTeamID ??= teamID;
|
||||
TeamID = newTeam;
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated)
|
||||
var order = OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority);
|
||||
SetOrder(order, isNewOrder: true, speak: false);
|
||||
|
||||
if (AIController is HumanAIController)
|
||||
{
|
||||
// clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated)
|
||||
var order = OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority);
|
||||
SetOrder(order, isNewOrder: true, speak: false);
|
||||
}
|
||||
#if SERVER
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new TeamChangeEventData());
|
||||
#endif
|
||||
@@ -292,7 +322,7 @@ namespace Barotrauma
|
||||
if (currentTeamChange == null)
|
||||
{
|
||||
// set team logic to use active team changes as soon as the first team change is added
|
||||
SetOriginalTeam(TeamID);
|
||||
SetOriginalTeamAndChangeTeam(TeamID);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -330,12 +360,13 @@ namespace Barotrauma
|
||||
bestTeamChange = desiredTeamChange.Value;
|
||||
}
|
||||
}
|
||||
if (TeamID != bestTeamChange.DesiredTeamId)
|
||||
if (TeamID != bestTeamChange.DesiredTeamId)
|
||||
{
|
||||
ChangeTeam(bestTeamChange.DesiredTeamId);
|
||||
currentTeamChange = bestTeamChange;
|
||||
|
||||
if (bestTeamChange.AggressiveBehavior) // this seemed like the least disruptive way to induce aggressive behavior
|
||||
// this seemed like the least disruptive way to induce aggressive behavior on human characters
|
||||
if (bestTeamChange.AggressiveBehavior && AIController is HumanAIController)
|
||||
{
|
||||
var order = OrderPrefab.Prefabs["fightintruders"].CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority);
|
||||
SetOrder(order, isNewOrder: true, speak: false);
|
||||
@@ -343,11 +374,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsOnPlayerTeam => teamID == CharacterTeamType.Team1 || teamID == CharacterTeamType.Team2;
|
||||
public bool IsOnPlayerTeam =>
|
||||
teamID == CharacterTeamType.Team1 ||
|
||||
(teamID == CharacterTeamType.Team2 && !IsFriendlyNPCTurnedHostile);
|
||||
|
||||
public bool IsOriginallyOnPlayerTeam => originalTeamID == CharacterTeamType.Team1 || originalTeamID == CharacterTeamType.Team2;
|
||||
|
||||
public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && teamID == CharacterTeamType.Team2;
|
||||
public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && (teamID == CharacterTeamType.Team2 || teamID == CharacterTeamType.None);
|
||||
|
||||
public bool IsInstigator => CombatAction is { IsInstigator: true };
|
||||
|
||||
@@ -413,15 +446,15 @@ namespace Barotrauma
|
||||
|
||||
public Identifier GetBaseCharacterSpeciesName() => Prefab.GetBaseCharacterSpeciesName(SpeciesName);
|
||||
|
||||
public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group;
|
||||
public Identifier Group => HumanPrefab is { Group.IsEmpty: false } prefab ? prefab.Group : Params.Group;
|
||||
|
||||
public bool IsHumanoid => Params.Humanoid;
|
||||
|
||||
public bool IsMachine => Params.IsMachine;
|
||||
|
||||
public bool IsHusk => Params.Husk;
|
||||
public bool IsDisguisedAsHusk => CharacterHealth.GetAfflictionStrengthByType("disguiseashusk".ToIdentifier()) > 0;
|
||||
public bool IsHuskInfected => CharacterHealth.GetActiveAfflictionTags().Contains("huskinfected".ToIdentifier());
|
||||
public bool IsDisguisedAsHusk => CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.DisguisedAsHuskType) > 0;
|
||||
public bool IsHuskInfected => CharacterHealth.GetActiveAfflictionTags().Contains(Tags.HuskInfected);
|
||||
|
||||
public bool IsMale => info?.IsMale ?? false;
|
||||
|
||||
@@ -668,6 +701,8 @@ namespace Barotrauma
|
||||
|
||||
// Eating is not implemented for humanoids. If we implement that at some point, we could remove this restriction.
|
||||
public bool CanEat => !IsHumanoid && Params.CanEat && AllowInput && AnimController.GetLimb(LimbType.Head) != null;
|
||||
|
||||
public bool CanClimb => Params.CanClimb && CanInteract;
|
||||
|
||||
public Vector2 CursorPosition
|
||||
{
|
||||
@@ -862,7 +897,7 @@ namespace Barotrauma
|
||||
private float ragdollingLockTimer;
|
||||
public bool IsRagdolled;
|
||||
public bool IsForceRagdolled;
|
||||
public bool dontFollowCursor;
|
||||
public bool FollowCursor = true;
|
||||
|
||||
public bool IsIncapacitated
|
||||
{
|
||||
@@ -1145,7 +1180,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsIncapacitated && (!IsRagdolled || AnimController.IsHoldingToRope);
|
||||
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsKnockedDown && (!IsRagdolled || AnimController.IsHoldingToRope);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1368,6 +1403,14 @@ namespace Barotrauma
|
||||
//no longer a new hire after spawning (only displayed as a new hire at the end of the outpost round, when the character hasn't spawned yet)
|
||||
Info.IsNewHire = false;
|
||||
}
|
||||
if (characterInfo?.HumanPrefabIds is { } prefabIds &&
|
||||
prefabIds.NpcSetIdentifier != default && prefabIds.NpcIdentifier != default)
|
||||
{
|
||||
humanPrefab = NPCSet.Get(
|
||||
characterInfo.HumanPrefabIds.NpcSetIdentifier,
|
||||
characterInfo.HumanPrefabIds.NpcIdentifier);
|
||||
}
|
||||
|
||||
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
|
||||
for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++)
|
||||
{
|
||||
@@ -1460,32 +1503,38 @@ namespace Barotrauma
|
||||
CharacterHealth = new CharacterHealth(selectedHealthElement, this, limbHealthElement);
|
||||
}
|
||||
|
||||
if (Params.Husk && speciesName != "husk" && Prefab.VariantOf != "husk")
|
||||
if (Params.Husk)
|
||||
{
|
||||
Identifier nonHuskedSpeciesName = Identifier.Empty;
|
||||
AfflictionPrefabHusk matchingAffliction = null;
|
||||
foreach (var huskPrefab in AfflictionPrefab.Prefabs.OfType<AfflictionPrefabHusk>())
|
||||
Identifier nonHuskedSpeciesName = Params.NonHuskedSpecies;
|
||||
if (!nonHuskedSpeciesName.IsEmpty || Params.UseHuskAppendage)
|
||||
{
|
||||
var nonHuskedName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, huskPrefab);
|
||||
if (huskPrefab.TargetSpecies.Contains(nonHuskedName))
|
||||
// Check that there's a matching species and affliction for the non-husked species definition.
|
||||
AfflictionPrefab matchingAffliction = null;
|
||||
foreach (var huskPrefab in AfflictionPrefab.Prefabs.OfType<AfflictionPrefabHusk>())
|
||||
{
|
||||
var huskedSpeciesName = AfflictionHusk.GetHuskedSpeciesName(nonHuskedName, huskPrefab);
|
||||
if (huskedSpeciesName.Equals(speciesName))
|
||||
if (huskPrefab.HuskedSpeciesName.IsEmpty) { continue; }
|
||||
Identifier nonHuskedSpecies = nonHuskedSpeciesName;
|
||||
if (nonHuskedSpeciesName.IsEmpty)
|
||||
{
|
||||
nonHuskedSpeciesName = nonHuskedName;
|
||||
nonHuskedSpecies = AfflictionHusk.GetNonHuskedSpeciesName(Params, huskPrefab);
|
||||
}
|
||||
if (huskPrefab.TargetSpecies.Contains(nonHuskedSpecies))
|
||||
{
|
||||
nonHuskedSpeciesName = nonHuskedSpecies;
|
||||
matchingAffliction = huskPrefab;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matchingAffliction == null || nonHuskedSpeciesName.IsEmpty)
|
||||
{
|
||||
DebugConsole.ThrowError($"Cannot find a husk infection that matches {speciesName}! Please make sure that the speciesname is added as 'targets' in the husk affliction prefab definition!\n"
|
||||
+ "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\"",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
// Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg.
|
||||
nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier();
|
||||
speciesName = nonHuskedSpeciesName;
|
||||
}
|
||||
if (matchingAffliction == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Cannot find a husk infection that matches {speciesName}! Please make sure that the speciesname is added as 'targets' in the husk affliction prefab definition! " +
|
||||
$"If the name of the character doesn't match the default pattern ('Crawlerhusk', 'Humanhusk', etc), you'll also need to define the non-husked species with {nameof(Params.NonHuskedSpecies)} attribute in the character config file.",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
|
||||
// Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg.
|
||||
nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier();
|
||||
speciesName = nonHuskedSpeciesName;
|
||||
}
|
||||
}
|
||||
if (ragdollParams == null && prefab.VariantOf == null)
|
||||
{
|
||||
@@ -1745,7 +1794,7 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
public void GiveJobItems(WayPoint spawnPoint = null)
|
||||
public void GiveJobItems(bool isPvPMode, WayPoint spawnPoint = null)
|
||||
{
|
||||
if (info == null) { return; }
|
||||
if (info.HumanPrefabIds != default)
|
||||
@@ -1760,7 +1809,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
}
|
||||
info.Job?.GiveJobItems(this, spawnPoint);
|
||||
info.Job?.GiveJobItems(this, isPvPMode, spawnPoint);
|
||||
}
|
||||
|
||||
public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false)
|
||||
@@ -1778,6 +1827,10 @@ namespace Barotrauma
|
||||
{
|
||||
item.AddTag(s);
|
||||
}
|
||||
if (GameMain.GameSession?.GameMode is PvPMode)
|
||||
{
|
||||
item.AddTag($"id_{TeamID}".ToIdentifier());
|
||||
}
|
||||
if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item));
|
||||
@@ -1785,9 +1838,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public float GetSkillLevel(string skillIdentifier) =>
|
||||
GetSkillLevel(skillIdentifier.ToIdentifier());
|
||||
|
||||
private static readonly ImmutableDictionary<Identifier, StatTypes> overrideStatTypes = new Dictionary<Identifier, StatTypes>
|
||||
{
|
||||
{ new("helm"), StatTypes.HelmSkillOverride },
|
||||
@@ -1797,6 +1847,9 @@ namespace Barotrauma
|
||||
{ new("mechanical"), StatTypes.MechanicalSkillOverride }
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
/// <summary>
|
||||
/// Get the character's current skill level, taking into account any temporary boosts from wearables and afflictions
|
||||
/// </summary>
|
||||
public float GetSkillLevel(Identifier skillIdentifier)
|
||||
{
|
||||
if (Info?.Job == null) { return 0.0f; }
|
||||
@@ -1839,7 +1892,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
skillLevel += GetStatValue(GetSkillStatType(skillIdentifier));
|
||||
var skillStatType = GetSkillStatType(skillIdentifier);
|
||||
if (skillStatType != StatTypes.None)
|
||||
{
|
||||
skillLevel += GetStatValue(skillStatType);
|
||||
}
|
||||
return Math.Max(skillLevel, 0);
|
||||
}
|
||||
|
||||
@@ -1876,11 +1933,30 @@ namespace Barotrauma
|
||||
// - dragging someone
|
||||
// - crouching
|
||||
// - moving backwards
|
||||
public bool CanRun => CanRunWhileDragging() &&
|
||||
public bool CanRun =>
|
||||
!DisableRunning &&
|
||||
CanRunWhileDragging() &&
|
||||
AnimController is not HumanoidAnimController { Crouching: true } &&
|
||||
!AnimController.IsMovingBackwards && !HasAbilityFlag(AbilityFlags.MustWalk) &&
|
||||
!AnimController.IsHoldingToRope;
|
||||
|
||||
private double disableRunningLastSet;
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to temporarily disable running using StatusEffects. Resets in 0.1 seconds if not set.
|
||||
/// </summary>
|
||||
public bool DisableRunning
|
||||
{
|
||||
get => disableRunningLastSet > Timing.TotalTime - 0.1;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
disableRunningLastSet = Timing.TotalTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanRunWhileDragging()
|
||||
{
|
||||
if (selectedCharacter is not { IsDraggable: true }) { return true; }
|
||||
@@ -1919,8 +1995,7 @@ namespace Barotrauma
|
||||
/// Can be used to modify the character's speed via StatusEffects
|
||||
/// </summary>
|
||||
public float SpeedMultiplier { get; private set; } = 1;
|
||||
|
||||
|
||||
|
||||
private double propulsionSpeedMultiplierLastSet;
|
||||
private float propulsionSpeedMultiplier;
|
||||
/// <summary>
|
||||
@@ -1984,6 +2059,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public float GetTemporarySpeedReduction()
|
||||
{
|
||||
if (!Params.Health.ApplyMovementPenalties) { return 0; }
|
||||
float reduction = 0;
|
||||
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightFoot, excludeSevered: false), reduction);
|
||||
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftFoot, excludeSevered: false), reduction);
|
||||
@@ -2021,6 +2097,7 @@ namespace Barotrauma
|
||||
|
||||
private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.8f)
|
||||
{
|
||||
if (!Params.Health.ApplyMovementPenalties) { return 0; }
|
||||
if (limb != null)
|
||||
{
|
||||
sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: AfflictionPrefab.DamageType));
|
||||
@@ -2111,7 +2188,7 @@ namespace Barotrauma
|
||||
((!IsClimbing && AnimController.OnGround) || (IsClimbing && IsKeyDown(InputType.Aim))) &&
|
||||
!AnimController.InWater)
|
||||
{
|
||||
if (dontFollowCursor)
|
||||
if (!FollowCursor)
|
||||
{
|
||||
AnimController.TargetDir = Direction.Right;
|
||||
}
|
||||
@@ -2242,7 +2319,7 @@ namespace Barotrauma
|
||||
if (attackTarget != null)
|
||||
{
|
||||
if (!attack.IsValidTarget(attackTarget as Entity)) { return false; }
|
||||
if (attackTarget is ISerializableEntity se && attackTarget is Character)
|
||||
if (attackTarget is ISerializableEntity se and Character)
|
||||
{
|
||||
if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; }
|
||||
}
|
||||
@@ -2279,7 +2356,7 @@ namespace Barotrauma
|
||||
|
||||
if (Inventory != null)
|
||||
{
|
||||
if (IsKeyHit(InputType.DropItem))
|
||||
if (IsKeyHit(InputType.DropItem) && Screen.Selected is { IsEditor: false })
|
||||
{
|
||||
foreach (Item item in HeldItems)
|
||||
{
|
||||
@@ -2423,6 +2500,7 @@ namespace Barotrauma
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(target != null);
|
||||
if (target == null || target.Removed) { return false; }
|
||||
if (seeingEntity == null) { return false; }
|
||||
if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
|
||||
if (!target.AnimController.SimplePhysicsEnabled)
|
||||
{
|
||||
@@ -2583,6 +2661,39 @@ namespace Barotrauma
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool HasHandsFull(out (Item leftHandItem, Item rightHandItem) items)
|
||||
{
|
||||
var leftHandItem = GetEquippedItem(slotType: InvSlotType.LeftHand);
|
||||
var rightHandItem = GetEquippedItem(slotType: InvSlotType.RightHand);
|
||||
items = (leftHandItem, rightHandItem);
|
||||
bool handsFull = leftHandItem != null && rightHandItem != null;
|
||||
return handsFull;
|
||||
}
|
||||
|
||||
public bool TryPutItem(Item item, IEnumerable<InvSlotType> allowedSlots) => Inventory.TryPutItem(item, user: this, allowedSlots);
|
||||
public bool TryPutItemInBag(Item item) => item != null && item.AllowedSlots.Contains(InvSlotType.Bag) && TryPutItem(item, CharacterInventory.BagSlot);
|
||||
public bool TryPutItemInAnySlot(Item item) => item != null && item.AllowedSlots.Contains(InvSlotType.Any) && TryPutItem(item, CharacterInventory.AnySlot);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to unequip an item.
|
||||
/// First tries to put the item in any slot.
|
||||
/// If that fails, tries to put in the bag slot.
|
||||
/// If that too fails, drops the item.
|
||||
/// </summary>
|
||||
/// <returns>false only if the item is not equipped.</returns>
|
||||
public bool Unequip(Item item)
|
||||
{
|
||||
if (!HasEquippedItem(item)) { return false; }
|
||||
if (!TryPutItemInAnySlot(item))
|
||||
{
|
||||
if (!TryPutItemInBag(item))
|
||||
{
|
||||
item.Drop(this);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited)
|
||||
{
|
||||
@@ -2608,7 +2719,6 @@ namespace Barotrauma
|
||||
if (container != null)
|
||||
{
|
||||
if (!container.HasRequiredItems(this, addMessage: false)) { return false; }
|
||||
if (!container.AllowAccess) { return false; }
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -2771,6 +2881,12 @@ namespace Barotrauma
|
||||
#endif
|
||||
if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; }
|
||||
|
||||
Controller controller = item.GetComponent<Controller>();
|
||||
if (controller != null && IsAnySelectedItem(item) && controller.IsAttachedUser(this))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.ParentInventory != null)
|
||||
{
|
||||
return CanAccessInventory(item.ParentInventory);
|
||||
@@ -2880,7 +2996,7 @@ namespace Barotrauma
|
||||
{
|
||||
//don't allow selecting another Controller if it'd try to turn the character in the opposite direction
|
||||
//(e.g. periscope that's facing the wrong way while sitting in a chair)
|
||||
if (item.GetComponent<Controller>() is { } controller && controller.Direction != 0 && controller.Direction != AnimController.Direction) { return false; }
|
||||
if (controller != null && controller.Direction != 0 && controller.Direction != AnimController.Direction) { return false; }
|
||||
|
||||
//if a Controller that controls the character's pose is selected,
|
||||
//don't allow selecting items that are behind the character's back
|
||||
@@ -3358,7 +3474,9 @@ namespace Barotrauma
|
||||
|
||||
if (Inventory != null)
|
||||
{
|
||||
foreach (Item item in Inventory.AllItems)
|
||||
//do not check for duplicates: this is code is called very frequently, and duplicates don't matter here,
|
||||
//so it's better just to avoid the relatively expensive duplicate check
|
||||
foreach (Item item in Inventory.GetAllItems(checkForDuplicates: false))
|
||||
{
|
||||
if (item.body == null || item.body.Enabled) { continue; }
|
||||
item.SetTransform(SimPosition, 0.0f);
|
||||
@@ -3751,41 +3869,51 @@ namespace Barotrauma
|
||||
|
||||
if (!IsDead || (CauseOfDeath?.Type == CauseOfDeathType.Disconnected && GameMain.GameSession?.Campaign != null)) { return; }
|
||||
|
||||
int subCorpseCount = 0;
|
||||
|
||||
if (Submarine != null)
|
||||
{
|
||||
subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine);
|
||||
if (subCorpseCount < GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { return; }
|
||||
}
|
||||
|
||||
if (SelectedBy != null)
|
||||
{
|
||||
despawnTimer = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
float distToClosestPlayer = GetDistanceToClosestPlayer();
|
||||
if (distToClosestPlayer > Params.DisableDistance)
|
||||
{
|
||||
//despawn in 1 minute if very far from all human players
|
||||
despawnTimer = Math.Max(despawnTimer, GameSettings.CurrentConfig.CorpseDespawnDelay - 60.0f);
|
||||
}
|
||||
|
||||
float despawnDelay = GameSettings.CurrentConfig.CorpseDespawnDelay;
|
||||
float despawnPriority = 1.0f;
|
||||
if (subCorpseCount > GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold)
|
||||
if (GameMain.GameSession?.GameMode is PvPMode &&
|
||||
GameMain.NetworkMember?.RespawnManager != null)
|
||||
{
|
||||
//despawn faster if there are lots of corpses in the sub (twice as many as the threshold -> despawn twice as fast)
|
||||
despawnPriority += (subCorpseCount - GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) / (float)GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold;
|
||||
//simpler despawning logic in PvP modes with respawning: just a short timer
|
||||
despawnDelay = GameSettings.CurrentConfig.CorpseDespawnDelayPvP;
|
||||
}
|
||||
if (AIController is EnemyAIController)
|
||||
else
|
||||
{
|
||||
//enemies despawn faster
|
||||
despawnPriority *= 2.0f;
|
||||
int subCorpseCount = 0;
|
||||
if (Submarine != null)
|
||||
{
|
||||
subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine);
|
||||
if (subCorpseCount < GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { return; }
|
||||
}
|
||||
|
||||
if (subCorpseCount > GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold)
|
||||
{
|
||||
//despawn faster if there are lots of corpses in the sub (twice as many as the threshold -> despawn twice as fast)
|
||||
despawnPriority += (subCorpseCount - GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) / (float)GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold;
|
||||
}
|
||||
|
||||
float distToClosestPlayer = GetDistanceToClosestPlayer();
|
||||
if (distToClosestPlayer > Params.DisableDistance)
|
||||
{
|
||||
//despawn in 1 minute if very far from all human players
|
||||
despawnTimer = Math.Max(despawnTimer, despawnDelay - 60.0f);
|
||||
}
|
||||
|
||||
if (AIController is EnemyAIController)
|
||||
{
|
||||
//enemies despawn faster
|
||||
despawnPriority *= 2.0f;
|
||||
}
|
||||
}
|
||||
|
||||
despawnTimer += deltaTime * despawnPriority;
|
||||
if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; }
|
||||
if (despawnTimer < despawnDelay) { return; }
|
||||
|
||||
Despawn();
|
||||
}
|
||||
@@ -3798,7 +3926,10 @@ namespace Barotrauma
|
||||
IsHuman ?
|
||||
Tags.DespawnContainer :
|
||||
Params.DespawnContainer;
|
||||
if (!despawnContainerId.IsEmpty)
|
||||
|
||||
//don't spawn duffel bags in PvP modes that include respawning, because it can lead to a ton of accumulated items in the sub/outpost
|
||||
bool pvpWithRespawning = GameMain.GameSession?.GameMode is PvPMode && GameMain.NetworkMember?.RespawnManager != null;
|
||||
if (!despawnContainerId.IsEmpty && !pvpWithRespawning)
|
||||
{
|
||||
var containerPrefab =
|
||||
MapEntityPrefab.FindByIdentifier(despawnContainerId) as ItemPrefab ??
|
||||
@@ -3933,8 +4064,6 @@ namespace Barotrauma
|
||||
targetRange = Math.Min(targetRange, maxAIRange);
|
||||
|
||||
float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed);
|
||||
|
||||
newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
|
||||
if (!float.IsNaN(newRange))
|
||||
{
|
||||
aiTarget.SoundRange = newRange;
|
||||
@@ -4165,13 +4294,24 @@ namespace Barotrauma
|
||||
{
|
||||
prevAiChatMessages.Remove(identifier);
|
||||
}
|
||||
|
||||
//already sent a similar message a moment ago
|
||||
if (identifier != Identifier.Empty && minDurationBetweenSimilar > 0.0f &&
|
||||
(aiChatMessageQueue.Any(m => m.Identifier == identifier) || prevAiChatMessages.ContainsKey(identifier)))
|
||||
|
||||
if (minDurationBetweenSimilar > 0)
|
||||
{
|
||||
return;
|
||||
if (identifier == Identifier.Empty)
|
||||
{
|
||||
#if DEBUG
|
||||
// TODO: This is stupid. We shouldn't allow passing minDurationBetweenSimilar without an identifier in the first place, but need to think how to refactor this.
|
||||
DebugConsole.AddWarning($"Called Character.Speak() with minDurationBetweenSimilar but didn't define the identifier! Cannot compare with the old messages. The message will be sent each time the function is called.");
|
||||
Debugger.Break();
|
||||
#endif
|
||||
}
|
||||
else if (aiChatMessageQueue.Any(m => m.Identifier == identifier) || prevAiChatMessages.ContainsKey(identifier))
|
||||
{
|
||||
//already sent a similar message a moment ago
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
aiChatMessageQueue.Add(new AIChatMessage(message, messageType, identifier, delay));
|
||||
}
|
||||
|
||||
@@ -4269,7 +4409,9 @@ namespace Barotrauma
|
||||
|
||||
Limb limbHit = targetLimb;
|
||||
|
||||
float impulseMagnitude = (attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier) * deltaTime;
|
||||
// TODO: should we apply deltatime only on TargetForce, not TargetImpulse? Changing this would have implications on many existing monster attacks, so all the monsters would have to be tested and possibly readjusted.
|
||||
// Should be (attack.TargetImpulse + attack.TargetForce * deltaTime) * attack.ImpactMultiplier?
|
||||
float impulseMagnitude = (attack.TargetImpulse + attack.TargetForce) * attack.ImpactMultiplier * deltaTime;
|
||||
|
||||
Vector2 attackImpulse = Vector2.Zero;
|
||||
if (Math.Abs(impulseMagnitude) > 0.0f)
|
||||
@@ -4303,7 +4445,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
if (limbHit == null) { return new AttackResult(); }
|
||||
Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld * attack.ImpactMultiplier;
|
||||
Vector2 forceWorld = (attack.TargetImpulseWorld + attack.TargetForceWorld) * attack.ImpactMultiplier;
|
||||
if (attacker != null)
|
||||
{
|
||||
forceWorld.X *= attacker.AnimController.Dir;
|
||||
@@ -4445,35 +4587,21 @@ namespace Barotrauma
|
||||
CreatureMetrics.RecordKill(target.SpeciesName);
|
||||
}
|
||||
|
||||
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false)
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="recalculateVitality">Set false as an optimization only when you manually call <see cref="CharacterHealth.RecalculateVitality"/>. Only applies to limb specific afflictions.</param>
|
||||
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false, bool ignoreDamageOverlay = false, bool recalculateVitality = true)
|
||||
{
|
||||
if (Removed) { return new AttackResult(); }
|
||||
|
||||
//character inside the sub received damage from a monster outside the sub
|
||||
//can happen during normal gameplay if someone for example fires a ranged weapon from outside,
|
||||
//the intention of this error message is to diagnose an issue with monsters being able to damage characters from outside
|
||||
|
||||
// Disabled, because this happens every now and then when the monsters can get in and out of the sub.
|
||||
|
||||
// if (attacker?.AIController is EnemyAIController && Submarine != null && attacker.Submarine == null)
|
||||
// {
|
||||
// string errorMsg = $"Character {Name} received damage from outside the sub while inside (attacker: {attacker.Name})";
|
||||
// GameAnalyticsManager.AddErrorEventOnce("Character.DamageLimb:DamageFromOutside" + Name + attacker.Name,
|
||||
// GameAnalyticsManager.ErrorSeverity.Warning,
|
||||
// errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
//#if DEBUG
|
||||
// DebugConsole.ThrowError(errorMsg);
|
||||
//#endif
|
||||
// }
|
||||
|
||||
SetStun(stun);
|
||||
|
||||
if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
|
||||
{
|
||||
if (attacker.TeamID == TeamID)
|
||||
{
|
||||
afflictions = afflictions.Where(a => a.Prefab.IsBuff);
|
||||
if (!afflictions.Any()) { return new AttackResult(); }
|
||||
if (afflictions.None(a => a.Prefab.IsBuff)) { return new AttackResult(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4493,9 +4621,8 @@ namespace Barotrauma
|
||||
}
|
||||
bool wasDead = IsDead;
|
||||
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
|
||||
float prevVitality = CharacterHealth.Vitality;
|
||||
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration, attacker: attacker);
|
||||
CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking);
|
||||
CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking, recalculateVitality);
|
||||
if (shouldImplode)
|
||||
{
|
||||
// Only used by assistant's True Potential talent. Has to run here in order to properly give kill credit when it activates.
|
||||
@@ -4504,8 +4631,16 @@ namespace Barotrauma
|
||||
|
||||
if (attacker != this)
|
||||
{
|
||||
bool wasDamageOverlayVisible = CharacterHealth.ShowDamageOverlay;
|
||||
if (ignoreDamageOverlay)
|
||||
{
|
||||
// Temporarily ignore damage overlay (husk transition damage)
|
||||
CharacterHealth.ShowDamageOverlay = false;
|
||||
}
|
||||
OnAttacked?.Invoke(attacker, attackResult);
|
||||
OnAttackedProjSpecific(attacker, attackResult, stun);
|
||||
// Reset damage overlay
|
||||
CharacterHealth.ShowDamageOverlay = wasDamageOverlayVisible;
|
||||
if (!wasDead)
|
||||
{
|
||||
TryAdjustAttackerSkill(attacker, attackResult);
|
||||
@@ -4633,6 +4768,13 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// apply pvp stun resistance to humans (reduce stun amount via resist multiplier)
|
||||
if (newStun > 0 && GameMain.NetworkMember is { } networkMember && GameMain.GameSession?.GameMode is PvPMode && IsHuman)
|
||||
{
|
||||
newStun = Math.Max(0, newStun - (newStun * networkMember.ServerSettings.PvPStunResist));
|
||||
}
|
||||
|
||||
if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; }
|
||||
if (Math.Sign(newStun) != Math.Sign(Stun))
|
||||
{
|
||||
@@ -4811,6 +4953,24 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
partial void ImplodeFX();
|
||||
|
||||
public void TurnIntoHusk(AfflictionPrefabHusk huskInfection = null, bool? playDead = null)
|
||||
{
|
||||
huskInfection ??= AfflictionPrefab.HuskInfection as AfflictionPrefabHusk;
|
||||
if (huskInfection == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Cannot turn {Name} into husk, because husk infection was not found!", contentPackage: AfflictionPrefab.Prefabs.First().ContentPackage);
|
||||
return;
|
||||
}
|
||||
// Randomize the start strength a bit, so that the husks don't turn at the same time, which can cause performance issues when turning multiple characters to husk at the same time.
|
||||
float startStrength = Rand.Range(Math.Max(huskInfection.MaxStrength - 2, huskInfection.ActiveThreshold), huskInfection.MaxStrength);
|
||||
startStrength *= MaxVitality / 100f;
|
||||
CharacterHealth.ApplyAffliction(AnimController.MainLimb, huskInfection.Instantiate(startStrength));
|
||||
if (playDead.HasValue)
|
||||
{
|
||||
AllowPlayDead = playDead.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true)
|
||||
{
|
||||
@@ -5387,6 +5547,27 @@ namespace Barotrauma
|
||||
|
||||
public IReadOnlyCollection<CharacterTalent> CharacterTalents => characterTalents;
|
||||
|
||||
/// <summary>
|
||||
/// Removes the talents the character has unlocked in their talent tree.
|
||||
/// </summary>
|
||||
public void ResetTalents(bool applyXpPenalty)
|
||||
{
|
||||
characterTalents.Clear();
|
||||
abilityResistances.Clear();
|
||||
abilityFlags = AbilityFlags.None;
|
||||
CharacterHealth.RemoveAfflictions(affliction => affliction.Prefab.AfflictionType == Tags.AfflictionTypeTalentBuff);
|
||||
statValues.Clear();
|
||||
|
||||
if (applyXpPenalty)
|
||||
{
|
||||
int currentLevel = info.GetCurrentLevel();
|
||||
if (currentLevel > 0)
|
||||
{
|
||||
info.SetExperience(info.ExperiencePoints - CharacterInfo.ExperienceRequiredPerLevel(currentLevel));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadTalents()
|
||||
{
|
||||
List<Identifier> toBeRemoved = null;
|
||||
@@ -5590,12 +5771,12 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
public void SetMoney(int amount)
|
||||
{
|
||||
if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; }
|
||||
if (amount == campaign.Wallet.Balance) { return; }
|
||||
if (Wallet == null) { return; }
|
||||
if (amount == Wallet.Balance) { return; }
|
||||
|
||||
int prevAmount = campaign.Wallet.Balance;
|
||||
campaign.Wallet.Balance = amount;
|
||||
OnMoneyChanged(prevAmount, campaign.Wallet.Balance);
|
||||
int prevAmount = Wallet.Balance;
|
||||
Wallet.Balance = amount;
|
||||
OnMoneyChanged(prevAmount, Wallet.Balance);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -5788,9 +5969,12 @@ namespace Barotrauma
|
||||
return myTeam switch
|
||||
{
|
||||
// NPCs are friendly to the same team and the friendly NPCs
|
||||
CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
|
||||
CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
|
||||
// Friendly NPCs are friendly to both player teams
|
||||
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
|
||||
// None (bandits and such) consider friendly NPCs friendly, not attacking them unless they attack first
|
||||
// Otherwise bandits would for example attach the hostages.
|
||||
CharacterTeamType.None => otherTeam == CharacterTeamType.FriendlyNPC,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
@@ -5802,6 +5986,8 @@ namespace Barotrauma
|
||||
public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other);
|
||||
|
||||
public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group);
|
||||
|
||||
public bool MatchesSpeciesNameOrGroup(Identifier speciesNameOrGroup) => Prefab.MatchesSpeciesNameOrGroup(speciesNameOrGroup);
|
||||
|
||||
public void StopClimbing()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,15 +344,37 @@ namespace Barotrauma
|
||||
/// Note: Can be null.
|
||||
/// </summary>
|
||||
public Character Character;
|
||||
|
||||
|
||||
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 +730,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 +749,8 @@ namespace Barotrauma
|
||||
}
|
||||
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
|
||||
|
||||
TalentRefundPoints = CharacterConfigElement.GetAttributeInt("refundpoints", 0);
|
||||
|
||||
int loadedLastRewardDistribution = CharacterConfigElement.GetAttributeInt("lastrewarddistribution", -1);
|
||||
if (loadedLastRewardDistribution >= 0)
|
||||
{
|
||||
@@ -799,6 +825,7 @@ 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", "");
|
||||
@@ -1047,6 +1074,7 @@ namespace Barotrauma
|
||||
|
||||
public string ReplaceVars(string str)
|
||||
{
|
||||
if (Head == null) { return str; }
|
||||
return Prefab.ReplaceVars(str, Head.Preset);
|
||||
}
|
||||
|
||||
@@ -1271,18 +1299,18 @@ namespace Barotrauma
|
||||
/// 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 +1340,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 +1362,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 +1370,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 +1465,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 +1478,45 @@ 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();
|
||||
|
||||
bool applyXpPenalty = talentResetCount > 0;
|
||||
|
||||
UnlockedTalents.Clear();
|
||||
SavedStatValues.Clear();
|
||||
Character?.ResetTalents(applyXpPenalty);
|
||||
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 +1559,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,6 +1569,7 @@ 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("renamingenabled", RenamingEnabled)
|
||||
@@ -1601,7 +1671,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 +1706,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));
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
+14
-8
@@ -337,18 +337,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,
|
||||
@@ -430,7 +436,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);
|
||||
|
||||
+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)
|
||||
{
|
||||
|
||||
+46
-26
@@ -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)
|
||||
@@ -314,6 +326,7 @@ namespace Barotrauma
|
||||
husk.Info.Character = husk;
|
||||
husk.Info.TeamID = CharacterTeamType.None;
|
||||
}
|
||||
husk.AllowPlayDead = character.AllowPlayDead;
|
||||
|
||||
if (Prefab is AfflictionPrefabHusk huskPrefab)
|
||||
{
|
||||
@@ -379,11 +392,9 @@ 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);
|
||||
CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
|
||||
if (huskPrefab?.ConfigElement == null)
|
||||
{
|
||||
@@ -406,10 +417,7 @@ namespace Barotrauma
|
||||
ContentPath pathToAppendage = element.GetAttributeContentPath("path") ?? ContentPath.Empty;
|
||||
XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage);
|
||||
if (doc == null) { return appendage; }
|
||||
if (ragdoll == null)
|
||||
{
|
||||
ragdoll = character.AnimController;
|
||||
}
|
||||
ragdoll ??= character.AnimController;
|
||||
if (ragdoll.Dir < 1.0f)
|
||||
{
|
||||
ragdoll.Flip();
|
||||
@@ -463,19 +471,31 @@ namespace Barotrauma
|
||||
ragdoll.AddLimb(huskAppendage);
|
||||
ragdoll.AddJoint(jointParams);
|
||||
appendage.Add(huskAppendage);
|
||||
}
|
||||
}
|
||||
}
|
||||
return appendage;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-3
@@ -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,14 @@ 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; }
|
||||
|
||||
/// <summary>
|
||||
/// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength.
|
||||
/// </summary>
|
||||
@@ -423,6 +436,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 +609,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 +627,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 +640,7 @@ 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 readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
|
||||
|
||||
@@ -898,7 +914,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);
|
||||
|
||||
@@ -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,6 +710,17 @@ 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();
|
||||
@@ -731,7 +755,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 +768,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,7 +811,7 @@ 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
|
||||
@@ -789,15 +822,17 @@ namespace Barotrauma
|
||||
existingAffliction.Strength = newStrength;
|
||||
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 +840,10 @@ namespace Barotrauma
|
||||
|
||||
Character.HealthUpdateInterval = 0.0f;
|
||||
|
||||
CalculateVitality();
|
||||
KillIfOutOfVitality();
|
||||
if (recalculateVitality)
|
||||
{
|
||||
RecalculateVitality();
|
||||
}
|
||||
#if CLIENT
|
||||
if (OpenHealthWindow != this && limbHealth != null)
|
||||
{
|
||||
@@ -817,7 +854,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();
|
||||
@@ -902,14 +939,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 +979,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 +989,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
|
||||
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab, LimbType.None);
|
||||
float prevOxygen = OxygenAmount;
|
||||
if (IsUnconscious)
|
||||
{
|
||||
@@ -991,13 +1029,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 +1058,12 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public void RecalculateVitality()
|
||||
{
|
||||
CalculateVitality();
|
||||
KillIfOutOfVitality();
|
||||
}
|
||||
|
||||
private static float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -94,6 +94,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; }
|
||||
|
||||
|
||||
@@ -21,9 +21,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 +40,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 +74,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()
|
||||
@@ -128,13 +128,18 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
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 +149,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 +177,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 +219,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)
|
||||
|
||||
@@ -95,20 +95,59 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
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 = 1;
|
||||
|
||||
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("showpreview", true);
|
||||
GameMode = element.GetAttributeEnum("gamemode", parentItem?.GameMode ?? GameModeType.Any);
|
||||
Amount = element.GetAttributeInt("amount", 1);
|
||||
Equip = element.GetAttributeBool("equip", false);
|
||||
Outfit = element.GetAttributeBool("outfit", 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>();
|
||||
@@ -211,7 +250,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 +258,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 +284,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);
|
||||
|
||||
@@ -40,10 +40,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
|
||||
@@ -20,19 +20,32 @@ namespace Barotrauma
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,10 +258,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!mouthPos.HasValue)
|
||||
{
|
||||
mouthPos = Params.MouthPos;
|
||||
}
|
||||
mouthPos ??= Params.MouthPos;
|
||||
return mouthPos.Value;
|
||||
}
|
||||
set
|
||||
@@ -366,7 +363,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 +383,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
|
||||
@@ -636,7 +641,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 +676,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;
|
||||
@@ -937,7 +942,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 +994,7 @@ namespace Barotrauma
|
||||
public void ReEnable()
|
||||
{
|
||||
if (!temporarilyDisabled) { return; }
|
||||
temporarilyDisabled = false;
|
||||
Hidden = false;
|
||||
Disabled = false;
|
||||
IgnoreCollisions = originalIgnoreCollisions;
|
||||
@@ -1008,6 +1014,8 @@ namespace Barotrauma
|
||||
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
|
||||
bool wasRunning = attack.IsRunning;
|
||||
attack.UpdateAttackTimer(deltaTime, character);
|
||||
attack.DamageMultiplier = 1.0f + character.GetStatValue(attack.Ranged ? StatTypes.NaturalRangedAttackMultiplier : StatTypes.NaturalMeleeAttackMultiplier);
|
||||
|
||||
if (attack.Blink)
|
||||
{
|
||||
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Any())
|
||||
@@ -1434,6 +1442,7 @@ namespace Barotrauma
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
ragdoll.SubtractMass(this);
|
||||
body?.Remove();
|
||||
body = null;
|
||||
if (pullJoint != null)
|
||||
|
||||
+43
-4
@@ -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
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
@@ -161,7 +183,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");
|
||||
@@ -433,17 +455,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 +565,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 +744,47 @@ 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 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));
|
||||
}
|
||||
|
||||
/// <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 +820,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 +871,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 +925,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]
|
||||
|
||||
+15
-3
@@ -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; }
|
||||
|
||||
@@ -654,7 +654,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 +705,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 +778,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 +804,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; }
|
||||
|
||||
+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)
|
||||
{
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
|
||||
+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
@@ -26,7 +26,7 @@ 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;
|
||||
if (characterJob is null) { continue; }
|
||||
|
||||
+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
@@ -20,7 +20,7 @@ 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);
|
||||
|
||||
+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; }
|
||||
|
||||
+5
-1
@@ -24,7 +24,11 @@ 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);
|
||||
|
||||
+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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
+15
-11
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -86,23 +86,27 @@ namespace Barotrauma
|
||||
|
||||
if (ragdollParams != null)
|
||||
{
|
||||
HashSet<string> texturePaths = new HashSet<string>
|
||||
{
|
||||
ContentPath.FromRaw(CharacterPrefab.Prefabs[speciesName].ContentPackage, ragdollParams.Texture).Value
|
||||
};
|
||||
HashSet<ContentPath> texturePaths = new HashSet<ContentPath>();
|
||||
AddTexturePath(ragdollParams.Texture);
|
||||
foreach (RagdollParams.LimbParams limb in ragdollParams.Limbs)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(limb.normalSpriteParams?.Texture)) { texturePaths.Add(limb.normalSpriteParams.Texture); }
|
||||
if (!string.IsNullOrEmpty(limb.deformSpriteParams?.Texture)) { texturePaths.Add(limb.deformSpriteParams.Texture); }
|
||||
if (!string.IsNullOrEmpty(limb.damagedSpriteParams?.Texture)) { texturePaths.Add(limb.damagedSpriteParams.Texture); }
|
||||
AddTexturePath(limb.normalSpriteParams?.Texture);
|
||||
AddTexturePath(limb.deformSpriteParams?.Texture);
|
||||
AddTexturePath(limb.damagedSpriteParams?.Texture);
|
||||
foreach (var decorativeSprite in limb.decorativeSpriteParams)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(decorativeSprite.Texture)) { texturePaths.Add(decorativeSprite.Texture); }
|
||||
AddTexturePath(decorativeSprite.Texture);
|
||||
}
|
||||
}
|
||||
foreach (string texturePath in texturePaths)
|
||||
foreach (ContentPath texturePath in texturePaths)
|
||||
{
|
||||
addPreloadedSprite(new Sprite(texturePath, Vector2.Zero));
|
||||
addPreloadedSprite(new Sprite(texturePath.Value, Vector2.Zero));
|
||||
}
|
||||
|
||||
void AddTexturePath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) { return; }
|
||||
texturePaths.Add(ContentPath.FromRaw(characterPrefab.ContentPackage, ragdollParams.Texture));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal sealed class DisembarkPerkFile : GenericPrefabFile<DisembarkPerkPrefab>
|
||||
{
|
||||
public DisembarkPerkFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
|
||||
|
||||
protected override bool MatchesSingular(Identifier identifier) => identifier == "disembarkperk";
|
||||
protected override bool MatchesPlural(Identifier identifier) => identifier == "disembarkperks";
|
||||
protected override PrefabCollection<DisembarkPerkPrefab> Prefabs => DisembarkPerkPrefab.Prefabs;
|
||||
protected override DisembarkPerkPrefab CreatePrefab(ContentXElement element)
|
||||
{
|
||||
return new DisembarkPerkPrefab(element, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
-4
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Steam;
|
||||
using System;
|
||||
@@ -9,6 +9,8 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using System.Xml;
|
||||
using Barotrauma.IO;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -34,9 +36,9 @@ namespace Barotrauma
|
||||
public const string FileListFileName = "filelist.xml";
|
||||
public const string DefaultModVersion = "1.0.0";
|
||||
|
||||
public readonly string Name;
|
||||
public string Name { get; private set; }
|
||||
public readonly ImmutableArray<string> AltNames;
|
||||
public readonly string Path;
|
||||
public string Path { get; private set; }
|
||||
public string Dir => Barotrauma.IO.Path.GetDirectoryName(Path) ?? "";
|
||||
public readonly Option<ContentPackageId> UgcId;
|
||||
|
||||
@@ -265,7 +267,7 @@ namespace Barotrauma
|
||||
//The game should be able to work just fine with a completely arbitrary file load order.
|
||||
//To make sure we don't mess this up, debug builds randomize it so it has a higher chance
|
||||
//of breaking anything that's not implemented correctly.
|
||||
.Randomize()
|
||||
.Randomize(Rand.RandSync.Unsynced)
|
||||
#endif
|
||||
;
|
||||
|
||||
@@ -397,5 +399,51 @@ namespace Barotrauma
|
||||
static string errorToStr(LoadError error)
|
||||
=> error.ToString();
|
||||
}
|
||||
|
||||
public bool TryRenameLocal(string newName)
|
||||
{
|
||||
if (!ContentPackageManager.LocalPackages.Contains(this)) { return false; }
|
||||
|
||||
if (newName.IsNullOrWhiteSpace())
|
||||
{
|
||||
DebugConsole.ThrowError($"New name is blank!");
|
||||
return false;
|
||||
}
|
||||
|
||||
string newDir = IO.Path.Combine(IO.Path.GetFullPath(LocalModsDir), File.SanitizeName(newName));
|
||||
if (ContentPackageManager.LocalPackages.Any(lp => lp.NameMatches(newName)) || Directory.Exists(newDir))
|
||||
{
|
||||
DebugConsole.ThrowError($"A local package with the name or directory \"{newName}\" already exists!");
|
||||
return false;
|
||||
}
|
||||
|
||||
XDocument doc = XMLExtensions.TryLoadXml(Path);
|
||||
doc.Root!.SetAttributeValue("name", newName);
|
||||
using (IO.XmlWriter writer = IO.XmlWriter.Create(Path, new XmlWriterSettings { Indent = true }))
|
||||
{
|
||||
doc.WriteTo(writer);
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
Directory.Move(Dir, newDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryDeleteLocal() => ContentPackageManager.LocalPackages.Contains(this) && Directory.TryDelete(Dir);
|
||||
|
||||
public bool TryCreateLocalFromWorkshop()
|
||||
{
|
||||
if (!ContentPackageManager.WorkshopPackages.Contains(this)) { return false; }
|
||||
|
||||
string newDir = IO.Path.Combine(IO.Path.GetFullPath(LocalModsDir), File.SanitizeName(Name));
|
||||
if (ContentPackageManager.LocalPackages.Any(lp => lp.NameMatches(Name)) || Directory.Exists(newDir))
|
||||
{
|
||||
DebugConsole.ThrowError($"A local package with the name or directory \"{Name}\" already exists!");
|
||||
return false;
|
||||
}
|
||||
|
||||
Directory.Copy(Dir, newDir);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,7 @@ namespace Barotrauma
|
||||
public Rectangle GetAttributeRect(string key, in Rectangle def) => Element.GetAttributeRect(key, def);
|
||||
public Version GetAttributeVersion(string key, Version def) => Element.GetAttributeVersion(key, def);
|
||||
public T GetAttributeEnum<T>(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def);
|
||||
public T[] GetAttributeEnumArray<T>(string key, T[] def) where T : struct, Enum => Element.GetAttributeEnumArray(key, def);
|
||||
public (T1, T2) GetAttributeTuple<T1, T2>(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def);
|
||||
public (T1, T2)[] GetAttributeTupleArray<T1, T2>(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def);
|
||||
public Range<int> GetAttributeRange(string key, in Range<int> def) => Element.GetAttributeRange(key, def);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
using System.Collections.Immutable;
|
||||
using Barotrauma.PerkBehaviors;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal sealed class DisembarkPerkPrefab : PrefabWithUintIdentifier
|
||||
{
|
||||
public static readonly PrefabCollection<DisembarkPerkPrefab> Prefabs = new PrefabCollection<DisembarkPerkPrefab>();
|
||||
|
||||
public LocalizedString Name { get; }
|
||||
public LocalizedString Description { get; }
|
||||
public Identifier SortCategory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// After the perks have been sorted by category and cost, they are sorted using this key.
|
||||
/// Use if you want the perks to be arranged in specific order when their cost are the same.
|
||||
/// </summary>
|
||||
public int SortKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// When set to an identifier of another perk, this perk cannot be selected unless the prerequisite perk is selected.
|
||||
/// </summary>
|
||||
public Identifier Prerequisite { get; }
|
||||
|
||||
/// <summary>
|
||||
/// When this perk is selected, the perks in this set cannot be selected at the same time.
|
||||
/// </summary>
|
||||
public ImmutableHashSet<Identifier> MutuallyExclusivePerks { get; }
|
||||
|
||||
public int Cost { get; }
|
||||
|
||||
public ImmutableArray<PerkBase> PerkBehaviors { get; }
|
||||
|
||||
public DisembarkPerkPrefab(ContentXElement element, DisembarkPerkFile prefabFile) : base(prefabFile, element.GetAttributeIdentifier("identifier", ""))
|
||||
{
|
||||
Name = TextManager.Get($"disembarkperk.{Identifier}").Fallback(Identifier.ToString());
|
||||
Description = TextManager.Get($"disembarkperkdescription.{Identifier}").Fallback("");
|
||||
Cost = element.GetAttributeInt("cost", 0);
|
||||
SortCategory = element.GetAttributeIdentifier("sortcategory", Identifier);
|
||||
Prerequisite = element.GetAttributeIdentifier("prerequisite", Identifier.Empty);
|
||||
MutuallyExclusivePerks = element.GetAttributeIdentifierImmutableHashSet("mutuallyexclusiveperks", ImmutableHashSet<Identifier>.Empty);
|
||||
SortKey = element.GetAttributeInt("sortkey", ToolBox.IdentifierToInt(Identifier));
|
||||
|
||||
var builder = ImmutableArray.CreateBuilder<PerkBase>();
|
||||
foreach (var child in element.Elements())
|
||||
{
|
||||
if (PerkBase.TryLoadFromXml(child, this, out var perk))
|
||||
{
|
||||
builder.Add(perk);
|
||||
}
|
||||
}
|
||||
|
||||
PerkBehaviors = builder.ToImmutable();
|
||||
}
|
||||
|
||||
public override void Dispose() { }
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma.PerkBehaviors
|
||||
{
|
||||
internal class GiveTalentPointPerk : PerkBase
|
||||
{
|
||||
[Serialize(0, IsPropertySaveable.Yes)]
|
||||
public int Amount { get; set; }
|
||||
|
||||
public GiveTalentPointPerk(ContentXElement element, DisembarkPerkPrefab prefab) : base(element, prefab) { }
|
||||
|
||||
public override void ApplyOnRoundStart(IReadOnlyCollection<Character> teamCharacters, Submarine teamSubmarine)
|
||||
{
|
||||
foreach (Character character in teamCharacters)
|
||||
{
|
||||
character.Info.AdditionalTalentPoints += Amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Barotrauma.PerkBehaviors
|
||||
{
|
||||
internal enum PerkSimulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Perk is only run on the server,
|
||||
/// other parts of the game handle the client-side effects.
|
||||
/// Like serializable properties and affliction syncing.
|
||||
/// </summary>
|
||||
ServerOnly,
|
||||
/// <summary>
|
||||
/// Both the server and clients run the perk.
|
||||
/// </summary>
|
||||
ServerAndClients
|
||||
}
|
||||
|
||||
internal abstract class PerkBase : ISerializableEntity
|
||||
{
|
||||
public string Name { get; }
|
||||
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; }
|
||||
|
||||
public virtual PerkSimulation Simulation => PerkSimulation.ServerOnly;
|
||||
|
||||
public readonly DisembarkPerkPrefab Prefab;
|
||||
|
||||
protected PerkBase(ContentXElement element, DisembarkPerkPrefab prefab)
|
||||
{
|
||||
Name = element.Name.ToString();
|
||||
Prefab = prefab;
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
}
|
||||
|
||||
public virtual bool CanApply(SubmarineInfo submarine)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// You might notice that this function is not virtual.
|
||||
/// It was at first, but there was a misunderstanding in design,
|
||||
/// so I turned it into a kill switch for all perks for now.
|
||||
/// If we ever want to add perks that do work when a submarine is present,
|
||||
/// this function can be made virtual again and set to true in the appropriate perks.
|
||||
/// </summary>
|
||||
public bool CanApplyWithoutSubmarine()
|
||||
=> false;
|
||||
|
||||
public abstract void ApplyOnRoundStart(IReadOnlyCollection<Character> teamCharacters, Submarine? teamSubmarine);
|
||||
|
||||
public static bool TryLoadFromXml(ContentXElement element, DisembarkPerkPrefab prefab, [NotNullWhen(true)] out PerkBase? perk)
|
||||
{
|
||||
Type? type = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.PerkBehaviors", element.Name.ToString(), throwOnError: false, ignoreCase: true);
|
||||
if (type is null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find a perk behavior of the type \"{element.Name}\".", contentPackage: element.ContentPackage);
|
||||
perk = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object? instance = Activator.CreateInstance(type, element, prefab);
|
||||
if (instance is PerkBase perkInstance)
|
||||
{
|
||||
perk = perkInstance;
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new InvalidCastException($"Could not cast the instance of type \"{type}\" to a {nameof(PerkBase)}.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError(e.InnerException != null ? e.InnerException.ToString() : e.ToString(), contentPackage: element.ContentPackage);
|
||||
perk = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma.PerkBehaviors
|
||||
{
|
||||
internal class SpawnItemPerk : PerkBase
|
||||
{
|
||||
public SpawnItemPerk(ContentXElement element, DisembarkPerkPrefab prefab) : base(element, prefab) { }
|
||||
|
||||
public override PerkSimulation Simulation
|
||||
=> PerkSimulation.ServerOnly;
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public Identifier Identifier { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public Identifier Tag { get; set; }
|
||||
|
||||
[Serialize(0, IsPropertySaveable.Yes)]
|
||||
public int MinAmount { get; set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes)]
|
||||
public float PerPlayer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When set to non-empty value, the perk will prioritize spawning items in containers
|
||||
/// with this tag or identifier over the item's primary and secondary preferred containers.
|
||||
/// </summary>
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public Identifier PriorityContainerTag { get; set; }
|
||||
|
||||
public override void ApplyOnRoundStart(IReadOnlyCollection<Character> teamCharacters, Submarine teamSubmarine)
|
||||
{
|
||||
if (teamSubmarine is null) { return; }
|
||||
|
||||
if (Entity.Spawner is null)
|
||||
{
|
||||
DebugConsole.ThrowError($"{nameof(SpawnItemPerk)} ({Prefab.Identifier}) failed to spawn items because EntitySpawner is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
int amount = Math.Max(MinAmount, (int)MathF.Ceiling(PerPlayer * teamCharacters.Count));
|
||||
|
||||
if (Identifier.IsEmpty)
|
||||
{
|
||||
if (Tag.IsEmpty)
|
||||
{
|
||||
DebugConsole.ThrowError($"{nameof(SpawnItemPerk)} ({Prefab.Identifier}) failed to spawn items: neither identifier or tag is set.",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
return;
|
||||
}
|
||||
var matchingItems = ItemPrefab.Prefabs.Where(ip => ip.Tags.Contains(Tag));
|
||||
if (matchingItems.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"{nameof(SpawnItemPerk)} ({Prefab.Identifier}) failed to spawn items: no items found with the tag \"{Tag}\".",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
SpawnItem(matchingItems.GetRandomUnsynced(), amount: 1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
ItemPrefab prefab = ItemPrefab.Find(null, Identifier);
|
||||
if (prefab is null)
|
||||
{
|
||||
DebugConsole.ThrowError($"{nameof(SpawnItemPerk)} ({Prefab.Identifier}) failed to spawn items because the ItemPrefab \"{Identifier}\" was not found.",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
return;
|
||||
}
|
||||
SpawnItem(prefab, amount);
|
||||
}
|
||||
|
||||
void SpawnItem(ItemPrefab prefab, int amount)
|
||||
{
|
||||
SuitableContainers suitableContainers = FindSuitableContainers(prefab, teamSubmarine);
|
||||
|
||||
if (!suitableContainers.Any())
|
||||
{
|
||||
SpawnItemInCrate(prefab, teamSubmarine, amount);
|
||||
return;
|
||||
}
|
||||
SpawnInContainer(prefab, amount, suitableContainers, teamSubmarine);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct SuitableContainers(
|
||||
ICollection<ItemContainer> PriorityContainers,
|
||||
ICollection<ItemContainer> PreferredContainers,
|
||||
ICollection<ItemContainer> SecondaryContainers)
|
||||
{
|
||||
public bool Any()
|
||||
=> PriorityContainers.Count > 0
|
||||
|| PreferredContainers.Count > 0
|
||||
|| SecondaryContainers.Count > 0;
|
||||
}
|
||||
|
||||
private SuitableContainers FindSuitableContainers(ItemPrefab prefab, Submarine submarine)
|
||||
{
|
||||
HashSet<ItemContainer> priorityContainers = new();
|
||||
HashSet<ItemContainer> primaryContainers = new();
|
||||
HashSet<ItemContainer> secondaryContainers = new();
|
||||
|
||||
foreach (Item item in submarine.GetItems(alsoFromConnectedSubs: true))
|
||||
{
|
||||
if (item.GetComponent<Fabricator>() != null || item.GetComponent<Deconstructor>() != null) { continue; }
|
||||
if (item.NonInteractable || item.NonPlayerTeamInteractable || item.IsHidden) { continue; }
|
||||
|
||||
if (item.GetComponent<ItemContainer>() is { } container)
|
||||
{
|
||||
if (!container.CanBeContained(prefab)) { continue; }
|
||||
|
||||
var tags = item.GetTags();
|
||||
|
||||
if (!PriorityContainerTag.IsEmpty && (tags.Contains(PriorityContainerTag) || item.Prefab.Identifier == PriorityContainerTag))
|
||||
{
|
||||
priorityContainers.Add(container);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prefab.PreferredContainers.Any(pc => pc.Primary.Any(tags.Contains)))
|
||||
{
|
||||
primaryContainers.Add(container);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prefab.PreferredContainers.Any(pc => pc.Secondary.Any(tags.Contains)))
|
||||
{
|
||||
secondaryContainers.Add(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SuitableContainers(priorityContainers, primaryContainers, secondaryContainers);
|
||||
}
|
||||
|
||||
private static void SpawnItemInCrate(ItemPrefab prefab, Submarine submarine, int amount)
|
||||
{
|
||||
var purchasedItem = new PurchasedItem(prefab, amount, buyer: null);
|
||||
CargoManager.DeliverItemsToSub(new []{ purchasedItem }, submarine, cargoManager: null, showNotification: false);
|
||||
}
|
||||
|
||||
private static void SpawnInContainer(ItemPrefab prefab, int amount, SuitableContainers containers, Submarine submarine)
|
||||
{
|
||||
Dictionary<ItemContainer, int> containerAllocation = new();
|
||||
|
||||
int remaining = amount;
|
||||
|
||||
TryAllocate(containers.PriorityContainers);
|
||||
if (remaining > 0)
|
||||
{
|
||||
TryAllocate(containers.PreferredContainers);
|
||||
if (remaining > 0)
|
||||
{
|
||||
TryAllocate(containers.SecondaryContainers);
|
||||
}
|
||||
}
|
||||
|
||||
void TryAllocate(ICollection<ItemContainer> targetContainers)
|
||||
=> AllocateContainers(prefab, targetContainers, ref remaining, ref containerAllocation);
|
||||
|
||||
foreach (var (container, howManyToPut) in containerAllocation)
|
||||
{
|
||||
for (int i = 0; i < howManyToPut; i++)
|
||||
{
|
||||
SpawnItem(prefab, container);
|
||||
}
|
||||
}
|
||||
|
||||
if (remaining > 0)
|
||||
{
|
||||
SpawnItemInCrate(prefab, submarine, remaining);
|
||||
}
|
||||
|
||||
static void AllocateContainers(ItemPrefab prefab, ICollection<ItemContainer> containers, ref int remaining, ref Dictionary<ItemContainer, int> containerAllocation)
|
||||
{
|
||||
foreach (ItemContainer ic in containers)
|
||||
{
|
||||
int fit = ic.Inventory.HowManyCanBePut(prefab);
|
||||
if (fit <= 0) { continue; }
|
||||
|
||||
fit = Math.Min(fit, remaining);
|
||||
|
||||
containerAllocation.Add(ic, fit);
|
||||
remaining -= fit;
|
||||
|
||||
if (remaining <= 0) { break; }
|
||||
}
|
||||
}
|
||||
|
||||
static void SpawnItem(ItemPrefab itemPrefab, ItemContainer container)
|
||||
{
|
||||
if (container?.Item is null) { return; }
|
||||
|
||||
Item item = new Item(itemPrefab, container.Item.Position, container.Item.Submarine);
|
||||
container.Inventory.TryPutItem(item, user: null);
|
||||
CargoManager.ItemSpawned(item);
|
||||
#if SERVER
|
||||
Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.PerkBehaviors
|
||||
{
|
||||
internal class SubItemSwapPerk : PerkBase
|
||||
{
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public Identifier TargetItem { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public Identifier ReplacementItem { get; set; }
|
||||
|
||||
public override PerkSimulation Simulation
|
||||
=> PerkSimulation.ServerOnly;
|
||||
|
||||
public SubItemSwapPerk(ContentXElement element, DisembarkPerkPrefab prefab) : base(element, prefab) { }
|
||||
|
||||
public override bool CanApply(SubmarineInfo submarine)
|
||||
{
|
||||
XElement subElement = submarine.SubmarineElement;
|
||||
|
||||
foreach (XElement element in subElement.Elements())
|
||||
{
|
||||
if (!element.Name.ToString().Equals(nameof(Item), StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
|
||||
Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
|
||||
if (identifier == TargetItem)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void ApplyOnRoundStart(IReadOnlyCollection<Character> teamCharacters, Submarine teamSubmarine)
|
||||
{
|
||||
if (teamSubmarine is null) { return; }
|
||||
|
||||
List<Item> items = teamSubmarine.GetItems(true);
|
||||
|
||||
ItemPrefab itemToInstall = ItemPrefab.Find(null, ReplacementItem);
|
||||
if (itemToInstall is null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find item \"{ReplacementItem}\" to swap with \"{TargetItem}\".");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Item item in items)
|
||||
{
|
||||
if (item.Prefab.Identifier == TargetItem)
|
||||
{
|
||||
item.ReplaceWithLinkedItems(itemToInstall);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma.PerkBehaviors
|
||||
{
|
||||
internal class UpgradeSubmarinePerk : PerkBase
|
||||
{
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public Identifier UpgradeIdentifier { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public Identifier CategoryIdentifier { get; set; }
|
||||
|
||||
[Serialize(0, IsPropertySaveable.Yes)]
|
||||
public int Level { get; set; }
|
||||
|
||||
public override PerkSimulation Simulation
|
||||
=> PerkSimulation.ServerAndClients;
|
||||
|
||||
public UpgradeSubmarinePerk(ContentXElement element, DisembarkPerkPrefab prefab) : base(element, prefab) { }
|
||||
|
||||
public override void ApplyOnRoundStart(IReadOnlyCollection<Character> teamCharacters, Submarine teamSubmarine)
|
||||
{
|
||||
if (teamSubmarine is null) { return; }
|
||||
|
||||
bool prefabFound = UpgradePrefab.Prefabs.TryGet(UpgradeIdentifier, out UpgradePrefab upgradePrefab);
|
||||
bool categoryFound = UpgradeCategory.Categories.TryGet(CategoryIdentifier, out UpgradeCategory upgradeCategory);
|
||||
|
||||
if (!prefabFound)
|
||||
{
|
||||
DebugConsole.ThrowError($"{nameof(UpgradeSubmarinePerk)}: Upgrade prefab not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (upgradePrefab.IsWallUpgrade)
|
||||
{
|
||||
foreach (Structure structure in teamSubmarine.GetWalls(UpgradeManager.UpgradeAlsoConnectedSubs))
|
||||
{
|
||||
structure.AddUpgrade(new Upgrade(structure, upgradePrefab, Level), createNetworkEvent: true);
|
||||
}
|
||||
}
|
||||
else if (categoryFound)
|
||||
{
|
||||
foreach (Item item in teamSubmarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs))
|
||||
{
|
||||
if (upgradeCategory.CanBeApplied(item, upgradePrefab))
|
||||
{
|
||||
item.AddUpgrade(new Upgrade(item, upgradePrefab, Level), createNetworkEvent: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"{nameof(UpgradeSubmarinePerk)}: Upgrade category not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,13 @@ namespace Barotrauma
|
||||
Exponential
|
||||
}
|
||||
|
||||
public enum SelectedSubType
|
||||
{
|
||||
Shuttle,
|
||||
Sub,
|
||||
EnemySub
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ActionTypes define when a <see cref="StatusEffect"/> is executed.
|
||||
/// </summary>
|
||||
@@ -116,6 +123,14 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
OnAbility = 23,
|
||||
/// <summary>
|
||||
/// Executes once when a specific Containable is placed inside an ItemContainer. Only valid for Containables defined in an ItemContainer component.
|
||||
/// </summary>
|
||||
OnInserted = 24,
|
||||
/// <summary>
|
||||
/// Executes once when a specific Containable is removed from an ItemContainer. Only valid for Containables defined in an ItemContainer component.
|
||||
/// </summary>
|
||||
OnRemoved = 25,
|
||||
/// <summary>
|
||||
/// Executes when the character dies. Only valid for characters.
|
||||
/// </summary>
|
||||
OnDeath = OnBroken
|
||||
@@ -588,7 +603,17 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Reduces the dual wielding penalty by a percentage.
|
||||
/// </summary>
|
||||
DualWieldingPenaltyReduction
|
||||
DualWieldingPenaltyReduction,
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier bonus to melee attacks coming from a natural weapon (limb).
|
||||
/// </summary>
|
||||
NaturalMeleeAttackMultiplier,
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier bonus to ranged attacks coming from a natural weapon (limb).
|
||||
/// </summary>
|
||||
NaturalRangedAttackMultiplier
|
||||
}
|
||||
|
||||
internal enum ItemTalentStats
|
||||
@@ -599,12 +624,13 @@ namespace Barotrauma
|
||||
EngineSpeed,
|
||||
EngineMaxSpeed,
|
||||
PumpSpeed,
|
||||
PumpMaxFlow,
|
||||
ReactorMaxOutput,
|
||||
ReactorFuelConsumption,
|
||||
DeconstructorSpeed,
|
||||
FabricationSpeed,
|
||||
ExtraStackSize
|
||||
ExtraStackSize,
|
||||
[Obsolete("Use PumpSpeed instead.")]
|
||||
PumpMaxFlow = PumpSpeed,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -723,4 +749,10 @@ namespace Barotrauma
|
||||
Local,
|
||||
Radio
|
||||
}
|
||||
|
||||
public enum PvpTeamSelectionMode
|
||||
{
|
||||
PlayerPreference,
|
||||
PlayerChoice,
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,6 @@ namespace Barotrauma
|
||||
foreach (ContentXElement subElement in conditionalElements)
|
||||
{
|
||||
conditionalList.AddRange(PropertyConditional.FromXElement(subElement));
|
||||
break;
|
||||
}
|
||||
Conditionals = conditionalList.ToImmutableArray();
|
||||
}
|
||||
|
||||
@@ -200,6 +200,10 @@ namespace Barotrauma
|
||||
{
|
||||
condition = $"{value1.ColorizeObject()} {Operator.ColorizeObject()} {value2.ColorizeObject()}";
|
||||
}
|
||||
else if (!Identifier.IsEmpty)
|
||||
{
|
||||
condition = $"{Identifier} {Condition}".ColorizeObject();
|
||||
}
|
||||
|
||||
return $"{ToolBox.GetDebugSymbol(succeeded.HasValue)} {nameof(CheckDataAction)} -> (Data: {Identifier.ColorizeObject()}, Success: {succeeded.ColorizeObject()}, Expression: {condition})";
|
||||
}
|
||||
|
||||
@@ -66,13 +66,12 @@ namespace Barotrauma
|
||||
|
||||
public CheckItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers();
|
||||
itemTags = ItemTags.Split(",").ToIdentifiers();
|
||||
itemIdentifierSplit = ItemIdentifiers.ToIdentifiers().ToArray();
|
||||
itemTags = ItemTags.ToIdentifiers().ToArray();
|
||||
var conditionalList = new List<PropertyConditional>();
|
||||
foreach (ContentXElement subElement in element.GetChildElements("conditional"))
|
||||
{
|
||||
conditionalList.AddRange(PropertyConditional.FromXElement(subElement));
|
||||
break;
|
||||
}
|
||||
conditionals = conditionalList;
|
||||
|
||||
|
||||
@@ -113,13 +113,15 @@ namespace Barotrauma
|
||||
Text = elem.GetAttributeString("tag", string.Empty);
|
||||
textElement = elem;
|
||||
}
|
||||
}
|
||||
if (element.GetChildElement("Replace") != null)
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
$"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" +
|
||||
$" - unrecognized child element \"Replace\".",
|
||||
contentPackage: element.ContentPackage);
|
||||
else
|
||||
{
|
||||
string thisName = nameof(ConversationAction);
|
||||
DebugConsole.ThrowError(
|
||||
$"Error in {thisName} in the event \"{parentEvent.Prefab.Identifier}\"" +
|
||||
$" - unrecognized child element \"{elem.Name}\". If it's an action intended to execute after the {thisName}, " +
|
||||
$"it should be after the {thisName}, not inside it.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +247,17 @@ namespace Barotrauma
|
||||
|
||||
public int[] GetEndingOptions()
|
||||
{
|
||||
List<int> endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList();
|
||||
List<int> endings = Options
|
||||
.Where(group =>
|
||||
group.EndConversation ||
|
||||
//no actions = safe to assume this must end the conversation
|
||||
!group.Actions.Any() ||
|
||||
//no follow-up conversation and a goto makes the event jump somewhere else
|
||||
//we cannot easily determine whether that goto will lead to a follow-up conversation,
|
||||
//so it's safest to close this conversation to prevent it from getting stuck (the potential follow-up will open a new one)
|
||||
(group.Actions.None(a => a is ConversationAction) && group.Actions.Any(a => a is GoTo { EndConversation: true })))
|
||||
.Select(group => Options.IndexOf(group))
|
||||
.ToList();
|
||||
if (!ContinueConversation) { endings.Add(-1); }
|
||||
return endings.ToArray();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ namespace Barotrauma
|
||||
{
|
||||
public string Text;
|
||||
public List<EventAction> Actions;
|
||||
/// <summary>
|
||||
/// Should this option end the conversation (closing the conversation prompt?). By default, options that don't have any actions inside them, or that only have a GoTo action, end the conversation.
|
||||
/// But if there are other actions inside the option, the game assumes there may be some kind of a follow-up coming to the conversation, and by default leaves it open.
|
||||
/// </summary>
|
||||
public bool EndConversation;
|
||||
|
||||
private int currentSubAction = 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Barotrauma
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Makes the event jump to a <see cref="Label"/> somewhere else in the event.
|
||||
@@ -11,6 +11,9 @@ namespace Barotrauma
|
||||
[Serialize(-1, IsPropertySaveable.Yes, description: "How many times can this GoTo action be repeated? Can be used to make some parts of an event repeat a limited number of times. If negative or zero, there's no limit.")]
|
||||
public int MaxTimes { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "By default, jumping to another part in the event closes the active conversation prompt. Use this if if you want to keep it open instead.")]
|
||||
public bool EndConversation { get; set; }
|
||||
|
||||
private int counter;
|
||||
|
||||
public GoTo(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user