OBT/1.2.1(Summer update)
Sync with upstream
This commit is contained in:
@@ -197,6 +197,18 @@ namespace Barotrauma
|
||||
|
||||
private readonly List<Body> myBodies;
|
||||
|
||||
#if SERVER
|
||||
/// <summary>
|
||||
/// How often the server can send messages about a limb targeting some attack target.
|
||||
/// Mainly relevant for attacks with no cooldown, e.g. fractal guardian's steam cannons which run continuously over time (we can't send events every frame)
|
||||
/// </summary>
|
||||
private const double MinSetAttackTargetEventInterval = 0.5;
|
||||
private IDamageable lastDamageTarget;
|
||||
private Limb lastTargetLimb;
|
||||
private Limb lastAttackLimb;
|
||||
private double lastSetAttackTargetEventTime;
|
||||
#endif
|
||||
|
||||
public LatchOntoAI LatchOntoAI { get; private set; }
|
||||
public SwarmBehavior SwarmBehavior { get; private set; }
|
||||
public PetBehavior PetBehavior { get; private set; }
|
||||
@@ -2679,11 +2691,19 @@ namespace Barotrauma
|
||||
if (!ActiveAttack.IsRunning)
|
||||
{
|
||||
#if SERVER
|
||||
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
|
||||
AttackLimb,
|
||||
damageTarget,
|
||||
targetLimb,
|
||||
SimPosition));
|
||||
if (Timing.TotalTime > lastSetAttackTargetEventTime + MinSetAttackTargetEventInterval ||
|
||||
damageTarget != lastDamageTarget || AttackLimb != lastAttackLimb || targetLimb != lastTargetLimb)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
|
||||
AttackLimb,
|
||||
damageTarget,
|
||||
targetLimb,
|
||||
SimPosition));
|
||||
lastSetAttackTargetEventTime = Timing.TotalTime;
|
||||
lastDamageTarget = damageTarget;
|
||||
lastAttackLimb = AttackLimb;
|
||||
lastTargetLimb = targetLimb;
|
||||
}
|
||||
#else
|
||||
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
|
||||
#endif
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace Barotrauma
|
||||
|
||||
private float respondToAttackTimer;
|
||||
private const float RespondToAttackInterval = 1.0f;
|
||||
private bool wasConscious;
|
||||
private bool wasDead;
|
||||
|
||||
private bool freezeAI;
|
||||
|
||||
@@ -201,7 +201,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (isIncapacitated) { return; }
|
||||
|
||||
wasConscious = true;
|
||||
wasDead = false;
|
||||
|
||||
respondToAttackTimer -= deltaTime;
|
||||
if (respondToAttackTimer <= 0.0f)
|
||||
@@ -1256,14 +1256,15 @@ namespace Barotrauma
|
||||
|
||||
public override void OnAttacked(Character attacker, AttackResult attackResult)
|
||||
{
|
||||
// The attack incapacitated/killed the character: respond immediately to trigger nearby characters because the update loop no longer runs
|
||||
if (wasConscious && (Character.IsIncapacitated || Character.Stun > 0.0f))
|
||||
// If the character is incapacitated or dead, respond to the attack anyway to let other nearby characters react to it
|
||||
// (But if the character is already dead, and was dead before this attack, don't react)
|
||||
if (Character.IsDead && wasDead) { return; }
|
||||
if (Character.IsIncapacitated || Character.Stun > 0.0f)
|
||||
{
|
||||
RespondToAttack(attacker, attackResult);
|
||||
wasConscious = false;
|
||||
wasDead = Character.IsDead;
|
||||
return;
|
||||
}
|
||||
if (Character.IsDead) { return; }
|
||||
if (attacker == null || Character.IsPlayer)
|
||||
{
|
||||
// The player characters need to "respond" to the attack always, because the update loop doesn't run for them.
|
||||
@@ -1467,10 +1468,10 @@ namespace Barotrauma
|
||||
otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) ||
|
||||
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
|
||||
if (!isWitnessing)
|
||||
{
|
||||
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID)
|
||||
{
|
||||
if (Character.IsKnockedDown || otherCharacter.TeamID != Character.TeamID)
|
||||
{
|
||||
// Dead or in different team -> cannot report.
|
||||
// Knocked down or in different team -> cannot report.
|
||||
continue;
|
||||
}
|
||||
if (otherHumanAI.objectiveManager.HasOrders())
|
||||
@@ -1494,6 +1495,14 @@ namespace Barotrauma
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (!otherCharacter.IsSecurity)
|
||||
{
|
||||
//witnessed the attack as non-security, trigger security
|
||||
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID))
|
||||
{
|
||||
TriggerSecurity(security.AIController as HumanAIController, attacker, DetermineCombatMode(security, cumulativeDamage, isWitnessing));
|
||||
}
|
||||
}
|
||||
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 3.0f, Rand.RandSync.Unsynced);
|
||||
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
|
||||
}
|
||||
@@ -1926,12 +1935,12 @@ namespace Barotrauma
|
||||
character.IsCriminal = true;
|
||||
character.IsActingOffensively = true;
|
||||
}
|
||||
if (!TriggerSecurity(otherHumanAI, combatMode))
|
||||
if (!TriggerSecurity(otherHumanAI, character, combatMode))
|
||||
{
|
||||
// Else call the others
|
||||
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
|
||||
{
|
||||
if (!TriggerSecurity(security.AIController as HumanAIController, combatMode))
|
||||
if (!TriggerSecurity(security.AIController as HumanAIController, character, combatMode))
|
||||
{
|
||||
// Only alert one guard at a time
|
||||
return;
|
||||
@@ -1941,25 +1950,25 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
bool TriggerSecurity(HumanAIController humanAI, AIObjectiveCombat.CombatMode combatMode)
|
||||
{
|
||||
if (humanAI == null) { return false; }
|
||||
if (!humanAI.Character.IsSecurity) { return false; }
|
||||
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
|
||||
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(),
|
||||
onCompleted: () =>
|
||||
{
|
||||
//if the target is arrested successfully, reset the damage accumulator
|
||||
foreach (Character anyCharacter in Character.CharacterList)
|
||||
}
|
||||
private static bool TriggerSecurity(HumanAIController humanAI, Character targetCharacter, AIObjectiveCombat.CombatMode combatMode)
|
||||
{
|
||||
if (humanAI == null) { return false; }
|
||||
if (!humanAI.Character.IsSecurity) { return false; }
|
||||
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
|
||||
humanAI.AddCombatObjective(combatMode, targetCharacter, delay: GetReactionTime(),
|
||||
onCompleted: () =>
|
||||
{
|
||||
//if the target is arrested successfully, reset the damage accumulator
|
||||
foreach (Character anyCharacter in Character.CharacterList)
|
||||
{
|
||||
if (anyCharacter.AIController is HumanAIController anyAI)
|
||||
{
|
||||
if (anyCharacter.AIController is HumanAIController anyAI)
|
||||
{
|
||||
anyAI.structureDamageAccumulator?.Remove(character);
|
||||
}
|
||||
anyAI.structureDamageAccumulator?.Remove(targetCharacter);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void ItemTaken(Item item, Character thief)
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ namespace Barotrauma
|
||||
bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull));
|
||||
if (operateExtinguisher)
|
||||
{
|
||||
character.CursorPosition = fs.Position;
|
||||
character.CursorPosition = FarseerPhysics.ConvertUnits.ToDisplayUnits(Submarine.GetRelativeSimPositionFromWorldPosition(fs.WorldPosition, character.Submarine, fs.Submarine));
|
||||
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
|
||||
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2);
|
||||
if (extinguisherItem.RequireAimToUse)
|
||||
|
||||
+10
@@ -173,6 +173,16 @@ namespace Barotrauma
|
||||
if (character.CanInteractWith(Item, out _, checkLinked: false))
|
||||
{
|
||||
waitTimer += deltaTime;
|
||||
|
||||
//if we're climbing upwards to the item, ensure the character stays within arm's length of it
|
||||
//without this, the character can get stuck in a loop where the GoTo objective takes them close enough to the item,
|
||||
//then the character shifts a bit downwards on the ladder and goes outside interaction range, and the GoTo objective kicks in again
|
||||
if (character.IsClimbing &&
|
||||
Item.WorldPosition.Y > character.WorldPosition.Y + FarseerPhysics.ConvertUnits.ToDisplayUnits(character.AnimController.ArmLength))
|
||||
{
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.UnitY);
|
||||
}
|
||||
|
||||
if (waitTimer < WaitTimeBeforeRepair) { return; }
|
||||
|
||||
HumanAIController.FaceTarget(Item);
|
||||
|
||||
@@ -758,6 +758,11 @@ namespace Barotrauma
|
||||
public float CoolDownTimer { get; set; }
|
||||
public float CurrentRandomCoolDown { get; private set; }
|
||||
public float SecondaryCoolDownTimer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The attack is considered to be running from the moment it starts until the <see cref="AttackTimer"/> reaches the <see cref="Duration"/> of the attack, or until the attack lands successfully.
|
||||
/// E.g. from the moment the monster decides to lunge itself towards the target until it hits a target or until it completes that lunge.
|
||||
/// </summary>
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
public float AfterAttackTimer { get; set; }
|
||||
|
||||
@@ -997,6 +997,9 @@ namespace Barotrauma
|
||||
public bool IsForceRagdolled;
|
||||
public bool FollowCursor = true;
|
||||
|
||||
/// <summary>
|
||||
/// Is the character currently dead, unconscious or paralyzed?
|
||||
/// </summary>
|
||||
public bool IsIncapacitated
|
||||
{
|
||||
get
|
||||
@@ -1006,6 +1009,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the character dead or below 0 vitality and not able to stay conscious?
|
||||
/// </summary>
|
||||
public bool IsUnconscious
|
||||
{
|
||||
get { return CharacterHealth.IsUnconscious; }
|
||||
@@ -1673,7 +1679,8 @@ namespace Barotrauma
|
||||
AnimController.FindHull(setInWater: true);
|
||||
if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; }
|
||||
|
||||
IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass <= 30.0f);
|
||||
//mass < 35 = husk chimera is the largest vanilla monster that can be contained by default
|
||||
IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass < 35.0f);
|
||||
|
||||
CharacterList.Add(this);
|
||||
|
||||
@@ -2262,7 +2269,10 @@ namespace Barotrauma
|
||||
{
|
||||
Vector2 targetMovement = GetTargetMovement();
|
||||
AnimController.TargetMovement = targetMovement;
|
||||
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
|
||||
if (SelectedItem?.GetComponent<Controller>() is not { ControlCharacterPose: true })
|
||||
{
|
||||
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
|
||||
}
|
||||
}
|
||||
|
||||
if (AnimController is HumanoidAnimController humanAnimController)
|
||||
@@ -3508,9 +3518,11 @@ namespace Barotrauma
|
||||
|
||||
UpdateAttackers(deltaTime);
|
||||
|
||||
foreach (var characterTalent in characterTalents)
|
||||
//use a for loop instead of foreach because talents can unlock other talents via StatusEffectAction (see #17328)
|
||||
//this way we'll just add them to the end of the list without causing a collection was modified exception
|
||||
for (int i = 0; i < characterTalents.Count; i++)
|
||||
{
|
||||
characterTalent.UpdateTalent(deltaTime);
|
||||
characterTalents[i].UpdateTalent(deltaTime);
|
||||
}
|
||||
|
||||
if (IsDead) { return; }
|
||||
@@ -5801,6 +5813,12 @@ namespace Barotrauma
|
||||
return info.UnlockedTalents.Contains(identifier);
|
||||
}
|
||||
|
||||
public bool IsTalentLocked(Identifier talentIdentifier)
|
||||
{
|
||||
if (info == null) { return true; }
|
||||
return Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1;
|
||||
}
|
||||
|
||||
public bool HasUnlockedAllTalents()
|
||||
{
|
||||
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
|
||||
|
||||
@@ -1022,6 +1022,15 @@ namespace Barotrauma
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
|
||||
#if SERVER
|
||||
/// <summary>
|
||||
/// How often the server can send messages about attacks being executed. Note that the timer is per-limb: if one limb executes an attack immediately after another, an network event can still be created.
|
||||
/// Mainly relevant for attacks with no cooldown, e.g. fractal guardian's steam cannons which run continuously over time (we can't send events every frame)
|
||||
/// </summary>
|
||||
private const double MinExecuteAttackEventInterval = 0.5f;
|
||||
private double lastExecuteAttackEventTime;
|
||||
#endif
|
||||
|
||||
private readonly List<Body> contactBodies = new List<Body>();
|
||||
/// <summary>
|
||||
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
|
||||
@@ -1142,9 +1151,13 @@ namespace Barotrauma
|
||||
ExecuteAttack(damageTarget, targetLimb, out attackResult);
|
||||
}
|
||||
#if SERVER
|
||||
GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData(
|
||||
attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb,
|
||||
targetSimPos: attackSimPos));
|
||||
if (Timing.TotalTime > lastExecuteAttackEventTime + MinExecuteAttackEventInterval)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData(
|
||||
attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb,
|
||||
targetSimPos: attackSimPos));
|
||||
lastExecuteAttackEventTime = Timing.TotalTime;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -43,14 +43,14 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
foreach (Identifier identifier in option.TalentIdentifiers)
|
||||
{
|
||||
if (IsShowCaseTalent(identifier, option) || TalentTree.IsTalentLocked(identifier, characters)) { continue; }
|
||||
if (IsShowCaseTalent(identifier, option) || Character.IsTalentLocked(identifier)) { continue; }
|
||||
|
||||
identifiers.Add(identifier);
|
||||
}
|
||||
|
||||
foreach (var (_, value) in option.ShowCaseTalents)
|
||||
{
|
||||
var ids = value.Where(i => !TalentTree.IsTalentLocked(i, characters)).ToImmutableHashSet();
|
||||
var ids = value.Where(i => !Character.IsTalentLocked(i)).ToImmutableHashSet();
|
||||
if (ids.Count is 0) { continue; }
|
||||
|
||||
identifiers.Add(value.GetRandomUnsynced());
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace Barotrauma
|
||||
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
|
||||
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
|
||||
|
||||
if (IsTalentLocked(talentIdentifier, Character.GetFriendlyCrew(character))) { return false; }
|
||||
if (character.IsTalentLocked(talentIdentifier)) { return false; }
|
||||
|
||||
if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier))
|
||||
{
|
||||
@@ -163,16 +163,6 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsTalentLocked(Identifier talentIdentifier, IEnumerable<Character> characterList)
|
||||
{
|
||||
foreach (Character c in characterList)
|
||||
{
|
||||
if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static List<Identifier> CheckTalentSelection(Character controlledCharacter, IEnumerable<Identifier> selectedTalents)
|
||||
{
|
||||
List<Identifier> viableTalents = new List<Identifier>();
|
||||
|
||||
Reference in New Issue
Block a user