Unstable 0.15.15.0 (and the one before it I forgor)

This commit is contained in:
Markus Isberg
2021-11-18 21:34:30 +09:00
parent 10e5fd5f3e
commit 80f39cd2a3
257 changed files with 4916 additions and 2582 deletions
@@ -229,7 +229,7 @@ namespace Barotrauma
{
if (sectorRad >= MathHelper.TwoPi) { return true; }
Vector2 diff = worldPosition - WorldPosition;
return MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir)) <= sectorRad * 0.5f;
return Math.Abs(MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir))) <= sectorRad * 0.5f;
}
public void Remove()
@@ -524,11 +524,12 @@ namespace Barotrauma
if (Character.LockHands) { return; }
if (ObjectiveManager.CurrentObjective == null) { return; }
if (Character.CurrentHull == null) { return; }
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold && Character.NeedsOxygen;
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
{
if (!Character.NeedsAir) { return false; }
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
Hull targetHull = gotoObjective.GetTargetHull();
return gotoObjective.Target != null && targetHull == null ||
@@ -567,6 +568,7 @@ namespace Barotrauma
Character.AnimController.HeadInWater ||
Character.Submarine == null ||
(Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOn) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
@@ -625,7 +627,7 @@ namespace Barotrauma
if (removeDivingSuit)
{
var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR);
if (divingSuit != null)
if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS))
{
if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
@@ -950,7 +952,7 @@ namespace Barotrauma
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.RepairIconThreshold)) { continue; }
if (!item.Repairables.Any(r => r.IsBelowRepairIconThreshold)) { continue; }
if (AddTargets<AIObjectiveRepairItems, Item>(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRepairItem>())
{
var orderPrefab = Order.GetPrefab("reportbrokendevices");
@@ -1117,6 +1119,7 @@ namespace Barotrauma
// Don't react to attackers that are outside of the sub (e.g. AoE attacks)
return;
}
bool isAttackerFightingEnemy = false;
if (IsFriendly(attacker))
{
if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
@@ -1136,7 +1139,6 @@ namespace Barotrauma
}
else
{
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
// Inform other NPCs
if (cumulativeDamage > 1 || totalDamage >= 10)
{
@@ -1184,6 +1186,10 @@ namespace Barotrauma
}
}
}
if (!isAttackerFightingEnemy)
{
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
}
}
}
else
@@ -1211,7 +1217,15 @@ namespace Barotrauma
if (!(otherCharacter.AIController is HumanAIController otherHumanAI)) { continue; }
if (!otherHumanAI.IsFriendly(Character)) { continue; }
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
if (!isWitnessing && !CheckReportRange(Character, otherCharacter, ReportRange)) { continue; }
if (!isWitnessing)
{
//if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
//don't react to the attack
if (Character.IsDead || Character.IsUnconscious || !CheckReportRange(Character, otherCharacter, ReportRange))
{
continue;
}
}
var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing, dmgThreshold: attacker.TeamID == Character.TeamID ? 50 : 10);
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
@@ -1244,18 +1258,20 @@ namespace Barotrauma
return AIObjectiveCombat.CombatMode.None;
}
// If there are any enemies around, just ignore the friendly fire
if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsDead && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
{
isAttackerFightingEnemy = true;
return AIObjectiveCombat.CombatMode.None;
}
else if (isWitnessing && Character.CombatAction != null && !c.IsSecurity)
{
return Character.CombatAction.WitnessReaction;
}
else if (Character.IsInstigator && attacker.IsPlayer)
else if (attacker.IsPlayer && FindInstigator() is Character instigator)
{
// The guards don't react when the player attacks instigators.
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
// The guards don't react when the player there's an instigator around
isAttackerFightingEnemy = true;
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
}
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable))
{
@@ -1295,6 +1311,22 @@ namespace Barotrauma
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
}
}
Character FindInstigator()
{
if (Character.IsInstigator)
{
return Character;
}
else if (c.AIController is HumanAIController humanAi)
{
return Character.CharacterList.FirstOrDefault(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && ch.IsInstigator && humanAi.VisibleHulls.Contains(ch.CurrentHull));
}
else
{
return null;
}
}
}
}
}
@@ -1416,15 +1448,20 @@ namespace Barotrauma
return true;
}
public static bool NeedsDivingGear(Hull hull, out bool needsSuit)
public bool NeedsDivingGear(Hull hull, out bool needsSuit)
{
if (!Character.NeedsAir)
{
needsSuit = false;
return false;
}
needsSuit = false;
if (hull == null ||
hull.WaterPercentage > 90 ||
hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.5f))
{
needsSuit = true;
needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
return true;
}
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
@@ -1570,9 +1607,9 @@ namespace Barotrauma
Character thief = character;
bool someoneSpoke = false;
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
if ((item.SpawnedInOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
{
foreach (Character otherCharacter in Character.CharacterList)
{
@@ -1624,7 +1661,7 @@ namespace Barotrauma
}
}
}
else if (item.OwnInventory?.FindItem(it => it.SpawnedInOutpost && !item.AllowStealing, true) is { } foundItem)
else if (item.OwnInventory?.FindItem(it => it.SpawnedInCurrentOutpost && !item.AllowStealing, true) is { } foundItem)
{
ItemTaken(foundItem, character);
}
@@ -1698,7 +1735,7 @@ namespace Barotrauma
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, character))
{
if (item.Repairables.All(r => item.ConditionPercentage >= r.RepairThreshold)) { continue; }
if (item.Repairables.All(r => r.IsBelowRepairThreshold)) { continue; }
AddTargets<AIObjectiveRepairItems, Item>(character, item);
}
}
@@ -1786,7 +1823,7 @@ namespace Barotrauma
{
if (isCurrentHull)
{
CurrentHullSafety = 0;
CurrentHullSafety = character.NeedsAir ? 0 : 100;
}
return CurrentHullSafety;
}
@@ -1809,8 +1846,8 @@ namespace Barotrauma
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
if (hull == null) { return 0; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; }
if (hull == null) { return character.NeedsAir ? 0 : 100; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0 && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure)) { return 0; }
// Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower.
// With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD.
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage));
@@ -204,11 +204,7 @@ namespace Barotrauma
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && character.PressureProtection <= 0;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0;
if (newPath.Unreachable || newPath.Nodes.None())
{
useNewPath = false;
}
else if (!useNewPath && currentPath != null && currentPath.CurrentNode != null)
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
// Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset).
if (IsIdenticalPath())
@@ -12,20 +12,19 @@ namespace Barotrauma
class LatchOntoAI
{
const float RaycastInterval = 5.0f;
private float raycastTimer;
private Structure targetWall;
private Body targetBody;
private Vector2 attachSurfaceNormal;
private Submarine targetSubmarine;
private Character targetCharacter;
private readonly Character character;
public bool AttachToSub { get; private set; }
public bool AttachToWalls { get; private set; }
public bool AttachToCharacters { get; private set; }
public Submarine TargetSubmarine { get; private set; }
public Structure TargetWall { get; private set; }
public Character TargetCharacter { get; private set; }
private readonly float minDeattachSpeed, maxDeattachSpeed, maxAttachDuration, coolDown;
private readonly float damageOnDetach, detachStun;
private readonly bool weld;
@@ -51,7 +50,7 @@ namespace Barotrauma
public bool IsAttached => AttachJoints.Count > 0;
public bool IsAttachedToSub => IsAttached && targetSubmarine != null && targetCharacter == null;
public bool IsAttachedToSub => IsAttached && TargetSubmarine != null && TargetCharacter == null;
public LatchOntoAI(XElement element, EnemyAIController enemyAI)
{
@@ -93,9 +92,9 @@ namespace Barotrauma
var sub = wall.Submarine;
if (sub == null) { return; }
Reset();
targetWall = wall;
targetSubmarine = sub;
targetBody = targetSubmarine.PhysicsBody.FarseerBody;
TargetWall = wall;
TargetSubmarine = sub;
targetBody = TargetSubmarine.PhysicsBody.FarseerBody;
this.attachSurfaceNormal = attachSurfaceNormal;
_attachPos = attachPos;
}
@@ -103,23 +102,20 @@ namespace Barotrauma
public void SetAttachTarget(Character target)
{
if (!AttachToCharacters) { return; }
if (target.Submarine != character.Submarine) { return; }
Reset();
targetCharacter = target;
targetSubmarine = target.Submarine;
TargetCharacter = target;
targetBody = target.AnimController.Collider.FarseerBody;
attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition);
}
public void Update(EnemyAIController enemyAI, float deltaTime)
{
if (character.Submarine != null)
if (TargetCharacter != null && character.Submarine != TargetCharacter.Submarine ||
character.Submarine != null && TargetSubmarine != null && TargetCharacter == null)
{
if (targetCharacter != null && targetCharacter.Submarine != targetSubmarine ||
character.Submarine != null && targetSubmarine != null && targetCharacter == null)
{
DeattachFromBody(reset: true);
return;
}
DeattachFromBody(reset: true);
return;
}
if (IsAttached)
{
@@ -150,7 +146,7 @@ namespace Barotrauma
return;
}
}
if (targetCharacter != null)
if (TargetCharacter != null)
{
if (enemyAI.AttackingLimb?.attack == null)
{
@@ -159,10 +155,14 @@ namespace Barotrauma
else
{
float range = enemyAI.AttackingLimb.attack.DamageRange * 2f;
if (Vector2.DistanceSquared(targetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range)
if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range)
{
DeattachFromBody(reset: true, cooldown: 1);
}
else
{
TargetCharacter.Latchers.Add(this);
}
}
}
}
@@ -176,15 +176,15 @@ namespace Barotrauma
deattachCheckTimer -= deltaTime;
}
if (targetCharacter != null)
if (TargetCharacter != null)
{
// Own sim pos -> target where we are
_attachPos = character.SimPosition;
}
Vector2 transformedAttachPos = _attachPos;
if (character.Submarine == null && targetSubmarine != null)
if (character.Submarine == null && TargetSubmarine != null)
{
transformedAttachPos += ConvertUnits.ToSimUnits(targetSubmarine.Position);
transformedAttachPos += ConvertUnits.ToSimUnits(TargetSubmarine.Position);
}
if (transformedAttachPos != Vector2.Zero)
{
@@ -267,7 +267,7 @@ namespace Barotrauma
if (enemyAI.AttackingLimb == 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 != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos);
if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
{
AttachToBody(transformedAttachPos);
@@ -286,11 +286,11 @@ namespace Barotrauma
deattach = true;
attachCooldown = coolDown;
}
if (!deattach && targetWall != null && targetSubmarine != null)
if (!deattach && TargetWall != null && TargetSubmarine != null)
{
// Deattach if the wall is broken enough where we are attached to
int targetSection = targetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true);
if (enemyAI.CanPassThroughHole(targetWall, targetSection))
int targetSection = TargetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true);
if (enemyAI.CanPassThroughHole(TargetWall, targetSection))
{
deattach = true;
attachCooldown = coolDown;
@@ -298,7 +298,7 @@ namespace Barotrauma
if (!deattach)
{
// Deattach if the velocity is high
float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length();
float velocity = TargetSubmarine.Velocity == Vector2.Zero ? 0.0f : TargetSubmarine.Velocity.Length();
deattach = velocity > maxDeattachSpeed;
if (!deattach)
{
@@ -385,11 +385,8 @@ namespace Barotrauma
} as Joint;
GameMain.World.Add(colliderJoint);
AttachJoints.Add(colliderJoint);
if (targetCharacter != null)
{
targetCharacter.Latchers.Add(this);
}
AttachJoints.Add(colliderJoint);
TargetCharacter?.Latchers.Add(this);
if (maxAttachDuration > 0)
{
deattachCheckTimer = maxAttachDuration;
@@ -407,25 +404,19 @@ namespace Barotrauma
{
attachCooldown = cooldown;
}
TargetCharacter?.Latchers.Remove(this);
if (reset)
{
Reset();
}
if (targetCharacter != null)
{
targetCharacter.Latchers.Remove(this);
}
}
private void Reset()
{
if (targetCharacter != null)
{
targetCharacter.Latchers.Remove(this);
}
targetCharacter = null;
targetWall = null;
targetSubmarine = null;
TargetCharacter?.Latchers.Remove(this);
TargetCharacter = null;
TargetWall = null;
TargetSubmarine = null;
targetBody = null;
AttachPos = null;
}
@@ -93,13 +93,6 @@ namespace Barotrauma
_abandon = value;
if (_abandon)
{
#if DEBUG
if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() && !objectiveManager.IsCurrentOrder<AIObjectiveReturn>())
{
// TODO: dismiss
throw new Exception("Order abandoned!");
}
#endif
OnAbandon();
}
}
@@ -247,7 +240,7 @@ namespace Barotrauma
}
}
protected bool IsAllowed
public bool IsAllowed
{
get
{
@@ -271,7 +264,7 @@ namespace Barotrauma
if (!IsAllowed)
{
Priority = 0;
Abandon = !isOrder;
Abandon = true;
return Priority;
}
if (isOrder)
@@ -94,7 +94,7 @@ namespace Barotrauma
if (item == null) { return false; }
if (item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (item.SpawnedInOutpost) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
{
if (item.Container == null)
@@ -294,10 +294,6 @@ namespace Barotrauma
}
}
private bool IsLoaded(ItemComponent weapon, bool checkContainedItems = true) =>
weapon.HasRequiredContainedItems(character, addMessage: false) &&
(!checkContainedItems || weapon.Item.OwnInventory == null || weapon.Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
private bool TryArm()
{
if (character.LockHands || Enemy == null)
@@ -325,7 +321,7 @@ namespace Barotrauma
Weapon = null;
continue;
}
if (IsLoaded(WeaponComponent, checkContainedItems: true))
if (WeaponComponent.IsLoaded(character))
{
// All good, the weapon is loaded
break;
@@ -380,6 +376,7 @@ namespace Barotrauma
constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false)
{
AllowStealing = HumanAIController.IsMentallyUnstable,
EvaluateCombatPriority = false, // Use a custom formula instead
GetItemPriority = i =>
{
if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; }
@@ -433,7 +430,7 @@ namespace Barotrauma
// Not in the inventory anymore or cannot find the weapon component
return false;
}
if (!IsLoaded(WeaponComponent))
if (!WeaponComponent.IsLoaded(character))
{
// Try reloading (and seek ammo)
if (!Reload(seekAmmo))
@@ -475,7 +472,7 @@ namespace Barotrauma
foreach (var weapon in weaponList)
{
float priority = weapon.CombatPriority;
if (!IsLoaded(weapon))
if (!weapon.IsLoaded(character))
{
if (weapon is RangedWeapon && enemyIsClose)
{
@@ -564,31 +561,6 @@ namespace Barotrauma
}
return weaponComponent.Item;
static Attack GetAttackDefinition(ItemComponent weapon)
{
Attack attack = null;
if (weapon is MeleeWeapon meleeWeapon)
{
attack = meleeWeapon.Attack;
}
else if (weapon is RangedWeapon rangedWeapon)
{
attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
}
return attack;
}
static float GetLethalDamage(ItemComponent weapon)
{
float lethalDmg = 0;
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
}
return lethalDmg;
}
float ApproximateStunDamage(ItemComponent weapon, Attack attack)
{
// Try to reduce the priority using the actual damage values and status effects.
@@ -628,6 +600,31 @@ namespace Barotrauma
}
}
public static float GetLethalDamage(ItemComponent weapon)
{
float lethalDmg = 0;
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
}
return lethalDmg;
}
private static Attack GetAttackDefinition(ItemComponent weapon)
{
Attack attack = null;
if (weapon is MeleeWeapon meleeWeapon)
{
attack = meleeWeapon.Attack;
}
else if (weapon is RangedWeapon rangedWeapon)
{
attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
}
return attack;
}
private HashSet<ItemComponent> FindWeaponsFromInventory()
{
weapons.Clear();
@@ -788,7 +785,6 @@ namespace Barotrauma
{
UsePathingOutside = false,
IgnoreIfTargetDead = true,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = Enemy.DisplayName,
AlwaysUseEuclideanDistance = false
},
@@ -812,7 +808,7 @@ namespace Barotrauma
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs");
if (prefab != null)
{
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInOutpost = true);
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true);
}
}
RemoveFollowTarget();
@@ -144,7 +144,6 @@ namespace Barotrauma
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = container.Item.Name,
AbortCondition = obj =>
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) ||
@@ -19,10 +19,15 @@ namespace Barotrauma
private AIObjectiveContainItem getOxygen;
private Item targetItem;
public static float MIN_OXYGEN = 10;
public static string HEAVY_DIVING_GEAR = "deepdiving";
public static string LIGHT_DIVING_GEAR = "lightdiving";
public static string OXYGEN_SOURCE = "oxygensource";
public const float MIN_OXYGEN = 10;
public const string HEAVY_DIVING_GEAR = "deepdiving";
public const string LIGHT_DIVING_GEAR = "lightdiving";
/// <summary>
/// Diving gear that's suitable for wearing indoors (-> the bots don't try to unequip it when they don't need diving gear)
/// </summary>
public const string DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors";
public const string OXYGEN_SOURCE = "oxygensource";
protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head);
@@ -46,10 +46,17 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() ||
objectiveManager.IsCurrentOrder<AIObjectiveReturn>() ||
objectiveManager.Objectives.Any(o => o.Priority > 0 && o is AIObjectiveCombat))
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
if (!character.NeedsAir)
{
Priority = 0;
}
else
{
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() ||
objectiveManager.IsCurrentOrder<AIObjectiveReturn>() ||
objectiveManager.Objectives.Any(o => o.Priority > 0 && o is AIObjectiveCombat))
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
}
}
else
{
@@ -1,6 +1,7 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Linq;
@@ -11,6 +12,7 @@ namespace Barotrauma
public override string Identifier { get; set; } = "get item";
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowMultipleInstances => true;
public HashSet<Item> ignoredItems = new HashSet<Item>();
@@ -19,7 +21,7 @@ namespace Barotrauma
public float TargetCondition { get; set; } = 1;
public bool AllowDangerousPressure { get; set; }
private readonly string[] identifiersOrTags;
private readonly ImmutableArray<string> identifiersOrTags;
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs)
private bool spawnItemIfNotFound = false;
@@ -31,6 +33,7 @@ namespace Barotrauma
public Item TargetItem => targetItem;
private int currSearchIndex;
public string[] ignoredContainerIdentifiers;
public string[] ignoredIdentifiersOrTags;
private AIObjectiveGoTo goToObjective;
private float currItemPriority;
private readonly bool checkInventory;
@@ -51,6 +54,10 @@ namespace Barotrauma
public bool AllowVariants { get; set; }
public bool Equip { get; set; }
public bool Wear { get; set; }
public bool RequireLoaded { get; set; }
public bool EvaluateCombatPriority { get; set; }
public bool CheckPathForEachItem { get; set; }
public bool SpeakIfFails { get; set; }
public InvSlotType? EquipSlotType { get; set; }
@@ -67,18 +74,41 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, string identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: this(character, new string[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { }
public AIObjectiveGetItem(Character character, string[] identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
public AIObjectiveGetItem(Character character, IEnumerable<string> identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
Equip = equip;
this.identifiersOrTags = identifiersOrTags;
this.spawnItemIfNotFound = spawnItemIfNotFound;
for (int i = 0; i < identifiersOrTags.Length; i++)
{
identifiersOrTags[i] = identifiersOrTags[i].ToLowerInvariant();
}
this.checkInventory = checkInventory;
this.identifiersOrTags = ParseGearTags(identifiersOrTags).ToImmutableArray();
ignoredIdentifiersOrTags = ParseIgnoredTags(identifiersOrTags).ToArray();
}
public static IEnumerable<string> ParseGearTags(IEnumerable<string> identifiersOrTags)
{
var tags = new List<string>();
foreach (string tag in identifiersOrTags)
{
if (!tag.Contains('!'))
{
tags.Add(tag.ToLowerInvariant());
}
}
return tags;
}
public static IEnumerable<string> ParseIgnoredTags(IEnumerable<string> identifiersOrTags)
{
var ignoredTags = new List<string>();
foreach (string tag in identifiersOrTags)
{
if (tag.Contains('!'))
{
ignoredTags.Add(tag.Remove("!").ToLowerInvariant());
}
}
return ignoredTags;
}
private bool CheckInventory()
@@ -219,6 +249,13 @@ namespace Barotrauma
}
else
{
if (!Equip)
{
// Try equipping and wearing the item
Wear = true;
Equip = true;
return;
}
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
@@ -243,6 +280,10 @@ namespace Barotrauma
{
// Try again
ignoredItems.Add(targetItem);
if (targetItem != moveToTarget && moveToTarget is Item item)
{
ignoredItems.Add(item);
}
ResetInternal();
}
else
@@ -269,7 +310,15 @@ namespace Barotrauma
}
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
bool checkPath = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.isFollowOrderObjective);
if (!CheckPathForEachItem)
{
// While following the player, let's ensure that there's a valid path to the target before accepting it.
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.isFollowOrderObjective);
}
bool checkPath = CheckPathForEachItem;
bool hasCalledPathFinder = false;
int itemsPerFrame = (int)priority;
for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++)
@@ -280,14 +329,20 @@ namespace Barotrauma
if (itemSub == null) { continue; }
Submarine mySub = character.Submarine;
if (mySub == null) { continue; }
if (!checkInventory)
{
// Ignore items in the inventory when defined not to check it.
if (item.IsOwnedBy(character)) { continue; }
}
if (!AllowStealing)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; }
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInCurrentOutpost) { continue; }
}
if (!CheckItem(item)) { continue; }
if (item.Container != null)
{
if (item.Container.HasTag("donttakeitems")) { continue; }
if (ignoredItems.Contains(item.Container)) { continue; }
if (ignoredContainerIdentifiers != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
@@ -315,17 +370,51 @@ namespace Barotrauma
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 10000, dist));
float minDistFactor = EvaluateCombatPriority ? 0.1f : 0;
float distanceFactor = MathHelper.Lerp(1, minDistFactor, MathUtils.InverseLerp(100, 10000, dist));
itemPriority *= distanceFactor;
itemPriority *= item.Condition / item.MaxCondition;
if (EvaluateCombatPriority)
{
var mw = item.GetComponent<MeleeWeapon>();
var rw = item.GetComponent<RangedWeapon>();
float combatFactor = 0;
if (mw != null)
{
if (mw.CombatPriority > 0)
{
combatFactor = mw.CombatPriority / 100;
}
else
{
// The combat factor of items with zero combat priority is not allowed to be greater than 0.1f
combatFactor = Math.Min(AIObjectiveCombat.GetLethalDamage(mw) / 1000, 0.1f);
}
}
else if (rw != null)
{
if (rw.CombatPriority > 0)
{
combatFactor = rw.CombatPriority / 100;
}
else
{
combatFactor = Math.Min(AIObjectiveCombat.GetLethalDamage(rw) / 1000, 0.1f);
}
}
else
{
combatFactor = Math.Min(item.Components.Sum(ic => AIObjectiveCombat.GetLethalDamage(ic)) / 1000, 0.1f);
}
itemPriority *= combatFactor;
}
else
{
itemPriority *= item.Condition / item.MaxCondition;
}
// Ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; }
if (!hasCalledPathFinder && PathSteering != null && checkPath)
{
// While following the player, let's ensure that there's a valid path to the target before accepting it.
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
hasCalledPathFinder = true;
var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable) { continue; }
@@ -355,7 +444,7 @@ namespace Barotrauma
targetItem = spawnedItem;
if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
{
spawnedItem.SpawnedInOutpost = true;
spawnedItem.SpawnedInCurrentOutpost = true;
}
});
}
@@ -365,7 +454,6 @@ namespace Barotrauma
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow);
#endif
SpeakCannotFind();
Abandon = true;
}
}
@@ -375,34 +463,19 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (IsCompleted) { return true; }
if (targetItem != null)
if (targetItem == null)
{
if (Equip && EquipSlotType.HasValue)
{
return character.HasEquippedItem(targetItem, EquipSlotType.Value);
}
else
{
return character.HasItem(targetItem, Equip);
}
}
else if (identifiersOrTags != null)
{
var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
if (matchingItem != null)
{
if (Equip && EquipSlotType.HasValue)
{
return character.HasEquippedItem(matchingItem, EquipSlotType.Value);
}
else
{
return !Equip || character.HasEquippedItem(matchingItem);
}
}
// Not yet ready
return false;
}
return false;
if (Equip && EquipSlotType.HasValue)
{
return character.HasEquippedItem(targetItem, EquipSlotType.Value);
}
else
{
return character.HasItem(targetItem, Equip);
}
}
private bool CheckItem(Item item)
@@ -410,8 +483,10 @@ namespace Barotrauma
if (!item.IsInteractable(character)) { return false; }
if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; }
if (ignoredItems.Contains(item)) { return false; };
if (ignoredIdentifiersOrTags != null && ignoredIdentifiersOrTags.Any(id => item.prefab.Identifier == id || item.HasTag(id))) { return false; }
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; }
return identifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id));
}
@@ -437,15 +512,20 @@ namespace Barotrauma
protected override void OnAbandon()
{
base.OnAbandon();
if (moveToTarget == null) { return; }
if (moveToTarget != null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow);
#endif
}
if (SpeakIfFails)
{
SpeakCannotFind();
}
}
private void SpeakCannotFind()
{
// TODO: Use the item name as the variable here.
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string msg = TextManager.Get("dialogcannotfinditem", true);
@@ -455,19 +535,5 @@ namespace Barotrauma
}
}
}
// TODO: remove?
private void SpeakCannotReach()
{
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString();
string msg = TargetName == null ? TextManager.Get("dialogcannotreachtarget", true) : TextManager.GetWithVariable("dialogcannotreachtarget", "[name]", TargetName, formatCapitals: !(moveToTarget is Character));
if (msg != null)
{
character.Speak(msg, identifier: "dialogcannotreachtarget", minDurationBetweenSimilar: 20.0f);
}
}
}
}
}
@@ -0,0 +1,91 @@
#nullable enable
using Barotrauma.Extensions;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma
{
class AIObjectiveGetItems : AIObjective
{
public override string Identifier { get; set; } = "get items";
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
public bool AllowStealing { get; set; }
public bool TakeWholeStack { get; set; }
public bool AllowVariants { get; set; }
public bool Equip { get; set; }
public bool Wear { get; set; }
public bool CheckInventory { get; set; }
public bool EvaluateCombatPriority { get; set; }
public bool CheckPathForEachItem { get; set; }
public bool RequireLoaded { get; set; }
private readonly ImmutableArray<string> gearTags;
private readonly string[] ignoredTags;
private bool subObjectivesCreated;
public readonly HashSet<Item> achievedItems = new HashSet<Item>();
public AIObjectiveGetItems(Character character, AIObjectiveManager objectiveManager, IEnumerable<string> identifiersOrTags, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
gearTags = AIObjectiveGetItem.ParseGearTags(identifiersOrTags).ToImmutableArray();
ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToArray();
}
protected override bool CheckObjectiveSpecific() => subObjectivesCreated && subObjectives.None();
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
if (!subObjectivesCreated)
{
foreach (string tag in gearTags)
{
AIObjectiveGetItem? getItem = null;
TryAddSubObjective(ref getItem, () =>
new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory)
{
AllowVariants = AllowVariants,
Wear = Wear,
TakeWholeStack = TakeWholeStack,
AllowStealing = AllowStealing,
ignoredIdentifiersOrTags = ignoredTags,
CheckPathForEachItem = CheckPathForEachItem,
RequireLoaded = RequireLoaded
},
onCompleted: () =>
{
var item = getItem?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
achievedItems.Add(item);
}
},
onAbandon: () =>
{
var item = getItem?.TargetItem;
if (item != null)
{
achievedItems.Remove(item);
}
RemoveSubObjective(ref getItem);
});
}
subObjectivesCreated = true;
}
}
public override void Reset()
{
base.Reset();
subObjectivesCreated = false;
achievedItems.Clear();
}
}
}
@@ -27,6 +27,7 @@ namespace Barotrauma
public bool isFollowOrderObjective;
public bool mimic;
public bool SpeakIfFails { get; set; } = true;
public bool DebugLogWhenFails { get; set; } = true;
public bool UsePathingOutside { get; set; } = true;
public float extraDistanceWhileSwimming;
@@ -61,6 +62,9 @@ namespace Barotrauma
}
}
// TODO: Currently we never check the visibility (to the end node), which is actually unintentional.
// I don't think it has caused any issues so far, so let's keep defaulting to false for now, because the less we do raycasts the better.
// However, if there are cases where the bots attempt to go through walls (select the end node that is behind an obstacle), we should set this true.
public bool CheckVisibility { get; set; }
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
@@ -77,7 +81,7 @@ namespace Barotrauma
public override bool AllowOutsideSubmarine => AllowGoingOutside;
public override bool AllowInAnySub => true;
public string DialogueIdentifier { get; set; }
public string DialogueIdentifier { get; set; } = "dialogcannotreachtarget";
public string TargetName { get; set; }
public ISpatialEntity Target { get; private set; }
@@ -149,7 +153,10 @@ namespace Barotrauma
private void SpeakCannotReach()
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
if (DebugLogWhenFails)
{
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
}
#endif
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective && DialogueIdentifier != null && SpeakIfFails)
{
@@ -170,17 +177,12 @@ namespace Barotrauma
Abandon = true;
return;
}
if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy))
if (cannotFollow || Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy))
{
// Wait
character.AIController.SteeringManager.Reset();
return;
}
if (cannotFollow)
{
// Wait
character.AIController.SteeringManager.Reset();
}
if (!character.IsClimbing)
{
character.SelectedConstruction = null;
@@ -211,26 +213,33 @@ namespace Barotrauma
}
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
bool isInside = character.CurrentHull != null;
bool targetIsOutside = (Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
if (isInside && targetIsOutside && !AllowGoingOutside)
bool hasOutdoorNodes = insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes;
if (isInside && hasOutdoorNodes && !AllowGoingOutside)
{
Abandon = true;
}
else if (HumanAIController.IsCurrentPathNullOrUnreachable)
else if (HumanAIController.SteeringManager == PathSteering)
{
waitUntilPathUnreachable -= deltaTime;
SteeringManager.Reset();
if (waitUntilPathUnreachable < 0)
if (HumanAIController.IsCurrentPathNullOrUnreachable)
{
SteeringManager.Reset();
if (waitUntilPathUnreachable < 0)
{
waitUntilPathUnreachable = pathWaitingTime;
if (repeat)
{
SpeakCannotReach();
}
else
{
Abandon = true;
}
}
}
else if (HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: false))
{
waitUntilPathUnreachable = pathWaitingTime;
if (repeat)
{
SpeakCannotReach();
}
else
{
Abandon = true;
}
}
}
if (!Abandon)
@@ -238,16 +247,16 @@ namespace Barotrauma
if (getDivingGearIfNeeded && !character.LockHands)
{
Character followTarget = Target as Character;
bool needsDivingSuit = !isInside || targetIsOutside;
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)) && character.NeedsAir;
if (mimic)
{
if (HumanAIController.HasDivingSuit(followTarget))
if (HumanAIController.HasDivingSuit(followTarget) && character.NeedsAir)
{
needsDivingGear = true;
needsDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget))
else if (HumanAIController.HasDivingMask(followTarget) && character.NeedsAir)
{
needsDivingGear = true;
}
@@ -323,7 +323,6 @@ namespace Barotrauma
SortObjectives();
}
private CoroutineHandle speakRoutine;
public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak)
{
if (character.IsDead)
@@ -379,6 +378,7 @@ namespace Barotrauma
var newCurrentOrder = CreateObjective(order, option, orderGiver);
if (newCurrentOrder != null)
{
newCurrentOrder.Abandoned += () => DismissSelf(order, option);
CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder));
}
if (!HasOrders())
@@ -386,53 +386,12 @@ namespace Barotrauma
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
CreateAutonomousObjectives();
}
else
else if (newCurrentOrder != null)
{
// This should be redundant, because all the objectives are reset when they are selected as active.
newCurrentOrder?.Reset();
if (speak && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
//if (speakRoutine != null)
//{
// CoroutineManager.StopCoroutines(speakRoutine);
//}
//speakRoutine = CoroutineManager.InvokeAfter(() =>
//{
// if (GameMain.GameSession == null || Level.Loaded == null) { return; }
// if (newCurrentOrder != null && character.SpeechImpediment < 100.0f)
// {
// if (newCurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets");
// }
// else if (newCurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries");
// }
// else if (newCurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire");
// }
// else if (newCurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks");
// }
// else if (newCurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies");
// }
// else if (newCurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
// }
// else if (newCurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps");
// }
// }
//}, 3);
string msg = newCurrentOrder.IsAllowed ? TextManager.Get("DialogAffirmative") : TextManager.Get("DialogNegative");
character.Speak(msg, delay: 1.0f);
}
}
}
@@ -465,7 +424,6 @@ namespace Barotrauma
break;
case "return":
newObjective = new AIObjectiveReturn(character, orderGiver, this, priorityModifier: priorityModifier);
newObjective.Abandoned += () => DismissSelf(order, option);
newObjective.Completed += () => DismissSelf(order, option);
break;
case "fixleaks":
@@ -491,12 +449,10 @@ namespace Barotrauma
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier)
{
IsLoop = true,
IsLoop = false,
Override = orderGiver != null && orderGiver.IsCommanding
};
// ItemComponent.AIOperate() returns false by default -> We'd have to set IsLoop = false and implement a custom override of AIOperate for the Pump.cs,
// if we want that the bot just switches the pump on/off and continues doing something else.
// If we want that the bot does the objective and then forgets about it, I think we could do the same plus dismiss when the bot is done.
newObjective.Completed += () => DismissSelf(order, option);
}
else
{
@@ -566,6 +522,26 @@ namespace Barotrauma
case "escapehandcuffs":
newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier);
break;
case "prepareforexpedition":
newObjective = new AIObjectivePrepare(character, this, order.TargetItems)
{
KeepActiveWhenReady = true,
CheckInventory = true,
Equip = false,
FindAllItems = true
};
break;
case "findweapon":
newObjective = new AIObjectivePrepare(character, this, order.TargetItems)
{
KeepActiveWhenReady = false,
CheckInventory = false,
Equip = true,
EvaluateCombatPriority = true,
FindAllItems = false
};
newObjective.Completed += () => DismissSelf(order, option);
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
@@ -213,7 +213,6 @@ namespace Barotrauma
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = target.Item.Name,
endNodeFilter = node => node.Waypoint.Ladders == null
},
@@ -0,0 +1,150 @@
#nullable enable
using Barotrauma.Extensions;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma
{
class AIObjectivePrepare : AIObjective
{
public override string Identifier { get; set; } = "prepare";
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
private AIObjectiveGetItem? getSingleItemObjective;
private AIObjectiveGetItems? getMultipleItemsObjective;
private bool subObjectivesCreated;
private readonly ImmutableArray<string> gearTags;
private readonly HashSet<Item> items = new HashSet<Item>();
public bool KeepActiveWhenReady { get; set; }
public bool CheckInventory { get; set; }
public bool FindAllItems { get; set; }
public bool Equip { get; set; }
public bool EvaluateCombatPriority { get; set; }
private AIObjective? GetSubObjective() => getSingleItemObjective ?? getMultipleItemsObjective as AIObjective;
public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, IEnumerable<string> items, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
gearTags = items.ToImmutableArray();
}
protected override bool CheckObjectiveSpecific() => IsCompleted;
protected override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
return Priority;
}
Priority = objectiveManager.GetOrderPriority(this);
var subObjective = GetSubObjective();
if (subObjective != null && subObjective.IsCompleted)
{
Priority = 0;
items.RemoveWhere(i => i == null || i.Removed || !i.IsOwnedBy(character));
if (items.None())
{
Abandon = true;
}
else if (items.Any(i => i.Components.Any(i => !i.IsLoaded(character))))
{
Reset();
}
}
return Priority;
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
if (!subObjectivesCreated)
{
if (FindAllItems)
{
if (!TryAddSubObjective(ref getMultipleItemsObjective, () => new AIObjectiveGetItems(character, objectiveManager, gearTags)
{
CheckInventory = CheckInventory,
Equip = Equip,
EvaluateCombatPriority = EvaluateCombatPriority,
RequireLoaded = true
},
onCompleted: () =>
{
if (KeepActiveWhenReady)
{
if (getMultipleItemsObjective != null)
{
foreach (var item in getMultipleItemsObjective.achievedItems)
{
if (item?.IsOwnedBy(character) != null)
{
items.Add(item);
}
}
}
}
else
{
IsCompleted = true;
}
},
onAbandon: () => Abandon = true))
{
Abandon = true;
}
}
else
{
if (!TryAddSubObjective(ref getSingleItemObjective, () => new AIObjectiveGetItem(character, gearTags, objectiveManager, equip: Equip, checkInventory: CheckInventory)
{
EvaluateCombatPriority = EvaluateCombatPriority,
SpeakIfFails = true,
RequireLoaded = true
},
onCompleted: () =>
{
if (KeepActiveWhenReady)
{
if (getSingleItemObjective != null)
{
var item = getSingleItemObjective?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
items.Add(item);
}
}
}
else
{
IsCompleted = true;
}
},
onAbandon: () => Abandon = true))
{
Abandon = true;
}
}
subObjectivesCreated = true;
}
}
public override void Reset()
{
base.Reset();
items.Clear();
subObjectivesCreated = false;
RemoveSubObjective(ref getMultipleItemsObjective);
RemoveSubObjective(ref getSingleItemObjective);
}
}
}
@@ -107,7 +107,7 @@ namespace Barotrauma
{
foreach (RelatedItem requiredItem in kvp.Value)
{
var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true)
var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, equip: true)
{
AllowVariants = requiredItem.AllowVariants
};
@@ -219,8 +219,7 @@ namespace Barotrauma
{
// Don't stop in ladders, because we can't interact with other items while holding the ladders.
endNodeFilter = node => node.Waypoint.Ladders == null,
// Allow repairing hatches and airlock doors.
AllowGoingOutside = HumanAIController.ObjectiveManager.IsCurrentOrder<AIObjectiveRepairItems>() && Item.GetComponent<Door>() != null
TargetName = Item.Name
};
if (repairTool != null)
{
@@ -91,8 +91,7 @@ namespace Barotrauma
public static bool NearlyFullCondition(Item item)
{
float condition = item.ConditionPercentage;
return item.Repairables.All(r => condition >= r.RepairThreshold);
return item.Repairables.All(r => !r.IsBelowRepairThreshold);
}
protected override float TargetEvaluation()
@@ -319,9 +319,32 @@ namespace Barotrauma
{
itemListStr = itemNameList[0];
}
else if (itemNameList.Count == 2)
{
//[treatment1] or [treatment2]
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsLast",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemNameList[0], itemNameList[1] });
}
else
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
//[treatment1], [treatment2], [treatment3] ... or [treatmentx]
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsFirst",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemNameList[0], itemNameList[1] });
for (int i = 2; i < itemNameList.Count - 1; i++)
{
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsFirst",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemListStr, itemNameList[i] });
}
itemListStr = TextManager.GetWithVariables(
"DialogRequiredTreatmentOptionsLast",
new string[] { "[treatment1]", "[treatment2]" },
new string[] { itemListStr, itemNameList.Last() });
}
if (targetCharacter != character && character.IsOnPlayerTeam)
{
@@ -79,6 +79,7 @@ namespace Barotrauma
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (target.IsInstigator) { return false; }
if (target.IsPet) { return false; }
if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
@@ -170,6 +170,7 @@ namespace Barotrauma
public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1;
public bool IsPrefab { get; private set; }
public readonly bool MustManuallyAssign;
public readonly bool AutoDismiss;
public readonly OrderTarget TargetPosition;
@@ -363,6 +364,7 @@ namespace Barotrauma
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis";
DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false);
AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Movement);
}
/// <summary>
@@ -1048,7 +1048,8 @@ namespace Barotrauma
void UpdateClimbing()
{
if (character.SelectedConstruction == null || character.SelectedConstruction.GetComponent<Ladder>() == null || character.IsIncapacitated)
var ladder = character.SelectedConstruction?.GetComponent<Ladder>();
if (ladder == null || character.IsIncapacitated)
{
Anim = Animation.None;
return;
@@ -1075,22 +1076,29 @@ namespace Barotrauma
if (leftHand == null || rightHand == null || head == null || torso == null) { return; }
Vector2 ladderSimPos = ConvertUnits.ToSimUnits(
character.SelectedConstruction.Rect.X + character.SelectedConstruction.Rect.Width / 2.0f,
character.SelectedConstruction.Rect.Y);
ladder.Item.Rect.X + ladder.Item.Rect.Width / 2.0f,
ladder.Item.Rect.Y);
Vector2 ladderSimSize = ConvertUnits.ToSimUnits(character.SelectedConstruction.Rect.Size.ToVector2());
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 (currentHull == null && character.SelectedConstruction.Submarine != null)
if (currentHull == null && ladder.Item.Submarine != null)
{
ladderSimPos += character.SelectedConstruction.Submarine.SimPosition;
ladderSimPos += ladder.Item.Submarine.SimPosition;
}
else if (currentHull?.Submarine != null && currentHull.Submarine != character.SelectedConstruction.Submarine && character.SelectedConstruction.Submarine != null)
else if (currentHull?.Submarine != null && currentHull.Submarine != ladder.Item.Submarine && ladder.Item.Submarine != null)
{
ladderSimPos += character.SelectedConstruction.Submarine.SimPosition - currentHull.Submarine.SimPosition;
ladderSimPos += ladder.Item.Submarine.SimPosition - currentHull.Submarine.SimPosition;
}
else if (currentHull?.Submarine != null && character.SelectedConstruction.Submarine == null)
else if (currentHull?.Submarine != null && ladder.Item.Submarine == null)
{
ladderSimPos -= currentHull.Submarine.SimPosition;
}
@@ -1174,25 +1182,35 @@ namespace Barotrauma
float movementFactor = (handPos.Y / stepHeight) * (float)Math.PI;
movementFactor = 0.8f + (float)Math.Abs(Math.Sin(movementFactor));
Vector2 subSpeed = currentHull != null || character.SelectedConstruction.Submarine == null
? Vector2.Zero : character.SelectedConstruction.Submarine.Velocity;
Vector2 subSpeed = currentHull != null || ladder.Item.Submarine == null
? Vector2.Zero : ladder.Item.Submarine.Velocity;
Vector2 climbForce = new Vector2(0.0f, movement.Y + 0.3f) * movementFactor;
//reached the top of the ladders -> can't go further up
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);
float movementMultiplier = targetMovement.Y < 0 ? 0 : 1;
head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque);
if (!character.SelectedConstruction.Prefab.Triggers.Any())
if (!ladder.Item.Prefab.Triggers.Any())
{
character.SelectedConstruction = null;
return;
}
Rectangle trigger = character.SelectedConstruction.Prefab.Triggers.FirstOrDefault();
trigger = character.SelectedConstruction.TransformTrigger(trigger);
Rectangle trigger = ladder.Item.Prefab.Triggers.FirstOrDefault();
trigger = ladder.Item.TransformTrigger(trigger);
bool isRemote = false;
bool isClimbing = true;
@@ -1221,6 +1239,19 @@ namespace Barotrauma
character.SelectedConstruction = null;
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 UpdateDying(float deltaTime)
@@ -1576,6 +1607,7 @@ namespace Barotrauma
Vector2 shoulderPos = rightShoulder.WorldAnchorA;
Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY;
if (!MathUtils.IsValid(dragDir)) { dragDir = Vector2.UnitY; }
targetAnchor = shoulderPos - dragDir * ConvertUnits.ToSimUnits(upperArmLength + forearmLength);
targetForce = 200.0f;
@@ -121,6 +121,7 @@ namespace Barotrauma
protected Vector2 overrideTargetMovement;
protected float floorY, standOnFloorY;
protected Fixture floorFixture;
protected Vector2 floorNormal = Vector2.UnitY;
protected float surfaceY;
@@ -488,6 +489,7 @@ namespace Barotrauma
limbDictionary = new Dictionary<LimbType, Limb>();
limbs = new Limb[RagdollParams.Limbs.Count];
RagdollParams.Limbs.ForEach(l => AddLimb(l));
if (limbs.Contains(null)) { return; }
SetupDrawOrder();
}
@@ -548,19 +550,23 @@ namespace Barotrauma
byte ID = Convert.ToByte(limbParams.ID);
Limb limb = new Limb(this, character, limbParams);
limb.body.FarseerBody.OnCollision += OnLimbCollision;
if (ID >= Limbs.Length)
{
throw new Exception($"Failed to add a limb to the character \"{Character?.ConfigPath ?? "null"}\" (limb index {ID} out of bounds). The ragdoll file may be configured incorrectly.");
}
Limbs[ID] = limb;
Mass += limb.Mass;
if (!limbDictionary.ContainsKey(limb.type)) limbDictionary.Add(limb.type, limb);
if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); }
}
public void AddLimb(Limb limb)
{
if (Limbs.Contains(limb)) return;
if (Limbs.Contains(limb)) { return; }
limb.body.FarseerBody.OnCollision += OnLimbCollision;
Array.Resize(ref limbs, Limbs.Length + 1);
Limbs[Limbs.Length - 1] = limb;
Mass += limb.Mass;
if (!limbDictionary.ContainsKey(limb.type)) limbDictionary.Add(limb.type, limb);
if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); }
SetupDrawOrder();
}
@@ -923,8 +929,8 @@ namespace Barotrauma
}
Hull newHull = Hull.FindHull(findPos, currentHull);
if (newHull == currentHull) return;
if (newHull == currentHull) { return; }
if (!CanEnterSubmarine || (character.AIController != null && !character.AIController.CanEnterSubmarine))
{
@@ -957,16 +963,16 @@ namespace Barotrauma
if (newHull?.Submarine == null && currentHull?.Submarine != null)
{
//don't teleport out yet if the character is going through a gap
if (Gap.FindAdjacent(currentHull.ConnectedGaps, findPos, 150.0f) != null) { return; }
if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f) != null) { return; }
if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine())) != null)) { return; }
character.MemLocalState?.Clear();
Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity);
Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity, detachProjectiles: false);
}
//out -> in
else if (currentHull == null && newHull.Submarine != null)
{
character.MemLocalState?.Clear();
Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity);
Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity, detachProjectiles: false);
}
//from one sub to another
else if (newHull != null && currentHull != null && newHull.Submarine != currentHull.Submarine)
@@ -974,13 +980,13 @@ namespace Barotrauma
character.MemLocalState?.Clear();
Vector2 newSubPos = newHull.Submarine == null ? Vector2.Zero : newHull.Submarine.Position;
Vector2 prevSubPos = currentHull.Submarine == null ? Vector2.Zero : currentHull.Submarine.Position;
Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero);
Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero, detachProjectiles: false);
}
}
CurrentHull = newHull;
character.Submarine = currentHull?.Submarine;
character.AttachedProjectiles.ForEach(p => p?.Item?.UpdateTransform());
}
private void PreventOutsideCollision()
@@ -1013,7 +1019,7 @@ namespace Barotrauma
}
}
public void Teleport(Vector2 moveAmount, Vector2 velocityChange)
public void Teleport(Vector2 moveAmount, Vector2 velocityChange, bool detachProjectiles = true)
{
foreach (Limb limb in Limbs)
{
@@ -1036,7 +1042,7 @@ namespace Barotrauma
character.DisableImpactDamageTimer = 0.25f;
SetPosition(Collider.SimPosition + moveAmount);
SetPosition(Collider.SimPosition + moveAmount, detachProjectiles: detachProjectiles);
character.CursorPosition += moveAmount;
Collider?.UpdateDrawPosition();
@@ -1500,6 +1506,7 @@ namespace Barotrauma
{
onGround = false;
Stairs = null;
floorFixture = null;
Vector2 rayStart = simPosition;
float height = ColliderHeightFromFloor;
if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) { height = Math.Max(height, HeadPosition.Value); }
@@ -1572,6 +1579,7 @@ namespace Barotrauma
if (standOnFloorFixture != null && !IsHanging)
{
floorFixture = standOnFloorFixture;
standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction;
if (rayStart.Y - standOnFloorY < Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f)
{
@@ -1612,7 +1620,7 @@ namespace Barotrauma
}
}
public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false)
public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true)
{
if (!MathUtils.IsValid(simPosition))
{
@@ -1625,13 +1633,28 @@ namespace Barotrauma
}
if (MainLimb == null) { return; }
// A Work-around for an issue with teleporting the characters:
// Detach every latcher when either one of the latchers or the target is teleported,
// because otherwise all the characters are teleported to invalid positions.
if (Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached)
{
var target = enemyAI.LatchOntoAI.TargetCharacter;
if (target != null)
{
target.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
target.Latchers.Clear();
}
enemyAI.LatchOntoAI.DeattachFromBody(reset: true);
}
Character.Latchers.ForEachMod(l => l.DeattachFromBody(reset: true));
Character.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
Character.Latchers.Clear();
if (detachProjectiles)
{
character.AttachedProjectiles.ForEachMod(p => p?.Unstick());
character.AttachedProjectiles.Clear();
}
Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition;
if (lerp)
{
@@ -1712,7 +1735,7 @@ namespace Barotrauma
if (distSqrd > resetDist * resetDist)
{
//ragdoll way too far, reset position
SetPosition(Collider.SimPosition, true, forceMainLimbToCollider: true);
SetPosition(Collider.SimPosition, lerp: true, forceMainLimbToCollider: true);
}
if (distSqrd > allowedDist * allowedDist)
{
@@ -1732,7 +1755,7 @@ namespace Barotrauma
else if (collisionsDisabled)
{
//set the position of the ragdoll to make sure limbs don't get stuck inside walls when re-enabling collisions
SetPosition(Collider.SimPosition, true);
SetPosition(Collider.SimPosition, lerp: true);
collisionsDisabled = false;
//force collision categories to be updated
prevCollisionCategory = Category.None;
@@ -130,6 +130,7 @@ namespace Barotrauma
}
public readonly HashSet<LatchOntoAI> Latchers = new HashSet<LatchOntoAI>();
public readonly HashSet<Projectile> AttachedProjectiles = new HashSet<Projectile>();
protected readonly Dictionary<string, ActiveTeamChange> activeTeamChanges = new Dictionary<string, ActiveTeamChange>();
protected ActiveTeamChange currentTeamChange;
@@ -265,7 +266,7 @@ namespace Barotrauma
private CharacterPrefab prefab;
public readonly CharacterParams Params;
public string SpeciesName => Params.SpeciesName;
public string SpeciesName => Params?.SpeciesName ?? "null";
public string Group => Params.Group;
public bool IsHumanoid => Params.Humanoid;
public bool IsHusk => Params.Husk;
@@ -2640,7 +2641,7 @@ namespace Barotrauma
ApplyStatusEffects(ActionType.Always, deltaTime);
PreviousHull = CurrentHull;
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, true);
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, useWorldCoordinates: true);
speechBubbleTimer = Math.Max(0.0f, speechBubbleTimer - deltaTime);
@@ -2710,7 +2711,7 @@ namespace Barotrauma
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
Implode();
if (IsDead) { return; }
if (IsDead) { return; }
}
}
}
@@ -2719,7 +2720,9 @@ namespace Barotrauma
PressureTimer = 0.0f;
}
}
else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && WorldPosition.Y < CharacterHealth.CrushDepth)
else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) &&
PressureProtection < (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f) &&
WorldPosition.Y < CharacterHealth.CrushDepth)
{
//implode if below crush depth, and either outside or in a high-pressure hull
if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)
@@ -3124,47 +3127,53 @@ namespace Barotrauma
//set the character order only if the character is close enough to hear the message
if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
if (order != null && order.OrderGiver != orderGiver)
if (order != null)
{
order.OrderGiver = orderGiver;
}
switch (order?.Category)
{
case OrderCategory.Operate when order?.TargetEntity != null:
// If there's another character operating the same device, make them dismiss themself
foreach (var character in CharacterList)
if (order.OrderGiver != orderGiver)
{
order.OrderGiver = orderGiver;
}
if (order.AutoDismiss)
{
switch (order.Category)
{
if (character == this) { continue; }
if (character.TeamID != TeamID) { continue; }
if (!(character.AIController is HumanAIController)) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Operate) { continue; }
if (currentOrder.Order.Identifier != order.Identifier) { continue; }
if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; }
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force);
case OrderCategory.Operate when order.TargetEntity != null:
// If there's another character operating the same device, make them dismiss themself
foreach (var character in CharacterList)
{
if (character == this) { continue; }
if (character.TeamID != TeamID) { continue; }
if (!(character.AIController is HumanAIController)) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Operate) { continue; }
if (currentOrder.Order.Identifier != order.Identifier) { continue; }
if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; }
if (!currentOrder.Order.AutoDismiss) { continue; }
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force);
break;
}
}
break;
case OrderCategory.Movement:
// If there character has another movement order, dismiss that order
OrderInfo? orderToReplace = null;
foreach (var currentOrder in CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Movement) { continue; }
orderToReplace = currentOrder;
break;
}
if (orderToReplace.HasValue && orderToReplace.Value.Order.AutoDismiss)
{
SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(orderToReplace.Value), orderToReplace.Value.ManualPriority, this, speak: speak, force: force);
}
break;
}
}
break;
case OrderCategory.Movement:
// If there character has another movement order, dismiss that order
OrderInfo? orderToReplace = null;
foreach (var currentOrder in CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Movement) { continue; }
orderToReplace = currentOrder;
break;
}
if (orderToReplace.HasValue)
{
SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(orderToReplace.Value), orderToReplace.Value.ManualPriority, this, speak: speak, force: force);
}
break;
}
}
// Prevent adding duplicate orders
@@ -3624,7 +3633,7 @@ namespace Barotrauma
{
if (attacker.TeamID == TeamID)
{
afflictions = afflictions.Where(a => !a.Prefab.IsBuff);
afflictions = afflictions.Where(a => a.Prefab.IsBuff);
if (!afflictions.Any()) { return new AttackResult(); }
}
}
@@ -3936,6 +3945,7 @@ namespace Barotrauma
}
SelectedConstruction = null;
SelectedCharacter = null;
AnimController.ResetPullJoints();
@@ -4063,10 +4073,11 @@ namespace Barotrauma
public void TeleportTo(Vector2 worldPos)
{
CurrentHull = null;
AnimController.CurrentHull = null;
Submarine = null;
AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), false);
AnimController.FindHull(worldPos, true);
AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), lerp: false);
AnimController.FindHull(worldPos, setSubmarine: true);
}
public static void SaveInventory(Inventory inventory, XElement parentElement)
@@ -1176,7 +1176,7 @@ namespace Barotrauma
int salary = 0;
foreach (Skill skill in Job.Skills)
{
salary += (int)(skill.Level * skill.Prefab.PriceMultiplier);
salary += (int)(skill.Level * skill.PriceMultiplier);
}
return (int)(salary * Job.Prefab.PriceMultiplier);
@@ -1274,19 +1274,18 @@ namespace Barotrauma
return Math.Max(GetTotalTalentPoints() - GetUnlockedTalentsInTree().Count(), 0);
}
public float GetProgressTowardsNextLevel()
public int GetProgressTowardsNextLevel()
{
float progress = (ExperiencePoints - GetExperienceRequiredForCurrentLevel()) / (GetExperienceRequiredToLevelUp() - GetExperienceRequiredForCurrentLevel());
return progress;
return (ExperiencePoints - GetExperienceRequiredForCurrentLevel()) / (GetExperienceRequiredToLevelUp() - GetExperienceRequiredForCurrentLevel());
}
public float GetExperienceRequiredForCurrentLevel()
public int GetExperienceRequiredForCurrentLevel()
{
GetCurrentLevel(out int experienceRequired);
return experienceRequired;
}
public float GetExperienceRequiredToLevelUp()
public int GetExperienceRequiredToLevelUp()
{
int level = GetCurrentLevel(out int experienceRequired);
return experienceRequired + ExperienceRequiredPerLevel(level);
@@ -174,7 +174,8 @@ namespace Barotrauma
private void CharacterDead(Character character, CauseOfDeath causeOfDeath)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (Strength < TransformThresholdOnDeath || character.Removed)
if (Strength < TransformThresholdOnDeath || character.Removed ||
character.CharacterHealth.GetAllAfflictions().Any(a => a.GetActiveEffect()?.BlockTransformation.Contains(Prefab.Identifier) ?? false))
{
UnsubscribeFromDeathEvent();
return;
@@ -193,7 +194,7 @@ namespace Barotrauma
CoroutineManager.StartCoroutine(CreateAIHusk());
}
private IEnumerable<object> CreateAIHusk()
private IEnumerable<CoroutineStatus> CreateAIHusk()
{
//character already in remove queue (being removed by something else, for example a modded affliction that uses AfflictionHusk as the base)
// -> don't spawn the AI husk
@@ -234,6 +234,11 @@ namespace Barotrauma
[Serialize("0,0,0,0", false)]
public Color MaxBodyTint { get; private set; }
/// <summary>
/// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character
/// </summary>
public string[] BlockTransformation { get; private set; }
public readonly Dictionary<StatTypes, (float minValue, float maxValue)> AfflictionStatValues = new Dictionary<StatTypes, (float minValue, float maxValue)>();
public readonly HashSet<AbilityFlags> AfflictionAbilityFlags = new HashSet<AbilityFlags>();
@@ -245,6 +250,7 @@ namespace Barotrauma
SerializableProperty.DeserializeProperties(this, element);
resistanceFor = element.GetAttributeStringArray("resistancefor", new string[0], convertToLowerInvariant: true);
BlockTransformation = element.GetAttributeStringArray("blocktransformation", new string[0], convertToLowerInvariant: true);
foreach (XElement subElement in element.Elements())
{
@@ -153,7 +153,10 @@ namespace Barotrauma
(GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, CampaignInteractionType);
if (positionToStayIn != null && humanAI != null)
{
humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200));
humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200)
{
DebugLogWhenFails = false
});
}
}
}
@@ -9,7 +9,7 @@ namespace Barotrauma
{
private readonly JobPrefab prefab;
private Dictionary<string, Skill> skills;
private readonly Dictionary<string, Skill> skills;
public string Name
{
@@ -282,7 +282,7 @@ namespace Barotrauma
Variants = variant;
Skills.Sort((x,y) => y.LevelRange.X.CompareTo(x.LevelRange.X));
Skills.Sort((x,y) => y.LevelRange.Start.CompareTo(x.LevelRange.Start));
// Disabled on purpose, TODO: remove all references?
//ClothingElement = element.GetChildElement("PortraitClothing");
@@ -34,14 +34,14 @@ namespace Barotrauma
}
}
internal SkillPrefab Prefab { get; private set; }
public readonly float PriceMultiplier = 1.0f;
public Skill(SkillPrefab prefab)
{
this.Prefab = prefab;
Identifier = prefab.Identifier;
level = Rand.Range(prefab.LevelRange.X, prefab.LevelRange.Y, Rand.RandSync.Server);
level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, Rand.RandSync.Server);
icon = GetIcon();
PriceMultiplier = prefab.PriceMultiplier;
}
public Skill(string identifier, float level)
@@ -7,7 +7,7 @@ namespace Barotrauma
{
public readonly string Identifier;
public Vector2 LevelRange { get; private set; }
public Range<float> LevelRange { get; private set; }
/// <summary>
/// How much this skill affects characters' hiring cost
@@ -23,12 +23,13 @@ namespace Barotrauma
var levelString = element.GetAttributeString("level", "");
if (levelString.Contains(","))
{
LevelRange = XMLExtensions.ParseVector2(levelString, false);
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 Vector2(skillLevel, skillLevel);
LevelRange = new Range<float>(skillLevel, skillLevel);
}
IsPrimarySkill = element.GetAttributeBool("primary", false);
@@ -430,8 +430,15 @@ namespace Barotrauma
public float Dir
{
get { return ((dir == Direction.Left) ? -1.0f : 1.0f); }
set { dir = (value == -1.0f) ? Direction.Left : Direction.Right; }
get { return (dir == Direction.Left) ? -1.0f : 1.0f; }
set
{
dir = (value == -1.0f) ? Direction.Left : Direction.Right;
if (body != null)
{
body.Dir = Dir;
}
}
}
public int RefJointIndex => Params.RefJoint;
@@ -16,10 +16,12 @@ namespace Barotrauma.Abilities
private readonly string itemIdentifier;
private readonly string[] tags;
private readonly WeaponType weapontype;
private bool ignoreNonHarmfulAttacks;
public AbilityConditionAttackData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
itemIdentifier = conditionElement.GetAttributeString("itemidentifier", "");
tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true);
ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool("ignorenonharmfulattacks", false);
switch (conditionElement.GetAttributeString("weapontype", ""))
{
case "melee":
@@ -35,8 +37,15 @@ namespace Barotrauma.Abilities
{
if (abilityObject is AbilityAttackData attackData)
{
Item item = attackData?.SourceAttack?.SourceItem;
if (ignoreNonHarmfulAttacks && attackData.SourceAttack != null)
{
if (attackData.SourceAttack.Stun <= 0.0f && (attackData.SourceAttack.Afflictions?.All(a => a.Key.Prefab.IsBuff) ?? true))
{
return false;
}
}
Item item = attackData?.SourceAttack?.SourceItem;
if (item == null)
{
DebugConsole.AddWarning($"Source Item was not found in {this} for talent {characterTalent.DebugIdentifier}!");
@@ -61,10 +70,13 @@ namespace Barotrauma.Abilities
switch (weapontype)
{
// it is possible that an item that has both a melee and a projectile component will return true
// even when not used as a melee/ranged weapon respectively
// attackdata should contain data regarding whether the attack is melee or not
case WeaponType.Melee:
return item.GetComponent<MeleeWeapon>() != null;
case WeaponType.Ranged:
return item.GetComponent<RangedWeapon>() != null;
return item.GetComponent<Projectile>() != null;
}
return true;
@@ -18,13 +18,13 @@ namespace Barotrauma.Abilities
protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData)
{
DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject}");
DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject} in talent {characterTalent.DebugIdentifier}");
}
protected abstract bool MatchesConditionSpecific(AbilityObject abilityObject);
public override bool MatchesCondition()
{
DebugConsole.ThrowError("Used data-reliant ability condition in a state-based ability! This is not allowed.");
DebugConsole.ThrowError($"Used data-reliant ability condition in a state-based ability in talent {characterTalent.DebugIdentifier}! This is not allowed.");
return false;
}
public override bool MatchesCondition(AbilityObject abilityObject)
@@ -79,17 +79,17 @@ namespace Barotrauma.Abilities
protected virtual void ApplyEffect()
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect");
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect in talent {CharacterTalent.DebugIdentifier}");
}
protected virtual void ApplyEffect(AbilityObject abilityObject)
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect");
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}");
}
protected void LogabilityObjectMismatch()
{
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type.");
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}");
}
// XML
@@ -10,8 +10,10 @@ namespace Barotrauma.Abilities
protected readonly List<StatusEffect> statusEffects;
private readonly bool applyToSelf;
private readonly bool nearbyCharactersAppliesToSelf;
private readonly bool nearbyCharactersAppliesToAllies;
private readonly bool nearbyCharactersAppliesToEnemies;
private readonly bool applyToSelected;
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
@@ -19,9 +21,11 @@ namespace Barotrauma.Abilities
public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects"));
applyToSelf = abilityElement.GetAttributeBool("applytoself", false);
applyToSelected = abilityElement.GetAttributeBool("applytoselected", false);
nearbyCharactersAppliesToSelf = abilityElement.GetAttributeBool("nearbycharactersappliestoself", true);
nearbyCharactersAppliesToAllies = abilityElement.GetAttributeBool("nearbycharactersappliestoallies", true);
nearbyCharactersAppliesToEnemies = abilityElement.GetAttributeBool("nearbycharactersappliestoenemies", true);
}
protected void ApplyEffectSpecific(Character targetCharacter)
@@ -46,6 +50,10 @@ namespace Barotrauma.Abilities
{
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character));
}
if (!nearbyCharactersAppliesToEnemies)
{
targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character));
}
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
}
@@ -75,7 +83,7 @@ namespace Barotrauma.Abilities
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter)
if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter && !applyToSelf)
{
ApplyEffectSpecific(targetCharacter);
}
@@ -31,5 +31,10 @@ namespace Barotrauma.Abilities
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffect();
}
}
}
@@ -5,7 +5,7 @@ namespace Barotrauma.Abilities
{
class CharacterAbilityModifyAttackData : CharacterAbility
{
private readonly List<Affliction> afflictions;
private readonly List<Affliction> afflictions = new List<Affliction>();
private readonly float addedDamageMultiplier;
private readonly float addedPenetration;
@@ -1,4 +1,5 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
@@ -15,14 +16,19 @@ namespace Barotrauma.Abilities
if (!TalentTree.JobTalentTrees.TryGetValue(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; }
var subTree = talentTree.TalentSubTrees.Find(t => t.TalentOptionStages.Any(ts => ts.Talents.Contains(CharacterTalent.Prefab)));
if (subTree != null)
{
subTree.ForceUnlock = true;
foreach (var talentOption in subTree.TalentOptionStages)
{
foreach (var talent in talentOption.Talents)
{
if (talent == CharacterTalent.Prefab) { continue; }
Character.GiveTalent(talent);
if (Character.GiveTalent(talent))
{
Character.Info.AdditionalTalentPoints++;
}
}
}
}
@@ -101,37 +101,40 @@ namespace Barotrauma
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var rootElement = doc.Root;
switch (rootElement.Name.ToString().ToLowerInvariant())
void loadSinglePrefab(XElement element, bool isOverride)
{
case "talent":
TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), false);
break;
case "talents":
foreach (var element in rootElement.Elements())
{
if (element.IsOverride())
{
var itemElement = element.GetChildElement("talent");
if (itemElement != null)
{
TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), true);
}
else
{
DebugConsole.ThrowError($"Cannot find a talent element from the children of the override element defined in {file.Path}");
}
}
else
{
TalentPrefabs.Add(new TalentPrefab(element, file.Path), false);
}
}
break;
default:
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}");
break;
TalentPrefabs.Add(new TalentPrefab(element, file.Path) { ContentPackage = file.ContentPackage }, isOverride);
}
void loadMultiplePrefabs(XElement element, bool isOverride)
{
foreach (var subElement in element.Elements())
{
interpretElement(subElement, isOverride);
}
}
void interpretElement(XElement subElement, bool isOverride)
{
if (subElement.IsOverride())
{
loadMultiplePrefabs(subElement, true);
}
else if (subElement.Name.LocalName.Equals("talents", StringComparison.OrdinalIgnoreCase))
{
loadMultiplePrefabs(subElement, isOverride);
}
else if (subElement.Name.LocalName.Equals("talent", StringComparison.OrdinalIgnoreCase))
{
loadSinglePrefab(subElement, isOverride);
}
else
{
DebugConsole.ThrowError($"Invalid XML element for the {nameof(TalentPrefab)} prefab type: '{subElement.Name}' in {file.Path}");
}
}
interpretElement(doc.Root, false);
}
public static void LoadAll(IEnumerable<ContentFile> files)
@@ -5,7 +5,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
class TalentTree
class TalentTree : IPrefab, IDisposable
{
public enum TalentTreeStageState
{
@@ -16,7 +16,7 @@ namespace Barotrauma
Highlighted
}
public static readonly Dictionary<string, TalentTree> JobTalentTrees = new Dictionary<string, TalentTree>();
public static readonly PrefabCollection<TalentTree> JobTalentTrees = new PrefabCollection<TalentTree>();
public readonly List<TalentSubTree> TalentSubTrees = new List<TalentSubTree>();
@@ -26,13 +26,21 @@ namespace Barotrauma
private set;
}
public string OriginalName => Identifier;
public string Identifier { get; }
public string FilePath { get; }
public ContentPackage ContentPackage { get; set; }
public TalentTree(XElement element, string filePath)
{
ConfigElement = element;
FilePath = filePath;
Identifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant();
string jobIdentifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant();
if (string.IsNullOrEmpty(jobIdentifier))
if (string.IsNullOrEmpty(Identifier))
{
DebugConsole.ThrowError($"No job defined for talent tree in \"{filePath}\"!");
return;
@@ -50,20 +58,15 @@ namespace Barotrauma
TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talent, StringComparison.OrdinalIgnoreCase));
if (talentPrefab == null)
{
DebugConsole.AddWarning($"Talent tree for job {jobIdentifier} contains non-existent talent {talent}! Talent tree not added.");
DebugConsole.AddWarning($"Talent tree for job {Identifier} contains non-existent talent {talent}! Talent tree not added.");
return;
}
if (!duplicateSet.Add(talent))
{
DebugConsole.ThrowError($"Talent tree for job {jobIdentifier} contains duplicate talent {talent}! Talent tree not added.");
DebugConsole.ThrowError($"Talent tree for job {Identifier} contains duplicate talent {talent}! Talent tree not added.");
return;
}
}
if (!JobTalentTrees.TryAdd(jobIdentifier, this))
{
DebugConsole.ThrowError($"Could not add talent tree for job {jobIdentifier}! A talent tree for this job is already likely defined");
}
}
public bool TalentIsInTree(string talentIdentifier)
@@ -78,37 +81,40 @@ namespace Barotrauma
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var rootElement = doc.Root;
switch (rootElement.Name.ToString().ToLowerInvariant())
void loadSinglePrefab(XElement element, bool isOverride)
{
case "talenttree":
new TalentTree(rootElement, file.Path);
break;
case "talenttrees":
foreach (var element in rootElement.Elements())
{
if (element.IsOverride())
{
var treeElement = element.GetChildElement("talenttree");
if (treeElement != null)
{
new TalentTree(rootElement, file.Path);
}
else
{
DebugConsole.ThrowError($"Cannot find a talent tree element from the children of the override element defined in {file.Path}");
}
}
else
{
new TalentTree(element, file.Path);
}
}
break;
default:
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}");
break;
JobTalentTrees.Add(new TalentTree(element, file.Path) { ContentPackage = file.ContentPackage }, isOverride);
}
void loadMultiplePrefabs(XElement element, bool isOverride)
{
foreach (var subElement in element.Elements())
{
interpretElement(subElement, isOverride);
}
}
void interpretElement(XElement subElement, bool isOverride)
{
if (subElement.IsOverride())
{
loadMultiplePrefabs(subElement, true);
}
else if (subElement.Name.LocalName.Equals("talenttrees", StringComparison.OrdinalIgnoreCase))
{
loadMultiplePrefabs(subElement, isOverride);
}
else if (subElement.Name.LocalName.Equals("talenttree", StringComparison.OrdinalIgnoreCase))
{
loadSinglePrefab(subElement, isOverride);
}
else
{
DebugConsole.ThrowError($"Invalid XML element for the {nameof(TalentTree)} prefab type: '{subElement.Name}' in {file.Path}");
}
}
interpretElement(doc.Root, false);
}
public static void LoadAll(IEnumerable<ContentFile> files)
@@ -190,6 +196,8 @@ namespace Barotrauma
foreach (var subTree in talentTree.TalentSubTrees)
{
if (subTree.ForceUnlock && subTree.TalentOptionStages.Any(option => option.Talents.Any(t => t.Identifier == talentIdentifier))) { return true; }
foreach (var talentOptionStage in subTree.TalentOptionStages)
{
bool hasTalentInThisTier = talentOptionStage.Talents.Any(t => selectedTalents.Contains(t.Identifier));
@@ -220,7 +228,7 @@ namespace Barotrauma
canStillUnlock = false;
foreach (string talent in selectedTalents)
{
if (IsViableTalentForCharacter(controlledCharacter, talent, viableTalents))
if (!viableTalents.Contains(talent) && IsViableTalentForCharacter(controlledCharacter, talent, viableTalents))
{
viableTalents.Add(talent);
canStillUnlock = true;
@@ -229,6 +237,14 @@ namespace Barotrauma
}
return viableTalents;
}
private bool disposed = false;
public void Dispose()
{
if (disposed) { return; }
disposed = true;
JobTalentTrees.Remove(this);
}
}
class TalentSubTree
@@ -237,6 +253,8 @@ namespace Barotrauma
public string DisplayName { get; }
public bool ForceUnlock;
public readonly List<TalentOption> TalentOptionStages = new List<TalentOption>();
public TalentSubTree(XElement subTreeElement)