Merge remote-tracking branch 'upstream/master' into develop
This commit is contained in:
@@ -184,6 +184,20 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is some condition met (e.g. entity null, indetectable, outside level) that prevents anyone from detecting the target?
|
||||
/// </summary>
|
||||
public bool ShouldBeIgnored()
|
||||
{
|
||||
if (InDetectable) { return true; }
|
||||
if (Entity == null) { return true; }
|
||||
if (Level.Loaded != null && WorldPosition.Y > Level.Loaded.Size.Y)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public AITarget(Entity e, XElement element) : this(e)
|
||||
{
|
||||
SightRange = element.GetAttributeFloat("sightrange", 0.0f);
|
||||
|
||||
@@ -216,7 +216,7 @@ namespace Barotrauma
|
||||
|
||||
private bool IsAttackingOwner(Character other) =>
|
||||
PetBehavior != null && PetBehavior.Owner != null &&
|
||||
!other.IsUnconscious && !other.IsArrested &&
|
||||
!other.IsUnconscious && !other.IsHandcuffed &&
|
||||
other.AIController is HumanAIController humanAI &&
|
||||
humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat &&
|
||||
combat.Enemy != null && combat.Enemy == PetBehavior.Owner;
|
||||
@@ -2694,13 +2694,8 @@ namespace Barotrauma
|
||||
float maxModifier = 5;
|
||||
foreach (AITarget aiTarget in AITarget.List)
|
||||
{
|
||||
if (aiTarget.InDetectable) { continue; }
|
||||
if (aiTarget.Entity == null) { continue; }
|
||||
if (aiTarget.ShouldBeIgnored()) { continue; }
|
||||
if (ignoredTargets.Contains(aiTarget)) { continue; }
|
||||
if (Level.Loaded != null && aiTarget.WorldPosition.Y > Level.Loaded.Size.Y)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; }
|
||||
if (!TargetOutposts)
|
||||
{
|
||||
|
||||
@@ -67,6 +67,12 @@ namespace Barotrauma
|
||||
private readonly float reportProblemsInterval = 1.0f;
|
||||
private float reportProblemsTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Affects how far the character can hear sounds created by AI targets with the tag ProvocativeToHumanAI.
|
||||
/// Used as a multiplier on the sound range of the target, e.g. a value of 0.5 would mean a target with a sound range of 1000 would need to be within 500 units for this character to hear it.
|
||||
/// Only affects the "fight intruders" objective, which makes the character go and inspect noises.
|
||||
/// </summary>
|
||||
public float Hearing { get; set; } = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity.
|
||||
@@ -339,39 +345,8 @@ namespace Barotrauma
|
||||
enemyCheckTimer -= deltaTime;
|
||||
if (enemyCheckTimer < 0)
|
||||
{
|
||||
CheckEnemies();
|
||||
enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
|
||||
if (!objectiveManager.IsCurrentObjective<AIObjectiveCombat>())
|
||||
{
|
||||
float closestDistance = 0;
|
||||
Character closestEnemy = null;
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.Submarine != Character.Submarine) { continue; }
|
||||
if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; }
|
||||
if (IsFriendly(c)) { continue; }
|
||||
Vector2 toTarget = c.WorldPosition - WorldPosition;
|
||||
float dist = toTarget.LengthSquared();
|
||||
float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside;
|
||||
if (dist > maxDistance * maxDistance) { continue; }
|
||||
if (EnemyAIController.IsLatchedToSomeoneElse(c, Character)) { continue; }
|
||||
var head = Character.AnimController.GetLimb(LimbType.Head);
|
||||
if (head == null) { continue; }
|
||||
float rotation = head.body.TransformedRotation;
|
||||
Vector2 forward = VectorExtensions.Forward(rotation);
|
||||
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward));
|
||||
if (angle > 70) { continue; }
|
||||
if (!Character.CanSeeTarget(c)) { continue; }
|
||||
if (dist < closestDistance || closestEnemy == null)
|
||||
{
|
||||
closestEnemy = c;
|
||||
closestDistance = dist;
|
||||
}
|
||||
}
|
||||
if (closestEnemy != null)
|
||||
{
|
||||
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer);
|
||||
@@ -586,6 +561,42 @@ namespace Barotrauma
|
||||
ShipCommandManager?.Update(deltaTime);
|
||||
}
|
||||
|
||||
private void CheckEnemies()
|
||||
{
|
||||
//already in combat, no need to check
|
||||
if (objectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return; }
|
||||
|
||||
float closestDistance = 0;
|
||||
Character closestEnemy = null;
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.Submarine != Character.Submarine) { continue; }
|
||||
if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; }
|
||||
if (IsFriendly(c)) { continue; }
|
||||
Vector2 toTarget = c.WorldPosition - WorldPosition;
|
||||
float dist = toTarget.LengthSquared();
|
||||
float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside;
|
||||
if (dist > maxDistance * maxDistance) { continue; }
|
||||
if (EnemyAIController.IsLatchedToSomeoneElse(c, Character)) { continue; }
|
||||
var head = Character.AnimController.GetLimb(LimbType.Head);
|
||||
if (head == null) { continue; }
|
||||
float rotation = head.body.TransformedRotation;
|
||||
Vector2 forward = VectorExtensions.Forward(rotation);
|
||||
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward));
|
||||
if (angle > 70) { continue; }
|
||||
if (!Character.CanSeeTarget(c)) { continue; }
|
||||
if (dist < closestDistance || closestEnemy == null)
|
||||
{
|
||||
closestEnemy = c;
|
||||
closestDistance = dist;
|
||||
}
|
||||
}
|
||||
if (closestEnemy != null)
|
||||
{
|
||||
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
|
||||
}
|
||||
}
|
||||
|
||||
private void UnequipUnnecessaryItems()
|
||||
{
|
||||
if (Character.LockHands) { return; }
|
||||
@@ -632,7 +643,9 @@ namespace Barotrauma
|
||||
isCurrentObjectiveFindSafety ||
|
||||
Character.AnimController.InWater ||
|
||||
Character.AnimController.HeadInWater ||
|
||||
Character.IsClimbing ||
|
||||
Character.Submarine == null ||
|
||||
Character.Submarine.Info.HasTag(SubmarineTag.Shuttle) ||
|
||||
(!Character.IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) ||
|
||||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) ||
|
||||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
|
||||
@@ -845,8 +858,9 @@ namespace Barotrauma
|
||||
{
|
||||
// In the campaign mode, undocking happens after leaving the outpost, so we can't use that.
|
||||
campaign.BeforeLevelLoading += Relocate;
|
||||
campaign.OnSaveAndQuit += Relocate;
|
||||
campaign.ItemsRelocatedToMainSub = true;
|
||||
}
|
||||
campaign.ItemsRelocatedToMainSub = true;
|
||||
#if CLIENT
|
||||
HintManager.OnItemMarkedForRelocation();
|
||||
#endif
|
||||
@@ -1011,6 +1025,7 @@ namespace Barotrauma
|
||||
{
|
||||
Order newOrder = null;
|
||||
Hull targetHull = null;
|
||||
|
||||
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
|
||||
bool speak = Character.SpeechImpediment < 100 && !Character.IsEscorted;
|
||||
if (Character.CurrentHull != null)
|
||||
@@ -1024,7 +1039,7 @@ namespace Barotrauma
|
||||
if (target.CurrentHull != hull || !target.Enabled) { continue; }
|
||||
if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false))
|
||||
{
|
||||
if (!target.IsArrested && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
|
||||
if (!target.IsHandcuffed && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
|
||||
{
|
||||
var orderPrefab = OrderPrefab.Prefabs["reportintruders"];
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
@@ -1436,10 +1451,7 @@ namespace Barotrauma
|
||||
{
|
||||
return AIObjectiveCombat.CombatMode.Offensive;
|
||||
}
|
||||
return
|
||||
humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() ||
|
||||
humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders) ?
|
||||
AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
|
||||
return humanAI.ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>() ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1479,28 +1491,36 @@ namespace Barotrauma
|
||||
{
|
||||
// The guards don't react to player's aggressions when there's an instigator around
|
||||
isAttackerFightingEnemy = true;
|
||||
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
|
||||
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : instigator.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat;
|
||||
}
|
||||
if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !eitherIsMentallyUnstable)
|
||||
{
|
||||
if (c.IsSecurity)
|
||||
{
|
||||
return attacker.CombatAction != null ? attacker.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.Offensive;
|
||||
return attacker.CombatAction?.GuardReaction ?? AIObjectiveCombat.CombatMode.Offensive;
|
||||
}
|
||||
else
|
||||
{
|
||||
return attacker.CombatAction != null ? attacker.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat;
|
||||
return attacker.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>()?.Enemy == attacker)
|
||||
if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>() is AIObjectiveCombat currentCombatObjective && currentCombatObjective.Enemy == attacker)
|
||||
{
|
||||
// Already targeting the attacker -> treat as a more serious threat.
|
||||
cumulativeDamage *= 2;
|
||||
currentCombatObjective.AllowHoldFire = false;
|
||||
c.IsCriminal = true;
|
||||
}
|
||||
if (c.IsCriminal)
|
||||
{
|
||||
// Always react if the attacker has been misbehaving earlier.
|
||||
cumulativeDamage = Math.Max(cumulativeDamage, minorDamageThreshold);
|
||||
}
|
||||
if (cumulativeDamage > majorDamageThreshold)
|
||||
{
|
||||
c.IsCriminal = true;
|
||||
if (c.IsSecurity)
|
||||
{
|
||||
return AIObjectiveCombat.CombatMode.Offensive;
|
||||
@@ -1544,7 +1564,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<AIObjective, bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
|
||||
public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<AIObjective, bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false, bool speakWarnings = false)
|
||||
{
|
||||
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
|
||||
if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; }
|
||||
@@ -1579,6 +1599,7 @@ namespace Barotrauma
|
||||
HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
|
||||
AbortCondition = abortCondition,
|
||||
AllowHoldFire = allowHoldFire,
|
||||
SpeakWarnings = speakWarnings
|
||||
};
|
||||
if (onAbort != null)
|
||||
{
|
||||
@@ -1777,7 +1798,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; }
|
||||
|
||||
if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); }
|
||||
otherHumanAI.structureDamageAccumulator.TryAdd(character, 0.0f);
|
||||
float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
|
||||
otherHumanAI.structureDamageAccumulator[character] += MathHelper.Clamp(damageAmount, -MaxDamagePerFrame, MaxDamagePerFrame);
|
||||
float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage);
|
||||
@@ -1789,27 +1810,36 @@ namespace Barotrauma
|
||||
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss, Reputation.MaxReputationLossFromWallDamage);
|
||||
}
|
||||
|
||||
if (accumulatedDamage <= WarningThreshold) { return; }
|
||||
|
||||
if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold &&
|
||||
!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
|
||||
if (!character.IsCriminal)
|
||||
{
|
||||
//if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene
|
||||
if (accumulatedDamage < ArrestThreshold)
|
||||
if (accumulatedDamage <= WarningThreshold) { return; }
|
||||
|
||||
if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold &&
|
||||
!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
|
||||
{
|
||||
if (otherHumanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
|
||||
//if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene
|
||||
if (accumulatedDamage < ArrestThreshold)
|
||||
{
|
||||
(otherHumanAI.ObjectiveManager.CurrentObjective as AIObjectiveIdle)?.FaceTargetAndWait(character, 5.0f);
|
||||
if (otherHumanAI.ObjectiveManager.CurrentObjective is AIObjectiveIdle idleObjective)
|
||||
{
|
||||
idleObjective.FaceTargetAndWait(character, 5.0f);
|
||||
}
|
||||
}
|
||||
otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f);
|
||||
someoneSpoke = true;
|
||||
}
|
||||
otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f);
|
||||
someoneSpoke = true;
|
||||
}
|
||||
|
||||
// React if we are security
|
||||
if ((accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) ||
|
||||
if (character.IsCriminal ||
|
||||
(accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) ||
|
||||
(accumulatedDamage > KillThreshold && prevAccumulatedDamage <= KillThreshold))
|
||||
{
|
||||
var combatMode = accumulatedDamage > KillThreshold ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest;
|
||||
if (combatMode == AIObjectiveCombat.CombatMode.Offensive)
|
||||
{
|
||||
character.IsCriminal = true;
|
||||
}
|
||||
if (!TriggerSecurity(otherHumanAI, combatMode))
|
||||
{
|
||||
// Else call the others
|
||||
@@ -1830,17 +1860,18 @@ namespace Barotrauma
|
||||
if (humanAI == null) { return false; }
|
||||
if (!humanAI.Character.IsSecurity) { return false; }
|
||||
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
|
||||
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(), allowHoldFire: true, onCompleted: () =>
|
||||
{
|
||||
//if the target is arrested successfully, reset the damage accumulator
|
||||
foreach (Character anyCharacter in Character.CharacterList)
|
||||
{
|
||||
if (anyCharacter.AIController is HumanAIController anyAI)
|
||||
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(),
|
||||
onCompleted: () =>
|
||||
{
|
||||
//if the target is arrested successfully, reset the damage accumulator
|
||||
foreach (Character anyCharacter in Character.CharacterList)
|
||||
{
|
||||
anyAI.structureDamageAccumulator?.Remove(character);
|
||||
if (anyCharacter.AIController is HumanAIController anyAI)
|
||||
{
|
||||
anyAI.structureDamageAccumulator?.Remove(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1848,11 +1879,14 @@ namespace Barotrauma
|
||||
public static void ItemTaken(Item item, Character thief)
|
||||
{
|
||||
if (item == null || thief == null || item.GetComponent<LevelResource>() != null) { return; }
|
||||
|
||||
bool someoneSpoke = false;
|
||||
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
|
||||
|
||||
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag(Tags.HandLockerItem))
|
||||
if (item.Illegitimate && item.GetRootInventoryOwner() is Character itemOwner && itemOwner != thief && itemOwner.TeamID == thief.TeamID)
|
||||
{
|
||||
// The player attempts to use a bot as a mule or get them arrested -> just arrest the player instead.
|
||||
thief.IsCriminal = true;
|
||||
}
|
||||
bool foundIllegitimateItems = item.Illegitimate || item.OwnInventory?.FindItem(it => it.Illegitimate, recursive: true) != null;
|
||||
if (foundIllegitimateItems && thief.TeamID != CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
@@ -1872,19 +1906,24 @@ namespace Barotrauma
|
||||
if (item.HasTag(Tags.FireExtinguisher) && connectedHulls.Any(h => h.FireSources.Any())) { continue; }
|
||||
if (item.HasTag(Tags.DivingGear) && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; }
|
||||
}
|
||||
if (!someoneSpoke)
|
||||
if (item.HasTag(Tags.Handcuffs) && thief.HasEquippedItem(item))
|
||||
{
|
||||
if (!item.StolenDuringRound)
|
||||
{
|
||||
ApplyStealingReputationLoss(item);
|
||||
item.StolenDuringRound = true;
|
||||
}
|
||||
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
|
||||
someoneSpoke = true;
|
||||
// Handcuffed -> don't react.
|
||||
continue;
|
||||
}
|
||||
if (!item.StolenDuringRound)
|
||||
{
|
||||
item.StolenDuringRound = true;
|
||||
ApplyStealingReputationLoss(item);
|
||||
#if CLIENT
|
||||
HintManager.OnStoleItem(thief, item);
|
||||
#endif
|
||||
}
|
||||
if (!someoneSpoke)
|
||||
{
|
||||
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
|
||||
someoneSpoke = true;
|
||||
}
|
||||
// React if we are security
|
||||
if (!TriggerSecurity(otherHumanAI))
|
||||
{
|
||||
@@ -1900,7 +1939,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (item.OwnInventory?.FindItem(it => it.SpawnedInCurrentOutpost && !item.AllowStealing, true) is { } foundItem)
|
||||
else if (item.OwnInventory?.FindItem(it => it.Illegitimate, true) is { } foundItem)
|
||||
{
|
||||
ItemTaken(foundItem, thief);
|
||||
}
|
||||
@@ -1914,11 +1953,12 @@ namespace Barotrauma
|
||||
{
|
||||
findThieves.InspectEveryone();
|
||||
}
|
||||
bool isCriminal = thief.IsCriminal;
|
||||
humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(),
|
||||
abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
|
||||
abortCondition: obj => !isCriminal && thief.Inventory.FindItem(it => it.Illegitimate, recursive: true) == null,
|
||||
onAbort: () =>
|
||||
{
|
||||
if (item != null && !item.Removed && humanAI != null && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
|
||||
if (!item.Removed && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
|
||||
{
|
||||
humanAI.ObjectiveManager.AddObjective(new AIObjectiveGetItem(humanAI.Character, item, humanAI.ObjectiveManager, equip: false)
|
||||
{
|
||||
@@ -1926,7 +1966,8 @@ namespace Barotrauma
|
||||
});
|
||||
}
|
||||
},
|
||||
allowHoldFire: true);
|
||||
allowHoldFire: !isCriminal,
|
||||
speakWarnings: !isCriminal);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2084,7 +2125,7 @@ namespace Barotrauma
|
||||
}
|
||||
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
|
||||
bool ignoreOxygen = HasDivingGear(character);
|
||||
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
|
||||
bool ignoreEnemies = ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>();
|
||||
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies);
|
||||
if (isCurrentHull)
|
||||
{
|
||||
@@ -2146,7 +2187,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!visibleHulls.Contains(c.CurrentHull)) { continue; }
|
||||
}
|
||||
if (IsActive(c) && !IsFriendly(character, c) && !c.IsArrested)
|
||||
if (IsActive(c) && !IsFriendly(character, c) && !c.IsHandcuffed)
|
||||
{
|
||||
enemyCount++;
|
||||
}
|
||||
|
||||
@@ -492,14 +492,20 @@ namespace Barotrauma
|
||||
var door = currentPath.CurrentNode.ConnectedDoor;
|
||||
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
|
||||
float colliderHeight = collider.Height / 2 + collider.Radius;
|
||||
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
|
||||
if (heightDiff < colliderHeight)
|
||||
if (currentPath.CurrentNode.Stairs == null)
|
||||
{
|
||||
//the waypoint is between the top and bottom of the collider, no need to move vertically.
|
||||
diff.Y = 0.0f;
|
||||
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
|
||||
if (heightDiff < colliderHeight)
|
||||
{
|
||||
// Original comment:
|
||||
//the waypoint is between the top and bottom of the collider, no need to move vertically.
|
||||
// Note that the waypoint can be below collider too! This might be incorrect.
|
||||
diff.Y = 0.0f;
|
||||
}
|
||||
}
|
||||
if (currentPath.CurrentNode.Stairs != null)
|
||||
else
|
||||
{
|
||||
// In stairs
|
||||
bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs;
|
||||
if (!isNextNodeInSameStairs)
|
||||
{
|
||||
@@ -883,7 +889,18 @@ namespace Barotrauma
|
||||
//steer away from edges of the hull
|
||||
bool wander = false;
|
||||
bool inWater = character.AnimController.InWater;
|
||||
var currentHull = character.CurrentHull;
|
||||
Hull currentHull = character.CurrentHull;
|
||||
// TODO: disabled for now, because seems to cause bots to walk towards walls/doors in some places. In some places it's because how the hulls are defined, but there is probably something else too, is it seems to happen also elsewhere.
|
||||
// if (!inWater)
|
||||
// {
|
||||
// Vector2 colliderBottomPos = ConvertUnits.ToDisplayUnits(character.AnimController.GetColliderBottom());
|
||||
// if (Hull.FindHull(colliderBottomPos, guess: currentHull, useWorldCoordinates: false) is Hull lowestHull)
|
||||
// {
|
||||
// // Use the hull found at the collider bottom, if found.
|
||||
// // Makes difference in some rooms that have multiple hulls, of which the lowest hull where the feet are might not be the same as where the center position of the main collider is.
|
||||
// currentHull = lowestHull;
|
||||
// }
|
||||
// }
|
||||
if (currentHull != null && !inWater)
|
||||
{
|
||||
float roomWidth = currentHull.Rect.Width;
|
||||
|
||||
@@ -388,6 +388,7 @@ namespace Barotrauma
|
||||
character.TeleportTo(ConvertUnits.ToDisplayUnits(forceColliderSimPosition.Value));
|
||||
}
|
||||
|
||||
// TODO: Shouldn't multiply by LimbScale here, because it's already applied in attachLimb.Scale!
|
||||
Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale;
|
||||
if (jointDir < 0.0f)
|
||||
{
|
||||
|
||||
@@ -15,7 +15,10 @@ namespace Barotrauma
|
||||
public virtual string DebugTag => Identifier.Value;
|
||||
public virtual bool ForceRun => false;
|
||||
public virtual bool IgnoreUnsafeHulls => false;
|
||||
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
|
||||
public virtual bool AbandonWhenCannotCompleteSubObjectives => true;
|
||||
/// <summary>
|
||||
/// Should subobjectives be sorted according to their priority?
|
||||
/// </summary>
|
||||
public virtual bool AllowSubObjectiveSorting => false;
|
||||
public virtual bool PrioritizeIfSubObjectivesActive => false;
|
||||
|
||||
@@ -28,8 +31,7 @@ namespace Barotrauma
|
||||
/// Run the main objective with all subobjectives concurrently?
|
||||
/// If false, the main objective will continue only when all the subobjectives have been removed (done).
|
||||
/// </summary>
|
||||
public virtual bool ConcurrentObjectives => false;
|
||||
|
||||
protected virtual bool ConcurrentObjectives => false;
|
||||
public virtual bool KeepDivingGearOn => false;
|
||||
public virtual bool KeepDivingGearOnAlsoWhenInactive => false;
|
||||
|
||||
@@ -37,10 +39,36 @@ namespace Barotrauma
|
||||
/// There's a separate property for diving suit and mask: KeepDivingGearOn.
|
||||
/// </summary>
|
||||
public virtual bool AllowAutomaticItemUnequipping => false;
|
||||
public virtual bool AllowOutsideSubmarine => false;
|
||||
public virtual bool AllowInFriendlySubs => false;
|
||||
public virtual bool AllowInAnySub => false;
|
||||
public virtual bool AllowWhileHandcuffed => true;
|
||||
|
||||
// These booleans are used for defining whether the objective is allowed in different contexts. E.g. AllowOutsideSubmarine needs to be true or the objective cannot be active when the bot is swimming outside.
|
||||
protected virtual bool AllowOutsideSubmarine => false;
|
||||
/// <summary>
|
||||
/// When true, the objective is allowed in the player subs (when in the same team) and on friendly outposts (regardless of the alignment).
|
||||
/// Note: ignored when <see cref="AllowInAnySub"/> is true.
|
||||
/// </summary>
|
||||
protected virtual bool AllowInFriendlySubs => false;
|
||||
protected virtual bool AllowInAnySub => false;
|
||||
protected virtual bool AllowWhileHandcuffed => true;
|
||||
|
||||
/// <summary>
|
||||
/// Should the objective abandon when it's not allowed in the current context or should it just stay inactive with 0 priority?
|
||||
/// Abandoned automatic objectives are removed and recreated automatically (when new orders are assigned or after a cooldown period).
|
||||
/// Abandoned orders are removed, but the most recent order can be reissued by clicking the small order icon with the arrow in the crew manager panel.
|
||||
/// </summary>
|
||||
protected virtual bool AbandonIfDisallowed => true;
|
||||
|
||||
public virtual bool CanBeCompleted => !Abandon;
|
||||
|
||||
protected virtual float MaxDevotion => 10;
|
||||
|
||||
/// <summary>
|
||||
/// Which event action (if any) created this objective
|
||||
/// </summary>
|
||||
public EventAction SourceEventAction;
|
||||
/// <summary>
|
||||
/// Which objective (if any) created this objective. When this is a subobjective, the parent objective is used by default.
|
||||
/// </summary>
|
||||
public AIObjective SourceObjective;
|
||||
|
||||
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
|
||||
private float _cumulatedDevotion;
|
||||
@@ -50,8 +78,6 @@ namespace Barotrauma
|
||||
set { _cumulatedDevotion = MathHelper.Clamp(value, 0, MaxDevotion); }
|
||||
}
|
||||
|
||||
protected virtual float MaxDevotion => 10;
|
||||
|
||||
/// <summary>
|
||||
/// Final priority value after all calculations.
|
||||
/// </summary>
|
||||
@@ -100,17 +126,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool CanBeCompleted => !Abandon;
|
||||
|
||||
/// <summary>
|
||||
/// When true, the objective is never completed, unless CanBeCompleted returns false.
|
||||
/// </summary>
|
||||
public virtual bool IsLoop { get; set; }
|
||||
|
||||
public IEnumerable<AIObjective> SubObjectives => subObjectives;
|
||||
|
||||
public AIObjective CurrentSubObjective => subObjectives.FirstOrDefault();
|
||||
|
||||
private readonly List<AIObjective> all = new List<AIObjective>();
|
||||
|
||||
public IEnumerable<AIObjective> GetSubObjectivesRecursive(bool includingSelf = false)
|
||||
{
|
||||
all.Clear();
|
||||
@@ -124,13 +146,11 @@ namespace Barotrauma
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0649
|
||||
|
||||
/// <summary>
|
||||
/// Aborts the objective when this condition is true.
|
||||
/// </summary>
|
||||
public Func<AIObjective, bool> AbortCondition;
|
||||
#pragma warning restore CS0649
|
||||
|
||||
/// <summary>
|
||||
/// A single shot event. Automatically cleared after launching. Use OnCompleted method for implementing (internal) persistent behavior.
|
||||
@@ -186,6 +206,7 @@ namespace Barotrauma
|
||||
public void AddSubObjective(AIObjective objective, bool addFirst = false)
|
||||
{
|
||||
var type = objective.GetType();
|
||||
objective.SourceObjective = this;
|
||||
subObjectives.RemoveAll(o => o.GetType() == type);
|
||||
if (addFirst)
|
||||
{
|
||||
@@ -259,17 +280,21 @@ namespace Barotrauma
|
||||
return character.Submarine.Info.IsOutpost && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC;
|
||||
}
|
||||
|
||||
protected void HandleNonAllowed()
|
||||
protected void HandleDisallowed()
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = !IsIgnoredAtOutpost();
|
||||
if (AbandonIfDisallowed && !IsIgnoredAtOutpost())
|
||||
{
|
||||
// Never abandon objectives inside a friendly outpost, because otherwise we'd have to reassign most orders every round.
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
HandleNonAllowed();
|
||||
HandleDisallowed();
|
||||
return Priority;
|
||||
}
|
||||
if (objectiveManager.IsOrder(this))
|
||||
@@ -360,7 +385,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Checks if the subobjectives in the given collection are removed from the subobjectives. And if so, removes it also from the dictionary.
|
||||
/// </summary>
|
||||
protected void SyncRemovedObjectives<T1, T2>(Dictionary<T1, T2> dictionary, IEnumerable<T1> collection) where T2 : AIObjective
|
||||
protected virtual void SyncRemovedObjectives<T1, T2>(Dictionary<T1, T2> dictionary, IEnumerable<T1> collection) where T2 : AIObjective
|
||||
{
|
||||
foreach (T1 key in collection)
|
||||
{
|
||||
@@ -398,6 +423,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (objective.AllowMultipleInstances)
|
||||
{
|
||||
objective.SourceObjective = this;
|
||||
subObjectives.Add(objective);
|
||||
}
|
||||
else
|
||||
@@ -482,6 +508,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the objective should be aborted (and abandon if it should), and return whether the objective is completed or not.
|
||||
/// </summary>
|
||||
private bool Check()
|
||||
{
|
||||
if (AbortCondition != null && AbortCondition(this))
|
||||
@@ -492,6 +521,9 @@ namespace Barotrauma
|
||||
return CheckObjectiveSpecific();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should return whether the objective is completed or not.
|
||||
/// </summary>
|
||||
protected abstract bool CheckObjectiveSpecific();
|
||||
|
||||
private bool CheckState()
|
||||
@@ -527,7 +559,7 @@ namespace Barotrauma
|
||||
DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.", Color.Red);
|
||||
#endif
|
||||
subObjectives.Remove(subObjective);
|
||||
if (AbandonWhenCannotCompleteSubjectives)
|
||||
if (AbandonWhenCannotCompleteSubObjectives)
|
||||
{
|
||||
if (objectiveManager.IsOrder(this))
|
||||
{
|
||||
|
||||
+2
-3
@@ -16,7 +16,7 @@ namespace Barotrauma
|
||||
public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, Identifier option, float priorityModifier)
|
||||
: base(character, objectiveManager, priorityModifier, option) { }
|
||||
|
||||
protected override bool Filter(PowerContainer battery)
|
||||
protected override bool IsValidTarget(PowerContainer battery)
|
||||
{
|
||||
if (battery == null) { return false; }
|
||||
if (battery.OutputDisabled) { return false; }
|
||||
@@ -37,7 +37,7 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
protected override float GetTargetPriority()
|
||||
{
|
||||
if (Targets.None()) { return 0; }
|
||||
if (Option == "charge")
|
||||
@@ -80,7 +80,6 @@ namespace Barotrauma
|
||||
protected override AIObjective ObjectiveConstructor(PowerContainer battery) =>
|
||||
new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier)
|
||||
{
|
||||
IsLoop = false,
|
||||
Override = !character.IsDismissed,
|
||||
completionCondition = () => IsReady(battery)
|
||||
};
|
||||
|
||||
+105
-34
@@ -8,8 +8,8 @@ namespace Barotrauma
|
||||
class AIObjectiveCheckStolenItems : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "check stolen items".ToIdentifier();
|
||||
public override bool AllowOutsideSubmarine => false;
|
||||
public override bool AllowInAnySub => false;
|
||||
protected override bool AllowOutsideSubmarine => false;
|
||||
protected override bool AllowInAnySub => false;
|
||||
|
||||
public float FindStolenItemsProbability = 1.0f;
|
||||
|
||||
@@ -21,36 +21,38 @@ namespace Barotrauma
|
||||
Done
|
||||
}
|
||||
|
||||
private float inspectDelay;
|
||||
private float warnDelay;
|
||||
private const float InspectTime = 5.0f;
|
||||
private const float NormalWarnDelay = 5.0f;
|
||||
private const float CriminalWarnDelay = 3.0f;
|
||||
private float inspectTimer;
|
||||
private float warnTimer;
|
||||
private float currentWarnDelay;
|
||||
|
||||
private State currentState;
|
||||
|
||||
public readonly Character TargetCharacter;
|
||||
public readonly Character Target;
|
||||
|
||||
private AIObjectiveGoTo? goToObjective;
|
||||
|
||||
private readonly List<Item> stolenItems = new List<Item>();
|
||||
|
||||
public AIObjectiveCheckStolenItems(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) :
|
||||
public AIObjectiveCheckStolenItems(Character character, Character target, AIObjectiveManager objectiveManager, float priorityModifier = 1) :
|
||||
base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
TargetCharacter = targetCharacter;
|
||||
inspectDelay = 5.0f;
|
||||
warnDelay = 5.0f;
|
||||
}
|
||||
|
||||
public override bool IsLoop
|
||||
{
|
||||
get => false;
|
||||
set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace());
|
||||
Target = target;
|
||||
InitTimers();
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
|
||||
if (character.IsClimbing)
|
||||
{
|
||||
// Target is climbing -> stop following the objective (soft abandon, without ignoring the target).
|
||||
Priority = 0;
|
||||
}
|
||||
else if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
|
||||
{
|
||||
Priority = objectiveManager.GetOrderPriority(this);
|
||||
}
|
||||
@@ -70,22 +72,32 @@ namespace Barotrauma
|
||||
{
|
||||
switch (currentState)
|
||||
{
|
||||
case State.Done:
|
||||
IsCompleted = true;
|
||||
break;
|
||||
case State.GotoTarget:
|
||||
TryAddSubObjective(ref goToObjective,
|
||||
constructor: () =>
|
||||
constructor: () => new AIObjectiveGoTo(Target, character, objectiveManager, repeat: false)
|
||||
{
|
||||
return new AIObjectiveGoTo(TargetCharacter, character, objectiveManager, repeat: false)
|
||||
{
|
||||
SpeakIfFails = false
|
||||
};
|
||||
SpeakIfFails = false
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
currentState = State.Inspect;
|
||||
stolenItems.Clear();
|
||||
TargetCharacter.Inventory.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true, stolenItems);
|
||||
character.Speak(TextManager.Get("dialogcheckstolenitems").Value);
|
||||
if (character.IsClimbing)
|
||||
{
|
||||
// Shouldn't start inspecting characters when they climb, nor get here, because the priority should be 0,
|
||||
// but if this still happens, we'll have to abandon the objective
|
||||
// because it's not currently possible to hold to characters and ladders at the same time.
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentState = State.Inspect;
|
||||
stolenItems.Clear();
|
||||
Target.Inventory.FindAllItems(it => it.Illegitimate, recursive: true, stolenItems);
|
||||
character.Speak(TextManager.Get(Target.IsCriminal ? "dialogcheckstolenitems.criminal" : "dialogcheckstolenitems").Value);
|
||||
}
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
@@ -103,10 +115,23 @@ namespace Barotrauma
|
||||
|
||||
private void Inspect(float deltaTime)
|
||||
{
|
||||
if (inspectDelay > 0.0f)
|
||||
if (inspectTimer > 0.0f)
|
||||
{
|
||||
character.SelectCharacter(TargetCharacter);
|
||||
inspectDelay -= deltaTime;
|
||||
character.SelectCharacter(Target);
|
||||
inspectTimer -= deltaTime;
|
||||
if (inspectTimer < InspectTime - 1)
|
||||
{
|
||||
if (Target.AnimController.IsMovingFast)
|
||||
{
|
||||
ArrestFleeing();
|
||||
}
|
||||
else if (Math.Abs(Target.AnimController.TargetMovement.X) > 1.0f)
|
||||
{
|
||||
// If the target moves, reset the inspect timer and tell to hold still
|
||||
character.Speak(TextManager.Get("dialogcheckstolenitems.holdstill").Value, identifier: "holdstill".ToIdentifier(), minDurationBetweenSimilar: 3f);
|
||||
inspectTimer = InspectTime;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,7 +143,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
character.Speak(TextManager.Get("dialogcheckstolenitems.nostolenitems").Value);
|
||||
character.Speak(TextManager.Get(Target.IsCriminal ? "dialogcheckstolenitems.nostolenitems.criminal" : "dialogcheckstolenitems.nostolenitems").Value);
|
||||
currentState = State.Done;
|
||||
IsCompleted = true;
|
||||
}
|
||||
@@ -127,16 +152,23 @@ namespace Barotrauma
|
||||
|
||||
private void Warn(float deltaTime)
|
||||
{
|
||||
if (warnDelay > 0.0f)
|
||||
if (warnTimer > 0.0f)
|
||||
{
|
||||
warnDelay -= deltaTime;
|
||||
warnTimer -= deltaTime;
|
||||
if (warnTimer < currentWarnDelay - 1)
|
||||
{
|
||||
if (Target.AnimController.IsMovingFast)
|
||||
{
|
||||
ArrestFleeing();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == TargetCharacter);
|
||||
var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == Target);
|
||||
if (stolenItemsOnCharacter.Any())
|
||||
{
|
||||
character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value);
|
||||
HumanAIController.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, TargetCharacter);
|
||||
character.Speak(TextManager.Get(character.IsCriminal ? "dialogcheckstolenitems.arrest.criminal" : "dialogcheckstolenitems.arrest").Value);
|
||||
Arrest(abortWhenItemsDropped: true, allowHoldFire: true);
|
||||
foreach (var stolenItem in stolenItemsOnCharacter)
|
||||
{
|
||||
HumanAIController.ApplyStealingReputationLoss(stolenItem);
|
||||
@@ -156,5 +188,44 @@ namespace Barotrauma
|
||||
currentState = State.Done;
|
||||
IsCompleted = true;
|
||||
}
|
||||
|
||||
private void ArrestFleeing()
|
||||
{
|
||||
character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value);
|
||||
currentState = State.Done;
|
||||
IsCompleted = true;
|
||||
Arrest(abortWhenItemsDropped: false, allowHoldFire: false);
|
||||
}
|
||||
|
||||
private void Arrest(bool abortWhenItemsDropped, bool allowHoldFire)
|
||||
{
|
||||
bool isCriminal = Target.IsCriminal;
|
||||
Func<AIObjective, bool>? abortCondition = null;
|
||||
if (abortWhenItemsDropped && !isCriminal)
|
||||
{
|
||||
abortCondition = obj => Target.Inventory.FindItem(it => it.Illegitimate, recursive: true) == null;
|
||||
}
|
||||
HumanAIController.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, Target, allowHoldFire: allowHoldFire && !isCriminal, speakWarnings: !isCriminal, abortCondition: abortCondition);
|
||||
}
|
||||
|
||||
public override void OnDeselected()
|
||||
{
|
||||
base.OnDeselected();
|
||||
character.DeselectCharacter();
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
currentState = State.GotoTarget;
|
||||
InitTimers();
|
||||
}
|
||||
|
||||
private void InitTimers()
|
||||
{
|
||||
inspectTimer = InspectTime;
|
||||
currentWarnDelay = Target.IsCriminal ? CriminalWarnDelay : NormalWarnDelay;
|
||||
warnTimer = currentWarnDelay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ namespace Barotrauma
|
||||
public override Identifier Identifier { get; set; } = "cleanup item".ToIdentifier();
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowAutomaticItemUnequipping => false;
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
public readonly Item item;
|
||||
public bool IsPriority { get; set; }
|
||||
@@ -24,7 +24,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Allows decontainObjective to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
|
||||
/// </summary>
|
||||
public override bool ConcurrentObjectives => true;
|
||||
protected override bool ConcurrentObjectives => true;
|
||||
|
||||
public AIObjectiveCleanupItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
@@ -36,7 +36,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
HandleNonAllowed();
|
||||
HandleDisallowed();
|
||||
return Priority;
|
||||
}
|
||||
else
|
||||
|
||||
+3
-3
@@ -31,7 +31,7 @@ namespace Barotrauma
|
||||
this.prioritizedItems.AddRange(prioritizedItems.Where(i => i != null));
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
protected override float GetTargetPriority()
|
||||
{
|
||||
if (Targets.None()) { return 0; }
|
||||
if (objectiveManager.IsOrder(this))
|
||||
@@ -47,7 +47,7 @@ namespace Barotrauma
|
||||
return AIObjectiveManager.RunPriority - 0.5f;
|
||||
}
|
||||
|
||||
protected override bool Filter(Item target)
|
||||
protected override bool IsValidTarget(Item target)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(target.GetComponent<Pickable>() is { } pickable && !pickable.IsAttached, "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking pickable, non-attached items.");
|
||||
System.Diagnostics.Debug.Assert(target.Prefab.PreferredContainers.Any(), "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking items that have preferred containers defined.");
|
||||
@@ -100,7 +100,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
if (item.DontCleanUp) { return false; }
|
||||
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
|
||||
if (item.Illegitimate == character.IsOnPlayerTeam) { return false; }
|
||||
if (item.ParentInventory != null)
|
||||
{
|
||||
if (item.Container == null)
|
||||
|
||||
+247
-137
@@ -16,23 +16,23 @@ namespace Barotrauma
|
||||
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
protected override bool AllowOutsideSubmarine => true;
|
||||
protected override bool AllowInAnySub => true;
|
||||
|
||||
private readonly CombatMode initialMode;
|
||||
|
||||
private float checkWeaponsTimer;
|
||||
private const float checkWeaponsInterval = 1;
|
||||
private const float CheckWeaponsInterval = 1;
|
||||
private float ignoreWeaponTimer;
|
||||
private const float ignoredWeaponsClearTime = 10;
|
||||
private const float IgnoredWeaponsClearTime = 10;
|
||||
|
||||
private const float goodWeaponPriority = 30;
|
||||
|
||||
private const float arrestHoldFireTime = 8;
|
||||
private const float GoodWeaponPriority = 30;
|
||||
|
||||
private float holdFireTimer;
|
||||
private bool hasAimed;
|
||||
private bool isLethalWeapon;
|
||||
private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID;
|
||||
private bool AllowCoolDown => allowCooldown || !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID;
|
||||
private bool allowCooldown;
|
||||
|
||||
public Character Enemy { get; private set; }
|
||||
public bool HoldPosition { get; set; }
|
||||
@@ -45,7 +45,6 @@ namespace Barotrauma
|
||||
{
|
||||
_weapon = value;
|
||||
_weaponComponent = null;
|
||||
hasAimed = false;
|
||||
}
|
||||
}
|
||||
private ItemComponent _weaponComponent;
|
||||
@@ -58,8 +57,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ConcurrentObjectives => true;
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
protected override bool ConcurrentObjectives => true;
|
||||
public override bool AbandonWhenCannotCompleteSubObjectives => false;
|
||||
|
||||
private readonly AIObjectiveFindSafety findSafety;
|
||||
private readonly HashSet<ItemComponent> weapons = new HashSet<ItemComponent>();
|
||||
@@ -72,6 +71,9 @@ namespace Barotrauma
|
||||
|
||||
private Hull retreatTarget;
|
||||
private float coolDownTimer;
|
||||
private float pathBackTimer;
|
||||
private const float DefaultCoolDown = 10.0f;
|
||||
private const float PathBackCheckTime = 1.0f;
|
||||
private IEnumerable<Body> myBodies;
|
||||
private float aimTimer;
|
||||
private float reloadTimer;
|
||||
@@ -79,17 +81,25 @@ namespace Barotrauma
|
||||
|
||||
private bool canSeeTarget;
|
||||
private float visibilityCheckTimer;
|
||||
private const float visibilityCheckInterval = 0.2f;
|
||||
private const float VisibilityCheckInterval = 0.2f;
|
||||
|
||||
private float sqrDistance;
|
||||
private const float maxDistance = 2000;
|
||||
private const float distanceCheckInterval = 0.2f;
|
||||
private const float MaxDistance = 2000;
|
||||
private const float DistanceCheckInterval = 0.2f;
|
||||
private float distanceTimer;
|
||||
|
||||
private const float closeDistanceThreshold = 300;
|
||||
private const float floorHeightApproximate = 100;
|
||||
private const float CloseDistanceThreshold = 300;
|
||||
private const float FloorHeightApproximate = 100;
|
||||
|
||||
public bool AllowHoldFire;
|
||||
public bool SpeakWarnings;
|
||||
private bool firstWarningTriggered;
|
||||
private bool lastWarningTriggered;
|
||||
|
||||
public float ArrestHoldFireTime { get; init; } = 10;
|
||||
|
||||
private const float ArrestTargetDistance = 100;
|
||||
private bool arrestingRegistered;
|
||||
|
||||
/// <summary>
|
||||
/// Don't start using a weapon if this condition is true
|
||||
@@ -123,7 +133,7 @@ namespace Barotrauma
|
||||
public CombatMode Mode { get; private set; }
|
||||
|
||||
private bool IsOffensiveOrArrest => initialMode is CombatMode.Offensive or CombatMode.Arrest;
|
||||
private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f || Enemy.IsArrested && !character.IsInstigator;
|
||||
private bool TargetEliminated => IsEnemyDisabled || (Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f) || (!character.IsInstigator && Enemy.IsHandcuffed && Enemy.IsKnockedDown);
|
||||
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
|
||||
|
||||
private float AimSpeed => HumanAIController.AimSpeed;
|
||||
@@ -141,7 +151,7 @@ namespace Barotrauma
|
||||
if (character.CurrentHull != null && Enemy.CurrentHull != null && character.CurrentHull != Enemy.CurrentHull)
|
||||
{
|
||||
// Inside, not in the same hull with the enemy
|
||||
if (Math.Abs(toEnemy.Y) > floorHeightApproximate)
|
||||
if (Math.Abs(toEnemy.Y) > FloorHeightApproximate)
|
||||
{
|
||||
// Different floor
|
||||
return false;
|
||||
@@ -156,7 +166,7 @@ namespace Barotrauma
|
||||
return Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin;
|
||||
}
|
||||
|
||||
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
|
||||
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = DefaultCoolDown)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
if (mode == CombatMode.None)
|
||||
@@ -187,48 +197,31 @@ namespace Barotrauma
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (Enemy == null || Enemy.Removed)
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine))
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
}
|
||||
if (TargetEliminated)
|
||||
{
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
}
|
||||
else
|
||||
// 91-100
|
||||
const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
|
||||
const float maxPriority = AIObjectiveManager.MaxObjectivePriority;
|
||||
const float priorityScale = maxPriority - minPriority;
|
||||
float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
|
||||
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
|
||||
{
|
||||
// 91-100
|
||||
const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
|
||||
const float maxPriority = AIObjectiveManager.MaxObjectivePriority;
|
||||
const float priorityScale = maxPriority - minPriority;
|
||||
float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
|
||||
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
|
||||
xDist /= 2;
|
||||
yDist /= 2;
|
||||
}
|
||||
float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5);
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1));
|
||||
Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority);
|
||||
if (Priority > 0)
|
||||
{
|
||||
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
|
||||
{
|
||||
xDist /= 2;
|
||||
yDist /= 2;
|
||||
}
|
||||
float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5);
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1));
|
||||
Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority);
|
||||
if (Priority > 0)
|
||||
{
|
||||
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
Priority = 0;
|
||||
}
|
||||
}
|
||||
return Priority;
|
||||
@@ -246,7 +239,7 @@ namespace Barotrauma
|
||||
if (ignoreWeaponTimer < 0)
|
||||
{
|
||||
ignoredWeapons.Clear();
|
||||
ignoreWeaponTimer = ignoredWeaponsClearTime;
|
||||
ignoreWeaponTimer = IgnoredWeaponsClearTime;
|
||||
}
|
||||
bool isFightingIntruders = objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
|
||||
if (findSafety != null && isFightingIntruders)
|
||||
@@ -258,7 +251,7 @@ namespace Barotrauma
|
||||
distanceTimer -= deltaTime;
|
||||
if (distanceTimer < 0)
|
||||
{
|
||||
distanceTimer = distanceCheckInterval;
|
||||
distanceTimer = DistanceCheckInterval;
|
||||
sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
|
||||
}
|
||||
}
|
||||
@@ -266,16 +259,62 @@ namespace Barotrauma
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
if (character.Submarine is not { TeamID: CharacterTeamType.FriendlyNPC })
|
||||
if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine)
|
||||
{
|
||||
// Can't lose the target in friendly outposts.
|
||||
if (sqrDistance > maxDistance * maxDistance)
|
||||
// Target still in the outpost
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsSecurity)
|
||||
{
|
||||
// The target escaped from us.
|
||||
return true;
|
||||
// Outpost guards shouldn't lose the target in friendly outposts,
|
||||
// However, if we are not a guard, let's ensure that we allow the cooldown.
|
||||
allowCooldown = true;
|
||||
}
|
||||
}
|
||||
return IsEnemyDisabled || (AllowCoolDown && coolDownTimer <= 0);
|
||||
else
|
||||
{
|
||||
if ((Enemy.Submarine == null && character.Submarine != null) || sqrDistance > MaxDistance * MaxDistance)
|
||||
{
|
||||
// The target escaped from us.
|
||||
Abandon = true;
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest)
|
||||
{
|
||||
Enemy.IsCriminal = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (Enemy.Submarine != null && character.Submarine != null && character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (Enemy.Submarine.TeamID != character.TeamID)
|
||||
{
|
||||
allowCooldown = true;
|
||||
// Target not in the outpost anymore.
|
||||
if (character.CanSeeTarget(Enemy))
|
||||
{
|
||||
allowCooldown = false;
|
||||
coolDownTimer = DefaultCoolDown;
|
||||
}
|
||||
else if (pathBackTimer <= 0)
|
||||
{
|
||||
// Check once per sec during the cooldown whether we can find a path back to the docking port
|
||||
pathBackTimer = PathBackCheckTime;
|
||||
foreach ((Submarine sub, DockingPort dockingPort) in character.Submarine.ConnectedDockingPorts)
|
||||
{
|
||||
if (sub.TeamID != character.TeamID) { continue; }
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(dockingPort.Item), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
|
||||
if (path.Unreachable)
|
||||
{
|
||||
allowCooldown = false;
|
||||
coolDownTimer = DefaultCoolDown;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (IsOffensiveOrArrest)
|
||||
{
|
||||
Enemy.IsCriminal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return TargetEliminated || (AllowCoolDown && coolDownTimer <= 0);
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
@@ -288,6 +327,10 @@ namespace Barotrauma
|
||||
if (AllowCoolDown)
|
||||
{
|
||||
coolDownTimer -= deltaTime;
|
||||
if (pathBackTimer > 0)
|
||||
{
|
||||
pathBackTimer -= deltaTime;
|
||||
}
|
||||
}
|
||||
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
|
||||
{
|
||||
@@ -303,27 +346,6 @@ namespace Barotrauma
|
||||
{
|
||||
Move(deltaTime);
|
||||
}
|
||||
switch (Mode)
|
||||
{
|
||||
case CombatMode.Offensive:
|
||||
if (TargetEliminated && objectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>())
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f);
|
||||
}
|
||||
break;
|
||||
case CombatMode.Arrest:
|
||||
if (HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out _, requireEquipped: true))
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
else if (Enemy.IsKnockedDown &&
|
||||
!objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>() &&
|
||||
!HumanAIController.HasItem(character, Tags.HandLockerItem, out _, requireEquipped: false))
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,7 +411,7 @@ namespace Barotrauma
|
||||
&& !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
|
||||
if (checkWeaponsTimer < 0)
|
||||
{
|
||||
checkWeaponsTimer = checkWeaponsInterval;
|
||||
checkWeaponsTimer = CheckWeaponsInterval;
|
||||
// First go through all weapons and try to reload without seeking ammunition
|
||||
HashSet<ItemComponent> allWeapons = FindWeaponsFromInventory();
|
||||
while (allWeapons.Any())
|
||||
@@ -412,7 +434,7 @@ namespace Barotrauma
|
||||
// All good, the weapon is loaded
|
||||
break;
|
||||
}
|
||||
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(closeDistanceThreshold);
|
||||
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(CloseDistanceThreshold);
|
||||
if (Reload(seekAmmo: seekAmmo))
|
||||
{
|
||||
// All good, we can use the weapon.
|
||||
@@ -458,7 +480,7 @@ namespace Barotrauma
|
||||
Mode = CombatMode.Retreat;
|
||||
}
|
||||
}
|
||||
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority && !IsEnemyClose(closeDistanceThreshold))))
|
||||
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistanceThreshold))))
|
||||
{
|
||||
// No weapon or only a poor weapon equipped -> try to find better.
|
||||
RemoveSubObjective(ref retreatObjective);
|
||||
@@ -485,7 +507,7 @@ namespace Barotrauma
|
||||
if (range is > 0 and < float.PositiveInfinity)
|
||||
{
|
||||
// Y distance is irrelevant when we are on the same floor. If we are on a different floor, let's double it.
|
||||
float yDiff = Math.Abs(toItem.Y) > floorHeightApproximate ? toItem.Y * 2 : 0;
|
||||
float yDiff = Math.Abs(toItem.Y) > FloorHeightApproximate ? toItem.Y * 2 : 0;
|
||||
Vector2 adjustedDiff = new Vector2(toItem.X, yDiff);
|
||||
if (adjustedDiff.LengthSquared() > MathUtils.Pow2(range))
|
||||
{
|
||||
@@ -501,7 +523,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (i.CurrentHull != null && !HumanAIController.VisibleHulls.Contains(i.CurrentHull))
|
||||
{
|
||||
if (Math.Abs(toItem.Y) > floorHeightApproximate && Math.Abs(toEnemy.Y) > floorHeightApproximate)
|
||||
if (Math.Abs(toItem.Y) > FloorHeightApproximate && Math.Abs(toEnemy.Y) > FloorHeightApproximate)
|
||||
{
|
||||
if (Math.Sign(toItem.Y) == Math.Sign(toEnemy.Y))
|
||||
{
|
||||
@@ -522,7 +544,7 @@ namespace Barotrauma
|
||||
SpeakNoWeapons();
|
||||
Mode = CombatMode.Retreat;
|
||||
}
|
||||
else if (!objectiveManager.HasActiveObjective<AIObjectiveFightIntruders>())
|
||||
else if (!objectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>())
|
||||
{
|
||||
// Poor weapon equipped
|
||||
Mode = CombatMode.Defensive;
|
||||
@@ -642,7 +664,7 @@ namespace Barotrauma
|
||||
priority /= 2;
|
||||
}
|
||||
}
|
||||
else if (Enemy.IsKnockedDown)
|
||||
else if (Enemy.IsKnockedDown && Mode != CombatMode.Arrest)
|
||||
{
|
||||
// Enemy is stunned, reduce the priority of stunner weapons.
|
||||
Attack attack = GetAttackDefinition(weapon);
|
||||
@@ -775,7 +797,7 @@ namespace Barotrauma
|
||||
float bestPriority = 0;
|
||||
float lethalDmg = -1;
|
||||
bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character);
|
||||
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(closeDistanceThreshold);
|
||||
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistanceThreshold);
|
||||
foreach (var weapon in weaponList)
|
||||
{
|
||||
float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg);
|
||||
@@ -801,9 +823,28 @@ namespace Barotrauma
|
||||
}
|
||||
isLethalWeapon = lethalDmg > 1;
|
||||
}
|
||||
if (AllowHoldFire && !hasAimed && holdFireTimer <= 0)
|
||||
if (AllowHoldFire)
|
||||
{
|
||||
holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f);
|
||||
if (!hasAimed && holdFireTimer <= 0)
|
||||
{
|
||||
holdFireTimer = ArrestHoldFireTime * Rand.Range(0.9f, 1.1f);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (SpeakWarnings)
|
||||
{
|
||||
if (!lastWarningTriggered && holdFireTimer < ArrestHoldFireTime * 0.3f)
|
||||
{
|
||||
FriendlyGuardSpeak("dialogarrest.lastwarning".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 0f);
|
||||
lastWarningTriggered = true;
|
||||
}
|
||||
else if (!firstWarningTriggered && holdFireTimer < ArrestHoldFireTime * 0.8f)
|
||||
{
|
||||
FriendlyGuardSpeak("dialogarrest.firstwarning".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 0f);
|
||||
firstWarningTriggered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return weaponComponent.Item;
|
||||
@@ -909,9 +950,10 @@ namespace Barotrauma
|
||||
|
||||
private void Retreat(float deltaTime)
|
||||
{
|
||||
if (!Enemy.IsHuman)
|
||||
if (!Enemy.IsHuman && !character.IsInFriendlySub)
|
||||
{
|
||||
SpeakRetreating();
|
||||
// Only relevant when we are retreating from monsters and are not inside a friendly sub.
|
||||
PlayerCrewSpeak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDurationBetweenSimilar: 20);
|
||||
}
|
||||
RemoveFollowTarget();
|
||||
RemoveSubObjective(ref seekAmmunitionObjective);
|
||||
@@ -931,7 +973,7 @@ namespace Barotrauma
|
||||
{
|
||||
RemoveSubObjective(ref retreatObjective);
|
||||
}
|
||||
if (character.Submarine == null && sqrDistance < MathUtils.Pow2(maxDistance))
|
||||
if (character.Submarine == null && sqrDistance < MathUtils.Pow2(MaxDistance))
|
||||
{
|
||||
// Swim away
|
||||
SteeringManager.Reset();
|
||||
@@ -1017,6 +1059,16 @@ namespace Barotrauma
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine != null && character.Submarine.TeamID != character.TeamID)
|
||||
{
|
||||
// An outpost guard following the target (possibly a player) to another sub -> don't go further, unless can see the enemy.
|
||||
if (!character.IsClimbing && !character.CanSeeTarget(Enemy))
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
RemoveFollowTarget();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (followTargetObjective != null && followTargetObjective.Target != Enemy)
|
||||
{
|
||||
RemoveFollowTarget();
|
||||
@@ -1045,32 +1097,27 @@ namespace Barotrauma
|
||||
}
|
||||
});
|
||||
if (followTargetObjective == null) { return; }
|
||||
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown)
|
||||
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown && !arrestingRegistered)
|
||||
{
|
||||
if (HumanAIController.HasItem(character, Tags.HandLockerItem, out _))
|
||||
bool hasHandCuffs = HumanAIController.HasItem(character, Tags.HandLockerItem, out _);
|
||||
if (!hasHandCuffs && character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (!arrestingRegistered)
|
||||
// Spawn handcuffs
|
||||
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier());
|
||||
if (prefab != null)
|
||||
{
|
||||
arrestingRegistered = true;
|
||||
followTargetObjective.Completed += OnArrestTargetReached;
|
||||
followTargetObjective.CloseEnough = 100;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier());
|
||||
if (prefab != null)
|
||||
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: i =>
|
||||
{
|
||||
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true);
|
||||
}
|
||||
i.SpawnedInCurrentOutpost = true;
|
||||
i.AllowStealing = false;
|
||||
});
|
||||
}
|
||||
RemoveFollowTarget();
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
arrestingRegistered = true;
|
||||
followTargetObjective.Completed += OnArrestTargetReached;
|
||||
followTargetObjective.CloseEnough = ArrestTargetDistance;
|
||||
}
|
||||
if (!arrestingRegistered && followTargetObjective != null)
|
||||
if (!arrestingRegistered)
|
||||
{
|
||||
followTargetObjective.CloseEnough =
|
||||
WeaponComponent switch
|
||||
@@ -1083,8 +1130,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private bool arrestingRegistered;
|
||||
|
||||
private void RemoveFollowTarget()
|
||||
{
|
||||
if (followTargetObjective != null)
|
||||
@@ -1110,9 +1155,9 @@ namespace Barotrauma
|
||||
// Confiscate stolen goods and all weapons
|
||||
foreach (var item in Enemy.Inventory.AllItemsMod)
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound ||
|
||||
item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) ||
|
||||
GetWeaponComponent(item) is { CombatPriority: > 0 })
|
||||
// Ignore handcuffs already on the target.
|
||||
if (item.HasTag(Tags.HandLockerItem) && Enemy.HasEquippedItem(item)) { continue; }
|
||||
if (item.Illegitimate || item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 })
|
||||
{
|
||||
item.Drop(character);
|
||||
character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
|
||||
@@ -1255,7 +1300,7 @@ namespace Barotrauma
|
||||
if (visibilityCheckTimer <= 0.0f)
|
||||
{
|
||||
canSeeTarget = character.CanSeeTarget(Enemy);
|
||||
visibilityCheckTimer = visibilityCheckInterval;
|
||||
visibilityCheckTimer = VisibilityCheckInterval;
|
||||
}
|
||||
if (!canSeeTarget)
|
||||
{
|
||||
@@ -1267,7 +1312,7 @@ namespace Barotrauma
|
||||
character.SetInput(InputType.Aim, hit: false, held: true);
|
||||
}
|
||||
hasAimed = true;
|
||||
if (holdFireTimer > 0)
|
||||
if (AllowHoldFire && holdFireTimer > 0)
|
||||
{
|
||||
holdFireTimer -= deltaTime;
|
||||
return;
|
||||
@@ -1280,7 +1325,7 @@ namespace Barotrauma
|
||||
if (reloadTimer > 0) { return; }
|
||||
if (holdFireCondition != null && holdFireCondition()) { return; }
|
||||
sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
|
||||
distanceTimer = distanceCheckInterval;
|
||||
distanceTimer = DistanceCheckInterval;
|
||||
if (WeaponComponent is MeleeWeapon meleeWeapon)
|
||||
{
|
||||
bool closeEnough = true;
|
||||
@@ -1354,8 +1399,8 @@ namespace Barotrauma
|
||||
|
||||
private void UseWeapon(float deltaTime)
|
||||
{
|
||||
// Never allow to attack characters with deadly weapons while trying to arrest.
|
||||
if (Mode == CombatMode.Arrest && isLethalWeapon) { return; }
|
||||
// Never allow friendly crew (bots) to attack with deadly weapons.
|
||||
if (Mode == CombatMode.Arrest && isLethalWeapon && character.IsOnPlayerTeam && Enemy.IsOnPlayerTeam) { return; }
|
||||
character.SetInput(InputType.Shoot, hit: false, held: true);
|
||||
Weapon.Use(deltaTime, user: character);
|
||||
SetReloadTime(WeaponComponent);
|
||||
@@ -1408,6 +1453,36 @@ namespace Barotrauma
|
||||
protected override void OnCompleted()
|
||||
{
|
||||
base.OnCompleted();
|
||||
if (Enemy != null)
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case CombatMode.Offensive when Enemy.IsUnconscious && objectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>():
|
||||
character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f);
|
||||
break;
|
||||
case CombatMode.Arrest when IsCompleted:
|
||||
if (!HumanAIController.IsTrueForAnyBotInTheCrew(bot =>
|
||||
(bot != HumanAIController && bot.ObjectiveManager.CurrentObjective is AIObjectiveCombat { Mode: CombatMode.Arrest } combatObj && combatObj.Enemy == Enemy) ||
|
||||
bot.ObjectiveManager.CurrentObjective is AIObjectiveGoTo { SourceObjective: AIObjectiveCombat combatObjective } && combatObjective.Enemy == Enemy))
|
||||
{
|
||||
// Go to the target and confiscate any stolen items, unless someone is already on it.
|
||||
// Added on the root level, because the lifetime of the new objective exceeds the lifetime of this objective.
|
||||
RemoveFollowTarget();
|
||||
var approachArrestTarget = new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: false, getDivingGearIfNeeded: false, closeEnough: ArrestTargetDistance)
|
||||
{
|
||||
UsePathingOutside = false,
|
||||
IgnoreIfTargetDead = true,
|
||||
TargetName = Enemy.DisplayName,
|
||||
AlwaysUseEuclideanDistance = false,
|
||||
SpeakIfFails = false,
|
||||
SourceObjective = this
|
||||
};
|
||||
approachArrestTarget.Completed += OnArrestTargetReached;
|
||||
objectiveManager.AddObjective(approachArrestTarget);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ShouldUnequipWeapon)
|
||||
{
|
||||
Unequip();
|
||||
@@ -1424,11 +1499,23 @@ namespace Barotrauma
|
||||
}
|
||||
SteeringManager?.Reset();
|
||||
}
|
||||
|
||||
|
||||
public override void OnDeselected()
|
||||
{
|
||||
base.OnDeselected();
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest && (!AllowHoldFire || (hasAimed && holdFireTimer <= 0)))
|
||||
{
|
||||
// Remember that the target resisted or acted offensively (we've aimed or tried to arrest/attack)
|
||||
Enemy.IsCriminal = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
hasAimed = false;
|
||||
holdFireTimer = 0;
|
||||
pathBackTimer = 0;
|
||||
isLethalWeapon = false;
|
||||
canSeeTarget = false;
|
||||
seekWeaponObjective = null;
|
||||
@@ -1436,20 +1523,43 @@ namespace Barotrauma
|
||||
retreatObjective = null;
|
||||
followTargetObjective = null;
|
||||
retreatTarget = null;
|
||||
firstWarningTriggered = false;
|
||||
lastWarningTriggered = false;
|
||||
}
|
||||
|
||||
private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30);
|
||||
private void SpeakRetreating() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20);
|
||||
|
||||
private void Speak(Identifier textIdentifier, float delay, float minDuration)
|
||||
/// <summary>
|
||||
/// Speak that we don't have weapons. But only outside of friendly subs (not that relevant there, reduces spam).
|
||||
/// </summary>
|
||||
private void SpeakNoWeapons()
|
||||
{
|
||||
if (character.IsOnPlayerTeam && !character.IsInFriendlySub)
|
||||
if (!character.IsInFriendlySub)
|
||||
{
|
||||
LocalizedString msg = TextManager.Get(textIdentifier);
|
||||
if (!msg.IsNullOrEmpty())
|
||||
{
|
||||
character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration);
|
||||
}
|
||||
PlayerCrewSpeak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 30);
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayerCrewSpeak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
|
||||
{
|
||||
if (character.IsOnPlayerTeam)
|
||||
{
|
||||
Speak(textIdentifier, delay, minDurationBetweenSimilar);
|
||||
}
|
||||
}
|
||||
|
||||
private void FriendlyGuardSpeak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && character.IsSecurity)
|
||||
{
|
||||
Speak(textIdentifier, delay, minDurationBetweenSimilar);
|
||||
}
|
||||
}
|
||||
|
||||
private void Speak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
|
||||
{
|
||||
LocalizedString msg = TextManager.Get(textIdentifier);
|
||||
if (!msg.IsNullOrEmpty())
|
||||
{
|
||||
character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDurationBetweenSimilar);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ namespace Barotrauma
|
||||
class AIObjectiveContainItem: AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "contain item".ToIdentifier();
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
|
||||
|
||||
+2
-2
@@ -6,9 +6,9 @@ namespace Barotrauma
|
||||
class AIObjectiveDeconstructItem : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "deconstruct item".ToIdentifier();
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
public override bool AllowInFriendlySubs => true;
|
||||
protected override bool AllowInFriendlySubs => true;
|
||||
|
||||
public readonly Item Item;
|
||||
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ namespace Barotrauma
|
||||
//Clear periodically, because we may ending up ignoring items when all deconstructors are full
|
||||
protected override float IgnoreListClearInterval => 30;
|
||||
|
||||
public override bool AllowInFriendlySubs => true;
|
||||
protected override bool AllowInFriendlySubs => true;
|
||||
|
||||
protected override int MaxTargets => 10;
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Barotrauma
|
||||
checkedDeconstructorExists = false;
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
protected override float GetTargetPriority()
|
||||
{
|
||||
if (Targets.None()) { return 0; }
|
||||
if (objectiveManager.IsOrder(this))
|
||||
@@ -57,7 +57,7 @@ namespace Barotrauma
|
||||
return AIObjectiveManager.RunPriority - 0.5f;
|
||||
}
|
||||
|
||||
protected override bool Filter(Item target)
|
||||
protected override bool IsValidTarget(Item target)
|
||||
{
|
||||
// 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.
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ namespace Barotrauma
|
||||
class AIObjectiveDecontainItem : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier();
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
|
||||
|
||||
+2
-3
@@ -8,8 +8,8 @@ namespace Barotrauma
|
||||
// Used for prisoner escorts to allow them to escape their binds
|
||||
public override Identifier Identifier { get; set; } = "escape handcuffs".ToIdentifier();
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
protected override bool AllowOutsideSubmarine => true;
|
||||
protected override bool AllowInAnySub => true;
|
||||
|
||||
private int escapeProgress;
|
||||
private bool isBeingWatched;
|
||||
@@ -28,7 +28,6 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public override bool CanBeCompleted => true;
|
||||
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
|
||||
// escape timer is set to 60 by default to allow players to locate prisoners in time
|
||||
|
||||
+4
-6
@@ -10,12 +10,10 @@ namespace Barotrauma
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "extinguish fire".ToIdentifier();
|
||||
public override bool ForceRun => true;
|
||||
public override bool ConcurrentObjectives => true;
|
||||
protected override bool ConcurrentObjectives => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowInAnySub => true;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
private readonly Hull targetHull;
|
||||
|
||||
@@ -32,7 +30,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
HandleNonAllowed();
|
||||
HandleDisallowed();
|
||||
return Priority;
|
||||
}
|
||||
bool isOrder = objectiveManager.HasOrder<AIObjectiveExtinguishFires>();
|
||||
|
||||
+3
-3
@@ -10,13 +10,13 @@ namespace Barotrauma
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "extinguish fires".ToIdentifier();
|
||||
public override bool ForceRun => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
protected override bool AllowInAnySub => true;
|
||||
|
||||
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool Filter(Hull hull) => IsValidTarget(hull, character);
|
||||
protected override bool IsValidTarget(Hull hull) => IsValidTarget(hull, character);
|
||||
|
||||
protected override float TargetEvaluation() =>
|
||||
protected override float GetTargetPriority() =>
|
||||
// If any target is visible -> 100 priority
|
||||
Targets.Any(t => t == character.CurrentHull || HumanAIController.VisibleHulls.Contains(t)) ? 100 :
|
||||
// Else based on the fire severity
|
||||
|
||||
+7
-9
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -9,19 +9,17 @@ namespace Barotrauma
|
||||
public override Identifier Identifier { get; set; } = "fight intruders".ToIdentifier();
|
||||
protected override float IgnoreListClearInterval => 30;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
|
||||
protected override float TargetUpdateTimeMultiplier => 0.2f;
|
||||
|
||||
public bool TargetCharactersInOtherSubs { get; init; }
|
||||
|
||||
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool Filter(Character target) => IsValidTarget(target, character, TargetCharactersInOtherSubs);
|
||||
protected override bool IsValidTarget(Character target) => IsValidTarget(target, character, TargetCharactersInOtherSubs);
|
||||
|
||||
protected override IEnumerable<Character> GetList() => Character.CharacterList;
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
protected override float GetTargetPriority()
|
||||
{
|
||||
if (Targets.None()) { return 0; }
|
||||
if (!character.IsOnPlayerTeam && !character.IsOriginallyOnPlayerTeam) { return 100; }
|
||||
@@ -68,14 +66,14 @@ namespace Barotrauma
|
||||
if (HumanAIController.IsFriendly(character, target)) { return false; }
|
||||
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
|
||||
if (!targetCharactersInOtherSubs)
|
||||
{
|
||||
{
|
||||
if (character.Submarine.TeamID != target.Submarine.TeamID && character.OriginalTeamID != target.Submarine.TeamID)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
|
||||
if (target.IsArrested) { return false; }
|
||||
if (target.IsHandcuffed && target.IsKnockedDown) { return false; }
|
||||
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ namespace Barotrauma
|
||||
public override string DebugTag => $"{Identifier} ({gearTag})";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
public override bool AbandonWhenCannotCompleteSubObjectives => false;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
private readonly Identifier gearTag;
|
||||
|
||||
|
||||
+18
-20
@@ -13,18 +13,16 @@ namespace Barotrauma
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
public override bool ConcurrentObjectives => true;
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
|
||||
protected override bool ConcurrentObjectives => true;
|
||||
protected override bool AllowOutsideSubmarine => true;
|
||||
protected override bool AllowInAnySub => true;
|
||||
public override bool AbandonWhenCannotCompleteSubObjectives => false;
|
||||
|
||||
// TODO: expose?
|
||||
const float priorityIncrease = 100;
|
||||
const float priorityDecrease = 10;
|
||||
const float SearchHullInterval = 3.0f;
|
||||
private const float PriorityIncrease = 100;
|
||||
private const float PriorityDecrease = 10;
|
||||
private const float SearchHullInterval = 3.0f;
|
||||
|
||||
private float currenthullSafety;
|
||||
private float currentHullSafety;
|
||||
|
||||
private float searchHullTimer;
|
||||
|
||||
@@ -111,15 +109,15 @@ namespace Barotrauma
|
||||
}
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
currenthullSafety = 0;
|
||||
currentHullSafety = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
currenthullSafety = HumanAIController.CurrentHullSafety;
|
||||
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
currentHullSafety = HumanAIController.CurrentHullSafety;
|
||||
if (currentHullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
Priority -= priorityDecrease * deltaTime;
|
||||
if (currenthullSafety >= 100 && !character.IsLowInOxygen)
|
||||
Priority -= PriorityDecrease * deltaTime;
|
||||
if (currentHullSafety >= 100 && !character.IsLowInOxygen)
|
||||
{
|
||||
// Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
|
||||
Priority = 0;
|
||||
@@ -127,8 +125,8 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
float dangerFactor = (100 - currenthullSafety) / 100;
|
||||
Priority += dangerFactor * priorityIncrease * deltaTime;
|
||||
float dangerFactor = (100 - currentHullSafety) / 100;
|
||||
Priority += dangerFactor * PriorityIncrease * deltaTime;
|
||||
}
|
||||
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
|
||||
}
|
||||
@@ -192,7 +190,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
|
||||
{
|
||||
if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
if (currentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
searchHullTimer = Math.Min(1, searchHullTimer);
|
||||
}
|
||||
@@ -231,7 +229,7 @@ namespace Barotrauma
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
|
||||
if (currentHullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
|
||||
HumanAIController.NeedsDivingGear(currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character)))
|
||||
{
|
||||
resetPriority = true;
|
||||
@@ -299,7 +297,7 @@ namespace Barotrauma
|
||||
}
|
||||
foreach (Character enemy in Character.CharacterList)
|
||||
{
|
||||
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; }
|
||||
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsHandcuffed) { continue; }
|
||||
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
|
||||
{
|
||||
Vector2 dir = character.Position - enemy.Position;
|
||||
|
||||
+67
-16
@@ -2,6 +2,7 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -13,17 +14,33 @@ namespace Barotrauma
|
||||
|
||||
protected override float TargetUpdateTimeMultiplier => 1.0f;
|
||||
|
||||
const float DefaultInspectDistance = 200.0f;
|
||||
/// <summary>
|
||||
/// How long the round must have ran before NPCs can start doing inspections
|
||||
/// (prevents "unfair" inspections you have no chance to react to if you happen to spawn right next to a security NPC with stolen items on you)
|
||||
/// </summary>
|
||||
private const float DelayOnRoundStart = 30.0f;
|
||||
|
||||
private const float DefaultInspectDistance = 200.0f;
|
||||
/// <summary>
|
||||
/// Used when something is stolen and when the guards decide to inspect everyone.
|
||||
/// </summary>
|
||||
private const float ExtendedInspectDistance = 400.0f;
|
||||
/// <summary>
|
||||
/// Used when the target is tagged as a criminal (= suspective).
|
||||
/// </summary>
|
||||
private const float CriminalInspectDistance = 500.0f;
|
||||
|
||||
private const float CriminalInspectProbability = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// How close the NPC must be to the target to the inspect them? You can use high values to make the NPC
|
||||
/// systematically go through targets no matter where they are, and low values to check targets they happen to come across.
|
||||
/// </summary>
|
||||
public float InspectDistance = DefaultInspectDistance;
|
||||
private float inspectDistance = DefaultInspectDistance;
|
||||
|
||||
private float? overrideInspectProbability;
|
||||
/// <summary>
|
||||
/// Chance of inspecting a valid target. The NPC won't try to inspect that target again for <see cref="inspectionInterval"/>
|
||||
/// Chance of inspecting a valid target. The NPC won't try to inspect that target again for <see cref="NormalInspectionInterval"/>
|
||||
/// regardless if the target is inspected or not.
|
||||
/// </summary>
|
||||
public float InspectProbability
|
||||
@@ -53,18 +70,25 @@ namespace Barotrauma
|
||||
/// When did the character last inspect whether some other character has stolen items on them?
|
||||
/// </summary>
|
||||
private static readonly Dictionary<Character, double> lastInspectionTimes = new Dictionary<Character, double>();
|
||||
|
||||
private readonly float inspectionInterval = 120.0f;
|
||||
|
||||
|
||||
private const float NormalInspectionInterval = 120.0f;
|
||||
private const float CriminalInspectionInterval = 30.0f;
|
||||
|
||||
public AIObjectiveFindThieves(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool Filter(Character target)
|
||||
protected override bool IsValidTarget(Character target)
|
||||
{
|
||||
if (GameMain.GameSession is not { RoundDuration: > DelayOnRoundStart })
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!IsValidTarget(target, character)) { return false; }
|
||||
if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > InspectDistance * InspectDistance) { return false; }
|
||||
float inspectDist = target.IsCriminal ? CriminalInspectDistance : inspectDistance;
|
||||
if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > inspectDist * inspectDist) { return false; }
|
||||
if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime))
|
||||
{
|
||||
float inspectionInterval = target.IsCriminal ? CriminalInspectionInterval : NormalInspectionInterval;
|
||||
if (Timing.TotalTime < lastInspectionTime + inspectionInterval)
|
||||
{
|
||||
return false;
|
||||
@@ -75,8 +99,14 @@ namespace Barotrauma
|
||||
|
||||
protected override IEnumerable<Character> GetList() => Character.CharacterList;
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
protected override float GetTargetPriority()
|
||||
{
|
||||
if (character.IsClimbing)
|
||||
{
|
||||
// Don't inspect while climbing, because cannot grab while holding the ladders.
|
||||
// Can lead to abandoning the objective when we need to climb the ladders to get to the target, but I think that's acceptable.
|
||||
return 0;
|
||||
}
|
||||
return subObjectives.Any() ? 50 : 0;
|
||||
}
|
||||
|
||||
@@ -84,13 +114,14 @@ namespace Barotrauma
|
||||
{
|
||||
lastInspectionTimes.Clear();
|
||||
overrideInspectProbability = 1.0f;
|
||||
InspectDistance = DefaultInspectDistance * 2;
|
||||
inspectDistance = ExtendedInspectDistance;
|
||||
}
|
||||
|
||||
protected override AIObjective ObjectiveConstructor(Character target)
|
||||
{
|
||||
var checkStolenItemsObjective = new AIObjectiveCheckStolenItems(character, target, objectiveManager);
|
||||
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= InspectProbability)
|
||||
float probabity = target.IsCriminal ? CriminalInspectProbability : InspectProbability;
|
||||
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= probabity)
|
||||
{
|
||||
checkStolenItemsObjective.ForceComplete();
|
||||
lastInspectionTimes[target] = Timing.TotalTime;
|
||||
@@ -104,26 +135,30 @@ namespace Barotrauma
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
if (checkVisibleStolenItemsTimer > 0.0f)
|
||||
if (checkVisibleStolenItemsTimer > 0.0f || character.IsClimbing)
|
||||
{
|
||||
checkVisibleStolenItemsTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
if (character.SelectedSecondaryItem?.GetComponent<Controller>() != null)
|
||||
{
|
||||
// Might be e.g. sitting on a chair.
|
||||
character.SelectedSecondaryItem = null;
|
||||
}
|
||||
foreach (var target in Character.CharacterList)
|
||||
{
|
||||
if (!IsValidTarget(target, character)) { continue; }
|
||||
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
|
||||
if (target.Inventory.AllItems.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing && target.HasEquippedItem(it)) &&
|
||||
if (target.Inventory.AllItems.Any(it => it.Illegitimate && target.HasEquippedItem(it)) &&
|
||||
character.CanSeeTarget(target, seeThroughWindows: true))
|
||||
{
|
||||
AIObjectiveCheckStolenItems? existingObjective =
|
||||
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.TargetCharacter == target);
|
||||
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.Target == target);
|
||||
if (existingObjective == null)
|
||||
{
|
||||
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
|
||||
lastInspectionTimes[target] = Timing.TotalTime;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
checkVisibleStolenItemsTimer = CheckVisibleStolenItemsInterval;
|
||||
@@ -140,7 +175,17 @@ namespace Barotrauma
|
||||
if (target.Submarine != character.Submarine) { return false; }
|
||||
//only player's crew can steal, ignore other teams
|
||||
if (!target.IsOnPlayerTeam) { return false; }
|
||||
if (target.IsArrested) { return false; }
|
||||
if (target.IsHandcuffed) { 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 =>
|
||||
bot != HumanAIController &&
|
||||
((bot.ObjectiveManager.GetActiveObjective() is AIObjectiveCheckStolenItems checkObj && checkObj.Target == target) ||
|
||||
(bot.ObjectiveManager.GetActiveObjective() is AIObjectiveCombat combatObj && combatObj.Enemy == target))))
|
||||
{
|
||||
// Already being inspected by someone or fighting with someone in our team.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -148,5 +193,11 @@ namespace Barotrauma
|
||||
{
|
||||
lastInspectionTimes[target] = Timing.TotalTime;
|
||||
}
|
||||
|
||||
public override void OnDeselected()
|
||||
{
|
||||
base.OnDeselected();
|
||||
character.DeselectCharacter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -12,9 +12,9 @@ namespace Barotrauma
|
||||
public override Identifier Identifier { get; set; } = "fix leak".ToIdentifier();
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowInFriendlySubs => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowInFriendlySubs => true;
|
||||
protected override bool AllowInAnySub => true;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
public Gap Leak { get; private set; }
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
HandleNonAllowed();
|
||||
HandleDisallowed();
|
||||
return Priority;
|
||||
}
|
||||
float coopMultiplier = 1;
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@ namespace Barotrauma
|
||||
public override Identifier Identifier { get; set; } = "fix leaks".ToIdentifier();
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowInFriendlySubs => true;
|
||||
protected override bool AllowInFriendlySubs => true;
|
||||
|
||||
private Hull PrioritizedHull { get; set; }
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Barotrauma
|
||||
PrioritizedHull = prioritizedHull;
|
||||
}
|
||||
|
||||
protected override bool Filter(Gap gap) => IsValidTarget(gap, character);
|
||||
protected override bool IsValidTarget(Gap gap) => IsValidTarget(gap, character);
|
||||
|
||||
public static float GetLeakSeverity(Gap leak)
|
||||
{
|
||||
@@ -37,7 +37,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
protected override float GetTargetPriority()
|
||||
{
|
||||
int totalLeaks = Targets.Count;
|
||||
if (totalLeaks == 0) { return 0; }
|
||||
|
||||
+3
-3
@@ -13,9 +13,9 @@ namespace Barotrauma
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "get item".ToIdentifier();
|
||||
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
public override bool AbandonWhenCannotCompleteSubObjectives => false;
|
||||
public override bool AllowMultipleInstances => true;
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
public HashSet<Item> ignoredItems = new HashSet<Item>();
|
||||
|
||||
@@ -444,7 +444,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (!AllowStealing && character.IsOnPlayerTeam)
|
||||
{
|
||||
if (item.SpawnedInCurrentOutpost && !item.AllowStealing) { continue; }
|
||||
if (item.Illegitimate) { continue; }
|
||||
}
|
||||
if (!CheckItem(item)) { continue; }
|
||||
if (item.Container != null)
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ namespace Barotrauma
|
||||
public override string DebugTag => $"{Identifier}";
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowMultipleInstances => true;
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
public bool AllowStealing { get; set; }
|
||||
public bool TakeWholeStack { get; set; }
|
||||
|
||||
+27
-21
@@ -16,7 +16,7 @@ namespace Barotrauma
|
||||
private readonly bool repeat;
|
||||
//how long until the path to the target is declared unreachable
|
||||
private float waitUntilPathUnreachable;
|
||||
private bool getDivingGearIfNeeded;
|
||||
private readonly bool getDivingGearIfNeeded;
|
||||
|
||||
/// <summary>
|
||||
/// Doesn't allow the objective to complete if this condition is false
|
||||
@@ -34,11 +34,6 @@ namespace Barotrauma
|
||||
public bool DebugLogWhenFails { get; set; } = true;
|
||||
public bool UsePathingOutside { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Which event action created this objective (if any)
|
||||
/// </summary>
|
||||
public EventAction SourceEventAction;
|
||||
|
||||
public float ExtraDistanceWhileSwimming;
|
||||
public float ExtraDistanceOutsideSub;
|
||||
private float _closeEnoughMultiplier = 1;
|
||||
@@ -94,10 +89,10 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public bool UseDistanceRelativeToAimSourcePos { get; set; } = false;
|
||||
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
public override bool AbandonWhenCannotCompleteSubObjectives => false;
|
||||
|
||||
public override bool AllowOutsideSubmarine => AllowGoingOutside;
|
||||
public override bool AllowInAnySub => true;
|
||||
protected override bool AllowOutsideSubmarine => AllowGoingOutside;
|
||||
protected override bool AllowInAnySub => true;
|
||||
|
||||
public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier();
|
||||
public LocalizedString TargetName { get; set; }
|
||||
@@ -287,10 +282,12 @@ namespace Barotrauma
|
||||
if (waitUntilPathUnreachable < 0)
|
||||
{
|
||||
waitUntilPathUnreachable = pathWaitingTime;
|
||||
if (repeat)
|
||||
if (repeat && !IsCompleted)
|
||||
{
|
||||
SpeakCannotReach();
|
||||
return;
|
||||
if (!IsDoneFollowing())
|
||||
{
|
||||
SpeakCannotReach();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -374,16 +371,10 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (repeat && IsCloseEnough)
|
||||
if (IsDoneFollowing())
|
||||
{
|
||||
if (requiredCondition == null || requiredCondition())
|
||||
{
|
||||
if (character.CanSeeTarget(Target) && (!character.IsClimbing || IsFollowOrder))
|
||||
{
|
||||
OnCompleted();
|
||||
return;
|
||||
}
|
||||
}
|
||||
OnCompleted();
|
||||
return;
|
||||
}
|
||||
float maxGapDistance = 500;
|
||||
Character targetCharacter = Target as Character;
|
||||
@@ -653,6 +644,21 @@ namespace Barotrauma
|
||||
character.SetInput(InputType.Aim, false, true);
|
||||
character.SetInput(InputType.Shoot, false, true);
|
||||
}
|
||||
|
||||
bool IsDoneFollowing()
|
||||
{
|
||||
if (repeat && IsCloseEnough)
|
||||
{
|
||||
if (requiredCondition == null || requiredCondition())
|
||||
{
|
||||
if (character.CanSeeTarget(Target) && (!character.IsClimbing || IsFollowOrder))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool useScooter;
|
||||
|
||||
+3
-5
@@ -12,7 +12,7 @@ namespace Barotrauma
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "idle".ToIdentifier();
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
protected override bool AllowInAnySub => true;
|
||||
|
||||
private BehaviorType behavior;
|
||||
public BehaviorType Behavior
|
||||
@@ -91,8 +91,6 @@ namespace Barotrauma
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
|
||||
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
|
||||
|
||||
public readonly HashSet<Identifier> PreferredOutpostModuleTypes = new HashSet<Identifier>();
|
||||
|
||||
public void CalculatePriority(float max = 0)
|
||||
@@ -266,7 +264,7 @@ namespace Barotrauma
|
||||
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
|
||||
return true;
|
||||
//don't stop at ladders when idling
|
||||
}, endNodeFilter: node => node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
|
||||
}, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
|
||||
if (path.Unreachable)
|
||||
{
|
||||
//can't go to this room, remove it from the list and try another room
|
||||
@@ -299,7 +297,7 @@ namespace Barotrauma
|
||||
{
|
||||
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1,
|
||||
nodeFilter: node => node.Waypoint.CurrentHull != null,
|
||||
endNodeFilter: node => node.Waypoint.Ladders == null);
|
||||
endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveInspectNoises : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "inspect noises".ToIdentifier();
|
||||
|
||||
private AIObjectiveGoTo inspectNoiseObjective;
|
||||
|
||||
/// <summary>
|
||||
/// Initial priority of the objective to check noises made by enemies
|
||||
/// </summary>
|
||||
const float InspectNoisePriority = 10.0f;
|
||||
/// <summary>
|
||||
/// How much the priority of the objective to check noises made by enemies increases per noise
|
||||
/// </summary>
|
||||
const float InspectNoisePriorityIncrease = 10.0f;
|
||||
private const float InspectNoiseInterval = 1.0f;
|
||||
private float inspectNoiseTimer;
|
||||
|
||||
/// <summary>
|
||||
/// If the character is not currently inspecting the noise (= if some other objective is taking priority)
|
||||
/// it forgets about it after this delay runs out. Otherwise they might unnecessarily go and inspect some
|
||||
/// noise that was emitted a long time ago once done with the higher-prio objective.
|
||||
/// </summary>
|
||||
private const float InspectNoiseExpirationDelay = 60.0f;
|
||||
private float inspectNoiseExpirationTimer = 0.0f;
|
||||
|
||||
protected override float GetPriority() => inspectNoiseObjective?.Priority ?? 0.0f;
|
||||
|
||||
public AIObjectiveInspectNoises(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
inspectNoiseTimer = Rand.Range(0.0f, InspectNoiseInterval);
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
inspectNoiseTimer -= deltaTime;
|
||||
if (inspectNoiseTimer <= 0.0f)
|
||||
{
|
||||
CheckEnemyNoises();
|
||||
inspectNoiseTimer = InspectNoiseInterval;
|
||||
}
|
||||
//if we're not currently inspecting the noise (something else taking priority), forget about it after a while
|
||||
if (inspectNoiseObjective != null && objectiveManager.GetActiveObjective() != inspectNoiseObjective)
|
||||
{
|
||||
inspectNoiseExpirationTimer += deltaTime;
|
||||
if (inspectNoiseExpirationTimer > InspectNoiseExpirationDelay)
|
||||
{
|
||||
inspectNoiseObjective.Abandon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if there's any loud provocative items used by enemies nearby (= if someone fired a gun somewhere), and go inspect them
|
||||
/// </summary>
|
||||
private void CheckEnemyNoises()
|
||||
{
|
||||
if (character.CurrentHull == null) { return; }
|
||||
|
||||
//forget about inspecting if we're doing another subobjective (= fighting something)
|
||||
if (inspectNoiseObjective != null &&
|
||||
CurrentSubObjective != inspectNoiseObjective)
|
||||
{
|
||||
inspectNoiseObjective.Abandon = true;
|
||||
}
|
||||
|
||||
foreach (var aiTarget in AITarget.List)
|
||||
{
|
||||
if (aiTarget.ShouldBeIgnored()) { continue; }
|
||||
if (!aiTarget.IsWithinSector(character.WorldPosition)) { continue; }
|
||||
if (aiTarget.Entity is not Item item) { continue; }
|
||||
if (!item.HasTag(Tags.ProvocativeToHumanAI)) { continue; }
|
||||
if (item.GetRootInventoryOwner() is Character targetCharacter &&
|
||||
AIObjectiveFightIntruders.IsValidTarget(targetCharacter, character, targetCharactersInOtherSubs: false))
|
||||
{
|
||||
float dist = character.CurrentHull.GetApproximateDistance(character.Position, targetCharacter.Position, targetCharacter.CurrentHull, aiTarget.SoundRange, distanceMultiplierPerClosedDoor: 2);
|
||||
if (dist * HumanAIController.Hearing > aiTarget.SoundRange) { continue; }
|
||||
|
||||
character.Speak(TextManager.Get("dialogheardenemy").Value, identifier: "heardenemy".ToIdentifier(), minDurationBetweenSimilar: 30.0f);
|
||||
if (inspectNoiseObjective != null && subObjectives.Contains(inspectNoiseObjective))
|
||||
{
|
||||
//priority of inspecting noises increases with each noise
|
||||
//but orders still remain a higher priority
|
||||
inspectNoiseObjective.Priority = Math.Min(inspectNoiseObjective.Priority + InspectNoisePriorityIncrease, AIObjectiveManager.LowestOrderPriority - 1);
|
||||
//only refresh the target if the character hasn't yet started inspecting the noise
|
||||
//(if it has, it should not switch the target, otherwise you could e.g. bounce an NPC back and forth by firing guns at different sides of an outpost)
|
||||
if (objectiveManager.GetActiveObjective() != inspectNoiseObjective &&
|
||||
inspectNoiseObjective.Target != targetCharacter.CurrentHull)
|
||||
{
|
||||
CreateInspectNoiseObjective(targetCharacter.CurrentHull, priority: inspectNoiseObjective.Priority);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CreateInspectNoiseObjective(targetCharacter.CurrentHull, priority: InspectNoisePriority);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CreateInspectNoiseObjective(ISpatialEntity target, float priority)
|
||||
{
|
||||
RemoveSubObjective(ref inspectNoiseObjective);
|
||||
inspectNoiseObjective = new AIObjectiveGoTo(target, character, objectiveManager)
|
||||
{
|
||||
Priority = priority,
|
||||
SourceObjective = this
|
||||
};
|
||||
inspectNoiseObjective.Completed += () => { inspectNoiseObjective = null; inspectNoiseExpirationTimer = 0.0f; };
|
||||
inspectNoiseObjective.Abandoned += () => { inspectNoiseObjective = null; inspectNoiseExpirationTimer = 0.0f; };
|
||||
AddSubObjective(inspectNoiseObjective);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
|
||||
}
|
||||
}
|
||||
+8
-11
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
@@ -11,13 +11,8 @@ namespace Barotrauma
|
||||
class AIObjectiveLoadItem : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "load item".ToIdentifier();
|
||||
public override bool IsLoop
|
||||
{
|
||||
get => true;
|
||||
set => throw new Exception("Trying to set the value for AIObjectiveLoadItem.IsLoop from: " + Environment.StackTrace.CleanupStackTrace());
|
||||
}
|
||||
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; }
|
||||
private Item Container { get; }
|
||||
@@ -163,7 +158,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
HandleNonAllowed();
|
||||
HandleDisallowed();
|
||||
return Priority;
|
||||
}
|
||||
else if (!AIObjectiveLoadItems.IsValidTarget(Container, character, targetCondition: TargetItemCondition))
|
||||
@@ -298,12 +293,14 @@ namespace Barotrauma
|
||||
if (item.Removed) { return false; }
|
||||
if (!ValidContainableItemIdentifiers.Contains(item.Prefab.Identifier)) { return false; }
|
||||
if (ignoredItems.Contains(item)) { return false; }
|
||||
if ((item.Illegitimate) == character.IsOnPlayerTeam) { return false; }
|
||||
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
|
||||
var rootInventoryOwner = item.GetRootInventoryOwner();
|
||||
if (rootInventoryOwner is Character owner && owner != character) { return false; }
|
||||
if (rootInventoryOwner is Item parentItem)
|
||||
if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; }
|
||||
Item parentItem = item.Container;
|
||||
while (parentItem != null)
|
||||
{
|
||||
if (parentItem.HasTag(Tags.DontTakeItems)) { return false; }
|
||||
parentItem = parentItem.Container;
|
||||
}
|
||||
if (!item.HasAccess(character)) { return false; }
|
||||
if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; }
|
||||
|
||||
+2
-2
@@ -50,7 +50,7 @@ namespace Barotrauma
|
||||
TargetCondition = option == "turretammo" ? ItemCondition.Empty : ItemCondition.Full;
|
||||
}
|
||||
|
||||
protected override bool Filter(Item target)
|
||||
protected override bool IsValidTarget(Item target)
|
||||
{
|
||||
//don't pass TargetContainerTags to the method (no need to filter by tags anymore, it's already done when populating TargetContainers)
|
||||
if (!IsValidTarget(target, character, null, TargetCondition)) { return false; }
|
||||
@@ -104,7 +104,7 @@ namespace Barotrauma
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
|
||||
=> HumanAIController.RemoveTargets<AIObjectiveLoadItems, Item>(character, target);
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
protected override float GetTargetPriority()
|
||||
{
|
||||
if (Targets.None()) { return 0; }
|
||||
if (objectiveManager.IsOrder(this))
|
||||
|
||||
+48
-25
@@ -4,21 +4,34 @@ using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// An objective that creates specific kinds of subobjectives for specific types of targets, and loops through those targets.
|
||||
/// For example, a cleanup objective that loops through items that need to be cleaned up, or a "fix leaks" objective that loops through leaks that need welding.
|
||||
/// </summary>
|
||||
abstract class AIObjectiveLoop<T> : AIObjective
|
||||
{
|
||||
public HashSet<T> Targets { get; private set; } = new HashSet<T>();
|
||||
public Dictionary<T, AIObjective> Objectives { get; private set; } = new Dictionary<T, AIObjective>();
|
||||
protected HashSet<T> ignoreList = new HashSet<T>();
|
||||
private float ignoreListTimer;
|
||||
private float ignoreListClearTimer;
|
||||
protected float targetUpdateTimer;
|
||||
protected virtual float TargetUpdateTimeMultiplier { get; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// How often are the subobjectives synced based on the available targets?
|
||||
/// </summary>
|
||||
private float syncTimer;
|
||||
private readonly float syncTime = 1;
|
||||
|
||||
// By default, doesn't clear the list automatically
|
||||
/// <summary>
|
||||
/// By default, doesn't clear the list automatically
|
||||
/// </summary>
|
||||
protected virtual float IgnoreListClearInterval => 0;
|
||||
|
||||
/// <summary>
|
||||
/// Contains targets that anyone in the same crew has reported about. Used for automatic the target has to be reported before it can be can be targeted, so characters don't magically know where e.g. enemies are.
|
||||
/// Ignored on orders: a bot explicitly ordered to repair leaks or fight intruders can find targets that haven't been reported.
|
||||
/// </summary>
|
||||
public HashSet<T> ReportedTargets { get; private set; } = new HashSet<T>();
|
||||
|
||||
public bool AddTarget(T target)
|
||||
@@ -28,7 +41,7 @@ namespace Barotrauma
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Filter(target))
|
||||
if (IsValidTarget(target))
|
||||
{
|
||||
ReportedTargets.Add(target);
|
||||
return true;
|
||||
@@ -42,24 +55,27 @@ namespace Barotrauma
|
||||
protected override void Act(float deltaTime) { }
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
public override bool AbandonWhenCannotCompleteSubObjectives => false;
|
||||
public override bool AllowSubObjectiveSorting => true;
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AbandonIfDisallowed => false;
|
||||
|
||||
public virtual bool InverseTargetEvaluation => false;
|
||||
/// <summary>
|
||||
/// Makes the priority inversely proportional to the value returned by <see cref="GetTargetPriority"/>.
|
||||
/// In other words, gives this objective a high priority when priority of the targets is low.
|
||||
/// </summary>
|
||||
public virtual bool InverseTargetPriority => false;
|
||||
protected virtual bool ResetWhenClearingIgnoreList => true;
|
||||
protected virtual bool ForceOrderPriority => true;
|
||||
|
||||
protected virtual int MaxTargets => int.MaxValue;
|
||||
|
||||
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); }
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
if (IgnoreListClearInterval > 0)
|
||||
{
|
||||
if (ignoreListTimer > IgnoreListClearInterval)
|
||||
if (ignoreListClearTimer > IgnoreListClearInterval)
|
||||
{
|
||||
if (ResetWhenClearingIgnoreList)
|
||||
{
|
||||
@@ -68,12 +84,12 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
ignoreList.Clear();
|
||||
ignoreListTimer = 0;
|
||||
ignoreListClearTimer = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ignoreListTimer += deltaTime;
|
||||
ignoreListClearTimer += deltaTime;
|
||||
}
|
||||
}
|
||||
if (targetUpdateTimer <= 0)
|
||||
@@ -104,14 +120,17 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
// the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
|
||||
//
|
||||
/// <summary>
|
||||
/// The timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
|
||||
/// </summary>
|
||||
private float CalculateTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1) * TargetUpdateTimeMultiplier;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
ignoreList.Clear();
|
||||
ignoreListTimer = 0;
|
||||
ignoreListClearTimer = 0;
|
||||
UpdateTargets();
|
||||
}
|
||||
|
||||
@@ -119,25 +138,25 @@ namespace Barotrauma
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
HandleNonAllowed();
|
||||
HandleDisallowed();
|
||||
return Priority;
|
||||
}
|
||||
// Allow the target value to be more than 100.
|
||||
float targetValue = TargetEvaluation();
|
||||
if (InverseTargetEvaluation)
|
||||
float targetPriority = GetTargetPriority();
|
||||
if (InverseTargetPriority)
|
||||
{
|
||||
targetValue = 100 - targetValue;
|
||||
targetPriority = 100 - targetPriority;
|
||||
}
|
||||
var currentSubObjective = CurrentSubObjective;
|
||||
if (currentSubObjective != null && currentSubObjective.Priority > targetValue)
|
||||
if (currentSubObjective != null && currentSubObjective.Priority > targetPriority)
|
||||
{
|
||||
// If the priority is higher than the target value, let's just use it.
|
||||
// The priority calculation is more precise, but it takes into account things like distances,
|
||||
// so it's better not to use it if it's lower than the rougher targetValue.
|
||||
targetValue = currentSubObjective.Priority;
|
||||
targetPriority = currentSubObjective.Priority;
|
||||
}
|
||||
// If the target value is less than 1% of the max value, let's just treat it as zero.
|
||||
if (targetValue < 1)
|
||||
if (targetPriority < 1)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
@@ -145,7 +164,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (objectiveManager.IsOrder(this))
|
||||
{
|
||||
Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue;
|
||||
Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetPriority;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -155,7 +174,7 @@ namespace Barotrauma
|
||||
// Allow higher prio
|
||||
max = AIObjectiveManager.EmergencyObjectivePriority;
|
||||
}
|
||||
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
|
||||
float value = MathHelper.Clamp((CumulatedDevotion + (targetPriority * PriorityModifier)) / 100, 0, 1);
|
||||
Priority = MathHelper.Lerp(0, max, value);
|
||||
}
|
||||
}
|
||||
@@ -181,7 +200,7 @@ namespace Barotrauma
|
||||
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater || this is AIObjectiveFindThieves;
|
||||
if (!ignore && !ReportedTargets.Contains(target)) { continue; }
|
||||
}
|
||||
if (!Filter(target)) { continue; }
|
||||
if (!IsValidTarget(target)) { continue; }
|
||||
if (!ignoreList.Contains(target))
|
||||
{
|
||||
Targets.Add(target);
|
||||
@@ -228,9 +247,13 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
protected abstract IEnumerable<T> GetList();
|
||||
|
||||
protected abstract float TargetEvaluation();
|
||||
/// <summary>
|
||||
/// Returns a priority value based on the current targets (e.g. high prio when there's lots of severe fires or leaks).
|
||||
/// The priority of this objective is based on the target priority.
|
||||
/// </summary>
|
||||
protected abstract float GetTargetPriority();
|
||||
|
||||
protected abstract AIObjective ObjectiveConstructor(T target);
|
||||
protected abstract bool Filter(T target);
|
||||
protected abstract bool IsValidTarget(T target);
|
||||
}
|
||||
}
|
||||
|
||||
+79
-32
@@ -20,10 +20,27 @@ namespace Barotrauma
|
||||
MaxValue = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Highest possible priority for any objective. Used in certain cases where the character needs to react immediately to survive,
|
||||
/// such as finding a suit when under pressure or getting out of a burning room.
|
||||
/// </summary>
|
||||
public const float MaxObjectivePriority = 100;
|
||||
/// <summary>
|
||||
/// Priority of objectives such as finding safety, rescuing someone in a critical state or defending against an attacker
|
||||
/// (= objectives that are critical for saving the character's or someone else's life)
|
||||
/// </summary>
|
||||
public const float EmergencyObjectivePriority = 90;
|
||||
/// <summary>
|
||||
/// Maximum priority of an order given to the character (forced order, or the leftmost order in the crew list)
|
||||
/// </summary>
|
||||
public const float HighestOrderPriority = 70;
|
||||
/// <summary>
|
||||
/// Maximum priority of an order given to the character (rightmost order in the crew list)
|
||||
/// </summary>
|
||||
public const float LowestOrderPriority = 60;
|
||||
/// <summary>
|
||||
/// Objectives with a priority equal to or higher than this make the character run.
|
||||
/// </summary>
|
||||
public const float RunPriority = 50;
|
||||
// Constantly increases the priority of the selected objective, unless overridden
|
||||
public const float baseDevotion = 5;
|
||||
@@ -138,6 +155,7 @@ namespace Barotrauma
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
|
||||
foreach (var delayedObjective in DelayedObjectives)
|
||||
{
|
||||
CoroutineManager.StopCoroutines(delayedObjective.Value);
|
||||
@@ -159,36 +177,46 @@ namespace Barotrauma
|
||||
AddObjective(newIdleObjective);
|
||||
|
||||
int objectiveCount = Objectives.Count;
|
||||
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
|
||||
if (character.Info?.Job != null)
|
||||
{
|
||||
var orderPrefab = OrderPrefab.Prefabs[autonomousObjective.Identifier];
|
||||
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.Identifier}'"); }
|
||||
Item item = null;
|
||||
if (orderPrefab.MustSetTarget)
|
||||
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
|
||||
{
|
||||
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandomUnsynced();
|
||||
}
|
||||
var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
|
||||
if (order == null) { continue; }
|
||||
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) &&
|
||||
Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC && !character.IsFriendlyNPCTurnedHostile)
|
||||
{
|
||||
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
|
||||
var orderPrefab = OrderPrefab.Prefabs[autonomousObjective.Identifier] ?? throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.Identifier}'");
|
||||
Item item = null;
|
||||
if (orderPrefab.MustSetTarget)
|
||||
{
|
||||
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandomUnsynced();
|
||||
}
|
||||
var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
|
||||
if (order == null) { continue; }
|
||||
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) &&
|
||||
Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC && !character.IsFriendlyNPCTurnedHostile)
|
||||
{
|
||||
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (autonomousObjective.IgnoreAtNonOutpost && !Level.IsLoadedFriendlyOutpost)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (autonomousObjective.IgnoreAtNonOutpost && !Level.IsLoadedFriendlyOutpost)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var objective = CreateObjective(order, autonomousObjective.PriorityModifier);
|
||||
if (objective != null && objective.CanBeCompleted)
|
||||
{
|
||||
AddObjective(objective, delay: Rand.Value() / 2);
|
||||
objectiveCount++;
|
||||
}
|
||||
}
|
||||
var objective = CreateObjective(order, autonomousObjective.PriorityModifier);
|
||||
if (objective != null && objective.CanBeCompleted)
|
||||
{
|
||||
AddObjective(objective, delay: Rand.Value() / 2);
|
||||
objectiveCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string warningMsg = character.Info == null ?
|
||||
$"The character {character.DisplayName} has been set to use human ai, but has no {nameof(CharacterInfo)}. This may cause issues with the AI. Consider adding {nameof(CharacterPrefab.HasCharacterInfo)}=\"True\" to the character config." :
|
||||
$"The character {character.DisplayName} has been set to use human ai, but has no job. This may cause issues with the AI. Consider configuring some jobs for the character type.";
|
||||
DebugConsole.AddWarning(warningMsg, character.Prefab.ContentPackage);
|
||||
}
|
||||
|
||||
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
|
||||
}
|
||||
|
||||
@@ -501,7 +529,6 @@ namespace Barotrauma
|
||||
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
|
||||
newObjective = new AIObjectiveOperateItem(targetPump, character, this, order.Option, false, priorityModifier: priorityModifier)
|
||||
{
|
||||
IsLoop = false,
|
||||
Override = order.OrderGiver is { IsCommanding: true }
|
||||
};
|
||||
newObjective.Completed += () => DismissSelf(order);
|
||||
@@ -531,7 +558,7 @@ namespace Barotrauma
|
||||
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option,
|
||||
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
|
||||
{
|
||||
IsLoop = true,
|
||||
Repeat = true,
|
||||
// Don't override unless it's an order by a player
|
||||
Override = order.OrderGiver != null && order.OrderGiver.IsCommanding
|
||||
};
|
||||
@@ -539,7 +566,6 @@ namespace Barotrauma
|
||||
case "setchargepct":
|
||||
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, false, priorityModifier: priorityModifier)
|
||||
{
|
||||
IsLoop = false,
|
||||
Override = !character.IsDismissed,
|
||||
completionCondition = () =>
|
||||
{
|
||||
@@ -603,7 +629,6 @@ namespace Barotrauma
|
||||
{
|
||||
prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems)
|
||||
{
|
||||
KeepActiveWhenReady = false,
|
||||
CheckInventory = false,
|
||||
EvaluateCombatPriority = true,
|
||||
FindAllItems = false,
|
||||
@@ -621,13 +646,16 @@ namespace Barotrauma
|
||||
case "deconstructitems":
|
||||
newObjective = new AIObjectiveDeconstructItems(character, this, priorityModifier);
|
||||
break;
|
||||
case "inspectnoises":
|
||||
newObjective = new AIObjectiveInspectNoises(character, this, priorityModifier);
|
||||
break;
|
||||
default:
|
||||
if (order.TargetItemComponent == null) { return null; }
|
||||
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
|
||||
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option,
|
||||
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
|
||||
{
|
||||
IsLoop = true,
|
||||
Repeat = true,
|
||||
// Don't override unless it's an order by a player
|
||||
Override = order.OrderGiver != null && order.OrderGiver.IsCommanding
|
||||
};
|
||||
@@ -689,10 +717,16 @@ namespace Barotrauma
|
||||
/// Only checks the current order. Deprecated, use pattern matching instead.
|
||||
/// </summary>
|
||||
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
|
||||
|
||||
/// <summary>
|
||||
/// Checks the current objective (which can be an order too). Deprecated, use pattern matching instead.
|
||||
/// </summary>
|
||||
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any objectives or orders are of the specified type. Regardless of whether the objective is active or inactive.
|
||||
/// </summary>
|
||||
public bool HasObjectiveOrOrder<T>() where T : AIObjective => Objectives.Any(o => o is T) || HasOrder<T>();
|
||||
|
||||
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
|
||||
|
||||
@@ -705,10 +739,23 @@ namespace Barotrauma
|
||||
/// Return the first order with the specified objective. Can return null.
|
||||
/// </summary>
|
||||
public Order GetOrder(AIObjective objective) => CurrentOrders.FirstOrDefault(o => o.Objective == objective);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last active objective of the specified objective type.
|
||||
/// Should generally be used to get the active objective (or subobjective) of objectives that don't sort their subobjectives by priority (see <see cref="AIObjective.AllowSubObjectiveSorting"/>.
|
||||
/// </summary>
|
||||
/// <returns>The last active objective of the specified type if found.
|
||||
/// </returns>
|
||||
public T GetLastActiveObjective<T>() where T : AIObjective
|
||||
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first active objective of the specified objective type.
|
||||
/// Should generally be used to get the active objective (or subobjective) of objectives that sort their subobjectives by priority, such as those that inherit <see cref="AIObjectiveLoop"/>.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The first active objective of the specified type if found.
|
||||
/// </returns>
|
||||
public T GetFirstActiveObjective<T>() where T : AIObjective
|
||||
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T;
|
||||
|
||||
|
||||
+10
-5
@@ -13,8 +13,8 @@ namespace Barotrauma
|
||||
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
public override bool AllowMultipleInstances => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowInAnySub => true;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret);
|
||||
|
||||
private readonly ItemComponent component, controller;
|
||||
@@ -29,7 +29,12 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public Func<PathNode, bool> EndNodeFilter;
|
||||
|
||||
public bool Override { get; set; } = true;
|
||||
public bool Override { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// When true, the operate objective is never completed, unless it's abandoned.
|
||||
/// </summary>
|
||||
public bool Repeat { get; init; }
|
||||
|
||||
public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
|
||||
|
||||
@@ -50,7 +55,7 @@ namespace Barotrauma
|
||||
bool isOrder = objectiveManager.IsOrder(this);
|
||||
if (!IsAllowed)
|
||||
{
|
||||
HandleNonAllowed();
|
||||
HandleDisallowed();
|
||||
return Priority;
|
||||
}
|
||||
if (!isOrder && component.Item.ConditionPercentage <= 0)
|
||||
@@ -307,7 +312,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => isDoneOperating && !IsLoop;
|
||||
protected override bool CheckObjectiveSpecific() => isDoneOperating && !Repeat;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
|
||||
+6
-32
@@ -13,7 +13,7 @@ namespace Barotrauma
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool KeepDivingGearOnAlsoWhenInactive => true;
|
||||
public override bool PrioritizeIfSubObjectivesActive => true;
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
private AIObjectiveGetItem getSingleItemObjective;
|
||||
private AIObjectiveGetItems getAllItemsObjective;
|
||||
@@ -22,7 +22,6 @@ namespace Barotrauma
|
||||
private readonly Item targetItem;
|
||||
private readonly ImmutableArray<Identifier> requiredItems;
|
||||
private readonly ImmutableArray<Identifier> optionalItems;
|
||||
private readonly HashSet<Item> items = new HashSet<Item>();
|
||||
public bool KeepActiveWhenReady { get; set; }
|
||||
public bool CheckInventory { get; set; }
|
||||
public bool FindAllItems { get; set; }
|
||||
@@ -61,12 +60,12 @@ namespace Barotrauma
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
HandleNonAllowed();
|
||||
HandleDisallowed();
|
||||
return Priority;
|
||||
}
|
||||
Priority = objectiveManager.GetOrderPriority(this);
|
||||
var subObjective = GetSubObjective();
|
||||
if (subObjective != null && subObjective.IsCompleted)
|
||||
if (subObjective is { IsCompleted: true })
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
@@ -113,20 +112,7 @@ namespace Barotrauma
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (KeepActiveWhenReady)
|
||||
{
|
||||
if (objectiveReference != null)
|
||||
{
|
||||
foreach (var item in objectiveReference.achievedItems)
|
||||
{
|
||||
if (item?.IsOwnedBy(character) != null)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
if (!KeepActiveWhenReady)
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
@@ -165,22 +151,11 @@ namespace Barotrauma
|
||||
if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor,
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (KeepActiveWhenReady)
|
||||
{
|
||||
if (getSingleItemObjective != null)
|
||||
{
|
||||
var item = getSingleItemObjective?.TargetItem;
|
||||
if (item?.IsOwnedBy(character) != null)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
if (!KeepActiveWhenReady)
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
onAbandon: () => Abandon = true))
|
||||
{
|
||||
Abandon = true;
|
||||
@@ -193,7 +168,6 @@ namespace Barotrauma
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
items.Clear();
|
||||
subObjectivesCreated = false;
|
||||
getMultipleItemsObjective = null;
|
||||
getSingleItemObjective = null;
|
||||
|
||||
+3
-4
@@ -12,7 +12,7 @@ namespace Barotrauma
|
||||
public override Identifier Identifier { get; set; } = "pump water".ToIdentifier();
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
private List<Pump> pumpList;
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Barotrauma
|
||||
base.FindTargets();
|
||||
}
|
||||
|
||||
protected override bool Filter(Pump pump)
|
||||
protected override bool IsValidTarget(Pump pump)
|
||||
{
|
||||
if (pump?.Item == null || pump.Item.Removed) { return false; }
|
||||
if (pump.Item.IgnoreByAI(character)) { return false; }
|
||||
@@ -62,7 +62,7 @@ namespace Barotrauma
|
||||
return pumpList;
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
protected override float GetTargetPriority()
|
||||
{
|
||||
if (Targets.None()) { return 0; }
|
||||
if (Option == "stoppumping")
|
||||
@@ -90,7 +90,6 @@ namespace Barotrauma
|
||||
protected override AIObjective ObjectiveConstructor(Pump pump)
|
||||
=> new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false)
|
||||
{
|
||||
IsLoop = false,
|
||||
completionCondition = () => IsReady(pump)
|
||||
};
|
||||
|
||||
|
||||
+3
-3
@@ -10,9 +10,9 @@ namespace Barotrauma
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "repair item".ToIdentifier();
|
||||
|
||||
public override bool AllowInFriendlySubs => true;
|
||||
protected override bool AllowInFriendlySubs => true;
|
||||
public override bool KeepDivingGearOn => Item?.CurrentHull == null;
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
public Item Item { get; private set; }
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Barotrauma
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed) { HandleNonAllowed(); }
|
||||
if (!IsAllowed) { HandleDisallowed(); }
|
||||
if (Item.IgnoreByAI(character))
|
||||
{
|
||||
Abandon = true;
|
||||
|
||||
+4
-4
@@ -19,9 +19,9 @@ namespace Barotrauma
|
||||
public Item PrioritizedItem { get; private set; }
|
||||
|
||||
public override bool AllowMultipleInstances => true;
|
||||
public override bool AllowInFriendlySubs => true;
|
||||
protected override bool AllowInFriendlySubs => true;
|
||||
|
||||
public readonly static float RequiredSuccessFactor = 0.4f;
|
||||
public const float RequiredSuccessFactor = 0.4f;
|
||||
|
||||
public override bool IsDuplicate<T>(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && objectiveManager.IsOrder(repairObjective) == objectiveManager.IsOrder(this);
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Filter(Item item)
|
||||
protected override bool IsValidTarget(Item item)
|
||||
{
|
||||
if (!ViableForRepair(item, character, HumanAIController)) { return false; };
|
||||
if (!Objectives.ContainsKey(item))
|
||||
@@ -94,7 +94,7 @@ namespace Barotrauma
|
||||
return item.Repairables.All(r => !r.IsBelowRepairThreshold);
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
protected override float GetTargetPriority()
|
||||
{
|
||||
var selectedItem = character.SelectedItem;
|
||||
if (selectedItem != null && AIObjectiveRepairItem.IsRepairing(character, selectedItem) && selectedItem.ConditionPercentage < 100)
|
||||
|
||||
+5
-6
@@ -13,10 +13,9 @@ namespace Barotrauma
|
||||
public override Identifier Identifier { get; set; } = "rescue".ToIdentifier();
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
public override bool AllowWhileHandcuffed => false;
|
||||
protected override bool AllowOutsideSubmarine => true;
|
||||
protected override bool AllowInAnySub => true;
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
const float TreatmentDelay = 0.5f;
|
||||
|
||||
@@ -484,7 +483,7 @@ namespace Barotrauma
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (Target == null) { Abandon = true; }
|
||||
if (!IsAllowed) { HandleNonAllowed(); }
|
||||
if (!IsAllowed) { HandleDisallowed(); }
|
||||
if (Abandon)
|
||||
{
|
||||
return Priority;
|
||||
@@ -531,8 +530,8 @@ namespace Barotrauma
|
||||
|
||||
public override void OnDeselected()
|
||||
{
|
||||
character.SelectedCharacter = null;
|
||||
base.OnDeselected();
|
||||
character.DeselectCharacter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -9,9 +9,9 @@ namespace Barotrauma
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "rescue all".ToIdentifier();
|
||||
public override bool ForceRun => true;
|
||||
public override bool InverseTargetEvaluation => true;
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
public override bool InverseTargetPriority => true;
|
||||
protected override bool AllowOutsideSubmarine => true;
|
||||
protected override bool AllowInAnySub => true;
|
||||
|
||||
private readonly HashSet<Character> charactersWithMinorInjuries = new HashSet<Character>();
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Barotrauma
|
||||
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool Filter(Character target)
|
||||
protected override bool IsValidTarget(Character target)
|
||||
{
|
||||
if (!IsValidTarget(target, character, out bool ignoredasMinorWounds))
|
||||
{
|
||||
@@ -61,7 +61,7 @@ namespace Barotrauma
|
||||
|
||||
protected override IEnumerable<Character> GetList() => Character.CharacterList;
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
protected override float GetTargetPriority()
|
||||
{
|
||||
if (Targets.None()) { return 100; }
|
||||
if (!objectiveManager.IsOrder(this))
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ namespace Barotrauma
|
||||
private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective;
|
||||
private bool usingEscapeBehavior, isSteeringThroughGap;
|
||||
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
protected override bool AllowOutsideSubmarine => true;
|
||||
protected override bool AllowInAnySub => true;
|
||||
|
||||
public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
|
||||
@@ -178,7 +178,6 @@ namespace Barotrauma
|
||||
public PetBehavior(XElement element, EnemyAIController aiController)
|
||||
{
|
||||
AIController = aiController;
|
||||
AIController.Character.CanBeDragged = true;
|
||||
|
||||
MaxHappiness = element.GetAttributeFloat(nameof(MaxHappiness), 100.0f);
|
||||
UnhappyThreshold = element.GetAttributeFloat(nameof(UnhappyThreshold), MaxHappiness * 0.25f);
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace Barotrauma
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
if (!Active || character.IsArrested) { return; }
|
||||
if (!Active || character.IsHandcuffed) { return; }
|
||||
decisionTimer -= deltaTime;
|
||||
if (decisionTimer <= 0.0f)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
@@ -109,7 +109,7 @@ namespace Barotrauma
|
||||
|
||||
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.MapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag));
|
||||
|
||||
private static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
|
||||
public static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
|
||||
|
||||
public static WreckAI Create(Submarine wreck)
|
||||
{
|
||||
@@ -213,7 +213,7 @@ namespace Barotrauma
|
||||
foreach (Item item in thalamusItems)
|
||||
{
|
||||
// Ensure that thalamus items are visible
|
||||
item.HiddenInGame = false;
|
||||
item.IsLayerHidden = false;
|
||||
if (item.HasTag(Config.Spawner))
|
||||
{
|
||||
if (!spawnOrgans.Contains(item))
|
||||
|
||||
@@ -154,7 +154,10 @@ namespace Barotrauma
|
||||
Collider.SetTransformIgnoreContacts(mainLimb.SimPosition, mainLimb.Rotation);
|
||||
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
|
||||
//(except when dragging, then we need the pull joints)
|
||||
if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); }
|
||||
if (!Draggable || character.SelectedBy == null)
|
||||
{
|
||||
ResetPullJoints();
|
||||
}
|
||||
}
|
||||
if (character.IsDead && deathAnimTimer < deathAnimDuration)
|
||||
{
|
||||
|
||||
+4
-1
@@ -309,7 +309,10 @@ namespace Barotrauma
|
||||
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
|
||||
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
|
||||
//(except when dragging, then we need the pull joints)
|
||||
if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); }
|
||||
if (!Draggable || character.SelectedBy == null)
|
||||
{
|
||||
ResetPullJoints();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1104,8 +1104,8 @@ 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(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; }
|
||||
if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f, allowRoomToRoom: true) != null) { return; }
|
||||
if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine()), allowRoomToRoom: true) != null)) { return; }
|
||||
character.MemLocalState?.Clear();
|
||||
Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Abilities;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.IO;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using Barotrauma.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Immutable;
|
||||
using Barotrauma.Abilities;
|
||||
using System.Diagnostics;
|
||||
#if SERVER
|
||||
using System.Text;
|
||||
#endif
|
||||
@@ -354,6 +354,15 @@ namespace Barotrauma
|
||||
|
||||
public bool IsInstigator => CombatAction is { IsInstigator: true };
|
||||
|
||||
/// <summary>
|
||||
/// Do the outpost security officers treat the character as a criminal?
|
||||
/// Triggers when the character has either committed a major crime or resisted being arrested (or fled).
|
||||
/// Only affects the reactions of friendly NPCs in the outposts.
|
||||
/// The NPCs still don't react immediately to "criminals", but take this into account when the character next time does something wrong.
|
||||
/// The consequences are that the guards will not hold fire and will not give more warnings before attacking.
|
||||
/// </summary>
|
||||
public bool IsCriminal;
|
||||
|
||||
/// <summary>
|
||||
/// Set true only, if the character is turned hostile from an escort mission (See <see cref="EscortMission"/>).
|
||||
/// </summary>
|
||||
@@ -798,15 +807,27 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private float obstructVisionAmount;
|
||||
public float ObstructVisionAmount
|
||||
{
|
||||
get { return obstructVisionAmount; }
|
||||
set
|
||||
{
|
||||
obstructVisionAmount = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provided for backwards compatibility: use <see cref="ObstructVisionAmount"/> instead.
|
||||
/// </summary>
|
||||
public bool ObstructVision
|
||||
{
|
||||
get
|
||||
{
|
||||
return obstructVisionAmount > 0.5f;
|
||||
return obstructVisionAmount > 0.01f;
|
||||
}
|
||||
set
|
||||
{
|
||||
obstructVisionAmount = value ? 1.0f : 0.0f;
|
||||
obstructVisionAmount = value ? 0.5f : 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,7 +881,7 @@ namespace Barotrauma
|
||||
get { return CharacterHealth.IsUnconscious; }
|
||||
}
|
||||
|
||||
public bool IsArrested
|
||||
public bool IsHandcuffed
|
||||
{
|
||||
get { return IsHuman && HasEquippedItem(Tags.HandLockerItem); }
|
||||
}
|
||||
@@ -958,6 +979,18 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private float textChatVolume;
|
||||
|
||||
/// <summary>
|
||||
/// How "loud" the player is when they use text chat.
|
||||
/// When the user speaks in text chat this gets set to 1 and then slowly decreases back to 0 over 5 seconds.
|
||||
/// </summary>
|
||||
public float TextChatVolume
|
||||
{
|
||||
get => textChatVolume;
|
||||
set => textChatVolume = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
public float PressureTimer
|
||||
{
|
||||
get;
|
||||
@@ -1108,37 +1141,8 @@ namespace Barotrauma
|
||||
return !Removed;
|
||||
}
|
||||
}
|
||||
|
||||
private bool canBeDragged = true;
|
||||
public bool CanBeDragged
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!canBeDragged) { return false; }
|
||||
if (Removed || !AnimController.Draggable) { return false; }
|
||||
return IsKnockedDown || LockHands || IsPet || CanInventoryBeAccessed;
|
||||
}
|
||||
set { canBeDragged = value; }
|
||||
}
|
||||
|
||||
//can other characters access the inventory of this character
|
||||
private bool canInventoryBeAccessed = true;
|
||||
public bool CanInventoryBeAccessed
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!canInventoryBeAccessed || Removed || Inventory == null) { return false; }
|
||||
if (!Inventory.AccessibleWhenAlive)
|
||||
{
|
||||
return IsDead;
|
||||
}
|
||||
else
|
||||
{
|
||||
return IsKnockedDown || LockHands || IsBot && IsOnPlayerTeam;
|
||||
}
|
||||
}
|
||||
set { canInventoryBeAccessed = value; }
|
||||
}
|
||||
|
||||
public bool IsDraggable => !Removed || AnimController.Draggable;
|
||||
|
||||
public bool CanAim
|
||||
{
|
||||
@@ -1328,6 +1332,10 @@ namespace Barotrauma
|
||||
: base(null, id)
|
||||
{
|
||||
wallet = new Wallet(Option<Character>.Some(this));
|
||||
if (GameMain.GameSession?.Campaign?.Bank is { } bank)
|
||||
{
|
||||
wallet.SetRewardDistribution(bank.RewardDistribution);
|
||||
}
|
||||
|
||||
this.Seed = seed;
|
||||
this.Prefab = prefab;
|
||||
@@ -1363,6 +1371,8 @@ namespace Barotrauma
|
||||
if (Info != null)
|
||||
{
|
||||
teamID = Info.TeamID;
|
||||
//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;
|
||||
}
|
||||
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
|
||||
for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++)
|
||||
@@ -1880,7 +1890,7 @@ namespace Barotrauma
|
||||
|
||||
public bool CanRunWhileDragging()
|
||||
{
|
||||
if (selectedCharacter == null || !selectedCharacter.CanBeDragged) { return true; }
|
||||
if (selectedCharacter is not { IsDraggable: true }) { return true; }
|
||||
//if the dragged character is conscious, don't allow running (the dragged character won't keep up, and the dragging gets interrupted)
|
||||
if (!selectedCharacter.IsIncapacitated && selectedCharacter.Stun <= 0.0f) { return false; }
|
||||
return HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging);
|
||||
@@ -2581,18 +2591,15 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool CanAccessInventory(Inventory inventory)
|
||||
public bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited)
|
||||
{
|
||||
if (!CanInteract || inventory.Locked) { return false; }
|
||||
|
||||
//the inventory belongs to some other character
|
||||
if (inventory.Owner is Character character && inventory.Owner != this)
|
||||
|
||||
if (inventory.Owner is Character inventoryOwner)
|
||||
{
|
||||
var owner = character;
|
||||
//can only be accessed if the character is incapacitated and has been selected
|
||||
return SelectedCharacter == owner && owner.CanInventoryBeAccessed;
|
||||
return inventoryOwner.IsInventoryAccessibleTo(this, accessLevel) && (inventoryOwner == this || CanInteractWith(inventoryOwner));
|
||||
}
|
||||
|
||||
|
||||
if (inventory.Owner is Item item)
|
||||
{
|
||||
if (!CanInteractWith(item))
|
||||
@@ -2613,7 +2620,47 @@ namespace Barotrauma
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public bool CanBeHealedBy(Character character, bool checkFriendlyTeam = true) =>
|
||||
!character.IsClimbing && !DisableHealthWindow &&
|
||||
UseHealthWindow && character.CanInteract &&
|
||||
(!checkFriendlyTeam || IsFriendly(character) || CanBeDraggedBy(character)) &&
|
||||
character.CanInteractWith(this, 160f, false);
|
||||
|
||||
public bool CanBeDraggedBy(Character character)
|
||||
{
|
||||
if (!IsDraggable) { return false; }
|
||||
return IsKnockedDown || LockHands || IsPet || (IsBot && character.TeamID == TeamID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the inventory accessible to the character? Doesn't check if the character can actually interact with it (distance checks etc).
|
||||
/// </summary>
|
||||
public bool IsInventoryAccessibleTo(Character character, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited)
|
||||
{
|
||||
if (Removed || Inventory == null) { return false; }
|
||||
if (!Inventory.AccessibleWhenAlive && !IsDead)
|
||||
{
|
||||
if (character == this)
|
||||
{
|
||||
return Inventory.AccessibleByOwner;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (character == this) { return true; }
|
||||
if (IsKnockedDown || LockHands) { return true; }
|
||||
return accessLevel switch
|
||||
{
|
||||
CharacterInventory.AccessLevel.Restricted => false,
|
||||
CharacterInventory.AccessLevel.Limited => (IsBot && IsOnSameTeam()) || IsFriendlyPet(),
|
||||
CharacterInventory.AccessLevel.Allowed => IsOnSameTeam() || IsFriendlyPet(),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
|
||||
bool IsOnSameTeam() => character.TeamID == teamID;
|
||||
bool IsFriendlyPet() => IsPet && character.IsFriendly(this);
|
||||
}
|
||||
|
||||
private Stopwatch sw;
|
||||
private Stopwatch StopWatch => sw ??= new Stopwatch();
|
||||
private float _selectedItemPriority;
|
||||
@@ -2701,7 +2748,7 @@ namespace Barotrauma
|
||||
public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true, bool skipDistanceCheck = false)
|
||||
{
|
||||
if (c == this || Removed || !c.Enabled || !c.CanBeSelected || c.InvisibleTimer > 0.0f) { return false; }
|
||||
if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; }
|
||||
if (!c.CharacterHealth.UseHealthWindow && !c.IsDraggable && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; }
|
||||
|
||||
if (!skipDistanceCheck)
|
||||
{
|
||||
@@ -2725,7 +2772,7 @@ namespace Barotrauma
|
||||
{
|
||||
distanceToItem = -1.0f;
|
||||
|
||||
bool hidden = item.HiddenInGame;
|
||||
bool hidden = item.IsHidden;
|
||||
#if CLIENT
|
||||
if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; }
|
||||
#endif
|
||||
@@ -2834,23 +2881,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (distanceToItem > interactDistance && item.InteractDistance > 0.0f) { return false; }
|
||||
|
||||
Vector2 itemPosition = item.SimPosition;
|
||||
if (Submarine == null && item.Submarine != null)
|
||||
{
|
||||
//character is outside, item inside
|
||||
itemPosition += item.Submarine.SimPosition;
|
||||
}
|
||||
else if (Submarine != null && item.Submarine == null)
|
||||
{
|
||||
//character is inside, item outside
|
||||
itemPosition -= Submarine.SimPosition;
|
||||
}
|
||||
else if (Submarine != item.Submarine)
|
||||
{
|
||||
//character and the item are inside different subs
|
||||
itemPosition += item.Submarine.SimPosition;
|
||||
itemPosition -= Submarine.SimPosition;
|
||||
}
|
||||
Vector2 itemPosition = GetPosition(Submarine, item, item.SimPosition);
|
||||
|
||||
if (SelectedSecondaryItem != null && !item.IsSecondaryItem)
|
||||
{
|
||||
@@ -2871,21 +2902,79 @@ namespace Barotrauma
|
||||
if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger)
|
||||
{
|
||||
var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true);
|
||||
if (body != null)
|
||||
bool itemCenterVisible = CheckBody(body, item);
|
||||
|
||||
if (!itemCenterVisible && item.Prefab.RequireCursorInsideTrigger)
|
||||
{
|
||||
var otherItem = body.UserData as Item ?? (body.UserData as ItemComponent)?.Item;
|
||||
if (otherItem != item &&
|
||||
(body.UserData as ItemComponent)?.Item != item &&
|
||||
/*allow interacting through open doors (e.g. duct blocks' colliders stay active despite being open)*/
|
||||
otherItem?.GetComponent<Door>() is not { IsOpen: true } &&
|
||||
Submarine.LastPickedFixture?.UserData as Item != item)
|
||||
{
|
||||
return false;
|
||||
foreach (Rectangle trigger in item.Prefab.Triggers)
|
||||
{
|
||||
Rectangle transformTrigger = item.TransformTrigger(trigger, world: false);
|
||||
|
||||
RectangleF simRect = new RectangleF(
|
||||
x: ConvertUnits.ToSimUnits(transformTrigger.X),
|
||||
y: ConvertUnits.ToSimUnits(transformTrigger.Y - transformTrigger.Height),
|
||||
width: ConvertUnits.ToSimUnits(transformTrigger.Width),
|
||||
height: ConvertUnits.ToSimUnits(transformTrigger.Height));
|
||||
|
||||
simRect.Location = GetPosition(Submarine, item, simRect.Location);
|
||||
|
||||
Vector2 closest = ToolBox.GetClosestPointOnRectangle(simRect, SimPosition);
|
||||
var triggerBody = Submarine.CheckVisibility(SimPosition, closest, ignoreLevel: true);
|
||||
|
||||
if (CheckBody(triggerBody, item)) { return true; }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return itemCenterVisible;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
static bool CheckBody(Body body, Item item)
|
||||
{
|
||||
if (body is null) { return true; }
|
||||
var otherItem = body.UserData as Item ?? (body.UserData as ItemComponent)?.Item;
|
||||
if (otherItem != item &&
|
||||
(body.UserData as ItemComponent)?.Item != item &&
|
||||
/*allow interacting through open doors (e.g. duct blocks' colliders stay active despite being open)*/
|
||||
otherItem?.GetComponent<Door>() is not { IsOpen: true } &&
|
||||
Submarine.LastPickedFixture?.UserData as Item != item)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static Vector2 GetPosition(Submarine submarine, Item item, Vector2 simPosition)
|
||||
{
|
||||
Vector2 position = simPosition;
|
||||
|
||||
Vector2 itemSubPos = item.Submarine?.SimPosition ?? Vector2.Zero;
|
||||
Vector2 subPos = submarine?.SimPosition ?? Vector2.Zero;
|
||||
|
||||
if (submarine == null && item.Submarine != null)
|
||||
{
|
||||
//character is outside, item inside
|
||||
position += itemSubPos;
|
||||
}
|
||||
else if (submarine != null && item.Submarine == null)
|
||||
{
|
||||
//character is inside, item outside
|
||||
position -= subPos;
|
||||
}
|
||||
else if (submarine != item.Submarine && submarine != null)
|
||||
{
|
||||
//character and the item are inside different subs
|
||||
position += itemSubPos;
|
||||
position -= subPos;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3058,15 +3147,15 @@ namespace Barotrauma
|
||||
{
|
||||
DeselectCharacter();
|
||||
}
|
||||
else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged && (CanInteract || FocusedCharacter.IsDead && CanEat))
|
||||
else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDraggedBy(this) && (CanInteract || FocusedCharacter.IsDead && CanEat))
|
||||
{
|
||||
SelectCharacter(FocusedCharacter);
|
||||
}
|
||||
else if (FocusedCharacter != null && !FocusedCharacter.IsIncapacitated && IsKeyHit(InputType.Use) && FocusedCharacter.IsPet && CanInteract)
|
||||
else if (FocusedCharacter is { IsIncapacitated: false } && IsKeyHit(InputType.Use) && FocusedCharacter.IsPet && CanInteract)
|
||||
{
|
||||
(FocusedCharacter.AIController as EnemyAIController).PetBehavior.Play(this);
|
||||
}
|
||||
else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CharacterHealth.UseHealthWindow && CanInteract && CanInteractWith(FocusedCharacter, 160f, false))
|
||||
else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CanBeHealedBy(this))
|
||||
{
|
||||
if (FocusedCharacter == SelectedCharacter)
|
||||
{
|
||||
@@ -3078,12 +3167,14 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else if (!IsClimbing)
|
||||
else
|
||||
{
|
||||
SelectCharacter(FocusedCharacter);
|
||||
#if CLIENT
|
||||
if (Controlled == this)
|
||||
{
|
||||
HealingCooldown.PutOnCooldown();
|
||||
CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth;
|
||||
}
|
||||
#elif SERVER
|
||||
if (GameMain.Server?.ConnectedClients is { } clients)
|
||||
@@ -3096,13 +3187,6 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
SelectCharacter(FocusedCharacter);
|
||||
#if CLIENT
|
||||
if (Controlled == this)
|
||||
{
|
||||
CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -3251,6 +3335,11 @@ namespace Barotrauma
|
||||
{
|
||||
UpdateProjSpecific(deltaTime, cam);
|
||||
|
||||
if (TextChatVolume > 0)
|
||||
{
|
||||
TextChatVolume -= 0.2f * deltaTime;
|
||||
}
|
||||
|
||||
if (InvisibleTimer > 0.0f)
|
||||
{
|
||||
if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
|
||||
@@ -3480,16 +3569,33 @@ namespace Barotrauma
|
||||
DoInteractionUpdate(deltaTime, mouseSimPos);
|
||||
}
|
||||
|
||||
if (SelectedItem != null && !CanInteractWith(SelectedItem))
|
||||
if (MustDeselect(SelectedItem))
|
||||
{
|
||||
SelectedItem = null;
|
||||
}
|
||||
if (SelectedSecondaryItem != null && !CanInteractWith(SelectedSecondaryItem))
|
||||
if (MustDeselect(SelectedSecondaryItem))
|
||||
{
|
||||
ReleaseSecondaryItem();
|
||||
}
|
||||
|
||||
if (!IsDead) { LockHands = false; }
|
||||
|
||||
bool MustDeselect(Item item)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
if (!CanInteractWith(item)) { return true; }
|
||||
bool hasSelectableComponent = false;
|
||||
foreach (var component in item.Components)
|
||||
{
|
||||
//the "selectability" of an item can change e.g. if the player unequips another item that's required to access it
|
||||
if (component.CanBeSelected && component.HasRequiredItems(this, addMessage: false))
|
||||
{
|
||||
hasSelectableComponent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return !hasSelectableComponent;
|
||||
}
|
||||
}
|
||||
|
||||
partial void UpdateControlled(float deltaTime, Camera cam);
|
||||
@@ -3805,6 +3911,9 @@ namespace Barotrauma
|
||||
|
||||
private void UpdateSoundRange(float deltaTime)
|
||||
{
|
||||
const float textChatVolumeMultiplier = 0.5f;
|
||||
const float voiceChatVolumeMultiplier = 1.5f;
|
||||
|
||||
if (aiTarget == null) { return; }
|
||||
if (IsDead)
|
||||
{
|
||||
@@ -3814,8 +3923,37 @@ namespace Barotrauma
|
||||
{
|
||||
float massFactor = (float)Math.Sqrt(Mass / 10);
|
||||
float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange);
|
||||
float speechImpedimentMultiplier = 1.0f - SpeechImpediment / 100.0f;
|
||||
if (TextChatVolume > 0)
|
||||
{
|
||||
targetRange = Math.Max(targetRange, TextChatVolume * textChatVolumeMultiplier * ChatMessage.SpeakRange * speechImpedimentMultiplier);
|
||||
}
|
||||
|
||||
if (IsPlayer)
|
||||
{
|
||||
float voipAmplitude = 0.0f;
|
||||
#if SERVER
|
||||
foreach (var c in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
if (c.Character != this) { continue; }
|
||||
voipAmplitude = c.VoipServerDecoder.Amplitude;
|
||||
break;
|
||||
}
|
||||
#elif CLIENT && DEBUG
|
||||
if (Controlled == this && GameMain.Client != null)
|
||||
{
|
||||
voipAmplitude = GameMain.Client.DebugServerVoipAmplitude;
|
||||
}
|
||||
#endif
|
||||
targetRange = Math.Max(targetRange, voipAmplitude * voiceChatVolumeMultiplier * ChatMessage.SpeakRange * speechImpedimentMultiplier);
|
||||
}
|
||||
|
||||
targetRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
|
||||
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;
|
||||
@@ -4712,7 +4850,31 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
|
||||
isDead = true;
|
||||
ApplyStatusEffects(ActionType.OnDeath, 1.0f);
|
||||
|
||||
AnimController.Frozen = false;
|
||||
|
||||
Character killer = causeOfDeathAffliction?.Source;
|
||||
if (IsBot)
|
||||
{
|
||||
foreach (var item in Inventory.AllItems)
|
||||
{
|
||||
if (item.Equipper is { IsPlayer: true } &&
|
||||
item.GetComponents<ItemContainer>().Any(ic => ic.BlameEquipperForDeath()))
|
||||
{
|
||||
killer = item.Equipper;
|
||||
if (AIController is HumanAIController humanAi)
|
||||
{
|
||||
humanAi.OnAttacked(killer, new AttackResult(damage: MaxVitality));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CauseOfDeath = new CauseOfDeath(
|
||||
causeOfDeath, causeOfDeathAffliction?.Prefab,
|
||||
killer, LastDamageSource);
|
||||
|
||||
// Save these resistances in the CharacterInfo object so that if they
|
||||
// are needed for respawning, they will be available (because there
|
||||
@@ -4723,13 +4885,25 @@ namespace Barotrauma
|
||||
info.LastResistanceMultiplierSkillLossRespawn = GetAbilityResistance(Tags.SkillLossRespawnResistance);
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnDeath, 1.0f);
|
||||
isDead = true;
|
||||
|
||||
AnimController.Frozen = false;
|
||||
#if CLIENT
|
||||
// Keep permadeath status in sync (to show it correctly in the UI, the server takes care of the actual logic)
|
||||
// NOTE: The opposite is done in Revive
|
||||
if (GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath } &&
|
||||
GameMain.Client.Character == this &&
|
||||
GameMain.Client.CharacterInfo is CharacterInfo characterInfo)
|
||||
{
|
||||
characterInfo.PermanentlyDead = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
CauseOfDeath = new CauseOfDeath(
|
||||
causeOfDeath, causeOfDeathAffliction?.Prefab,
|
||||
causeOfDeathAffliction?.Source, LastDamageSource);
|
||||
#if SERVER
|
||||
if (Info is not null)
|
||||
{
|
||||
Info.LastRewardDistribution = Option.Some(Wallet.RewardDistribution);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (GameAnalyticsManager.SendUserStatistics && Prefab?.ContentPackage == ContentPackageManager.VanillaCorePackage)
|
||||
{
|
||||
@@ -4843,6 +5017,15 @@ namespace Barotrauma
|
||||
if (info != null)
|
||||
{
|
||||
info.CauseOfDeath = null;
|
||||
|
||||
// Keep permadeath status in sync (to show it correctly in the UI, the server takes care of the actual logic)
|
||||
// NOTE: The opposite is done in Kill
|
||||
// FYI: In case you're wondering, it's alright to revive a "permanently" dead character here, because if
|
||||
// this gets called, the character wasn't actually dead anyway (eg. returning to lobby without saving)
|
||||
if (GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath })
|
||||
{
|
||||
info.PermanentlyDead = false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (LimbJoint joint in AnimController.LimbJoints)
|
||||
@@ -5470,7 +5653,14 @@ namespace Barotrauma
|
||||
{
|
||||
statValue += wearableValue;
|
||||
}
|
||||
|
||||
foreach (var heldItem in HeldItems)
|
||||
{
|
||||
if (heldItem.GetComponent<Holdable>() is Holdable holdable &&
|
||||
holdable.HoldableStatValues.TryGetValue(statType, out float holdableValue))
|
||||
{
|
||||
statValue += holdableValue;
|
||||
}
|
||||
}
|
||||
return statValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
@@ -44,8 +44,13 @@ namespace Barotrauma
|
||||
public readonly Identifier MenuCategoryVar;
|
||||
public readonly Identifier Pronouns;
|
||||
|
||||
public CharacterInfoPrefab(ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement)
|
||||
public CharacterInfoPrefab(CharacterPrefab characterPrefab, ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement)
|
||||
{
|
||||
if (headsElement == null)
|
||||
{
|
||||
throw new Exception($"No heads configured for the character \"{characterPrefab.Identifier}\". Characters with CharacterInfo must have head sprites. Please add a <Heads> element to the character's config.");
|
||||
}
|
||||
|
||||
Heads = headsElement.Elements().Select(e => new CharacterInfo.HeadPreset(this, e)).ToImmutableArray();
|
||||
if (varsElement != null)
|
||||
{
|
||||
@@ -82,6 +87,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores information about the Character that is needed between rounds in the
|
||||
/// menu etc., whereas Character itself is the object actually spawned in-game.
|
||||
/// </summary>
|
||||
partial class CharacterInfo
|
||||
{
|
||||
public class HeadInfo
|
||||
@@ -289,7 +298,9 @@ namespace Barotrauma
|
||||
public XElement HealthData;
|
||||
public XElement OrderData;
|
||||
|
||||
private static ushort idCounter;
|
||||
public bool PermanentlyDead;
|
||||
|
||||
private static ushort idCounter = 1;
|
||||
private const string disguiseName = "???";
|
||||
|
||||
public bool HasNickname => Name != OriginalName;
|
||||
@@ -492,6 +503,9 @@ namespace Barotrauma
|
||||
|
||||
public bool StartItemsGiven;
|
||||
|
||||
/// <summary>
|
||||
/// Newly hired bot that hasn't spawned yet
|
||||
/// </summary>
|
||||
public bool IsNewHire;
|
||||
|
||||
public CauseOfDeath CauseOfDeath;
|
||||
@@ -642,6 +656,15 @@ namespace Barotrauma
|
||||
=> element.GetAttributeBool("specifiertags",
|
||||
element.GetAttributeBool("genders",
|
||||
element.GetAttributeBool("races", false)));
|
||||
|
||||
/// <summary>
|
||||
/// Keeps track of the last reward distribution that was set on the character's wallet.
|
||||
/// Is used to keep salary when the character respawns since CharacterInfo is preserved between deaths.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// None means the salary has not been set yet, which is not always 0 if default salary is set.
|
||||
/// </remarks>
|
||||
public Option<int> LastRewardDistribution = Option.None;
|
||||
|
||||
// Used for creating the data
|
||||
public CharacterInfo(
|
||||
@@ -662,6 +685,7 @@ namespace Barotrauma
|
||||
}
|
||||
ID = idCounter;
|
||||
idCounter++;
|
||||
if (idCounter == 0) { idCounter++; }
|
||||
SpeciesName = speciesName;
|
||||
SpriteTags = new List<Identifier>();
|
||||
CharacterConfigElement = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ConfigElement;
|
||||
@@ -699,6 +723,12 @@ namespace Barotrauma
|
||||
Salary = CalculateSalary();
|
||||
}
|
||||
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
|
||||
|
||||
int loadedLastRewardDistribution = CharacterConfigElement.GetAttributeInt("lastrewarddistribution", -1);
|
||||
if (loadedLastRewardDistribution >= 0)
|
||||
{
|
||||
LastRewardDistribution = Option.Some(loadedLastRewardDistribution);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPersonalityTrait()
|
||||
@@ -771,6 +801,7 @@ namespace Barotrauma
|
||||
HashSet<Identifier> tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
|
||||
LoadTagsBackwardsCompatibility(infoElement, tags);
|
||||
SpeciesName = infoElement.GetAttributeIdentifier("speciesname", "");
|
||||
PermanentlyDead = infoElement.GetAttributeBool("permanentlydead", false);
|
||||
ContentXElement element;
|
||||
if (!SpeciesName.IsEmpty)
|
||||
{
|
||||
@@ -951,7 +982,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a presumably (not guaranteed) unique hash using the (current) Name, appearence, and job.
|
||||
/// Returns a presumably (not guaranteed) unique and persistent hash using the (current) Name, appearence, and job.
|
||||
/// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique.
|
||||
/// </summary>
|
||||
public int GetIdentifier()
|
||||
@@ -960,7 +991,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a presumably (not guaranteed) unique hash using the OriginalName, appearence, and job.
|
||||
/// Returns a presumably (not guaranteed) unique hash and persistent using the OriginalName, appearence, and job.
|
||||
/// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique.
|
||||
/// </summary>
|
||||
public int GetIdentifierUsingOriginalName()
|
||||
@@ -1466,7 +1497,10 @@ namespace Barotrauma
|
||||
new XAttribute("haircolor", XMLExtensions.ColorToString(Head.HairColor)),
|
||||
new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)),
|
||||
new XAttribute("startitemsgiven", StartItemsGiven),
|
||||
new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty));
|
||||
new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty),
|
||||
new XAttribute("lastrewarddistribution", LastRewardDistribution.Match(some: value => value, none: () => -1).ToString()),
|
||||
new XAttribute("permanentlydead", PermanentlyDead)
|
||||
);
|
||||
|
||||
if (HumanPrefabIds != default)
|
||||
{
|
||||
|
||||
@@ -32,6 +32,8 @@ namespace Barotrauma
|
||||
return speciesName;
|
||||
}
|
||||
|
||||
public bool HasCharacterInfo { get; private set; }
|
||||
|
||||
public void InheritFrom(CharacterPrefab parent)
|
||||
{
|
||||
ConfigElement = CharacterParams.CreateVariantXml(originalElement, parent.ConfigElement).FromPackage(ConfigElement.ContentPackage);
|
||||
@@ -45,9 +47,10 @@ namespace Barotrauma
|
||||
var menuCategoryElement = ConfigElement.GetChildElement("MenuCategory");
|
||||
var pronounsElement = ConfigElement.GetChildElement("Pronouns");
|
||||
|
||||
if (headsElement != null)
|
||||
HasCharacterInfo = headsElement != null || ConfigElement.GetAttributeBool(nameof(HasCharacterInfo), false);
|
||||
if (HasCharacterInfo)
|
||||
{
|
||||
CharacterInfoPrefab = new CharacterInfoPrefab(headsElement, varsElement, menuCategoryElement, pronounsElement);
|
||||
CharacterInfoPrefab = new CharacterInfoPrefab(this, headsElement, varsElement, menuCategoryElement, pronounsElement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,12 @@ namespace Barotrauma
|
||||
[Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)]
|
||||
public AIObjectiveIdle.BehaviorType Behavior { get; protected set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.No, description:
|
||||
"Affects how far the character can hear sounds created by AI targets with the tag ProvocativeToHumanAI. "+
|
||||
"Used as a multiplier on the sound range of the target, e.g. a value of 0.5 would mean a target with a sound range of 1000 would need to be within 500 units for this character to hear it. "+
|
||||
"Only affects the \"fight intruders\" objective, which makes the character go and inspect noises.")]
|
||||
public float Hearing { get; set; } = 1.0f;
|
||||
|
||||
[Serialize(float.PositiveInfinity, IsPropertySaveable.No)]
|
||||
public float ReportRange { get; protected set; }
|
||||
|
||||
@@ -174,6 +180,7 @@ namespace Barotrauma
|
||||
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
|
||||
}
|
||||
}
|
||||
humanAI.ReportRange = Hearing;
|
||||
humanAI.ReportRange = ReportRange;
|
||||
humanAI.FindWeaponsRange = FindWeaponsRange;
|
||||
humanAI.AimSpeed = AimSpeed;
|
||||
|
||||
@@ -10,15 +10,26 @@ namespace Barotrauma
|
||||
|
||||
private float level;
|
||||
|
||||
/// <summary>
|
||||
/// The highest skill level during the round (before any death penalties were applied)
|
||||
/// </summary>
|
||||
public float HighestLevelDuringRound { get; private set; }
|
||||
|
||||
public float Level
|
||||
{
|
||||
get { return level; }
|
||||
set { level = value; }
|
||||
set
|
||||
{
|
||||
HighestLevelDuringRound = MathHelper.Max(value, HighestLevelDuringRound);
|
||||
level = value;
|
||||
}
|
||||
}
|
||||
|
||||
public LocalizedString DisplayName { get; private set; }
|
||||
|
||||
public void IncreaseSkill(float value, bool increasePastMax)
|
||||
{
|
||||
level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill);
|
||||
Level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill);
|
||||
}
|
||||
|
||||
private readonly Identifier iconJobId;
|
||||
@@ -32,16 +43,18 @@ namespace Barotrauma
|
||||
public Skill(SkillPrefab prefab, Rand.RandSync randSync)
|
||||
{
|
||||
Identifier = prefab.Identifier;
|
||||
level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync);
|
||||
Level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync);
|
||||
iconJobId = GetIconJobId();
|
||||
PriceMultiplier = prefab.PriceMultiplier;
|
||||
DisplayName = TextManager.Get("SkillName." + Identifier);
|
||||
}
|
||||
|
||||
public Skill(Identifier identifier, float level)
|
||||
{
|
||||
Identifier = identifier;
|
||||
this.level = level;
|
||||
Level = level;
|
||||
iconJobId = GetIconJobId();
|
||||
DisplayName = TextManager.Get("SkillName." + Identifier);
|
||||
}
|
||||
|
||||
private Identifier GetIconJobId()
|
||||
|
||||
+11
-3
@@ -22,6 +22,7 @@ namespace Barotrauma
|
||||
|
||||
abstract class GroundedMovementParams : AnimationParams
|
||||
{
|
||||
[Header("Legs")]
|
||||
[Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)]
|
||||
public Vector2 StepSize
|
||||
{
|
||||
@@ -29,12 +30,14 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Header("Standing")]
|
||||
[Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
|
||||
public float HeadPosition { get; set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
|
||||
public float TorsoPosition { get; set; }
|
||||
|
||||
[Header("Step lift")]
|
||||
[Serialize(1f, IsPropertySaveable.Yes, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)]
|
||||
public float StepLiftHeadMultiplier { get; set; }
|
||||
|
||||
@@ -50,6 +53,7 @@ namespace Barotrauma
|
||||
[Serialize(2f, IsPropertySaveable.Yes, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)]
|
||||
public float StepLiftFrequency { get; set; }
|
||||
|
||||
[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; }
|
||||
}
|
||||
@@ -69,11 +73,15 @@ namespace Barotrauma
|
||||
public bool IsGroundedAnimation => AnimationType is AnimationType.Walk or AnimationType.Run or AnimationType.Crouch;
|
||||
public bool IsSwimAnimation => AnimationType is AnimationType.SwimSlow or AnimationType.SwimFast;
|
||||
|
||||
[Header("General")]
|
||||
[Serialize(AnimationType.NotDefined, IsPropertySaveable.Yes), Editable]
|
||||
public virtual AnimationType AnimationType { get; protected set; }
|
||||
/// <summary>
|
||||
/// The cached animations of all the characters that have been loaded.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
|
||||
|
||||
[Header("Movement")]
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
|
||||
public float MovementSpeed { get; set; }
|
||||
|
||||
@@ -84,6 +92,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// In degrees.
|
||||
/// </summary>
|
||||
[Header("Standing")]
|
||||
[Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)]
|
||||
public float HeadAngle
|
||||
{
|
||||
@@ -122,12 +131,11 @@ namespace Barotrauma
|
||||
[Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
||||
public float TorsoTorque { get; set; }
|
||||
|
||||
[Header("Legs")]
|
||||
[Serialize(25.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
||||
public float FootTorque { get; set; }
|
||||
|
||||
[Serialize(AnimationType.NotDefined, IsPropertySaveable.Yes), Editable]
|
||||
public virtual AnimationType AnimationType { get; protected set; }
|
||||
|
||||
[Header("Arms")]
|
||||
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
|
||||
public float ArmIKStrength { get; set; }
|
||||
|
||||
|
||||
+17
-9
@@ -69,21 +69,13 @@ namespace Barotrauma
|
||||
|
||||
abstract class HumanSwimParams : SwimParams, IHumanAnimation
|
||||
{
|
||||
[Header("Legs")]
|
||||
[Serialize(0.5f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
|
||||
public float LegMoveAmount { get; set; }
|
||||
|
||||
[Serialize(5.0f, IsPropertySaveable.Yes), Editable]
|
||||
public float LegCycleLength { get; set; }
|
||||
|
||||
[Serialize("0.5, 0.1", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
|
||||
public Vector2 HandMoveAmount { get; set; }
|
||||
|
||||
[Serialize(5.0f, IsPropertySaveable.Yes), Editable]
|
||||
public float HandCycleSpeed { get; set; }
|
||||
|
||||
[Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
|
||||
public Vector2 HandMoveOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// In degrees.
|
||||
/// </summary>
|
||||
@@ -96,20 +88,33 @@ namespace Barotrauma
|
||||
FootAngleInRadians = MathHelper.ToRadians(value);
|
||||
}
|
||||
}
|
||||
|
||||
public float FootAngleInRadians { get; private set; }
|
||||
|
||||
[Header("Arms")]
|
||||
[Serialize("0.5, 0.1", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
|
||||
public Vector2 HandMoveAmount { get; set; }
|
||||
|
||||
[Serialize(5.0f, IsPropertySaveable.Yes), Editable]
|
||||
public float HandCycleSpeed { get; set; }
|
||||
|
||||
[Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
|
||||
public Vector2 HandMoveOffset { get; set; }
|
||||
|
||||
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 20, DecimalCount = 2)]
|
||||
public float ArmMoveStrength { get; set; }
|
||||
|
||||
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
|
||||
public float HandMoveStrength { get; set; }
|
||||
|
||||
[Header("Other")]
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable]
|
||||
public bool FixedHeadAngle { get; set; }
|
||||
}
|
||||
|
||||
abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation
|
||||
{
|
||||
[Header("Standing")]
|
||||
[Serialize(0.3f, IsPropertySaveable.Yes, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
|
||||
public float GetUpForce { get; set; }
|
||||
|
||||
@@ -119,6 +124,7 @@ namespace Barotrauma
|
||||
[Serialize(0.25f, IsPropertySaveable.Yes, description: "How much the character's torso leans forwards when moving."), Editable(DecimalCount = 2)]
|
||||
public float TorsoLeanAmount { get; set; }
|
||||
|
||||
[Header("Legs")]
|
||||
[Serialize(15.0f, IsPropertySaveable.Yes, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
|
||||
public float FootMoveStrength { get; set; }
|
||||
|
||||
@@ -152,6 +158,7 @@ namespace Barotrauma
|
||||
[Serialize(10.0f, IsPropertySaveable.Yes, description: "How much torque is used to bend the characters legs when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
|
||||
public float LegBendTorque { get; set; }
|
||||
|
||||
[Header("Arms")]
|
||||
[Serialize("0.4, 0.15", IsPropertySaveable.Yes, description: "How much the hands move along each axis."), Editable(DecimalCount = 2)]
|
||||
public Vector2 HandMoveAmount { get; set; }
|
||||
|
||||
@@ -167,6 +174,7 @@ namespace Barotrauma
|
||||
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
|
||||
public float HandMoveStrength { get; set; }
|
||||
|
||||
[Header("Other")]
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable]
|
||||
public bool FixedHeadAngle { get; set; }
|
||||
}
|
||||
|
||||
+24
-1
@@ -87,9 +87,20 @@ namespace Barotrauma
|
||||
set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); }
|
||||
}
|
||||
|
||||
// Don't show in the editor, because shouldn't be edited in runtime. Requires that the limb scale and the collider sizes are adjusted. TODO: automatize?
|
||||
/// <summary>
|
||||
/// Can be used for scaling the textures without having to readjust the entire ragdoll.
|
||||
/// Note that we'll still have to readjust the source rects and the colliders sizes, unless we also adjust <see cref="SourceRectScale"/>.
|
||||
/// E.g. for upscaling the textures 2x, set <see cref="TextureScale"/> to 0.5 and <see cref="SourceRectScale"/> to 2.
|
||||
/// </summary>
|
||||
[Serialize(1f, IsPropertySaveable.No)]
|
||||
public float TextureScale { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies both the position and the size of the source rects.
|
||||
/// Used for scaling the textures when we cannot/don't want to touch the source rect definitions (e.g. on variants).
|
||||
/// </summary>
|
||||
[Serialize(1f, IsPropertySaveable.No)]
|
||||
public float SourceRectScale { get; set; }
|
||||
|
||||
[Serialize(45f, IsPropertySaveable.Yes, description: "How high from the ground the main collider levitates when the character is standing? Doesn't affect swimming."), Editable(0f, 1000f)]
|
||||
public float ColliderHeightFromFloor { get; set; }
|
||||
@@ -491,6 +502,18 @@ namespace Barotrauma
|
||||
float scaleMultiplier = ragdollElement.GetAttributeFloat("scalemultiplier", 1f);
|
||||
JointScale *= scaleMultiplier;
|
||||
LimbScale *= scaleMultiplier;
|
||||
float textureScale = ragdollElement.GetAttributeFloat(nameof(TextureScale), 0f);
|
||||
if (textureScale > 0)
|
||||
{
|
||||
// Override, if defined.
|
||||
TextureScale = textureScale;
|
||||
}
|
||||
float sourceRectScale = ragdollElement.GetAttributeFloat(nameof(SourceRectScale), 0f);
|
||||
if (sourceRectScale > 0)
|
||||
{
|
||||
// Override, if defined.
|
||||
SourceRectScale = sourceRectScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
isVariantScaleApplied = true;
|
||||
|
||||
@@ -71,9 +71,9 @@ namespace Barotrauma
|
||||
|
||||
private Vector2 position;
|
||||
|
||||
public List<CircuitBoxConnection> ExternallyConnectedFrom = new();
|
||||
public readonly List<CircuitBoxConnection> ExternallyConnectedFrom = new();
|
||||
|
||||
public static float Size = CircuitBoxSizes.ConnectorSize;
|
||||
public static readonly float Size = CircuitBoxSizes.ConnectorSize;
|
||||
|
||||
public Vector2 Position
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
@@ -8,7 +8,7 @@ using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal sealed class CircuitBoxInputOutputNode : CircuitBoxNode
|
||||
internal sealed partial class CircuitBoxInputOutputNode : CircuitBoxNode
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
@@ -17,22 +17,104 @@ namespace Barotrauma
|
||||
Output
|
||||
}
|
||||
|
||||
public Type NodeType;
|
||||
public readonly Type NodeType;
|
||||
|
||||
private const int MaxConnectionLabelLength = 32;
|
||||
private const string ConnectionLabelOverrideElementName = "ConnectionLabelOverride";
|
||||
|
||||
public Dictionary<string, string> ConnectionLabelOverrides = new();
|
||||
|
||||
public CircuitBoxInputOutputNode(IReadOnlyList<CircuitBoxConnection> conns, Vector2 initialPosition, Type type, CircuitBox circuitBox): base(circuitBox)
|
||||
{
|
||||
Size = CalculateSize(conns);
|
||||
InitSize(conns);
|
||||
Connectors = conns.ToImmutableArray();
|
||||
Position = initialPosition;
|
||||
NodeType = type;
|
||||
UpdatePositions();
|
||||
}
|
||||
|
||||
public XElement Save() => new XElement($"{NodeType}Node", new XAttribute("pos", XMLExtensions.Vector2ToString(Position)));
|
||||
public void ReplaceAllConnectionLabelOverrides(Dictionary<string, string> replace)
|
||||
{
|
||||
foreach (var (_, value) in replace)
|
||||
{
|
||||
if (value.Length > MaxConnectionLabelLength)
|
||||
{
|
||||
DebugConsole.ThrowError($"Label override value \"{value}\" is too long (max {MaxConnectionLabelLength} characters)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (name, value) in replace)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
ConnectionLabelOverrides.Remove(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
ConnectionLabelOverrides[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
InitSize(Connectors);
|
||||
UpdatePositions();
|
||||
}
|
||||
|
||||
private void InitSize(IReadOnlyList<CircuitBoxConnection> conns)
|
||||
{
|
||||
#if CLIENT
|
||||
foreach (CircuitBoxConnection conn in conns)
|
||||
{
|
||||
if (ConnectionLabelOverrides.TryGetValue(conn.Name, out string? value))
|
||||
{
|
||||
LocalizedString newLabel =
|
||||
string.IsNullOrWhiteSpace(value)
|
||||
? conn.Connection.DisplayName
|
||||
: TextManager.Get(value).Fallback(value);
|
||||
|
||||
conn.SetLabel(newLabel, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
conn.SetLabel(conn.Connection.DisplayName, this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Size = CalculateSize(conns);
|
||||
}
|
||||
|
||||
public XElement Save()
|
||||
{
|
||||
XElement element = new XElement($"{NodeType}Node", new XAttribute("pos", XMLExtensions.Vector2ToString(Position)));
|
||||
|
||||
foreach (var (name, value) in ConnectionLabelOverrides)
|
||||
{
|
||||
element.Add(new XElement(ConnectionLabelOverrideElementName,
|
||||
new XAttribute("name", name),
|
||||
new XAttribute("value", value)));
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
public void Load(ContentXElement element)
|
||||
{
|
||||
Position = element.GetAttributeVector2("pos", Vector2.Zero);
|
||||
|
||||
Dictionary<string, string> loadedOverrides = new();
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name != ConnectionLabelOverrideElementName) { continue; }
|
||||
|
||||
string name = subElement.GetAttributeString("name", string.Empty);
|
||||
string value = subElement.GetAttributeString("value", string.Empty);
|
||||
|
||||
loadedOverrides[name] = value;
|
||||
}
|
||||
|
||||
ConnectionLabelOverrides = loadedOverrides;
|
||||
InitSize(Connectors);
|
||||
UpdatePositions();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ namespace Barotrauma
|
||||
AddLabel,
|
||||
RemoveLabel,
|
||||
ResizeLabel,
|
||||
RenameConnections,
|
||||
ServerInitialize
|
||||
}
|
||||
|
||||
@@ -156,6 +157,10 @@ namespace Barotrauma
|
||||
[NetworkSerialize]
|
||||
internal readonly record struct CircuitBoxRenameLabelEvent(ushort LabelId, Color Color, NetLimitedString NewHeader, NetLimitedString NewBody) : INetSerializableStruct;
|
||||
|
||||
[NetworkSerialize]
|
||||
internal readonly record struct CircuitBoxRenameConnectionLabelsEvent(CircuitBoxInputOutputNode.Type Type, NetDictionary<string, string> Override) : INetSerializableStruct;
|
||||
|
||||
|
||||
[NetworkSerialize]
|
||||
internal readonly record struct CircuitBoxErrorEvent(string Message) : INetSerializableStruct;
|
||||
|
||||
@@ -164,6 +169,7 @@ namespace Barotrauma
|
||||
ImmutableArray<CircuitBoxServerCreateComponentEvent> Components,
|
||||
ImmutableArray<CircuitBoxServerCreateWireEvent> Wires,
|
||||
ImmutableArray<CircuitBoxServerAddLabelEvent> Labels,
|
||||
ImmutableArray<CircuitBoxRenameConnectionLabelsEvent> LabelOverrides,
|
||||
Vector2 InputPos,
|
||||
Vector2 OutputPos) : INetSerializableStruct;
|
||||
|
||||
@@ -198,6 +204,8 @@ namespace Barotrauma
|
||||
=> CircuitBoxOpcode.RemoveLabel,
|
||||
CircuitBoxResizeLabelEvent
|
||||
=> CircuitBoxOpcode.ResizeLabel,
|
||||
CircuitBoxRenameConnectionLabelsEvent
|
||||
=> CircuitBoxOpcode.RenameConnections,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(Data))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ using System.Threading.Tasks;
|
||||
using Barotrauma.MapCreatures.Behavior;
|
||||
using System.Text;
|
||||
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
readonly struct ColoredText
|
||||
@@ -249,7 +250,7 @@ namespace Barotrauma
|
||||
GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats;
|
||||
}));
|
||||
|
||||
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
|
||||
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)] [add to crew (true/false)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
|
||||
() =>
|
||||
{
|
||||
string[] creatureAndJobNames =
|
||||
@@ -261,7 +262,10 @@ namespace Barotrauma
|
||||
return new string[][]
|
||||
{
|
||||
creatureAndJobNames.ToArray(),
|
||||
new string[] { "near", "inside", "outside", "cursor" }
|
||||
new string[] { "near", "inside", "outside", "cursor" },
|
||||
new string[] { "0", "1", "2", "3" },
|
||||
new string[] { "true", "false" },
|
||||
|
||||
};
|
||||
}, isCheat: true));
|
||||
|
||||
@@ -567,11 +571,30 @@ namespace Barotrauma
|
||||
|
||||
commands.Add(new Command("banaddress|banip", "banaddress [endpoint]: Ban the IP address/SteamID from the server.", null));
|
||||
|
||||
commands.Add(new Command("teleportcharacter|teleport", "teleport [character name]: Teleport the specified character to the position of the cursor. If the name parameter is omitted, the controlled character will be teleported.", null,
|
||||
() =>
|
||||
commands.Add(new Command("teleportcharacter|teleport", "teleport [character name] [location]: Teleport the specified character to a location , or the position of the cursor if location is omitted. If the name parameter is omitted, the controlled character will be teleported.",
|
||||
onExecute: null,
|
||||
getValidArgs:() =>
|
||||
{
|
||||
return new string[][] { ListCharacterNames() };
|
||||
var characterList = Character.Controlled != null ? new[] { "Me" } : Array.Empty<string>();
|
||||
var subList = Submarine.MainSub != null ? new[] { "mainsub" } : Array.Empty<string>();
|
||||
return new string[][]
|
||||
{
|
||||
characterList.Concat(ListCharacterNames()).ToArray(),
|
||||
subList.Concat(ListAvailableLocations()).ToArray()
|
||||
};
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("listlocations|locations", "listlocations: List all the locations in the level: subs, outposts, ruins, caves.",
|
||||
onExecute:(string[] args) =>
|
||||
{
|
||||
var availableLocations = ListAvailableLocations();
|
||||
NewMessage("***************", Color.Cyan);
|
||||
foreach (var location in availableLocations)
|
||||
{
|
||||
NewMessage(location, Color.Cyan);
|
||||
}
|
||||
NewMessage("***************", Color.Cyan);
|
||||
}));
|
||||
|
||||
commands.Add(new Command("godmode", "godmode [character name]: Toggle character godmode. Makes the targeted character invulnerable to damage. If the name parameter is omitted, the controlled character will receive godmode.",
|
||||
(string[] args) =>
|
||||
@@ -782,6 +805,17 @@ namespace Barotrauma
|
||||
{
|
||||
if (c.Character != revivedCharacter) { continue; }
|
||||
|
||||
// If killed in ironman mode, the character has been wiped from the save mid-round, so its
|
||||
// original data needs to be restored to the save file (without making a backup of the dead character)
|
||||
if (GameMain.Server.ServerSettings.IronmanMode && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
|
||||
{
|
||||
if (mpCampaign.RestoreSingleCharacterFromBackup(c) is CharacterCampaignData characterToRestore)
|
||||
{
|
||||
characterToRestore.CharacterInfo.PermanentlyDead = false;
|
||||
mpCampaign.SaveSingleCharacter(characterToRestore, skipBackup: true);
|
||||
}
|
||||
}
|
||||
|
||||
//clients stop controlling the character when it dies, force control back
|
||||
GameMain.Server.SetClientCharacter(c, revivedCharacter);
|
||||
break;
|
||||
@@ -1176,7 +1210,8 @@ namespace Barotrauma
|
||||
}
|
||||
},null));
|
||||
|
||||
commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) =>
|
||||
commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.",
|
||||
onExecute:(string[] args) =>
|
||||
{
|
||||
if (Submarine.MainSub == null) { return; }
|
||||
|
||||
@@ -1233,7 +1268,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
},
|
||||
() =>
|
||||
getValidArgs:() =>
|
||||
{
|
||||
return new string[][]
|
||||
{
|
||||
@@ -1924,6 +1959,7 @@ namespace Barotrauma
|
||||
}));
|
||||
|
||||
#if DEBUG
|
||||
commands.Add(new Command("debugvoip", "Toggle the server writing VOIP into audio files.", null, isCheat: false));
|
||||
|
||||
commands.Add(new Command("simulatedlongloadingtime", "simulatedlongloadingtime [minimum loading time]: forces loading a round to take at least the specified amount of seconds.", (string[] args) =>
|
||||
{
|
||||
@@ -2047,6 +2083,11 @@ namespace Barotrauma
|
||||
string[] validArgs = allArgs[autoCompletedArgIndex].Where(arg =>
|
||||
currentAutoCompletedCommand.Trim().Length <= arg.Length &&
|
||||
arg.Substring(0, currentAutoCompletedCommand.Trim().Length).ToLower() == currentAutoCompletedCommand.Trim().ToLower()).ToArray();
|
||||
|
||||
// add all completions that contain the current argument, to the end of the list
|
||||
validArgs = validArgs.Concat(allArgs[autoCompletedArgIndex].Where(arg =>
|
||||
arg.ToLower().Contains(currentAutoCompletedCommand.Trim().ToLower()) &&
|
||||
!validArgs.Contains(arg))).ToArray();
|
||||
|
||||
if (validArgs.Length == 0) { return command; }
|
||||
|
||||
@@ -2095,95 +2136,219 @@ namespace Barotrauma
|
||||
currentAutoCompletedIndex = 0;
|
||||
}
|
||||
|
||||
public static void ExecuteCommand(string command)
|
||||
/// <summary>
|
||||
/// Executes the specific command or commands
|
||||
/// </summary>
|
||||
/// <param name="inputtedCommands">Command, or multiple commands separated by newlines.</param>
|
||||
public static void ExecuteCommand(string inputtedCommands)
|
||||
{
|
||||
if (activeQuestionCallback != null)
|
||||
if (string.IsNullOrWhiteSpace(inputtedCommands) || inputtedCommands == "\\" || inputtedCommands == "\n") { return; }
|
||||
|
||||
string[] commandsToExecute = inputtedCommands.Split("\n");
|
||||
foreach (string command in commandsToExecute)
|
||||
{
|
||||
#if CLIENT
|
||||
activeQuestionText = null;
|
||||
#endif
|
||||
NewCommand(command);
|
||||
//reset the variable before invoking the delegate because the method may need to activate another question
|
||||
var temp = activeQuestionCallback;
|
||||
activeQuestionCallback = null;
|
||||
temp(command);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(command) || command == "\\" || command == "\n") { return; }
|
||||
|
||||
string[] splitCommand = ToolBox.SplitCommand(command);
|
||||
if (splitCommand.Length == 0)
|
||||
{
|
||||
ThrowError("Failed to execute command \"" + command + "\"!");
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"DebugConsole.ExecuteCommand:LengthZero",
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
"Failed to execute command \"" + command + "\"!");
|
||||
return;
|
||||
}
|
||||
|
||||
Identifier firstCommand = splitCommand[0].ToIdentifier();
|
||||
|
||||
if (firstCommand != "admin")
|
||||
{
|
||||
NewCommand(command);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (GameMain.Client != null)
|
||||
{
|
||||
Command matchingCommand = commands.Find(c => c.Names.Contains(firstCommand));
|
||||
if (matchingCommand == null)
|
||||
if (activeQuestionCallback != null)
|
||||
{
|
||||
//if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side
|
||||
GameMain.Client.SendConsoleCommand(command);
|
||||
NewMessage("Server command: " + command, Color.Cyan);
|
||||
#if CLIENT
|
||||
activeQuestionText = null;
|
||||
#endif
|
||||
NewCommand(command);
|
||||
//reset the variable before invoking the delegate because the method may need to activate another question
|
||||
var temp = activeQuestionCallback;
|
||||
activeQuestionCallback = null;
|
||||
temp(command);
|
||||
return;
|
||||
}
|
||||
else if (GameMain.Client.HasConsoleCommandPermission(firstCommand))
|
||||
|
||||
if (string.IsNullOrWhiteSpace(command) || command == "\\") { return; }
|
||||
|
||||
string[] splitCommand = ToolBox.SplitCommand(command);
|
||||
if (splitCommand.Length == 0)
|
||||
{
|
||||
if (matchingCommand.RelayToServer)
|
||||
ThrowError("Failed to execute command \"" + command + "\"!");
|
||||
GameAnalyticsManager.AddErrorEventOnce(
|
||||
"DebugConsole.ExecuteCommand:LengthZero",
|
||||
GameAnalyticsManager.ErrorSeverity.Error,
|
||||
"Failed to execute command \"" + command + "\"!");
|
||||
return;
|
||||
}
|
||||
|
||||
Identifier firstCommand = splitCommand[0].ToIdentifier();
|
||||
|
||||
if (firstCommand != "admin")
|
||||
{
|
||||
NewCommand(command);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (GameMain.Client != null)
|
||||
{
|
||||
Command matchingCommand = commands.Find(c => c.Names.Contains(firstCommand));
|
||||
if (matchingCommand == null)
|
||||
{
|
||||
//if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side
|
||||
GameMain.Client.SendConsoleCommand(command);
|
||||
NewMessage("Server command: " + command, Color.Cyan);
|
||||
return;
|
||||
}
|
||||
else
|
||||
else if (GameMain.Client.HasConsoleCommandPermission(firstCommand))
|
||||
{
|
||||
matchingCommand.ClientExecute(splitCommand.Skip(1).ToArray());
|
||||
if (matchingCommand.RelayToServer)
|
||||
{
|
||||
GameMain.Client.SendConsoleCommand(command);
|
||||
NewMessage("Server command: " + command, Color.Cyan);
|
||||
}
|
||||
else
|
||||
{
|
||||
matchingCommand.ClientExecute(splitCommand.Skip(1).ToArray());
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!IsCommandPermitted(firstCommand, GameMain.Client))
|
||||
{
|
||||
if (!IsCommandPermitted(firstCommand, GameMain.Client))
|
||||
{
|
||||
#if DEBUG
|
||||
AddWarning($"You're not permitted to use the command \"{firstCommand}\". Executing the command anyway because this is a debug build.");
|
||||
AddWarning($"You're not permitted to use the command \"{firstCommand}\". Executing the command anyway because this is a debug build.");
|
||||
#else
|
||||
ThrowError($"You're not permitted to use the command \"{firstCommand}\"!");
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool commandFound = false;
|
||||
foreach (Command c in commands)
|
||||
{
|
||||
if (!c.Names.Contains(firstCommand)) { continue; }
|
||||
c.Execute(splitCommand.Skip(1).ToArray());
|
||||
commandFound = true;
|
||||
break;
|
||||
}
|
||||
bool commandFound = false;
|
||||
foreach (Command c in commands)
|
||||
{
|
||||
if (!c.Names.Contains(firstCommand)) { continue; }
|
||||
c.Execute(splitCommand.Skip(1).ToArray());
|
||||
commandFound = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!commandFound)
|
||||
{
|
||||
ThrowError("Command \"" + splitCommand[0] + "\" not found.");
|
||||
if (!commandFound)
|
||||
{
|
||||
ThrowError("Command \"" + splitCommand[0] + "\" not found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ThenBy(c => c.Name).Select(c => c.Name).Distinct().ToArray();
|
||||
|
||||
private static string[] ListAvailableLocations()
|
||||
{
|
||||
List<string> locationNames = new();
|
||||
foreach(var submarine in Submarine.Loaded)
|
||||
{
|
||||
locationNames.Add(submarine.Info.Name);
|
||||
}
|
||||
|
||||
private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null)
|
||||
if (Level.Loaded != null)
|
||||
{
|
||||
foreach (var cave in Level.Loaded.Caves)
|
||||
{
|
||||
string caveName = cave.CaveGenerationParams.Name;
|
||||
// add index in case there are duplicate names
|
||||
int index = 1;
|
||||
while (locationNames.Contains($"{caveName}_{index}"))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
locationNames.Add($"{caveName}_{index}");
|
||||
}
|
||||
}
|
||||
|
||||
return locationNames.ToArray();
|
||||
}
|
||||
|
||||
private static bool TryFindTeleportPosition(string locationName, out Vector2 teleportPosition)
|
||||
{
|
||||
if (Submarine.MainSub is Submarine mainSub && string.Equals(locationName, "mainsub", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var randomWaypoint = GetRandomWaypoint(mainSub.GetWaypoints(alsoFromConnectedSubs:false));
|
||||
if (randomWaypoint != null)
|
||||
{
|
||||
teleportPosition = randomWaypoint.WorldPosition;
|
||||
return true;
|
||||
}
|
||||
LogError("No waypoints found in the main sub!");
|
||||
}
|
||||
|
||||
foreach (var submarine in Submarine.Loaded)
|
||||
{
|
||||
if (string.Equals(submarine.Info.Name, locationName, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var randomWaypoint = GetRandomWaypoint(submarine.GetWaypoints(alsoFromConnectedSubs:false));
|
||||
if (randomWaypoint != null)
|
||||
{
|
||||
teleportPosition = randomWaypoint.WorldPosition;
|
||||
return true;
|
||||
}
|
||||
LogError($"No waypoints found in sub {submarine.Info.Name}!");
|
||||
}
|
||||
}
|
||||
|
||||
if (Level.Loaded is Level loadedLevel)
|
||||
{
|
||||
(string locationNameNoIndex, int locationIndex) = SplitIndex(locationName);
|
||||
int caveIndex = 1;
|
||||
foreach (var cave in loadedLevel.Caves)
|
||||
{
|
||||
if (string.Equals(cave.CaveGenerationParams.Name, locationNameNoIndex, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
if (caveIndex != locationIndex)
|
||||
{
|
||||
caveIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var randomWaypoint = GetRandomWaypoint(cave.Tunnels.GetRandom(Rand.RandSync.Unsynced).WayPoints);
|
||||
if (randomWaypoint != null)
|
||||
{
|
||||
teleportPosition = randomWaypoint.WorldPosition;
|
||||
return true;
|
||||
}
|
||||
LogError($"No waypoints found in cave {cave.CaveGenerationParams.Name}!");
|
||||
}
|
||||
}
|
||||
}
|
||||
teleportPosition = Vector2.Zero;
|
||||
return false;
|
||||
|
||||
WayPoint GetRandomWaypoint(IReadOnlyList<WayPoint> waypoints)
|
||||
{
|
||||
if (waypoints.None())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (waypoints.Any(point => point.SpawnType == SpawnType.Human))
|
||||
{
|
||||
return waypoints.GetRandom(point => point.SpawnType == SpawnType.Human, Rand.RandSync.Unsynced);
|
||||
}
|
||||
|
||||
if (waypoints.Any(point => point.SpawnType == SpawnType.Path))
|
||||
{
|
||||
return waypoints.GetRandom(point => point.SpawnType == SpawnType.Path, Rand.RandSync.Unsynced);
|
||||
}
|
||||
|
||||
return waypoints.GetRandom(Rand.RandSync.Unsynced);
|
||||
}
|
||||
|
||||
(string, int) SplitIndex(string caveName)
|
||||
{
|
||||
string[] splitName = caveName.Split('_');
|
||||
if (splitName.Length == 1)
|
||||
{
|
||||
return (splitName[0], -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (splitName[0], int.Parse(splitName[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null, bool botsOnly = false)
|
||||
{
|
||||
if (args.Length == 0) return null;
|
||||
|
||||
@@ -2202,6 +2367,11 @@ namespace Barotrauma
|
||||
c.Name.Equals(characterName, StringComparison.OrdinalIgnoreCase) &&
|
||||
(!c.IsRemotePlayer || !ignoreRemotePlayers || allowedRemotePlayer?.Character == c));
|
||||
|
||||
if (botsOnly)
|
||||
{
|
||||
matchingCharacters = matchingCharacters.FindAll(c => c is AICharacter);
|
||||
}
|
||||
|
||||
if (!matchingCharacters.Any())
|
||||
{
|
||||
NewMessage("Character \""+ characterName + "\" not found", Color.Red);
|
||||
@@ -2232,6 +2402,68 @@ namespace Barotrauma
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void TeleportCharacter(Vector2 cursorWorldPos, Character controlledCharacter, string[] args)
|
||||
{
|
||||
if (Screen.Selected != GameMain.GameScreen)
|
||||
{
|
||||
NewMessage("Cannot teleport a character in the menu or the editor screens.", color: Color.Yellow);
|
||||
return;
|
||||
}
|
||||
|
||||
Character targetCharacter = controlledCharacter;
|
||||
Vector2 worldPosition = cursorWorldPos;
|
||||
string locationNameArgument = "";
|
||||
|
||||
var availableLocations = ListAvailableLocations();
|
||||
if (args.Length > 0)
|
||||
{
|
||||
if (args.Length > 1)
|
||||
{
|
||||
// remove location name from args
|
||||
if (availableLocations.Contains(args.Last())
|
||||
|| string.Equals(args.Last(), "mainsub", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
locationNameArgument = args.Last();
|
||||
args = args.Take(args.Length - 1).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
NewMessage("Invalid arguments", color: Color.Yellow);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// the remaining args should be the character name and a possible index
|
||||
if (args[0].ToLowerInvariant() != "me")
|
||||
{
|
||||
Character match = FindMatchingCharacter(args, ignoreRemotePlayers:false);
|
||||
targetCharacter = match;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(locationNameArgument))
|
||||
{
|
||||
if (TryFindTeleportPosition(locationNameArgument, out Vector2 teleportPosition))
|
||||
{
|
||||
worldPosition = teleportPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
ThrowError($"No teleport position for location \"{locationNameArgument}\" was found.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
targetCharacter.TeleportTo(worldPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
NewMessage("Invalid arguments", color: Color.Yellow);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SpawnCharacter(string[] args, Vector2 cursorWorldPos, out string errorMsg)
|
||||
{
|
||||
@@ -2253,8 +2485,8 @@ namespace Barotrauma
|
||||
{
|
||||
job = JobPrefab.Prefabs[characterLowerCase];
|
||||
}
|
||||
bool human = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
|
||||
|
||||
bool isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
|
||||
bool addToCrew = false;
|
||||
if (args.Length > 1)
|
||||
{
|
||||
switch (args[1].ToLowerInvariant())
|
||||
@@ -2288,13 +2520,17 @@ namespace Barotrauma
|
||||
spawnPosition = cursorWorldPos;
|
||||
break;
|
||||
default:
|
||||
spawnPoint = WayPoint.GetRandom(human ? SpawnType.Human : SpawnType.Enemy);
|
||||
spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy);
|
||||
break;
|
||||
}
|
||||
addToCrew =
|
||||
args.Length > 3 ?
|
||||
args[3].Equals("true", StringComparison.OrdinalIgnoreCase) :
|
||||
isHuman;
|
||||
}
|
||||
else
|
||||
{
|
||||
spawnPoint = WayPoint.GetRandom(human ? SpawnType.Human : SpawnType.Enemy);
|
||||
spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(args[0])) { return; }
|
||||
@@ -2313,29 +2549,36 @@ namespace Barotrauma
|
||||
|
||||
if (spawnPoint != null) { spawnPosition = spawnPoint.WorldPosition; }
|
||||
|
||||
if (human)
|
||||
if (isHuman)
|
||||
{
|
||||
var variant = job != null ? Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient) : 0;
|
||||
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant);
|
||||
spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8));
|
||||
if (GameMain.GameSession != null)
|
||||
{
|
||||
spawnedCharacter.TeamID = teamType;
|
||||
#if CLIENT
|
||||
GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter);
|
||||
#endif
|
||||
}
|
||||
|
||||
spawnedCharacter.GiveJobItems(spawnPoint);
|
||||
spawnedCharacter.GiveIdCardTags(spawnPoint);
|
||||
spawnedCharacter.Info.StartItemsGiven = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier()) != null)
|
||||
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier());
|
||||
if (prefab != null)
|
||||
{
|
||||
Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8));
|
||||
CharacterInfo characterInfo = null;
|
||||
if (prefab.HasCharacterInfo)
|
||||
{
|
||||
characterInfo = new CharacterInfo(prefab.Identifier);
|
||||
}
|
||||
spawnedCharacter = Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8), characterInfo);
|
||||
}
|
||||
}
|
||||
if (addToCrew && GameMain.GameSession != null)
|
||||
{
|
||||
spawnedCharacter.TeamID = teamType;
|
||||
#if CLIENT
|
||||
GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public static void SpawnItem(string[] args, Vector2 cursorPos, Character controlledCharacter, out string errorMsg)
|
||||
|
||||
@@ -253,6 +253,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
SwimmingSpeed,
|
||||
|
||||
/// <summary>
|
||||
/// Increases the character's speed by a percentage when using an item that propels the character forwards (such as a diving scooter).
|
||||
/// </summary>
|
||||
PropulsionSpeed,
|
||||
|
||||
/// <summary>
|
||||
/// Decreases how long it takes for buffs applied to the character decay over time by a percentage.
|
||||
/// Buffs are afflictions that have isBuff set to true.
|
||||
|
||||
@@ -107,6 +107,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (spawnPending)
|
||||
{
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
isFinished = true;
|
||||
return;
|
||||
}
|
||||
SpawnItem();
|
||||
spawnPending = false;
|
||||
}
|
||||
|
||||
+4
-9
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
@@ -30,7 +30,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (TargetTag.IsEmpty)
|
||||
{
|
||||
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.",
|
||||
DebugConsole.LogError($"CheckConditionalAction error: {GetEventDebugName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.",
|
||||
contentPackage: parentEvent.Prefab.ContentPackage);
|
||||
}
|
||||
var conditionalElements = element.GetChildElements("Conditional");
|
||||
@@ -52,7 +52,7 @@ namespace Barotrauma
|
||||
|
||||
if (Conditionals.None())
|
||||
{
|
||||
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.",
|
||||
DebugConsole.LogError($"CheckConditionalAction error: {GetEventDebugName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.",
|
||||
contentPackage: parentEvent.Prefab.ContentPackage);
|
||||
}
|
||||
|
||||
@@ -67,11 +67,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private string GetEventName()
|
||||
{
|
||||
return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event";
|
||||
}
|
||||
|
||||
protected override bool? DetermineSuccess()
|
||||
{
|
||||
IEnumerable<ISerializableEntity> targets = null;
|
||||
@@ -82,7 +77,7 @@ namespace Barotrauma
|
||||
|
||||
if (targets.None())
|
||||
{
|
||||
DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.",
|
||||
DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventDebugName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.",
|
||||
contentPackage: ParentEvent.Prefab.ContentPackage);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#nullable enable
|
||||
namespace Barotrauma;
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the difficulty of the current level is within some specific range.
|
||||
/// </summary>
|
||||
class CheckDifficultyAction : BinaryOptionAction
|
||||
{
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum difficulty of the current level for the check to succeed.")]
|
||||
public float MinDifficulty { get; set; }
|
||||
|
||||
[Serialize(100.0f, IsPropertySaveable.Yes, description: "Maximum difficulty of the current level for the check to succeed.")]
|
||||
public float MaxDifficulty { get; set; }
|
||||
|
||||
public CheckDifficultyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
if (MaxDifficulty <= MinDifficulty)
|
||||
{
|
||||
DebugConsole.LogError($"Potential error in event {GetEventDebugName()}: maximum difficulty ({MaxDifficulty}) is not larger than minimum difficulty ({MinDifficulty}) in {nameof(CheckDifficultyAction)}.",
|
||||
contentPackage: parentEvent.Prefab.ContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool? DetermineSuccess()
|
||||
{
|
||||
if (Level.Loaded == null) { return false; }
|
||||
return Level.Loaded.Difficulty >= MinDifficulty && Level.Loaded.Difficulty <= MaxDifficulty;
|
||||
}
|
||||
|
||||
public override string ToDebugString()
|
||||
{
|
||||
return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(CheckDifficultyAction)} -> (min: {MinDifficulty}, max: {MaxDifficulty}" +
|
||||
$" Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
@@ -36,6 +36,9 @@ namespace Barotrauma
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Does the item need to be equipped for the check to succeed?")]
|
||||
public bool RequireEquipped { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Does the item need to be worn for the check to succeed?")]
|
||||
public bool RequireWorn { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "If enabled, the doesn't need to be directly inside the container/character we're checking, but can be nested inside multiple containers (e.g. in a toolbelt in a character's inventory).")]
|
||||
public bool Recursive { get; set; }
|
||||
|
||||
@@ -256,6 +259,19 @@ namespace Barotrauma
|
||||
if (character == null) { return false; }
|
||||
return character.HasEquippedItem(item);
|
||||
}
|
||||
if (RequireWorn)
|
||||
{
|
||||
if (character == null) { return false; }
|
||||
foreach (var wearable in item.GetComponents<Wearable>())
|
||||
{
|
||||
foreach (var allowedSlot in wearable.AllowedSlots)
|
||||
{
|
||||
if (allowedSlot == InvSlotType.Any) { continue; }
|
||||
if (character.HasEquippedItem(item, allowedSlot)) { return true; }
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
@@ -134,6 +134,10 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
text = TextManager.Get(Text).Fallback(Text);
|
||||
if (text.Value.IsNullOrEmpty())
|
||||
{
|
||||
text = text.Fallback(Text);
|
||||
}
|
||||
}
|
||||
return ParentEvent.ReplaceVariablesInEventText(text);
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
#nullable enable
|
||||
namespace Barotrauma;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to disconnect wires and break devices and walls in beacon stations. Useful if you want the beacon to be in tact by default, and use events to determine whether it should be e.g. manned by bandits, or destroyed and infested by monsters.
|
||||
/// </summary>
|
||||
class DamageBeaconStationAction : EventAction
|
||||
{
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Probability of disconnecting wires (0.5 = 50% chance of disconnecting any given wire, 1 = all wires disconnected).")]
|
||||
public float DisconnectWireProbability { get; set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Probability of a wall sections leaking (0.5 = 50% creating a leak on any given wall section, 1 = all walls leak).")]
|
||||
public float DamageWallProbability { get; set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Probability of devices being damaged (0.5 = 50% chance of damaging any given devices, 1 = all devices are damaged).")]
|
||||
public float DamageDeviceProbability { get; set; }
|
||||
|
||||
private bool isFinished;
|
||||
|
||||
public DamageBeaconStationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
if (DisconnectWireProbability <= 0.0f && DamageWallProbability <= 0.0f && DamageDeviceProbability <= 0.0f)
|
||||
{
|
||||
DebugConsole.LogError($"Potential error in event {GetEventDebugName()}: {DisconnectWireProbability}, {DamageWallProbability} and {DamageDeviceProbability} are all set to 0 in {nameof(DamageBeaconStationAction)}, and the action will do nothing.",
|
||||
contentPackage: parentEvent.Prefab.ContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsFinished(ref string goToLabel) => isFinished;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
isFinished = false;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
|
||||
if (Level.Loaded != null)
|
||||
{
|
||||
Level.Loaded.DisconnectBeaconStationWires(DisconnectWireProbability);
|
||||
Level.Loaded.DamageBeaconStationWalls(DamageWallProbability);
|
||||
Level.Loaded.DamageBeaconStationDevices(DamageDeviceProbability);
|
||||
}
|
||||
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
public override string ToDebugString()
|
||||
{
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(DamageBeaconStationAction)}";
|
||||
}
|
||||
}
|
||||
@@ -198,6 +198,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected string GetEventDebugName()
|
||||
{
|
||||
return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event";
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Rich test to display in debugdraw
|
||||
/// </summary>
|
||||
|
||||
+5
-2
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -31,6 +31,9 @@ namespace Barotrauma
|
||||
[Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of NPCs the action can target. For example, you could only make a specific number of security officers man a periscope.")]
|
||||
public int MaxTargets { get; set; }
|
||||
|
||||
[Serialize(100, IsPropertySaveable.Yes, description: "Priority of operating the item (0-100). Higher values will make the AI prefer operating the item over other orders (priority 60-70) or e.g. reacting to emergencies (priority 90).")]
|
||||
public int Priority { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC stop operating the item when the event resets?")]
|
||||
public bool AbandonOnReset { get; set; }
|
||||
|
||||
@@ -72,7 +75,7 @@ namespace Barotrauma
|
||||
{
|
||||
var newObjective = new AIObjectiveOperateItem(itemComponent, npc, humanAiController.ObjectiveManager, OrderOption, RequireEquip)
|
||||
{
|
||||
OverridePriority = 100.0f
|
||||
OverridePriority = Priority
|
||||
};
|
||||
humanAiController.ObjectiveManager.AddObjective(newObjective);
|
||||
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
|
||||
|
||||
@@ -68,6 +68,7 @@ namespace Barotrauma
|
||||
("bot", v => TagBots(playerCrewOnly: false)),
|
||||
("crew", v => TagCrew()),
|
||||
("humanprefabidentifier", TagHumansByIdentifier),
|
||||
("humanprefabtag", TagHumansByTag),
|
||||
("jobidentifier", TagHumansByJobIdentifier),
|
||||
("structureidentifier", TagStructuresByIdentifier),
|
||||
("structurespecialtag", TagStructuresBySpecialTag),
|
||||
@@ -153,6 +154,11 @@ namespace Barotrauma
|
||||
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab?.Identifier == identifier));
|
||||
}
|
||||
|
||||
private void TagHumansByTag(Identifier tag)
|
||||
{
|
||||
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag)));
|
||||
}
|
||||
|
||||
private void TagHumansByJobIdentifier(Identifier jobIdentifier)
|
||||
{
|
||||
AddTarget(Tag, Character.CharacterList.Where(c => c.HasJob(jobIdentifier)));
|
||||
@@ -217,6 +223,7 @@ namespace Barotrauma
|
||||
private bool IsValidItem(Item it)
|
||||
{
|
||||
return
|
||||
!it.IsLayerHidden && /*items in hidden layers are treated as if they didn't exist, regardless if hidden items should be allowed*/
|
||||
(!it.HiddenInGame || AllowHiddenItems) &&
|
||||
ModuleTagMatches(it) &&
|
||||
//if the item has just spawned, it may be in a hull but not moved into the coordinate space of the hull yet
|
||||
|
||||
@@ -424,7 +424,7 @@ namespace Barotrauma
|
||||
public void RegisterEventHistory(bool registerFinishedOnly = false)
|
||||
{
|
||||
if (level?.LevelData == null) { return; }
|
||||
|
||||
|
||||
level.LevelData.EventsExhausted = !registerFinishedOnly;
|
||||
|
||||
if (level.LevelData.Type == LevelData.LevelType.Outpost)
|
||||
@@ -433,15 +433,15 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var finishedEvent in finishedEvents)
|
||||
{
|
||||
var key = finishedEvent.ParentSet;
|
||||
if (key == null) { continue; }
|
||||
if (level.LevelData.FinishedEvents.ContainsKey(key))
|
||||
EventSet parentSet = finishedEvent.ParentSet;
|
||||
if (parentSet == null) { continue; }
|
||||
if (parentSet.Exhaustible)
|
||||
{
|
||||
level.LevelData.FinishedEvents[key] += 1;
|
||||
level.LevelData.EventsExhausted = true;
|
||||
}
|
||||
else
|
||||
if (!level.LevelData.FinishedEvents.TryAdd(parentSet, 1))
|
||||
{
|
||||
level.LevelData.FinishedEvents.Add(key, 1);
|
||||
level.LevelData.FinishedEvents[parentSet] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -691,6 +691,7 @@ namespace Barotrauma
|
||||
{
|
||||
return
|
||||
(e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) &&
|
||||
(e.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(e.RequiredLayer)) &&
|
||||
!level.LevelData.NonRepeatableEvents.Contains(e.Identifier);
|
||||
}
|
||||
|
||||
@@ -702,8 +703,9 @@ namespace Barotrauma
|
||||
private static bool IsValidForLevel(EventSet eventSet, Level level)
|
||||
{
|
||||
return
|
||||
level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty &&
|
||||
level.IsAllowedDifficulty(eventSet.MinLevelDifficulty, eventSet.MaxLevelDifficulty) &&
|
||||
level.LevelData.Type == eventSet.LevelType &&
|
||||
(eventSet.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(eventSet.RequiredLayer)) &&
|
||||
(eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier);
|
||||
}
|
||||
|
||||
@@ -953,7 +955,7 @@ namespace Barotrauma
|
||||
monsterStrength = 0;
|
||||
foreach (Character character in Character.CharacterList)
|
||||
{
|
||||
if (character.IsIncapacitated || character.IsArrested || !character.Enabled || character.IsPet) { continue; }
|
||||
if (character.IsIncapacitated || character.IsHandcuffed || !character.Enabled || character.IsPet) { continue; }
|
||||
|
||||
if (character.AIController is EnemyAIController enemyAI)
|
||||
{
|
||||
|
||||
@@ -32,6 +32,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public readonly Identifier BiomeIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// If set, this layer must be present somewhere in the level.
|
||||
/// </summary>
|
||||
public readonly Identifier RequiredLayer;
|
||||
|
||||
/// <summary>
|
||||
/// If set, the event set can only be chosen in locations that belong to this faction.
|
||||
/// </summary>
|
||||
@@ -94,6 +99,8 @@ namespace Barotrauma
|
||||
Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1);
|
||||
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent));
|
||||
|
||||
RequiredLayer = element.GetAttributeIdentifier(nameof(RequiredLayer), Identifier.Empty);
|
||||
|
||||
UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false);
|
||||
UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip");
|
||||
UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0);
|
||||
|
||||
@@ -106,6 +106,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public readonly LevelData.LevelType LevelType;
|
||||
|
||||
/// <summary>
|
||||
/// If set, this layer must be present somewhere in the level.
|
||||
/// </summary>
|
||||
public readonly Identifier RequiredLayer;
|
||||
|
||||
/// <summary>
|
||||
/// If set, the event set can only be chosen in locations of this type.
|
||||
/// </summary>
|
||||
@@ -368,7 +373,7 @@ namespace Barotrauma
|
||||
ChooseRandom = element.GetAttributeBool("chooserandom", false);
|
||||
eventCount = element.GetAttributeInt("eventcount", 1);
|
||||
SubSetCount = element.GetAttributeInt("setcount", 1);
|
||||
Exhaustible = element.GetAttributeBool("exhaustible", false);
|
||||
Exhaustible = element.GetAttributeBool("exhaustible", parentSet?.Exhaustible ?? false);
|
||||
MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f);
|
||||
MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f);
|
||||
|
||||
@@ -386,6 +391,8 @@ namespace Barotrauma
|
||||
ResetTime = element.GetAttributeFloat(nameof(ResetTime), parentSet?.ResetTime ?? 0);
|
||||
CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), parentSet?.CampaignTutorialOnly ?? false);
|
||||
|
||||
RequiredLayer = element.GetAttributeIdentifier(nameof(RequiredLayer), Identifier.Empty);
|
||||
|
||||
ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1);
|
||||
ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1);
|
||||
if (ForceAtDiscoveredNr >= 0 && ForceAtVisitedNr >= 0)
|
||||
|
||||
+41
-10
@@ -7,7 +7,8 @@ using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class AlienRuinMission : Mission
|
||||
[TypePreviouslyKnownAs("AlienRuinMission")]
|
||||
partial class EliminateTargetsMission : Mission
|
||||
{
|
||||
private readonly Identifier[] targetItemIdentifiers;
|
||||
private readonly Identifier[] targetEnemyIdentifiers;
|
||||
@@ -16,7 +17,10 @@ namespace Barotrauma
|
||||
private readonly HashSet<Character> spawnedTargets = new HashSet<Character>();
|
||||
private readonly HashSet<Entity> allTargets = new HashSet<Entity>();
|
||||
|
||||
private Ruin TargetRuin { get; set; }
|
||||
public readonly SubmarineType TargetSubType;
|
||||
public readonly bool PrioritizeThalamus;
|
||||
|
||||
private Submarine TargetSub { get; set; }
|
||||
|
||||
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
|
||||
{
|
||||
@@ -35,11 +39,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public AlienRuinMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
public EliminateTargetsMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
{
|
||||
targetItemIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("targetitems", Array.Empty<Identifier>());
|
||||
targetEnemyIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("targetenemies", Array.Empty<Identifier>());
|
||||
minEnemyCount = prefab.ConfigElement.GetAttributeInt("minenemycount", 0);
|
||||
TargetSubType = prefab.ConfigElement.GetAttributeEnum("targetsub", SubmarineType.Ruin);
|
||||
PrioritizeThalamus = prefab.RequireThalamusWreck;
|
||||
}
|
||||
|
||||
protected override void StartMissionSpecific(Level level)
|
||||
@@ -48,23 +54,48 @@ namespace Barotrauma
|
||||
spawnedTargets.Clear();
|
||||
allTargets.Clear();
|
||||
if (IsClient) { return; }
|
||||
TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient);
|
||||
if (TargetRuin == null)
|
||||
|
||||
TargetSub = TargetSubType switch
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): level contains no alien ruins",
|
||||
SubmarineType.Wreck => FindWreck(),
|
||||
SubmarineType.Ruin => Level.Loaded?.Ruins?.GetRandom(Rand.RandSync.ServerAndClient).Submarine,
|
||||
SubmarineType.BeaconStation => Level.Loaded?.BeaconStation,
|
||||
_ => null
|
||||
};
|
||||
|
||||
Submarine FindWreck()
|
||||
{
|
||||
var wrecks = Level.Loaded?.Wrecks;
|
||||
if (wrecks == null || wrecks.None()) { return null; }
|
||||
|
||||
if (PrioritizeThalamus)
|
||||
{
|
||||
var thalamusWrecks = wrecks.Where(w => w.WreckAI != null).ToArray();
|
||||
if (thalamusWrecks.Any())
|
||||
{
|
||||
return thalamusWrecks.GetRandom(Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
}
|
||||
|
||||
return wrecks.GetRandom(Rand.RandSync.ServerAndClient);
|
||||
}
|
||||
|
||||
if (TargetSub == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to initialize an {nameof(EliminateTargetsMission)} mission (\"{Prefab.Identifier}\"): level contains no submarines",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
return;
|
||||
}
|
||||
if (targetItemIdentifiers.Length < 1 && targetEnemyIdentifiers.Length < 1)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): no target identifiers set in the mission definition",
|
||||
DebugConsole.ThrowError($"Failed to initialize an {nameof(EliminateTargetsMission)} mission (\"{Prefab.Identifier}\"): no target identifiers set in the mission definition",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
return;
|
||||
}
|
||||
foreach (var item in Item.ItemList)
|
||||
{
|
||||
if (!targetItemIdentifiers.Contains(item.Prefab.Identifier)) { continue; }
|
||||
if (item.Submarine != TargetRuin.Submarine) { continue; }
|
||||
if (item.Submarine != TargetSub) { continue; }
|
||||
existingTargets.Add(item);
|
||||
allTargets.Add(item);
|
||||
}
|
||||
@@ -73,7 +104,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (character.SpeciesName.IsEmpty) { continue; }
|
||||
if (!targetEnemyIdentifiers.Contains(character.SpeciesName)) { continue; }
|
||||
if (character.Submarine != TargetRuin.Submarine) { continue; }
|
||||
if (character.Submarine != TargetSub) { continue; }
|
||||
existingTargets.Add(character);
|
||||
allTargets.Add(character);
|
||||
existingEnemyCount++;
|
||||
@@ -103,7 +134,7 @@ namespace Barotrauma
|
||||
for (int i = 0; i < (minEnemyCount - existingEnemyCount); i++)
|
||||
{
|
||||
var prefab = enemyPrefabs.GetRandomUnsynced();
|
||||
var spawnPos = TargetRuin.Submarine.GetWaypoints(false).GetRandomUnsynced(w => w.CurrentHull != null)?.WorldPosition;
|
||||
var spawnPos = TargetSub.GetWaypoints(false).GetRandomUnsynced(w => w.CurrentHull != null)?.WorldPosition;
|
||||
if (!spawnPos.HasValue)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no valid spawn positions could be found for the additional ({minEnemyCount - existingEnemyCount}) enemies to be spawned",
|
||||
@@ -211,12 +211,12 @@ namespace Barotrauma
|
||||
|
||||
public virtual void SetLevel(LevelData level) { }
|
||||
|
||||
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
|
||||
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null)
|
||||
{
|
||||
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer);
|
||||
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer, difficultyLevel);
|
||||
}
|
||||
|
||||
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
|
||||
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null)
|
||||
{
|
||||
List<MissionPrefab> allowedMissions = new List<MissionPrefab>();
|
||||
if (missionType == MissionType.None)
|
||||
@@ -225,32 +225,20 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => ((int)(missionType & m.Type)) != 0));
|
||||
allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => m.Type.HasAnyFlag(missionType)));
|
||||
}
|
||||
|
||||
allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
|
||||
allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
|
||||
if (requireCorrectLocationType)
|
||||
{
|
||||
allowedMissions.RemoveAll(m => !m.IsAllowed(locations[0], locations[1]));
|
||||
}
|
||||
|
||||
if (allowedMissions.Count == 0)
|
||||
if (difficultyLevel.HasValue)
|
||||
{
|
||||
return null;
|
||||
allowedMissions.RemoveAll(m => !m.IsAllowedDifficulty(difficultyLevel.Value));
|
||||
}
|
||||
|
||||
int probabilitySum = allowedMissions.Sum(m => m.Commonness);
|
||||
int randomNumber = rand.NextInt32() % probabilitySum;
|
||||
foreach (MissionPrefab missionPrefab in allowedMissions)
|
||||
{
|
||||
if (randomNumber <= missionPrefab.Commonness)
|
||||
{
|
||||
return missionPrefab.Instantiate(locations, Submarine.MainSub);
|
||||
}
|
||||
randomNumber -= missionPrefab.Commonness;
|
||||
}
|
||||
|
||||
return null;
|
||||
if (allowedMissions.Count == 0) { return null; }
|
||||
MissionPrefab missionPrefab = ToolBox.SelectWeightedRandom(allowedMissions, m => m.Commonness, rand);
|
||||
return missionPrefab.Instantiate(locations, Submarine.MainSub);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -288,7 +276,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (MapEntity entityToShow in MapEntity.MapEntityList.Where(me => me.Prefab?.HasSubCategory(categoryToShow) ?? false))
|
||||
{
|
||||
entityToShow.HiddenInGame = false;
|
||||
entityToShow.IsLayerHidden = false;
|
||||
}
|
||||
}
|
||||
this.level = level;
|
||||
@@ -381,9 +369,12 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public void End()
|
||||
{
|
||||
completed =
|
||||
DetermineCompleted() &&
|
||||
(completeCheckDataAction == null ||completeCheckDataAction.GetSuccess());
|
||||
if (GameMain.NetworkMember is not { IsClient: true })
|
||||
{
|
||||
completed =
|
||||
DetermineCompleted() &&
|
||||
(completeCheckDataAction == null ||completeCheckDataAction.GetSuccess());
|
||||
}
|
||||
if (completed)
|
||||
{
|
||||
if (Prefab.LocationTypeChangeOnCompleted != null)
|
||||
|
||||
@@ -23,9 +23,9 @@ namespace Barotrauma
|
||||
Pirate = 0x200,
|
||||
GoTo = 0x400,
|
||||
ScanAlienRuins = 0x800,
|
||||
ClearAlienRuins = 0x1000,
|
||||
EliminateTargets = 0x1000,
|
||||
End = 0x2000,
|
||||
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins | End
|
||||
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | EliminateTargets | End
|
||||
}
|
||||
|
||||
partial class MissionPrefab : PrefabWithUintIdentifier
|
||||
@@ -45,7 +45,7 @@ namespace Barotrauma
|
||||
{ MissionType.Pirate, typeof(PirateMission) },
|
||||
{ MissionType.GoTo, typeof(GoToMission) },
|
||||
{ MissionType.ScanAlienRuins, typeof(ScanMission) },
|
||||
{ MissionType.ClearAlienRuins, typeof(AlienRuinMission) },
|
||||
{ MissionType.EliminateTargets, typeof(EliminateTargetsMission) },
|
||||
{ MissionType.End, typeof(EndMission) }
|
||||
};
|
||||
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
|
||||
@@ -122,7 +122,7 @@ namespace Barotrauma
|
||||
|
||||
public readonly bool AllowOtherMissionsInLevel;
|
||||
|
||||
public readonly bool RequireWreck, RequireRuin;
|
||||
public readonly bool RequireWreck, RequireRuin, RequireThalamusWreck;
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, locations this mission takes place in cannot change their type
|
||||
@@ -216,6 +216,10 @@ namespace Barotrauma
|
||||
IsSideObjective = element.GetAttributeBool("sideobjective", false);
|
||||
RequireWreck = element.GetAttributeBool("requirewreck", false);
|
||||
RequireRuin = element.GetAttributeBool("requireruin", false);
|
||||
RequireThalamusWreck = element.GetAttributeBool("requirethalamuswreck", false);
|
||||
|
||||
if (RequireThalamusWreck) { RequireWreck = true; }
|
||||
|
||||
BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false);
|
||||
RequiredLocationFaction = element.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty);
|
||||
Commonness = element.GetAttributeInt("commonness", 1);
|
||||
@@ -360,11 +364,16 @@ namespace Barotrauma
|
||||
|
||||
Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty);
|
||||
//backwards compatibility
|
||||
if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue")
|
||||
{
|
||||
missionTypeName = "AbandonedOutpost".ToIdentifier();
|
||||
}
|
||||
|
||||
if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue")
|
||||
{
|
||||
missionTypeName = nameof(MissionType.AbandonedOutpost).ToIdentifier();
|
||||
}
|
||||
else if (missionTypeName == "clearalienruins")
|
||||
{
|
||||
missionTypeName = nameof(MissionType.EliminateTargets).ToIdentifier();
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(missionTypeName.Value, true, out Type))
|
||||
{
|
||||
DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
|
||||
@@ -434,19 +443,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (Type == MissionType.Beacon)
|
||||
{
|
||||
var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to));
|
||||
if (connection?.LevelData == null || !connection.LevelData.HasBeaconStation || connection.LevelData.IsBeaconActive) { return false; }
|
||||
}
|
||||
else if (Type == MissionType.ScanAlienRuins || Type == MissionType.ClearAlienRuins)
|
||||
{
|
||||
var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to));
|
||||
if (connection?.LevelData == null || connection.LevelData.GenerationParams.GetMaxRuinCount() < 1) { return false; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inclusive (matching the min an max values is accepted).
|
||||
/// </summary>
|
||||
public bool IsAllowedDifficulty(float difficulty) => difficulty >= MinLevelDifficulty && difficulty <= MaxLevelDifficulty;
|
||||
|
||||
public Mission Instantiate(Location[] locations, Submarine sub)
|
||||
{
|
||||
|
||||
@@ -518,7 +518,9 @@ namespace Barotrauma
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
target.Item.DontCleanUp = true;
|
||||
|
||||
if (target.ParentTarget.Item.GetComponent<ItemContainer>() is ItemContainer container)
|
||||
{
|
||||
if (!container.Inventory.TryPutItem(target.Item, user: null))
|
||||
|
||||
@@ -533,7 +533,7 @@ namespace Barotrauma
|
||||
.Distinct();
|
||||
|
||||
public static IEnumerable<Item> FilterCargoCrates(IEnumerable<Item> items, Func<Item, bool> conditional = null)
|
||||
=> items.Where(it => it.HasTag(Tags.Crate) && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed && (conditional == null || conditional(it)));
|
||||
=> items.Where(it => it.HasTag(Tags.Crate) && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.IsHidden && !it.Removed && (conditional == null || conditional(it)));
|
||||
|
||||
public static IEnumerable<ItemContainer> FindReusableCargoContainers(IEnumerable<Submarine> subs, IEnumerable<Hull> cargoRooms = null) =>
|
||||
FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && !it.HasTag(Tags.CargoMissionItem) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull)))
|
||||
@@ -685,7 +685,14 @@ namespace Barotrauma
|
||||
var idCard = item.GetComponent<IdCard>();
|
||||
if (cargoManager != null && idCard != null && purchased.BuyerCharacterInfoIdentifier != 0)
|
||||
{
|
||||
cargoManager.purchasedIDCards.Add((purchased, idCard));
|
||||
if (purchased.DeliverImmediately)
|
||||
{
|
||||
InitPurchasedIDCard(purchased, idCard);
|
||||
}
|
||||
else
|
||||
{
|
||||
cargoManager.purchasedIDCards.Add((purchased, idCard));
|
||||
}
|
||||
}
|
||||
|
||||
Submarine sub = item.Submarine ?? item.RootContainer?.Submarine;
|
||||
@@ -703,18 +710,23 @@ namespace Barotrauma
|
||||
{
|
||||
foreach ((PurchasedItem purchased, IdCard idCard) in purchasedIDCards)
|
||||
{
|
||||
if (idCard != null && purchased.BuyerCharacterInfoIdentifier != 0)
|
||||
{
|
||||
var owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == purchased.BuyerCharacterInfoIdentifier);
|
||||
if (owner?.Info != null)
|
||||
{
|
||||
var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(new List<CharacterInfo>() { owner.Info }, Submarine.MainSub);
|
||||
idCard.Initialize(mainSubSpawnPoints.FirstOrDefault(), owner);
|
||||
}
|
||||
}
|
||||
InitPurchasedIDCard(purchased, idCard);
|
||||
}
|
||||
}
|
||||
|
||||
private static void InitPurchasedIDCard(PurchasedItem purchased, IdCard idCard)
|
||||
{
|
||||
if (idCard != null && purchased.BuyerCharacterInfoIdentifier != 0)
|
||||
{
|
||||
var owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == purchased.BuyerCharacterInfoIdentifier);
|
||||
if (owner?.Info != null)
|
||||
{
|
||||
var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(new List<CharacterInfo>() { owner.Info }, Submarine.MainSub);
|
||||
idCard.Initialize(mainSubSpawnPoints.FirstOrDefault(), owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab)
|
||||
{
|
||||
float floorPos = hull.Rect.Y - hull.Rect.Height;
|
||||
|
||||
@@ -99,14 +99,20 @@ namespace Barotrauma
|
||||
{
|
||||
if (order.Identifier == Tags.DeconstructThis)
|
||||
{
|
||||
Item.DeconstructItems.Add(item);
|
||||
foreach (var stackedItem in item.GetStackedItems())
|
||||
{
|
||||
Item.DeconstructItems.Add(stackedItem);
|
||||
}
|
||||
#if CLIENT
|
||||
HintManager.OnItemMarkedForDeconstruction(order.OrderGiver);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
Item.DeconstructItems.Remove(item);
|
||||
foreach (var stackedItem in item.GetStackedItems())
|
||||
{
|
||||
Item.DeconstructItems.Remove(stackedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,6 +186,17 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError("Tried to add a dead character to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
return;
|
||||
}
|
||||
if (character.Info == null)
|
||||
{
|
||||
if (character.Prefab.ContentPackage == GameMain.VanillaContent)
|
||||
{
|
||||
DebugConsole.ThrowError($"Added a character with no {nameof(CharacterInfo)} to the crew." + Environment.StackTrace.CleanupStackTrace());
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Added add a character with no {nameof(CharacterInfo)} to the crew. This may lead to issues: consider adding {nameof(CharacterPrefab.HasCharacterInfo)}=\"True\" to the character config.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!characters.Contains(character))
|
||||
{
|
||||
@@ -275,11 +292,7 @@ namespace Barotrauma
|
||||
|
||||
if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost())
|
||||
{
|
||||
spawnWaypoints = WayPoint.WayPointList.FindAll(wp =>
|
||||
wp.SpawnType == SpawnType.Human &&
|
||||
wp.Submarine == Level.Loaded.StartOutpost &&
|
||||
wp.CurrentHull != null &&
|
||||
wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier()));
|
||||
spawnWaypoints = GetOutpostSpawnpoints();
|
||||
while (spawnWaypoints.Count > characterInfos.Count)
|
||||
{
|
||||
spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count));
|
||||
@@ -301,46 +314,8 @@ namespace Barotrauma
|
||||
var info = characterInfos[i];
|
||||
info.TeamID = CharacterTeamType.Team1;
|
||||
Character character = Character.Create(info, spawnWaypoints[i].WorldPosition, info.Name);
|
||||
if (character.Info != null)
|
||||
{
|
||||
if (!character.Info.StartItemsGiven && character.Info.InventoryData != null)
|
||||
{
|
||||
DebugConsole.AddWarning($"Error when initializing a round: character \"{character.Name}\" has not been given their initial items but has saved inventory data. Using the saved inventory data instead of giving the character new items.");
|
||||
}
|
||||
if (character.Info.InventoryData != null)
|
||||
{
|
||||
character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData.FromPackage(null));
|
||||
}
|
||||
else if (!character.Info.StartItemsGiven)
|
||||
{
|
||||
character.GiveJobItems(mainSubWaypoints[i]);
|
||||
foreach (Item item in character.Inventory.AllItems)
|
||||
{
|
||||
//if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in
|
||||
//we don't want that in this case, the crew's cards shouldn't be submarine-specific
|
||||
var idCard = item.GetComponent<Items.Components.IdCard>();
|
||||
if (idCard != null)
|
||||
{
|
||||
idCard.SubmarineSpecificID = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (character.Info.HealthData != null)
|
||||
{
|
||||
CharacterInfo.ApplyHealthData(character, character.Info.HealthData);
|
||||
}
|
||||
InitializeCharacter(character, mainSubWaypoints[i], spawnWaypoints[i]);
|
||||
|
||||
character.LoadTalents();
|
||||
|
||||
character.GiveIdCardTags(mainSubWaypoints[i]);
|
||||
character.GiveIdCardTags(spawnWaypoints[i]);
|
||||
character.Info.StartItemsGiven = true;
|
||||
if (character.Info.OrderData != null)
|
||||
{
|
||||
character.Info.ApplyOrderData();
|
||||
}
|
||||
}
|
||||
|
||||
AddCharacter(character, sortCrewList: false);
|
||||
#if CLIENT
|
||||
if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; }
|
||||
@@ -355,6 +330,61 @@ namespace Barotrauma
|
||||
conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the potential crew spawnpositions for the crew in the loaded outpost
|
||||
/// </summary>
|
||||
public List<WayPoint> GetOutpostSpawnpoints()
|
||||
{
|
||||
return WayPoint.WayPointList.FindAll(wp =>
|
||||
wp.SpawnType == SpawnType.Human &&
|
||||
wp.Submarine == Level.Loaded.StartOutpost &&
|
||||
wp.CurrentHull != null &&
|
||||
wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier()));
|
||||
}
|
||||
|
||||
public void InitializeCharacter(Character character, WayPoint mainSubWaypoint, WayPoint spawnWaypoint)
|
||||
{
|
||||
if (character.Info != null)
|
||||
{
|
||||
if (!character.Info.StartItemsGiven && character.Info.InventoryData != null)
|
||||
{
|
||||
DebugConsole.AddWarning($"Error when initializing a round: character \"{character.Name}\" has not been given their initial items but has saved inventory data. Using the saved inventory data instead of giving the character new items.");
|
||||
}
|
||||
if (character.Info.InventoryData != null)
|
||||
{
|
||||
character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData.FromPackage(null));
|
||||
}
|
||||
else if (!character.Info.StartItemsGiven)
|
||||
{
|
||||
character.GiveJobItems(mainSubWaypoint);
|
||||
foreach (Item item in character.Inventory.AllItems)
|
||||
{
|
||||
//if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in
|
||||
//we don't want that in this case, the crew's cards shouldn't be submarine-specific
|
||||
var idCard = item.GetComponent<Items.Components.IdCard>();
|
||||
if (idCard != null)
|
||||
{
|
||||
idCard.SubmarineSpecificID = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (character.Info.HealthData != null)
|
||||
{
|
||||
CharacterInfo.ApplyHealthData(character, character.Info.HealthData);
|
||||
}
|
||||
|
||||
character.LoadTalents();
|
||||
|
||||
character.GiveIdCardTags(mainSubWaypoint);
|
||||
character.GiveIdCardTags(spawnWaypoint);
|
||||
character.Info.StartItemsGiven = true;
|
||||
if (character.Info.OrderData != null)
|
||||
{
|
||||
character.Info.ApplyOrderData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RenameCharacter(CharacterInfo characterInfo, string newName)
|
||||
{
|
||||
int identifier = characterInfo.GetIdentifierUsingOriginalName();
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Barotrauma
|
||||
internal struct NetWalletSetSalaryUpdate : INetSerializableStruct
|
||||
{
|
||||
[NetworkSerialize]
|
||||
public ushort Target;
|
||||
public Option<ushort> Target;
|
||||
|
||||
[NetworkSerialize(MinValueInt = 0, MaxValueInt = 100)]
|
||||
public int NewRewardDistribution;
|
||||
@@ -117,13 +117,13 @@ namespace Barotrauma
|
||||
public override int Balance
|
||||
{
|
||||
get => 0;
|
||||
set => new InvalidOperationException("Tried to set the balance on an invalid wallet");
|
||||
set => throw new InvalidOperationException("Tried to set the balance on an invalid wallet");
|
||||
}
|
||||
|
||||
public override int RewardDistribution
|
||||
{
|
||||
get => 0;
|
||||
set => new InvalidOperationException("Tried to set the reward distribution on an invalid wallet");
|
||||
set => throw new InvalidOperationException("Tried to set the reward distribution on an invalid wallet");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ namespace Barotrauma
|
||||
public const string LowerCaseSaveElementName = "wallet";
|
||||
|
||||
private const string AttributeNameBalance = "balance",
|
||||
AttrubuteNameRewardDistribution = "rewarddistribution",
|
||||
AttributeNameRewardDistribution = "rewarddistribution",
|
||||
SaveElementName = "Wallet";
|
||||
|
||||
public readonly Option<Character> Owner;
|
||||
@@ -152,7 +152,15 @@ namespace Barotrauma
|
||||
public virtual int RewardDistribution
|
||||
{
|
||||
get => rewardDistribution;
|
||||
set => rewardDistribution = ClampRewardDistribution(value);
|
||||
set
|
||||
{
|
||||
rewardDistribution = ClampRewardDistribution(value);
|
||||
|
||||
if (Owner.TryUnwrap(out var character) && character.Info is { } info)
|
||||
{
|
||||
info.LastRewardDistribution = Option.Some(rewardDistribution);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Wallet(Option<Character> owner)
|
||||
@@ -163,12 +171,14 @@ namespace Barotrauma
|
||||
public Wallet(Option<Character> owner, XElement element): this(owner)
|
||||
{
|
||||
balance = ClampBalance(element.GetAttributeInt(AttributeNameBalance, 0));
|
||||
rewardDistribution = ClampBalance(element.GetAttributeInt(AttrubuteNameRewardDistribution, 0));
|
||||
rewardDistribution = ClampBalance(element.GetAttributeInt(AttributeNameRewardDistribution, 0));
|
||||
}
|
||||
|
||||
public XElement Save()
|
||||
{
|
||||
XElement element = new XElement(SaveElementName, new XAttribute(AttributeNameBalance, Balance), new XAttribute(AttrubuteNameRewardDistribution, RewardDistribution));
|
||||
XElement element = new XElement(SaveElementName,
|
||||
new XAttribute(AttributeNameBalance, Balance),
|
||||
new XAttribute(AttributeNameRewardDistribution, RewardDistribution));
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -195,6 +205,11 @@ namespace Barotrauma
|
||||
SettingsChanged(balanceChanged: Option<int>.Some(-price), rewardChanged: Option<int>.None());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets how much salary the wallet owner should receive from mission rewards.
|
||||
/// Bank's salary determines the default salary for new characters.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public void SetRewardDistribution(int value)
|
||||
{
|
||||
int oldValue = RewardDistribution;
|
||||
|
||||
@@ -347,6 +347,10 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public event Action BeforeLevelLoading;
|
||||
|
||||
/// <summary>
|
||||
/// Triggers when saving and quitting mid-round (as in, not just transferring to a new level). Automatically cleared after triggering -> no need to unregister
|
||||
/// </summary>
|
||||
public event Action OnSaveAndQuit;
|
||||
|
||||
public override void AddExtraMissions(LevelData levelData)
|
||||
{
|
||||
@@ -472,7 +476,7 @@ namespace Barotrauma
|
||||
var missionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t == automaticMission.MissionTag)).OrderBy(m => m.UintIdentifier);
|
||||
if (missionPrefabs.Any())
|
||||
{
|
||||
var missionPrefab = ToolBox.SelectWeightedRandom(missionPrefabs, p => (float)p.Commonness, rand);
|
||||
var missionPrefab = ToolBox.SelectWeightedRandom(missionPrefabs, p => p.Commonness, rand);
|
||||
if (missionPrefab.Type == MissionType.Pirate && Missions.Any(m => m.Prefab.Type == MissionType.Pirate))
|
||||
{
|
||||
continue;
|
||||
@@ -515,8 +519,8 @@ namespace Barotrauma
|
||||
if (endLevelMissionPrefabs.Any())
|
||||
{
|
||||
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
|
||||
var endLevelMissionPrefab = ToolBox.SelectWeightedRandom(endLevelMissionPrefabs, p => (float)p.Commonness, rand);
|
||||
if (!Missions.Any(m => m.Prefab.Type == endLevelMissionPrefab.Type))
|
||||
var endLevelMissionPrefab = ToolBox.SelectWeightedRandom(endLevelMissionPrefabs, p => p.Commonness, rand);
|
||||
if (Missions.All(m => m.Prefab.Type != endLevelMissionPrefab.Type))
|
||||
{
|
||||
if (levelData.Type == LevelData.LevelType.LocationConnection)
|
||||
{
|
||||
@@ -913,6 +917,20 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles updating store stock, registering event history and relocating items (i.e. things that need to be done when saving and quitting mid-round)
|
||||
/// </summary>
|
||||
public void HandleSaveAndQuit()
|
||||
{
|
||||
OnSaveAndQuit?.Invoke();
|
||||
OnSaveAndQuit = null;
|
||||
if (Level.IsLoadedFriendlyOutpost)
|
||||
{
|
||||
UpdateStoreStock();
|
||||
}
|
||||
GameMain.GameSession.EventManager?.RegisterEventHistory(registerFinishedOnly: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates store stock before saving the game
|
||||
/// </summary>
|
||||
@@ -1368,13 +1386,13 @@ namespace Barotrauma
|
||||
{
|
||||
if (item.Removed) { continue; }
|
||||
if (item.NonInteractable || item.NonPlayerTeamInteractable) { continue; }
|
||||
if (item.HiddenInGame) { continue; }
|
||||
if (item.IsHidden) { continue; }
|
||||
if (!connectedSubs.Contains(item.Submarine)) { continue; }
|
||||
if (item.Prefab.DontTransferBetweenSubs) { continue; }
|
||||
if (AnyParentInventoryDisableTransfer(item)) { continue; }
|
||||
var rootOwner = item.GetRootInventoryOwner();
|
||||
if (rootOwner is Character) { continue; }
|
||||
if (rootOwner is Item ownerItem && (ownerItem.NonInteractable || item.NonPlayerTeamInteractable || ownerItem.HiddenInGame)) { continue; }
|
||||
if (rootOwner is Item ownerItem && (ownerItem.NonInteractable || item.NonPlayerTeamInteractable || ownerItem.IsHidden)) { continue; }
|
||||
if (item.GetComponent<Door>() != null) { continue; }
|
||||
if (item.Components.None(c => c is Pickable)) { continue; }
|
||||
if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; }
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Barotrauma
|
||||
: base(preset)
|
||||
{
|
||||
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
|
||||
var mission = Mission.LoadRandom(locations, seed, false, missionType);
|
||||
var mission = Mission.LoadRandom(locations, seed, requireCorrectLocationType: false, missionType, difficultyLevel: GameMain.NetworkMember.ServerSettings.SelectedLevelDifficulty);
|
||||
if (mission != null)
|
||||
{
|
||||
missions.Add(mission);
|
||||
|
||||
@@ -231,6 +231,12 @@ namespace Barotrauma
|
||||
{
|
||||
campaign.Bank.Deduct(selectedSub.Price);
|
||||
campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0);
|
||||
#if SERVER
|
||||
if (GameMain.Server?.ServerSettings?.NewCampaignDefaultSalary is { } salary)
|
||||
{
|
||||
campaign.Bank.SetRewardDistribution((int)Math.Round(salary, digits: 0));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
return campaign;
|
||||
}
|
||||
@@ -970,6 +976,8 @@ namespace Barotrauma
|
||||
{
|
||||
ToggleTabMenu();
|
||||
}
|
||||
DeathPrompt?.Close();
|
||||
DeathPrompt.CloseBotPanel();
|
||||
|
||||
GUI.PreventPauseMenuToggle = true;
|
||||
|
||||
|
||||
@@ -42,16 +42,25 @@ namespace Barotrauma
|
||||
{
|
||||
AvailableCharacters.ForEach(c => c.Remove());
|
||||
AvailableCharacters.Clear();
|
||||
|
||||
foreach (var missingJob in location.Type.GetHireablesMissingFromCrew())
|
||||
{
|
||||
AddCharacter(missingJob);
|
||||
amount--;
|
||||
}
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
JobPrefab job = location.Type.GetRandomHireable();
|
||||
if (job == null) { return; }
|
||||
|
||||
var variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient);
|
||||
AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant));
|
||||
AddCharacter(location.Type.GetRandomHireable());
|
||||
}
|
||||
if (location.Faction != null) { GenerateFactionCharacters(location.Faction.Prefab); }
|
||||
if (location.SecondaryFaction != null) { GenerateFactionCharacters(location.SecondaryFaction.Prefab); }
|
||||
|
||||
void AddCharacter(JobPrefab job)
|
||||
{
|
||||
if (job == null) { return; }
|
||||
int variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient);
|
||||
AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant));
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateFactionCharacters(FactionPrefab faction)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -125,7 +125,7 @@ namespace Barotrauma
|
||||
|
||||
public NetCrewMember(CharacterInfo info)
|
||||
{
|
||||
CharacterInfoID = info.GetIdentifierUsingOriginalName();
|
||||
CharacterInfoID = info.ID;
|
||||
Afflictions = ImmutableArray<NetAffliction>.Empty;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (CharacterInfo info in crew)
|
||||
{
|
||||
if (info.GetIdentifierUsingOriginalName() == CharacterInfoID)
|
||||
if (info.ID == CharacterInfoID)
|
||||
{
|
||||
return info;
|
||||
}
|
||||
@@ -159,6 +159,14 @@ namespace Barotrauma
|
||||
|
||||
private readonly CampaignMode? campaign;
|
||||
|
||||
/// <summary>
|
||||
/// Characters whose afflictions have changed (processed periodically, refreshing the UI client-side, sending updates to clients server-side
|
||||
/// </summary>
|
||||
private readonly HashSet<Character> charactersWithAfflictionChanges = new HashSet<Character>();
|
||||
|
||||
private float processAfflictionChangesTimer;
|
||||
private const float ProcessAfflictionChangesInterval = 1.0f;
|
||||
|
||||
public MedicalClinic(CampaignMode campaign)
|
||||
{
|
||||
this.campaign = campaign;
|
||||
@@ -305,35 +313,8 @@ namespace Barotrauma
|
||||
|
||||
private void OnAfflictionCountChangedPrivate(Character character)
|
||||
{
|
||||
if (character is not { CharacterHealth: { } health, Info: { } info }) { return; }
|
||||
|
||||
ImmutableArray<NetAffliction> afflictions = GetAllAfflictions(health);
|
||||
|
||||
#if CLIENT
|
||||
if (GameMain.NetworkMember is null)
|
||||
{
|
||||
ui?.UpdateAfflictions(new NetCrewMember(info, afflictions));
|
||||
}
|
||||
|
||||
ui?.UpdateCrewPanel();
|
||||
#elif SERVER
|
||||
foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList())
|
||||
{
|
||||
if (sub.Expiry < DateTimeOffset.Now)
|
||||
{
|
||||
afflictionSubscribers.Remove(sub);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sub.Target == info)
|
||||
{
|
||||
ServerSend(new NetCrewMember(info, afflictions),
|
||||
header: NetworkHeader.AFFLICTION_UPDATE,
|
||||
deliveryMethod: DeliveryMethod.Unreliable,
|
||||
targetClient: sub.Subscriber);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (character?.Info == null) { return; }
|
||||
charactersWithAfflictionChanges.Add(character);
|
||||
}
|
||||
|
||||
public int GetTotalCost() => PendingHeals.SelectMany(static h => h.Afflictions).Aggregate(0, static (current, affliction) => current + affliction.Price);
|
||||
|
||||
@@ -295,7 +295,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Purchases an item swap and handles logic for deducting the credit.
|
||||
/// </summary>
|
||||
public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false, Client? client = null)
|
||||
public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool isNetworkMessage = false, Client? client = null)
|
||||
{
|
||||
if (!CanUpgradeSub())
|
||||
{
|
||||
@@ -343,12 +343,14 @@ namespace Barotrauma
|
||||
price = itemToInstall.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count;
|
||||
}
|
||||
|
||||
if (force)
|
||||
if (isNetworkMessage)
|
||||
{
|
||||
price = 0;
|
||||
}
|
||||
|
||||
if (Campaign.TryPurchase(client, price))
|
||||
//do not try to purchase if this is a network message (if the server is telling us that an item swap was purchased)
|
||||
//we want to do the purchase no matter what, and the server handles deducting the money
|
||||
if (isNetworkMessage || Campaign.TryPurchase(client, price))
|
||||
{
|
||||
PurchasedItemSwaps.RemoveAll(p => linkedItems.Contains(p.ItemToRemove));
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
@@ -433,8 +435,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (itemToCancel.PendingItemSwap == null)
|
||||
{
|
||||
var replacement = MapEntityPrefab.Find("", swappableItem.ReplacementOnUninstall) as ItemPrefab;
|
||||
if (replacement == null)
|
||||
if (MapEntityPrefab.FindByIdentifier(swappableItem.ReplacementOnUninstall) is not ItemPrefab replacement)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to uninstall item \"{itemToCancel.Name}\". Could not find the replacement item \"{swappableItem.ReplacementOnUninstall}\".");
|
||||
return;
|
||||
@@ -786,11 +787,10 @@ namespace Barotrauma
|
||||
|
||||
private void LoadPendingUpgrades(XElement? element, bool isSingleplayer = true)
|
||||
{
|
||||
if (!(element is { HasElements: true })) { return; }
|
||||
if (element is not { HasElements: true }) { return; }
|
||||
|
||||
List<PurchasedUpgrade> pendingUpgrades = new List<PurchasedUpgrade>();
|
||||
|
||||
// ReSharper disable once LoopCanBeConvertedToQuery
|
||||
|
||||
foreach (XElement upgrade in element.Elements())
|
||||
{
|
||||
Identifier categoryIdentifier = upgrade.GetAttributeIdentifier("category", Identifier.Empty);
|
||||
|
||||
@@ -15,6 +15,19 @@ namespace Barotrauma
|
||||
|
||||
partial class CharacterInventory : Inventory
|
||||
{
|
||||
/// <summary>
|
||||
/// How much access other characters have to the inventory?
|
||||
/// <see cref="Restricted"/> = Only accessible when character is knocked down or handcuffed.
|
||||
/// <see cref="Limited"/> = Can also access inventories of bots on the same team and friendly pets.
|
||||
/// <see cref="Allowed"/> = Can also access other players in the same team (used for drag and drop give).
|
||||
/// </summary>
|
||||
public enum AccessLevel
|
||||
{
|
||||
Restricted,
|
||||
Limited,
|
||||
Allowed
|
||||
}
|
||||
|
||||
private readonly Character character;
|
||||
|
||||
public InvSlotType[] SlotTypes
|
||||
@@ -22,8 +35,7 @@ namespace Barotrauma
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static readonly List<InvSlotType> AnySlot = new List<InvSlotType>() { InvSlotType.Any };
|
||||
|
||||
public static bool IsHandSlotType(InvSlotType s) => s.HasFlag(InvSlotType.LeftHand) || s.HasFlag(InvSlotType.RightHand);
|
||||
@@ -546,6 +558,7 @@ namespace Barotrauma
|
||||
{
|
||||
item.AssignCampaignInteractionType(CampaignMode.InteractionType.None);
|
||||
}
|
||||
item.Equipper = user;
|
||||
}
|
||||
|
||||
protected override void CreateNetworkEvent(Range slotRange)
|
||||
|
||||
@@ -658,7 +658,8 @@ namespace Barotrauma.Items.Components
|
||||
hulls[i] = new Hull(hullRects[i], subs[i])
|
||||
{
|
||||
RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch",
|
||||
AvoidStaying = true
|
||||
AvoidStaying = true,
|
||||
IsWetRoom = true
|
||||
};
|
||||
hulls[i].AddToGrid(subs[i]);
|
||||
hulls[i].FreeID();
|
||||
|
||||
@@ -180,6 +180,22 @@ namespace Barotrauma.Items.Components
|
||||
OpenState = isOpen ? 1.0f : 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used by status effects to tell the door to open (setting IsOpen directly would make it immediately fully open)
|
||||
/// </summary>
|
||||
public bool ShouldBeOpen
|
||||
{
|
||||
get { return isOpen; }
|
||||
set
|
||||
{
|
||||
if (isOpen != value)
|
||||
{
|
||||
ToggleState(ActionType.OnUse, user: null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsClosed => !IsOpen;
|
||||
|
||||
public bool IsFullyOpen => IsOpen && OpenState >= 1.0f;
|
||||
|
||||
@@ -896,9 +896,9 @@ namespace Barotrauma.Items.Components
|
||||
return element;
|
||||
}
|
||||
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap);
|
||||
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
|
||||
flowerTiles = componentElement.GetAttributeIntArray("flowertiles", Array.Empty<int>())!;
|
||||
Decayed = componentElement.GetAttributeBool("decayed", false);
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Abilities;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using FarseerPhysics.Dynamics.Contacts;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -41,6 +43,9 @@ namespace Barotrauma.Items.Components
|
||||
private bool attachable, attached, attachedByDefault;
|
||||
private Voronoi2.VoronoiCell attachTargetCell;
|
||||
private PhysicsBody body;
|
||||
|
||||
public readonly ImmutableDictionary<StatTypes, float> HoldableStatValues;
|
||||
|
||||
public PhysicsBody Pusher
|
||||
{
|
||||
get;
|
||||
@@ -287,6 +292,22 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
characterUsable = element.GetAttributeBool("characterusable", true);
|
||||
|
||||
Dictionary<StatTypes, float> statValues = new Dictionary<StatTypes, float>();
|
||||
foreach (var subElement in element.GetChildElements("statvalue"))
|
||||
{
|
||||
StatTypes statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), Name);
|
||||
float statValue = subElement.GetAttributeFloat("value", 0f);
|
||||
if (statValues.ContainsKey(statType))
|
||||
{
|
||||
statValues[statType] += statValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
statValues.TryAdd(statType, statValue);
|
||||
}
|
||||
}
|
||||
HoldableStatValues = statValues.ToImmutableDictionary();
|
||||
}
|
||||
|
||||
private bool OnPusherCollision(Fixture sender, Fixture other, Contact contact)
|
||||
@@ -304,9 +325,9 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
private bool loadedFromInstance;
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap);
|
||||
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
|
||||
|
||||
loadedFromInstance = true;
|
||||
|
||||
|
||||
@@ -296,7 +296,7 @@ namespace Barotrauma.Items.Components
|
||||
impactQueue.Clear();
|
||||
item.body.FarseerBody.OnCollision -= OnCollision;
|
||||
item.body.CollisionCategories = Physics.CollisionItem;
|
||||
item.body.CollidesWith = Physics.CollisionWall;
|
||||
item.body.CollidesWith = Physics.DefaultItemCollidesWith;
|
||||
item.body.FarseerBody.IsBullet = false;
|
||||
item.body.PhysEnabled = false;
|
||||
}
|
||||
|
||||
@@ -251,6 +251,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
foreach (ConnectionPanel connectionPanel in item.GetComponents<ConnectionPanel>())
|
||||
{
|
||||
connectionPanel.DisconnectedWires.Clear();
|
||||
foreach (Connection c in connectionPanel.Connections)
|
||||
{
|
||||
foreach (Wire w in c.Wires.ToArray())
|
||||
@@ -260,7 +261,7 @@ namespace Barotrauma.Items.Components
|
||||
w.Item.SetTransform(pos, 0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Drop(Character dropper, bool setTransform = true)
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components
|
||||
if (!MathUtils.IsValid(dir)) { return true; }
|
||||
float length = 200;
|
||||
dir = dir.ClampLength(length) / length;
|
||||
Vector2 propulsion = dir * Force * character.PropulsionSpeedMultiplier;
|
||||
Vector2 propulsion = dir * Force * character.PropulsionSpeedMultiplier * (1.0f + character.GetStatValue(StatTypes.PropulsionSpeed));
|
||||
if (character.AnimController.InWater && Force > 0.0f) { character.AnimController.TargetMovement = dir; }
|
||||
|
||||
foreach (Limb limb in character.AnimController.Limbs)
|
||||
|
||||
+19
-16
@@ -272,22 +272,6 @@ namespace Barotrauma.Items.Components
|
||||
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
|
||||
}
|
||||
|
||||
ignoredBodies.Clear();
|
||||
foreach (Limb l in character.AnimController.Limbs)
|
||||
{
|
||||
if (l.IsSevered) { continue; }
|
||||
ignoredBodies.Add(l.body.FarseerBody);
|
||||
}
|
||||
|
||||
foreach (Item heldItem in character.HeldItems)
|
||||
{
|
||||
var holdable = heldItem.GetComponent<Holdable>();
|
||||
if (holdable?.Pusher != null)
|
||||
{
|
||||
ignoredBodies.Add(holdable.Pusher.FarseerBody);
|
||||
}
|
||||
}
|
||||
|
||||
float degreeOfFailure = 1.0f - DegreeOfSuccess(character);
|
||||
degreeOfFailure *= degreeOfFailure;
|
||||
if (degreeOfFailure > Rand.Range(0.0f, 1.0f))
|
||||
@@ -311,6 +295,25 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier;
|
||||
projectile.Launcher = item;
|
||||
|
||||
ignoredBodies.Clear();
|
||||
if (!projectile.DamageUser)
|
||||
{
|
||||
foreach (Limb l in character.AnimController.Limbs)
|
||||
{
|
||||
if (l.IsSevered) { continue; }
|
||||
ignoredBodies.Add(l.body.FarseerBody);
|
||||
}
|
||||
|
||||
foreach (Item heldItem in character.HeldItems)
|
||||
{
|
||||
var holdable = heldItem.GetComponent<Holdable>();
|
||||
if (holdable?.Pusher != null)
|
||||
{
|
||||
ignoredBodies.Add(holdable.Pusher.FarseerBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse);
|
||||
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
|
||||
if (projectile.Item.body != null)
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace Barotrauma.Items.Components
|
||||
else
|
||||
{
|
||||
throwAngle = ThrowAngleStart;
|
||||
ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, holdAngle);
|
||||
ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -941,14 +941,16 @@ namespace Barotrauma.Items.Components
|
||||
#endif
|
||||
}
|
||||
|
||||
public virtual void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
|
||||
public virtual void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
|
||||
{
|
||||
if (componentElement != null)
|
||||
{
|
||||
foreach (XAttribute attribute in componentElement.Attributes())
|
||||
{
|
||||
if (!SerializableProperties.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; }
|
||||
if (property.OverridePrefabValues || !usePrefabValues)
|
||||
if (property.OverridePrefabValues ||
|
||||
!usePrefabValues ||
|
||||
(isItemSwap && property.GetAttribute<Editable>() is { TransferToSwappedItem: true }))
|
||||
{
|
||||
property.TrySetValue(this, attribute.Value);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -12,7 +13,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class ItemContainer : ItemComponent, IDrawableComponent
|
||||
{
|
||||
readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition);
|
||||
readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition, bool BlameEquipperForDeath);
|
||||
|
||||
readonly record struct ContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation);
|
||||
|
||||
@@ -252,6 +253,8 @@ namespace Barotrauma.Items.Components
|
||||
private float autoInjectCooldown = 1.0f;
|
||||
const float AutoInjectInterval = 1.0f;
|
||||
|
||||
private bool subContainersCanAutoInject;
|
||||
|
||||
|
||||
public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined)
|
||||
{
|
||||
@@ -277,6 +280,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public readonly bool HasSubContainers;
|
||||
|
||||
public bool hasSignalConnections;
|
||||
|
||||
private string totalConditionValueString = "", totalConditionPercentageString = "", totalItemsString = "";
|
||||
private float prevTotalConditionValue = 0, prevTotalConditionPercentage = 0; int prevTotalItems = 0;
|
||||
|
||||
public ItemContainer(Item item, ContentXElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -323,6 +331,8 @@ namespace Barotrauma.Items.Components
|
||||
int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize);
|
||||
bool autoInject = subElement.GetAttributeBool("autoinject", false);
|
||||
|
||||
subContainersCanAutoInject |= autoInject;
|
||||
|
||||
var subContainableItems = new List<RelatedItem>();
|
||||
foreach (var subSubElement in subElement.Elements())
|
||||
{
|
||||
@@ -411,7 +421,12 @@ namespace Barotrauma.Items.Components
|
||||
relatedItem ??= containableItem;
|
||||
foreach (StatusEffect effect in containableItem.StatusEffects)
|
||||
{
|
||||
activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition));
|
||||
activeContainedItems.Add(new ActiveContainedItem(
|
||||
containedItem,
|
||||
effect,
|
||||
containableItem.ExcludeBroken,
|
||||
containableItem.ExcludeFullCondition,
|
||||
containableItem.BlameEquipperForDeath));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -448,8 +463,8 @@ namespace Barotrauma.Items.Components
|
||||
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningPlanted:" + containedItem.Prefab.Identifier);
|
||||
}
|
||||
|
||||
//no need to Update() if this item has no statuseffects and no physics body
|
||||
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
|
||||
//no need to Update() if this item has no statuseffects and no physics body, and if there are no signal connections.
|
||||
IsActive = hasSignalConnections || activeContainedItems.Count > 0 || Inventory.AllItems.Any(static it => it.body != null);
|
||||
|
||||
if (IsActive && item.GetRootInventoryOwner() is Character owner &&
|
||||
owner.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand)))
|
||||
@@ -481,11 +496,16 @@ namespace Barotrauma.Items.Components
|
||||
containedItems.RemoveAll(i => i.Item == containedItem);
|
||||
item.SetContainedItemPositions();
|
||||
//deactivate if the inventory is empty
|
||||
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
|
||||
IsActive = hasSignalConnections || activeContainedItems.Count > 0 || Inventory.AllItems.Any(static it => it.body != null);
|
||||
CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
|
||||
OnContainedItemsChanged.Invoke(this);
|
||||
}
|
||||
|
||||
public bool BlameEquipperForDeath()
|
||||
{
|
||||
return activeContainedItems.Any(c => c.BlameEquipperForDeath);
|
||||
}
|
||||
|
||||
public bool CanBeContained(Item item)
|
||||
{
|
||||
if (!AllowAccessWhenDropped && this.item.body is { Enabled: true }) { return false; }
|
||||
@@ -545,11 +565,47 @@ namespace Barotrauma.Items.Components
|
||||
alwaysContainedItemsSpawned = true;
|
||||
}
|
||||
|
||||
if (hasSignalConnections)
|
||||
{
|
||||
float totalConditionValue = 0, totalConditionPercentage = 0; int totalItems = 0;
|
||||
foreach (var item in Inventory.AllItems)
|
||||
{
|
||||
if (!MathUtils.NearlyEqual(item.Condition, 0))
|
||||
{
|
||||
totalConditionValue += item.Condition;
|
||||
totalConditionPercentage += item.ConditionPercentage;
|
||||
totalItems++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!MathUtils.NearlyEqual(totalConditionValue, prevTotalConditionValue))
|
||||
{
|
||||
totalConditionValueString = ((int)totalConditionValue).ToString(CultureInfo.InvariantCulture);
|
||||
prevTotalConditionValue = totalConditionValue;
|
||||
}
|
||||
|
||||
if (!MathUtils.NearlyEqual(totalConditionPercentage, prevTotalConditionPercentage))
|
||||
{
|
||||
totalConditionPercentageString = ((int)totalConditionPercentage).ToString(CultureInfo.InvariantCulture);
|
||||
prevTotalConditionPercentage = totalConditionPercentage;
|
||||
}
|
||||
|
||||
if (totalItems != prevTotalItems)
|
||||
{
|
||||
totalItemsString = totalItems.ToString(CultureInfo.InvariantCulture);
|
||||
prevTotalItems = totalItems;
|
||||
}
|
||||
|
||||
item.SendSignal(totalConditionValueString, "contained_conditions");
|
||||
item.SendSignal(totalConditionPercentageString, "contained_conditions_percentage");
|
||||
item.SendSignal(totalItemsString, "contained_items");
|
||||
}
|
||||
|
||||
if (item.ParentInventory is CharacterInventory ownerInventory)
|
||||
{
|
||||
SetContainedItemPositionsIfNeeded();
|
||||
|
||||
if (AutoInject || slotRestrictions.Any(s => s.AutoInject))
|
||||
if (AutoInject || subContainersCanAutoInject)
|
||||
{
|
||||
//normally autoinjection should delete the (medical) item, so it only gets applied once
|
||||
//but in multiplayer clients aren't allowed to remove items themselves, so they may be able to trigger this dozens of times
|
||||
@@ -595,7 +651,7 @@ namespace Barotrauma.Items.Components
|
||||
SetContainedItemPositionsIfNeeded();
|
||||
}
|
||||
}
|
||||
else if (activeContainedItems.Count == 0)
|
||||
else if (!hasSignalConnections && activeContainedItems.Count == 0)
|
||||
{
|
||||
IsActive = false;
|
||||
return;
|
||||
@@ -987,6 +1043,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
Inventory.AllowSwappingContainedItems = AllowSwappingContainedItems;
|
||||
containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty<Identifier>()).ToImmutableHashSet();
|
||||
hasSignalConnections = item.Connections?.Any(c => c.Name is "contained_conditions" or "contained_conditions_percentage" or "contained_items") ?? false;
|
||||
if (item.Submarine == null || !item.Submarine.Loading)
|
||||
{
|
||||
SpawnAlwaysContainedItems();
|
||||
@@ -1087,9 +1144,9 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap);
|
||||
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
|
||||
|
||||
string containedString = componentElement.GetAttributeString("contained", "");
|
||||
string[] itemIdStrings = containedString.Split(',');
|
||||
|
||||
@@ -589,9 +589,9 @@ namespace Barotrauma.Items.Components
|
||||
return SaveLimbPositions(base.Save(parentElement));
|
||||
}
|
||||
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap);
|
||||
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
|
||||
if (GameMain.GameSession?.GameMode?.Preset == GameModePreset.TestMode)
|
||||
{
|
||||
LoadLimbPositions(componentElement);
|
||||
|
||||
@@ -6,7 +6,7 @@ using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Engine : Powered, IServerSerializable, IClientSerializable
|
||||
partial class Engine : Powered, IServerSerializable, IClientSerializable, IDeteriorateUnderStress
|
||||
{
|
||||
private float force;
|
||||
|
||||
@@ -76,10 +76,7 @@ namespace Barotrauma.Items.Components
|
||||
set { force = MathHelper.Clamp(value, -100.0f, 100.0f); }
|
||||
}
|
||||
|
||||
public float CurrentVolume
|
||||
{
|
||||
get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage, 1.0f))); }
|
||||
}
|
||||
public float CurrentVolume => CurrentStress;
|
||||
|
||||
public float CurrentBrokenVolume
|
||||
{
|
||||
@@ -90,6 +87,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public float CurrentStress => Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage, 1.0f)));
|
||||
|
||||
private const float TinkeringForceIncrease = 1.5f;
|
||||
|
||||
public Engine(Item item, ContentXElement element)
|
||||
|
||||
@@ -707,9 +707,15 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item)
|
||||
{
|
||||
CharacterType mustHaveRecipe = GameMain.GameSession?.GameMode is { IsSinglePlayer: true } ?
|
||||
//in single player it doesn't matter if it's a bot or a player who has the recipe
|
||||
//(the bots can turn into a "player" when switching characters, and that could interrupt the fabrication)
|
||||
CharacterType.Both :
|
||||
//in MP the recipes other players have don't cound
|
||||
CharacterType.Bot;
|
||||
return
|
||||
(user != null && user.HasRecipeForItem(item.Identifier)) ||
|
||||
GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier));
|
||||
GameSession.GetSessionCrewCharacters(mustHaveRecipe).Any(c => c.HasRecipeForItem(item.Identifier));
|
||||
}
|
||||
|
||||
private readonly HashSet<Item> usedIngredients = new HashSet<Item>();
|
||||
@@ -986,9 +992,9 @@ namespace Barotrauma.Items.Components
|
||||
return componentElement;
|
||||
}
|
||||
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap);
|
||||
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
|
||||
savedFabricatedItem = componentElement.GetAttributeString("fabricateditemidentifier", "");
|
||||
savedTimeUntilReady = componentElement.GetAttributeFloat("savedtimeuntilready", 0.0f);
|
||||
savedRequiredTime = componentElement.GetAttributeFloat("savedrequiredtime", 0.0f);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user