Merge branch 'heads/upstream' into OBT/1.2.0(SpringUpdate)
This commit is contained in:
@@ -551,9 +551,14 @@ namespace Barotrauma
|
||||
|
||||
private static void UnlockKillAchievement(Character killer, Character target, Identifier identifier)
|
||||
{
|
||||
if (killer != null &&
|
||||
target.Params.UnlockKillAchievementForWholeCrew &&
|
||||
GameSession.GetSessionCrewCharacters(CharacterType.Player).Contains(killer))
|
||||
bool alwaysUnlockForWholeCrew = false;
|
||||
#if CLIENT
|
||||
alwaysUnlockForWholeCrew = GameMain.GameSession?.Campaign is SinglePlayerCampaign;
|
||||
#endif
|
||||
|
||||
if (killer != null &&
|
||||
(alwaysUnlockForWholeCrew || target.Params.UnlockKillAchievementForWholeCrew) &&
|
||||
GameSession.GetSessionCrewCharacters(CharacterType.Both).Contains(killer))
|
||||
{
|
||||
UnlockAchievement(identifier, unlockClients: true, characterConditions: c => c != null);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A multiplier for the sound range for the purposes of displaying the target on sonar.
|
||||
/// E.g. a value of 10 would mean the sonar can detect the target from x10 further than monsters.
|
||||
/// </summary>
|
||||
public float SoundRangeOnSonarMultiplier { get; private set; } = 1.0f;
|
||||
|
||||
public float SightRange
|
||||
{
|
||||
get { return sightRange; }
|
||||
@@ -206,6 +213,7 @@ namespace Barotrauma
|
||||
MinSoundRange = element.GetAttributeFloat("minsoundrange", 0f);
|
||||
MaxSightRange = element.GetAttributeFloat("maxsightrange", SightRange);
|
||||
MaxSoundRange = element.GetAttributeFloat("maxsoundrange", SoundRange);
|
||||
SoundRangeOnSonarMultiplier = element.GetAttributeFloat(nameof(SoundRangeOnSonarMultiplier), 1.0f);
|
||||
FadeOutTime = element.GetAttributeFloat("fadeouttime", FadeOutTime);
|
||||
Static = element.GetAttributeBool("static", Static);
|
||||
StaticSight = element.GetAttributeBool("staticsight", StaticSight);
|
||||
|
||||
@@ -242,16 +242,44 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The monster won't try to damage these submarines
|
||||
/// The monster won't try to damage these submarines. Applies to hulls, structures and static items (items without a physics body) belonging to these submarines. Does not apply to non-static items, e.g. flares or other provocative items.
|
||||
/// </summary>
|
||||
public HashSet<Submarine> UnattackableSubmarines
|
||||
private readonly HashSet<Submarine> unattackableSubmarines = [];
|
||||
|
||||
/// <summary>
|
||||
/// Set the submarine(s) the monster won't attack. Applies to hulls, structures and static items (items without a physics body) belonging to these submarines. Does not apply to non-static items, e.g. flares or other provocative items.
|
||||
/// </summary>
|
||||
public void SetUnattackableSubmarines(Submarine submarine, bool includeOwnSub = true, bool includeConnectedSubs = true, bool clearExisting = true)
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = new HashSet<Submarine>();
|
||||
if (clearExisting)
|
||||
{
|
||||
unattackableSubmarines.Clear();
|
||||
}
|
||||
if (submarine != null)
|
||||
{
|
||||
AddSubs(submarine);
|
||||
}
|
||||
if (includeOwnSub && Character.Submarine is Submarine ownSub && ownSub != submarine)
|
||||
{
|
||||
AddSubs(ownSub);
|
||||
}
|
||||
|
||||
void AddSubs(Submarine sub)
|
||||
{
|
||||
unattackableSubmarines.Add(sub);
|
||||
if (includeConnectedSubs)
|
||||
{
|
||||
foreach (Submarine connectedSub in sub.DockedTo)
|
||||
{
|
||||
unattackableSubmarines.Add(connectedSub);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsTargetBeingChasedBy(Character target, Character character)
|
||||
=> character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && enemyAI.State is AIState.Attack or AIState.Aggressive;
|
||||
|
||||
public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c);
|
||||
private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character);
|
||||
|
||||
@@ -539,26 +567,7 @@ namespace Barotrauma
|
||||
//doesn't do anything usually, but events may sometimes change monsters' (or pets' that use enemy AI) teams
|
||||
Character.UpdateTeam();
|
||||
|
||||
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
|
||||
if (steeringManager == insideSteering)
|
||||
{
|
||||
var currPath = PathSteering.CurrentPath;
|
||||
if (currPath != null && currPath.CurrentNode != null)
|
||||
{
|
||||
if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
|
||||
{
|
||||
// Don't allow to jump from too high.
|
||||
float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2;
|
||||
float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y);
|
||||
ignorePlatforms = height < allowedJumpHeight;
|
||||
}
|
||||
}
|
||||
if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent)
|
||||
{
|
||||
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
|
||||
}
|
||||
}
|
||||
Character.AnimController.IgnorePlatforms = ignorePlatforms;
|
||||
HandleLaddersAndPlatforms(deltaTime);
|
||||
|
||||
if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater &&
|
||||
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character))
|
||||
@@ -986,6 +995,69 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
//how often the character can try ragdolling to drop down
|
||||
private const float MaxDroppingInterval = 5.0f;
|
||||
|
||||
//last time the character tried ragdolling to drop down
|
||||
private double lastDroppingTime;
|
||||
|
||||
//how long the character can stay ragdolled to drop down
|
||||
private const float MaxDroppingTime = 1.0f;
|
||||
|
||||
//timer for the duration of the ragdolling
|
||||
private float droppingTimer;
|
||||
|
||||
private void HandleLaddersAndPlatforms(float deltaTime)
|
||||
{
|
||||
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
|
||||
if (steeringManager == insideSteering)
|
||||
{
|
||||
var currPath = PathSteering.CurrentPath;
|
||||
if (currPath is { CurrentNode: WayPoint currentNode })
|
||||
{
|
||||
Vector2 colliderBottom = Character.AnimController.GetColliderBottom();
|
||||
if (Character.Submarine != currentNode.Submarine)
|
||||
{
|
||||
colliderBottom = Submarine.GetRelativeSimPosition(colliderBottom, currentNode.Submarine, Character.Submarine);
|
||||
}
|
||||
if (currentNode.SimPosition.Y < colliderBottom.Y)
|
||||
{
|
||||
// Don't allow to jump from too high.
|
||||
float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2;
|
||||
Vector2 diff = currentNode.WorldPosition - Character.WorldPosition;
|
||||
float height = ConvertUnits.ToSimUnits(Math.Abs(diff.Y));
|
||||
ignorePlatforms = height < allowedJumpHeight;
|
||||
|
||||
//trying to head down ladders, but can't climb -> periodically try ragdolling to get down
|
||||
//(may be required by large monsters like mudraptors to fit through hatches)
|
||||
if (ignorePlatforms && !Character.CanClimb && PathSteering.IsCurrentNodeLadder &&
|
||||
ConvertUnits.ToSimUnits(Math.Abs(diff.X)) < Character.AnimController.Collider.GetMaxExtent())
|
||||
{
|
||||
if (lastDroppingTime < Timing.TotalTime - MaxDroppingInterval)
|
||||
{
|
||||
Character.IsRagdolled = true;
|
||||
Character.SetInput(InputType.Ragdoll, hit: false, held: true);
|
||||
droppingTimer += deltaTime;
|
||||
if (droppingTimer > MaxDroppingTime)
|
||||
{
|
||||
lastDroppingTime = Timing.TotalTime;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
droppingTimer = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent)
|
||||
{
|
||||
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
|
||||
}
|
||||
}
|
||||
Character.AnimController.IgnorePlatforms = ignorePlatforms;
|
||||
}
|
||||
|
||||
#region Idle
|
||||
|
||||
private void UpdateIdle(float deltaTime, bool followLastTarget = true)
|
||||
@@ -1229,6 +1301,8 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (Character.IsAttachedToController()) { return; }
|
||||
|
||||
attackWorldPos = SelectedAiTarget.WorldPosition;
|
||||
attackSimPos = SelectedAiTarget.SimPosition;
|
||||
|
||||
@@ -1751,6 +1825,7 @@ namespace Barotrauma
|
||||
{
|
||||
SelectTarget(door.Item.AiTarget, currentTargetMemory.Priority);
|
||||
State = AIState.Attack;
|
||||
AttackLimb = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1761,12 +1836,20 @@ namespace Barotrauma
|
||||
float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max;
|
||||
if ((!canAttack || distance > margin) && !IsTryingToSteerThroughGap)
|
||||
{
|
||||
bool useManualSteering = false;
|
||||
// Steer towards the target if in the same room and swimming
|
||||
// Ruins have walls/pillars inside hulls and therefore we should navigate around them using the path steering.
|
||||
if (Character.CurrentHull != null &&
|
||||
Character.Submarine != null && !Character.Submarine.Info.IsRuin &&
|
||||
(Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
|
||||
targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull))
|
||||
{
|
||||
if (CanSeeTarget(targetCharacter))
|
||||
{
|
||||
useManualSteering = true;
|
||||
}
|
||||
}
|
||||
if (useManualSteering)
|
||||
{
|
||||
Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition;
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - myPos));
|
||||
@@ -2311,18 +2394,49 @@ namespace Barotrauma
|
||||
{
|
||||
float prio = 1 + limb.attack.Priority;
|
||||
if (Character.AnimController.SimplePhysicsEnabled) { return prio; }
|
||||
float dist = Vector2.Distance(limb.WorldPosition, attackPos);
|
||||
float distanceFactor = 1;
|
||||
float distance = Vector2.Distance(limb.WorldPosition, attackPos);
|
||||
float maxDistance = Math.Max(limb.attack.Range * 3, 1000);
|
||||
if (distance > maxDistance)
|
||||
{
|
||||
// Far enough to ignore the attack.
|
||||
return 0;
|
||||
}
|
||||
// Not in range, but relatively close. Let's use the distance factor as a multiplier.
|
||||
float distanceFactor;
|
||||
if (limb.attack.Ranged)
|
||||
{
|
||||
float min = 100;
|
||||
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, Math.Max(limb.attack.Range / 2, min), dist));
|
||||
if (distance < min)
|
||||
{
|
||||
// Too close -> smoothly but steeply reduce the preference (and prefer other attacks, like melee instead)
|
||||
float t = MathUtils.InverseLerp(0, min, distance);
|
||||
distanceFactor = MathHelper.Lerp(0.01f, 1, t * t);
|
||||
}
|
||||
else
|
||||
{
|
||||
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, maxDistance, distance));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it.
|
||||
// We also need a max value that is more than the actual range.
|
||||
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist));
|
||||
if (distance <= limb.attack.Range)
|
||||
{
|
||||
// In range.
|
||||
if (!Character.InWater)
|
||||
{
|
||||
// On dry land vertical distance works a bit differently, as we can't necessarily reach the target above/below us.
|
||||
float verticalDistance = Math.Abs(limb.WorldPosition.Y - attackPos.Y);
|
||||
if (verticalDistance > limb.attack.DamageRange)
|
||||
{
|
||||
// Most likely can't reach.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// Highly prefer attacks which we can use to hit immediately.
|
||||
return prio * 10;
|
||||
}
|
||||
float min = limb.attack.Range;
|
||||
distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, maxDistance, distance));
|
||||
}
|
||||
return prio * distanceFactor;
|
||||
}
|
||||
@@ -2521,6 +2635,7 @@ namespace Barotrauma
|
||||
{
|
||||
SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound: true).Priority);
|
||||
State = AIState.Attack;
|
||||
AttackLimb = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2555,14 +2670,10 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (damageTarget != null)
|
||||
{
|
||||
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
|
||||
item.Use(deltaTime, user: Character);
|
||||
}
|
||||
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
|
||||
item.Use(deltaTime, user: Character);
|
||||
}
|
||||
}
|
||||
if (damageTarget == null) { return true; }
|
||||
//simulate attack input to get the character to attack client-side
|
||||
Character.SetInput(InputType.Attack, true, true);
|
||||
if (!ActiveAttack.IsRunning)
|
||||
@@ -2609,10 +2720,24 @@ namespace Barotrauma
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private const float VisibilityCheckStep = 0.2f;
|
||||
private double lastVisibilityCheckTime;
|
||||
private bool canSeeTarget;
|
||||
/// <summary>
|
||||
/// This method uses <see cref="Character.CanSeeTarget"/> and caches the results.
|
||||
/// </summary>
|
||||
private bool CanSeeTarget(ISpatialEntity target)
|
||||
{
|
||||
if (Timing.TotalTime > lastVisibilityCheckTime + VisibilityCheckStep)
|
||||
{
|
||||
canSeeTarget = Character.CanSeeTarget(target);
|
||||
lastVisibilityCheckTime = Timing.TotalTime;
|
||||
}
|
||||
return canSeeTarget;
|
||||
}
|
||||
|
||||
private float aimTimer;
|
||||
private float visibilityCheckTimer;
|
||||
private bool canSeeTarget;
|
||||
private float sinTime;
|
||||
private bool Aim(float deltaTime, ISpatialEntity target, Item weapon)
|
||||
{
|
||||
@@ -2630,13 +2755,7 @@ namespace Barotrauma
|
||||
{
|
||||
Character.CursorPosition -= Character.Submarine.Position;
|
||||
}
|
||||
visibilityCheckTimer -= deltaTime;
|
||||
if (visibilityCheckTimer <= 0.0f)
|
||||
{
|
||||
canSeeTarget = Character.CanSeeTarget(target);
|
||||
visibilityCheckTimer = 0.2f;
|
||||
}
|
||||
if (!canSeeTarget)
|
||||
if (!CanSeeTarget(target))
|
||||
{
|
||||
SetAimTimer();
|
||||
return false;
|
||||
@@ -2817,7 +2936,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff) * 3);
|
||||
Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos);
|
||||
if (Character.AnimController.OnGround || Character.InWater)
|
||||
{
|
||||
Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, maxVelocity: 10.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2956,12 +3078,18 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ignore all structures, items, and hulls inside these subs.
|
||||
if (aiTarget.Entity.Submarine != null)
|
||||
if (aiTarget.Entity.Submarine != null)
|
||||
{
|
||||
//ignore all items, structures and hulls in wrecks and beacon stations
|
||||
//(we don't want monsters to be distracted by them during missions,
|
||||
//nor have monsters inside them attack "their home" rather than the player)
|
||||
if (aiTarget.Entity.Submarine.Info.IsWreck ||
|
||||
aiTarget.Entity.Submarine.Info.IsBeacon ||
|
||||
UnattackableSubmarines.Contains(aiTarget.Entity.Submarine))
|
||||
aiTarget.Entity.Submarine.Info.IsBeacon)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (aiTarget.Entity is Structure or Hull or Item { body: null } &&
|
||||
unattackableSubmarines.Contains(aiTarget.Entity.Submarine))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -3509,13 +3637,16 @@ namespace Barotrauma
|
||||
{
|
||||
if (targetCharacter.Submarine != null)
|
||||
{
|
||||
// Target is inside -> reduce the priority
|
||||
valueModifier *= 0.5f;
|
||||
if (Character.Submarine != null)
|
||||
if (Character.Submarine != null && !targetCharacter.Submarine.IsConnectedTo(Character.Submarine))
|
||||
{
|
||||
// Both inside different submarines -> can ignore safely
|
||||
// Both inside different, unconnected submarines -> can ignore safely
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Target is inside a submarine that we are not -> reduce the priority
|
||||
valueModifier *= 0.5f;
|
||||
}
|
||||
}
|
||||
else if (Character.CurrentHull != null)
|
||||
{
|
||||
@@ -4402,6 +4533,7 @@ namespace Barotrauma
|
||||
{
|
||||
SelectTarget(doorAiTarget, CurrentTargetMemory.Priority);
|
||||
State = AIState.Attack;
|
||||
AttackLimb = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1380,7 +1380,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.AlienInfectedType) > 0;
|
||||
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.AlienInfectionType) > 0;
|
||||
// Inform other NPCs
|
||||
if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using FarseerPhysics;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -50,7 +51,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if any node in the path is in stairs
|
||||
/// Returns true if any node in the path is on stairs
|
||||
/// </summary>
|
||||
public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
|
||||
|
||||
@@ -285,14 +286,17 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 diff = DiffToCurrentNode();
|
||||
Vector2 diff = GetDiffAndAdvance();
|
||||
if (diff == Vector2.Zero) { return Vector2.Zero; }
|
||||
return Vector2.Normalize(diff) * weight;
|
||||
}
|
||||
|
||||
protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight);
|
||||
|
||||
private Vector2 DiffToCurrentNode()
|
||||
|
||||
/// <summary>
|
||||
/// Decides whether and when we should skip to the next node. Returns the difference to the current node (after skipping).
|
||||
/// </summary>
|
||||
private Vector2 GetDiffAndAdvance()
|
||||
{
|
||||
if (currentPath == null || currentPath.Unreachable)
|
||||
{
|
||||
@@ -320,26 +324,37 @@ namespace Barotrauma
|
||||
Reset();
|
||||
return Vector2.Zero;
|
||||
}
|
||||
Vector2 pos = host.WorldPosition;
|
||||
Vector2 diff = currentPath.CurrentNode.WorldPosition - pos;
|
||||
WayPoint currentNode = currentPath.CurrentNode;
|
||||
WayPoint nextNode = currentPath.NextNode;
|
||||
Vector2 diff = currentNode.WorldPosition - host.WorldPosition;
|
||||
float horizontalDistance = Math.Abs(diff.X);
|
||||
float verticalDistance = Math.Abs(diff.Y);
|
||||
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
|
||||
bool canClimb = character.CanClimb;
|
||||
Ladder currentLadder = GetCurrentLadder();
|
||||
Ladder nextLadder = GetNextLadder();
|
||||
var ladders = currentLadder ?? nextLadder;
|
||||
Ladder ladders = currentLadder ?? nextLadder;
|
||||
bool useLadders = canClimb && ladders != null;
|
||||
var collider = character.AnimController.Collider;
|
||||
Vector2 colliderSize = collider.GetSize();
|
||||
Vector2 colliderSize = ConvertUnits.ToDisplayUnits(collider.GetSize());
|
||||
float colliderHeight = colliderSize.Y;
|
||||
if (character.AnimController.CurrentAnimationParams is FishGroundedParams fishGrounded)
|
||||
{
|
||||
// On monsters, the main collider might be rotated, so we need to take that into account here.
|
||||
float standAngle = fishGrounded.ColliderStandAngleInRadians * character.AnimController.Dir;
|
||||
Vector2 transformedColliderSize = PhysicsBody.RotateVector(colliderSize, standAngle);
|
||||
colliderHeight = Math.Abs(transformedColliderSize.Y);
|
||||
}
|
||||
if (useLadders)
|
||||
{
|
||||
if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y))
|
||||
if (character.IsClimbing && Math.Abs(diff.X) - colliderSize.X > Math.Abs(diff.Y))
|
||||
{
|
||||
// If the current node is horizontally farther from us than vertically, we don't want to keep climbing the ladders.
|
||||
useLadders = false;
|
||||
}
|
||||
else if (!character.IsClimbing && currentPath.NextNode != null && nextLadder == null)
|
||||
else if (!character.IsClimbing && nextNode != null && nextLadder == null)
|
||||
{
|
||||
Vector2 diffToNextNode = currentPath.NextNode.WorldPosition - pos;
|
||||
Vector2 diffToNextNode = nextNode.WorldPosition - host.WorldPosition;
|
||||
if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y))
|
||||
{
|
||||
// If the next node is horizontally farther from us than vertically, we don't want to start climbing.
|
||||
@@ -356,7 +371,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (currentPath.IsAtEndNode && canClimb && ladders != null)
|
||||
{
|
||||
// Don't release the ladders when ending a path in ladders.
|
||||
// Don't release the ladders when ending a path on ladders.
|
||||
useLadders = true;
|
||||
}
|
||||
else
|
||||
@@ -388,20 +403,18 @@ namespace Barotrauma
|
||||
if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item)
|
||||
{
|
||||
// Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
|
||||
NextNode(!doorsChecked);
|
||||
return NextNode(!doorsChecked);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool nextLadderSameAsCurrent = currentLadder == nextLadder;
|
||||
float colliderHeight = collider.Height / 2 + collider.Radius;
|
||||
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
|
||||
float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
|
||||
float distanceMargin = colliderSize.X;
|
||||
if (currentLadder != null && nextLadder != null)
|
||||
{
|
||||
//climbing ladders -> don't move horizontally
|
||||
diff.X = 0.0f;
|
||||
}
|
||||
if (Math.Abs(heightDiff) < colliderHeight * 1.25f)
|
||||
if (verticalDistance < colliderHeight / 2 * 1.25f)
|
||||
{
|
||||
if (nextLadder != null && !nextLadderSameAsCurrent)
|
||||
{
|
||||
@@ -410,7 +423,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
|
||||
{
|
||||
NextNode(!doorsChecked);
|
||||
return NextNode(!doorsChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,9 +445,9 @@ namespace Barotrauma
|
||||
}
|
||||
if (isAboveFloor)
|
||||
{
|
||||
if (Math.Abs(diff.Y) < distanceMargin)
|
||||
if (verticalDistance < distanceMargin)
|
||||
{
|
||||
NextNode(!doorsChecked);
|
||||
return NextNode(!doorsChecked);
|
||||
}
|
||||
else if (!currentPath.IsAtEndNode && (nextLadder == null || (currentLadder != null && Math.Abs(currentLadder.Item.WorldPosition.X - nextLadder.Item.WorldPosition.X) > distanceMargin)))
|
||||
{
|
||||
@@ -443,14 +456,21 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (currentLadder != null && currentPath.NextNode != null)
|
||||
else if (currentLadder != null && nextNode != null)
|
||||
{
|
||||
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
|
||||
if (Math.Sign(currentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(nextNode.WorldPosition.Y - character.WorldPosition.Y))
|
||||
{
|
||||
//if the current node is below the character and the next one is above (or vice versa)
|
||||
//and both are on ladders, we can skip directly to the next one
|
||||
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
|
||||
NextNode(!doorsChecked);
|
||||
return NextNode(!doorsChecked);
|
||||
}
|
||||
//heading towards a ladder waypoint below the character, but the next waypoint is above it on the same ladder
|
||||
// -> allow skipping to that waypoint.
|
||||
// Otherwise the character may get stuck trying to move to a waypoint near the floor at the bottom of the ladder, failing to get close enough because they can't move any lower.
|
||||
else if (nextLadderSameAsCurrent && diff.Y < 0 && nextNode.WorldPosition.Y > currentNode.WorldPosition.Y)
|
||||
{
|
||||
return NextNode(!doorsChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -458,21 +478,20 @@ namespace Barotrauma
|
||||
else if (character.AnimController.InWater)
|
||||
{
|
||||
// Swimming
|
||||
var door = currentPath.CurrentNode.ConnectedDoor;
|
||||
var door = currentNode.ConnectedDoor;
|
||||
if (door == null || door.CanBeTraversed)
|
||||
{
|
||||
float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
|
||||
float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f);
|
||||
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
|
||||
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
|
||||
if (character.CurrentHull != currentPath.CurrentNode.CurrentHull)
|
||||
float distanceMultiplier = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
|
||||
float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * distanceMultiplier, 0.5f);
|
||||
float modifiedVerticalDist = verticalDistance;
|
||||
if (character.CurrentHull != currentNode.CurrentHull)
|
||||
{
|
||||
verticalDistance *= 2;
|
||||
modifiedVerticalDist *= 2;
|
||||
}
|
||||
float distance = horizontalDistance + verticalDistance;
|
||||
if (ConvertUnits.ToSimUnits(distance) < targetDistance)
|
||||
float distance = horizontalDistance + modifiedVerticalDist;
|
||||
if (distance < targetDistance)
|
||||
{
|
||||
NextNode(!doorsChecked);
|
||||
return NextNode(!doorsChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -480,6 +499,10 @@ namespace Barotrauma
|
||||
{
|
||||
// Walking horizontally
|
||||
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
|
||||
if (character.Submarine != currentNode.Submarine)
|
||||
{
|
||||
colliderBottom = Submarine.GetRelativeSimPosition(colliderBottom, currentNode.Submarine, character.Submarine);
|
||||
}
|
||||
Vector2 velocity = collider.LinearVelocity;
|
||||
// If the character is very short, it would fail to use the waypoint nodes because they are always too high.
|
||||
// If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small.
|
||||
@@ -487,60 +510,113 @@ namespace Barotrauma
|
||||
float minHeight = 1.6125001f;
|
||||
float minWidth = 0.3225f;
|
||||
// Cannot use the head position, because not all characters have head or it can be below the total height of the character
|
||||
float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
|
||||
float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
|
||||
bool isTargetTooHigh = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y + characterHeight;
|
||||
bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y;
|
||||
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;
|
||||
if (currentPath.CurrentNode.Stairs == null)
|
||||
float characterHeight = Math.Max(ConvertUnits.ToSimUnits(colliderHeight) + character.AnimController.ColliderHeightFromFloor, minHeight);
|
||||
bool isTargetTooHigh = currentNode.SimPosition.Y > colliderBottom.Y + characterHeight;
|
||||
bool isTargetTooLow = currentNode.SimPosition.Y < colliderBottom.Y;
|
||||
var door = currentNode.ConnectedDoor;
|
||||
float targetDistanceMultiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
|
||||
if (currentNode.Stairs == null)
|
||||
{
|
||||
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
|
||||
if (heightDiff < colliderHeight)
|
||||
// Only attempt dropping if the node is below the collider bottom.
|
||||
// Using the next node position here, because the current node might be on the top of the ladder, which can be at the same level with the character or even above it.
|
||||
bool isBelowEnough = (nextNode ?? currentNode).WorldPosition.Y < character.WorldPosition.Y - colliderHeight / 2;
|
||||
bool drop = false;
|
||||
if (isBelowEnough)
|
||||
{
|
||||
// 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.
|
||||
if (!canClimb)
|
||||
{
|
||||
// Can't climb -> check if we should drop.
|
||||
Door nextDoor = door ?? nextNode?.ConnectedDoor;
|
||||
if (nextDoor is Door { IsHorizontal: true, CanBeTraversed: true } openHatch)
|
||||
{
|
||||
bool isHatchBelowCharacter = openHatch.LinkedGap.WorldPosition.Y < character.WorldPosition.Y;
|
||||
if (isHatchBelowCharacter)
|
||||
{
|
||||
// Trying to go through an open hatch below us -> drop.
|
||||
drop = true;
|
||||
}
|
||||
}
|
||||
else if (currentLadder != null && !isTargetTooLow && nextDoor == null)
|
||||
{
|
||||
// On ladders -> drop.
|
||||
drop = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (drop)
|
||||
{
|
||||
return NextNode(!doorsChecked);
|
||||
}
|
||||
else if (verticalDistance < colliderHeight / 2)
|
||||
{
|
||||
// The waypoint is between the top and bottom of the collider, and we don't intend to drop -> no need to move vertically.
|
||||
diff.Y = 0.0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// In stairs
|
||||
bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs;
|
||||
// On stairs
|
||||
bool isNextNodeInSameStairs = nextNode?.Stairs == currentNode.Stairs;
|
||||
if (!isNextNodeInSameStairs)
|
||||
{
|
||||
margin = 1;
|
||||
if (currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f)
|
||||
targetDistanceMultiplier = 1;
|
||||
if (currentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f)
|
||||
{
|
||||
isTargetTooLow = true;
|
||||
}
|
||||
Structure nextStairs = nextNode?.Stairs;
|
||||
if (character.AnimController.Stairs != null && nextStairs != null)
|
||||
{
|
||||
//currently on stairs, and the next node is not in the same stairs
|
||||
// -> we must get off the current stairs first before we can skip to the next node, otherwise the character
|
||||
// would attempt to get "through the stairs" to the next ones
|
||||
if (character.AnimController.Stairs.StairDirection == Direction.Right)
|
||||
{
|
||||
//the direction in which the bot should keep moving depends on the direction of the stairs and whether we're going up or down
|
||||
diff = nextStairs.WorldPosition.Y > character.AnimController.Stairs.WorldPosition.Y ? Vector2.UnitX : -Vector2.UnitX;
|
||||
}
|
||||
else
|
||||
{
|
||||
diff = nextStairs.WorldPosition.Y > character.AnimController.Stairs.WorldPosition.Y ? -Vector2.UnitX : Vector2.UnitX;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
|
||||
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow)
|
||||
// Walking horizontally, check whether we are close enough to the current node.
|
||||
float targetDistance = Math.Max(colliderSize.X / 2 * targetDistanceMultiplier, ConvertUnits.ToDisplayUnits(minWidth / 2));
|
||||
Debug.Assert(targetDistance < 500, "Target distance too large (a character is trying to skip on their path to a waypoint far away), something is probably off here.");
|
||||
if (!isTargetTooHigh && !isTargetTooLow && horizontalDistance < targetDistance)
|
||||
{
|
||||
if (door is not { CanBeTraversed: false } && (currentLadder == null || nextLadder == null))
|
||||
bool isBlockedByDoor = door is { CanBeTraversed: false };
|
||||
// If both the current ladder and the next ladder are not null, we are in the middle of ladders and should let the code above handle advancing the nodes.
|
||||
// However, if either one is null, and we get here, we are probably walking to or from ladders.
|
||||
bool notOnLadders = currentLadder == null || nextLadder == null;
|
||||
if (!isBlockedByDoor && notOnLadders)
|
||||
{
|
||||
NextNode(!doorsChecked);
|
||||
return NextNode(!doorsChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentPath.CurrentNode == null)
|
||||
return ReturnDiff();
|
||||
|
||||
Vector2 NextNode(bool checkDoors)
|
||||
{
|
||||
return Vector2.Zero;
|
||||
if (checkDoors)
|
||||
{
|
||||
CheckDoorsInPath();
|
||||
}
|
||||
currentPath.SkipToNextNode();
|
||||
return ReturnDiff();
|
||||
}
|
||||
return ConvertUnits.ToSimUnits(diff);
|
||||
}
|
||||
|
||||
private void NextNode(bool checkDoors)
|
||||
{
|
||||
if (checkDoors)
|
||||
|
||||
Vector2 ReturnDiff()
|
||||
{
|
||||
CheckDoorsInPath();
|
||||
if (currentPath.CurrentNode == null)
|
||||
{
|
||||
return Vector2.Zero;
|
||||
}
|
||||
return ConvertUnits.ToSimUnits(diff);
|
||||
}
|
||||
currentPath.SkipToNextNode();
|
||||
}
|
||||
|
||||
public bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
|
||||
@@ -600,8 +676,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 GetColliderSize() => ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize());
|
||||
|
||||
private float GetColliderLength()
|
||||
{
|
||||
Vector2 colliderSize = character.AnimController.Collider.GetSize();
|
||||
@@ -676,7 +750,7 @@ namespace Barotrauma
|
||||
if (door.LinkedGap.IsHorizontal)
|
||||
{
|
||||
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
|
||||
float size = character.AnimController.InWater ? colliderLength : GetColliderSize().X;
|
||||
float size = character.AnimController.InWater ? colliderLength : ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize()).X;
|
||||
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -size;
|
||||
}
|
||||
else
|
||||
@@ -794,12 +868,17 @@ namespace Barotrauma
|
||||
if (character == null) { return 0.0f; }
|
||||
float? penalty = GetSingleNodePenalty(nextNode);
|
||||
if (penalty == null) { return null; }
|
||||
Vector2 nextNodePosition = nextNode.Position;
|
||||
if (nextNode.Waypoint.Submarine != node.Waypoint.Submarine)
|
||||
{
|
||||
nextNodePosition = Submarine.GetRelativeSimPosition(nextNodePosition, node.Waypoint.Submarine, nextNode.Waypoint.Submarine);
|
||||
}
|
||||
bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y;
|
||||
if (!character.CanClimb && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null)
|
||||
{
|
||||
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands) ||
|
||||
(nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
|
||||
nextNodeAboveWaterLevel)) //upper node not underwater
|
||||
(nextNodePosition.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
|
||||
nextNodeAboveWaterLevel)) //upper node not underwater
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -830,7 +909,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y);
|
||||
float yDist = Math.Abs(node.Position.Y - nextNodePosition.Y);
|
||||
if (nextNodeAboveWaterLevel && node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null)
|
||||
{
|
||||
penalty += yDist * 10.0f;
|
||||
@@ -898,18 +977,14 @@ namespace Barotrauma
|
||||
//steer away from edges of the hull
|
||||
bool wander = false;
|
||||
bool inWater = character.AnimController.InWater;
|
||||
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;
|
||||
// }
|
||||
// }
|
||||
|
||||
//use the hull the legs are in (if one is found), so the character won't walk against the wall when their torso is in a different hull where there'd be room to walk further
|
||||
//(e.g. if the character is in a shallow pool-type room, like in ResearchModule_01_Colony)
|
||||
Hull currentHull =
|
||||
character.AnimController.GetLimb(LimbType.RightLeg)?.Hull ??
|
||||
character.AnimController.GetLimb(LimbType.LeftLeg)?.Hull ??
|
||||
character.CurrentHull;
|
||||
|
||||
if (currentHull != null && !inWater)
|
||||
{
|
||||
float roomWidth = currentHull.Rect.Width;
|
||||
|
||||
@@ -103,9 +103,19 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
// For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something.
|
||||
// The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority.
|
||||
public bool ForceWalk { get; set; }
|
||||
/// <summary>
|
||||
/// For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something.
|
||||
/// The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority.
|
||||
/// </summary>
|
||||
public bool ForceWalkTemporarily { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Forces the character to walk when executing this objective, even if the priority is above <see cref="AIObjectiveManager.RunPriority"/>.
|
||||
/// Unlike <see cref="ForceWalkTemporarily"/>, this value is not automatically reset.
|
||||
/// </summary>
|
||||
public bool ForceWalkPermanently { get; set; }
|
||||
|
||||
public bool ForceWalk => ForceWalkTemporarily || ForceWalkPermanently;
|
||||
|
||||
public bool IgnoreAtOutpost { get; set; }
|
||||
|
||||
@@ -313,7 +323,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public float CalculatePriority()
|
||||
{
|
||||
ForceWalk = false;
|
||||
ForceWalkTemporarily = false;
|
||||
Priority = GetPriority();
|
||||
ForceHighestPriority = false;
|
||||
return Priority;
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ namespace Barotrauma
|
||||
if (subObjectives.All(so => so.SubObjectives.None()))
|
||||
{
|
||||
// If none of the subobjectives have subobjectives, no valid container was found. Don't allow running.
|
||||
ForceWalk = true;
|
||||
ForceWalkTemporarily = true;
|
||||
}
|
||||
return prio;
|
||||
}
|
||||
|
||||
+10
-8
@@ -258,13 +258,14 @@ namespace Barotrauma
|
||||
|
||||
protected override bool CheckObjectiveState()
|
||||
{
|
||||
if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine)
|
||||
// In a friendly outpost, and the target is still in the outpost
|
||||
if (character.Submarine is { Info.IsOutpost: true } && character.IsOnFriendlyTeam(character.Submarine.TeamID) &&
|
||||
character.Submarine == Enemy.Submarine)
|
||||
{
|
||||
// Target still in the outpost
|
||||
// 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.
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsSecurity)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -286,7 +287,8 @@ namespace Barotrauma
|
||||
{
|
||||
allowCooldown = true;
|
||||
// Target not in the outpost anymore.
|
||||
if (character.CanSeeTarget(Enemy))
|
||||
if (character.Submarine.IsConnectedTo(Enemy.Submarine) &&
|
||||
character.CanSeeTarget(Enemy))
|
||||
{
|
||||
allowCooldown = false;
|
||||
coolDownTimer = DefaultCoolDown;
|
||||
@@ -389,7 +391,7 @@ namespace Barotrauma
|
||||
HumanAIController.AutoFaceMovement = false;
|
||||
if (!gotoObjective.ShouldRun(true))
|
||||
{
|
||||
ForceWalk = true;
|
||||
ForceWalkTemporarily = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -468,7 +470,7 @@ namespace Barotrauma
|
||||
isMoving = true;
|
||||
if (!IsEnemyClose(MeleeDistance))
|
||||
{
|
||||
ForceWalk = true;
|
||||
ForceWalkTemporarily = true;
|
||||
}
|
||||
HumanAIController.FaceTarget(Enemy);
|
||||
HumanAIController.AutoFaceMovement = false;
|
||||
@@ -1234,7 +1236,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (isAimBlocked)
|
||||
{
|
||||
ForceWalk = true;
|
||||
ForceWalkTemporarily = true;
|
||||
}
|
||||
if (!followTargetObjective.IsCloseEnough)
|
||||
{
|
||||
|
||||
+5
-1
@@ -95,7 +95,11 @@ namespace Barotrauma
|
||||
if (potentialDeconstructor?.InputContainer == null) { continue; }
|
||||
if (!potentialDeconstructor.InputContainer.Inventory.CanBePut(Item)) { continue; }
|
||||
if (!potentialDeconstructor.Item.HasAccess(character)) { continue; }
|
||||
if (Item.Prefab.DeconstructItems.None(it => it.IsValidDeconstructor(otherItem))) { continue; }
|
||||
if (Item.Prefab.DeconstructItems.Any() &&
|
||||
Item.Prefab.DeconstructItems.None(it => it.IsValidDeconstructor(otherItem)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
float distFactor = GetDistanceFactor(Item.WorldPosition, potentialDeconstructor.Item.WorldPosition, factorAtMaxDistance: 0.2f);
|
||||
if (distFactor > bestDistFactor)
|
||||
{
|
||||
|
||||
+5
-1
@@ -64,7 +64,11 @@ namespace Barotrauma
|
||||
if (target == null || target.Removed) { return false; }
|
||||
//bots can't handle deconstructing items that require another item to deconstruct, let's not try to do that
|
||||
//in the vanilla game, this means unidentified genetic materials, which we don't want to "deconstruct" anyway
|
||||
if (target.Prefab.DeconstructItems.All(d => d.RequiredOtherItem.Length > 0)) { return false; }
|
||||
if (target.Prefab.DeconstructItems.Any() &&
|
||||
target.Prefab.DeconstructItems.All(d => d.RequiredOtherItem.Length > 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// If the target was selected as a valid target, we'll have to accept it so that the objective can be completed.
|
||||
// The validity changes when a character picks the item up.
|
||||
if (!IsValidTarget(target, character, checkInventory: true))
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ namespace Barotrauma
|
||||
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f);
|
||||
}
|
||||
// Prevents running into the flames.
|
||||
objectiveManager.CurrentObjective.ForceWalk = true;
|
||||
objectiveManager.CurrentObjective.ForceWalkTemporarily = true;
|
||||
}
|
||||
if (moveCloser)
|
||||
{
|
||||
|
||||
+2
@@ -11,6 +11,8 @@ namespace Barotrauma
|
||||
public override Identifier Identifier { get; set; } = "extinguish fires".ToIdentifier();
|
||||
public override bool ForceRun => true;
|
||||
protected override bool AllowInAnySub => true;
|
||||
// Periodically clear the ignore list so that fires abandoned when fumbling with finding an extinguisher, navigating etc get reconsidered
|
||||
protected override float IgnoreListClearInterval => 30;
|
||||
|
||||
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
|
||||
+22
-7
@@ -49,6 +49,13 @@ namespace Barotrauma
|
||||
public const float DefaultReach = 100;
|
||||
public const float MaxReach = 150;
|
||||
|
||||
/// <summary>
|
||||
/// How long it takes for the objective to be abandoned if no suitable item is found.
|
||||
/// Intended to be an optimization: if the bots are constantly trying to find some item (like a diving suit),
|
||||
/// it can easily lead to performance issues when e.g. AIObjectiveFindDivingGear constantly starts up new GetItem objectives.
|
||||
/// </summary>
|
||||
private float abandonDelayIfItemNotFound = 5.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Is the goal of this objective to get diving gear (i.e. has it been created by <see cref="AIObjectiveFindDivingGear"/>)?
|
||||
/// If so, the objective won't attempt to create another objective if the path requires diving gear
|
||||
@@ -213,7 +220,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (isDoneSeeking)
|
||||
{
|
||||
HandlePotentialItems();
|
||||
HandlePotentialItems(deltaTime);
|
||||
}
|
||||
if (objectiveManager.CurrentOrder is not AIObjectiveGoTo)
|
||||
{
|
||||
@@ -389,6 +396,8 @@ namespace Barotrauma
|
||||
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
|
||||
AbortCondition = obj => targetItem == null || (targetItem.GetRootInventoryOwner() is Entity owner && owner != moveToTarget && owner != character),
|
||||
SpeakIfFails = false,
|
||||
ForceWalkTemporarily = this.ForceWalkTemporarily,
|
||||
ForceWalkPermanently = this.ForceWalkPermanently,
|
||||
endNodeFilter = CreateEndNodeFilter(moveToTarget)
|
||||
};
|
||||
},
|
||||
@@ -598,7 +607,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePotentialItems()
|
||||
private void HandlePotentialItems(float deltaTime)
|
||||
{
|
||||
Debug.Assert(isDoneSeeking);
|
||||
if (itemCandidates.Any())
|
||||
@@ -652,10 +661,14 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow);
|
||||
#endif
|
||||
Abandon = true;
|
||||
abandonDelayIfItemNotFound -= deltaTime;
|
||||
if (abandonDelayIfItemNotFound <= 0.0f)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow);
|
||||
#endif
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -718,13 +731,15 @@ namespace Barotrauma
|
||||
|
||||
private bool CheckItem(Item item)
|
||||
{
|
||||
bool matchesIdentifiersOrTags = item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
|
||||
if (!matchesIdentifiersOrTags) { return false; }
|
||||
if (!item.HasAccess(character)) { return false; }
|
||||
if (ignoredItems.Contains(item)) { return false; };
|
||||
if (ignoredIdentifiersOrTags != null && item.HasIdentifierOrTags(ignoredIdentifiersOrTags)) { return false; }
|
||||
if (item.Condition < TargetCondition) { return false; }
|
||||
if (ItemFilter != null && !ItemFilter(item)) { return false; }
|
||||
if (RequireNonEmpty && item.Components.Any(i => i.IsEmpty(character))) { return false; }
|
||||
return item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
|
||||
@@ -958,6 +958,7 @@ namespace Barotrauma
|
||||
|
||||
public bool ShouldRun(bool run)
|
||||
{
|
||||
if (ForceWalk) { return false; }
|
||||
if (run && objectiveManager.ForcedOrder == this && IsWaitOrder && !character.IsOnPlayerTeam)
|
||||
{
|
||||
// NPCs with a wait order don't run.
|
||||
|
||||
+4
-1
@@ -267,7 +267,10 @@ 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.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
|
||||
}, endNodeFilter: node =>
|
||||
node.Waypoint.Stairs == null && node.Waypoint.CurrentHull == currentTarget && 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
|
||||
|
||||
+4
-3
@@ -78,9 +78,10 @@ namespace Barotrauma
|
||||
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; }
|
||||
|
||||
float range = aiTarget.SoundRange * HumanAIController.Hearing;
|
||||
float dist = character.CurrentHull.GetApproximateDistance(character.Position, targetCharacter.Position, targetCharacter.CurrentHull, range, distanceMultiplierPerClosedDoor: 2);
|
||||
if (dist > range) { continue; }
|
||||
|
||||
character.Speak(TextManager.Get("dialogheardenemy").Value, identifier: "heardenemy".ToIdentifier(), minDurationBetweenSimilar: 30.0f);
|
||||
if (inspectNoiseObjective != null && subObjectives.Contains(inspectNoiseObjective))
|
||||
{
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ namespace Barotrauma
|
||||
float prio = objectiveManager.GetOrderPriority(this);
|
||||
if (subObjectives.All(so => so.SubObjectives.None() || so.Priority <= 0))
|
||||
{
|
||||
ForceWalk = true;
|
||||
ForceWalkTemporarily = true;
|
||||
}
|
||||
return prio;
|
||||
}
|
||||
|
||||
-4
@@ -103,10 +103,6 @@ namespace Barotrauma
|
||||
|
||||
public void AddObjective<T>(T objective) where T : AIObjective
|
||||
{
|
||||
var result = GameMain.LuaCs.Hook.Call<bool?>("AI.addObjective", this, objective);
|
||||
|
||||
if (result != null && result.Value) return;
|
||||
|
||||
if (objective == null)
|
||||
{
|
||||
#if DEBUG
|
||||
|
||||
+1
@@ -257,6 +257,7 @@ namespace Barotrauma
|
||||
{
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
|
||||
TargetName = target.Item.Name,
|
||||
ForceWalkPermanently = ForceWalk,
|
||||
endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item)
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (pump?.Item == null || pump.Item.Removed) { return false; }
|
||||
if (pump.Item.IgnoreByAI(character)) { return false; }
|
||||
if (!pump.Item.IsInteractable(character)) { return false; }
|
||||
if (!pump.Item.IsInteractable(character) || !pump.CanBeSelected) { return false; }
|
||||
if (pump.IsAutoControlled) { return false; }
|
||||
if (pump.Item.ConditionPercentage <= 0) { return false; }
|
||||
if (pump.Item.CurrentHull == null) { return false; }
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ namespace Barotrauma
|
||||
public static bool IsValidTarget(Character target, Character character, out bool ignoredAsMinorWounds)
|
||||
{
|
||||
ignoredAsMinorWounds = false;
|
||||
if (target == null || target.IsDead || target.Removed) { return false; }
|
||||
if (target == null || target.IsDead || target.Removed || target.InvisibleTimer > 0.0f) { return false; }
|
||||
if (target.IsInstigator) { return false; }
|
||||
if (target.IsPet) { return false; }
|
||||
if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; }
|
||||
|
||||
@@ -42,7 +42,9 @@ namespace Barotrauma
|
||||
{
|
||||
enemyAi.PetBehavior?.Update(deltaTime);
|
||||
}
|
||||
if (IsDead || IsUnconscious || Stun > 0.0f || IsIncapacitated)
|
||||
if (IsDead || IsUnconscious || IsIncapacitated ||
|
||||
//only check "real" stuns here, ignoring ragdolling, so the AI can run and decide whether to ragdoll or unragdoll
|
||||
CharacterHealth.Stun > 0.0f)
|
||||
{
|
||||
//don't enable simple physics on dead/incapacitated characters
|
||||
//the ragdoll controls the movement of incapacitated characters instead of the collider,
|
||||
|
||||
@@ -685,7 +685,7 @@ namespace Barotrauma
|
||||
{
|
||||
movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f);
|
||||
|
||||
if (Collider.BodyType == BodyType.Dynamic)
|
||||
if (Collider.BodyType == BodyType.Dynamic && onGround)
|
||||
{
|
||||
Collider.LinearVelocity = new Vector2(
|
||||
movement.X,
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
@@ -1305,11 +1306,11 @@ namespace Barotrauma
|
||||
//increase oxygen and clamp it above zero
|
||||
// -> the character should be revived if there are no major afflictions in addition to lack of oxygen
|
||||
target.Oxygen = Math.Max(target.Oxygen + 10.0f, 10.0f);
|
||||
GameMain.LuaCs.Hook.Call("human.CPRSuccess", this);
|
||||
LuaCsSetup.Instance.EventService.PublishEvent<IEventHumanCPRSuccess>(x => x.OnCharacterCPRSuccess(this));
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMain.LuaCs.Hook.Call("human.CPRFailed", this);
|
||||
LuaCsSetup.Instance.EventService.PublishEvent<IEventHumanCPRFailed>(x => x.OnCharacterCPRFailed(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using FarseerPhysics.Dynamics.Contacts;
|
||||
using FarseerPhysics.Dynamics.Joints;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoonSharp.Interpreter;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using LimbParams = Barotrauma.RagdollParams.LimbParams;
|
||||
using JointParams = Barotrauma.RagdollParams.JointParams;
|
||||
using MoonSharp.Interpreter;
|
||||
using LimbParams = Barotrauma.RagdollParams.LimbParams;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -31,6 +32,7 @@ namespace Barotrauma
|
||||
{
|
||||
public Fixture F1, F2;
|
||||
public Vector2 LocalNormal;
|
||||
public Vector2 WorldNormal;
|
||||
public Vector2 Velocity;
|
||||
public Vector2 ImpactPos;
|
||||
|
||||
@@ -40,7 +42,7 @@ namespace Barotrauma
|
||||
F2 = f2;
|
||||
Velocity = velocity;
|
||||
LocalNormal = contact.Manifold.LocalNormal;
|
||||
contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2<Vector2> points);
|
||||
contact.GetWorldManifold(out WorldNormal, out FarseerPhysics.Common.FixedArray2<Vector2> points);
|
||||
ImpactPos = points[0];
|
||||
}
|
||||
}
|
||||
@@ -827,7 +829,7 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ApplyImpact(Fixture f1, Fixture f2, Vector2 localNormal, Vector2 impactPos, Vector2 velocity)
|
||||
private void ApplyImpact(Fixture f1, Fixture f2, Vector2 worldNormal, Vector2 impactPos, Vector2 velocity)
|
||||
{
|
||||
if (character.DisableImpactDamageTimer > 0.0f) { return; }
|
||||
|
||||
@@ -839,7 +841,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 normal = localNormal;
|
||||
Vector2 normal = worldNormal;
|
||||
float impact = Vector2.Dot(velocity, -normal);
|
||||
if (f1.Body == Collider.FarseerBody || !Collider.Enabled)
|
||||
{
|
||||
@@ -857,7 +859,8 @@ namespace Barotrauma
|
||||
|
||||
float impactDamage = GetImpactDamage(impact, impactTolerance);
|
||||
|
||||
var should = GameMain.LuaCs.Hook.Call<float?>("changeFallDamage", impactDamage, character, impactPos, velocity);
|
||||
float? should = null;
|
||||
LuaCsSetup.Instance.EventService.PublishEvent<IEventChangeFallDamage>(x => should = x.OnChangeFallDamage(impactDamage, character, impactPos, velocity) ?? should);
|
||||
|
||||
if (should != null)
|
||||
{
|
||||
@@ -1077,9 +1080,12 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
Hull newHull = Hull.FindHull(findPos, currentHull);
|
||||
if (setInWater && newHull == null)
|
||||
if (setInWater)
|
||||
{
|
||||
inWater = true;
|
||||
if (newHull == null || findPos.Y < newHull.WorldSurface)
|
||||
{
|
||||
inWater = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (newHull == currentHull) { return; }
|
||||
@@ -1122,7 +1128,10 @@ namespace Barotrauma
|
||||
{
|
||||
//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, allowRoomToRoom: true) != null) { return; }
|
||||
if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine()), allowRoomToRoom: true) != null)) { return; }
|
||||
if (Limbs.Any(l => !l.IsSevered && 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);
|
||||
}
|
||||
@@ -1259,6 +1268,9 @@ namespace Barotrauma
|
||||
|
||||
private float BodyInRestDelay = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Controls the sleeping state of this character
|
||||
/// </summary>
|
||||
public bool BodyInRest
|
||||
{
|
||||
get { return bodyInRestTimer > BodyInRestDelay; }
|
||||
@@ -1282,7 +1294,7 @@ namespace Barotrauma
|
||||
while (impactQueue.Count > 0)
|
||||
{
|
||||
var impact = impactQueue.Dequeue();
|
||||
ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity);
|
||||
ApplyImpact(impact.F1, impact.F2, impact.WorldNormal, impact.ImpactPos, impact.Velocity);
|
||||
}
|
||||
|
||||
CheckValidity();
|
||||
@@ -1325,9 +1337,18 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
float MaxVel = NetConfig.MaxPhysicsBodyVelocity;
|
||||
Collider.LinearVelocity = new Vector2(
|
||||
NetConfig.Quantize(Collider.LinearVelocity.X, -MaxVel, MaxVel, 12),
|
||||
NetConfig.Quantize(Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12));
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
Collider.LinearVelocity = new Vector2(
|
||||
NetConfig.Quantize(Collider.LinearVelocity.X, -MaxVel, MaxVel, 12),
|
||||
NetConfig.Quantize(Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12));
|
||||
}
|
||||
else
|
||||
{
|
||||
Collider.LinearVelocity = new Vector2(
|
||||
MathHelper.Clamp(Collider.LinearVelocity.X, -MaxVel, MaxVel),
|
||||
MathHelper.Clamp(Collider.LinearVelocity.Y, -MaxVel, MaxVel));
|
||||
}
|
||||
|
||||
if (forceStanding)
|
||||
{
|
||||
@@ -1381,9 +1402,19 @@ namespace Barotrauma
|
||||
|
||||
UpdateHullFlowForces(deltaTime);
|
||||
|
||||
if (currentHull == null ||
|
||||
bool applyWaterForces =
|
||||
currentHull == null ||
|
||||
currentHull.WaterVolume > currentHull.Volume * 0.95f ||
|
||||
ConvertUnits.ToSimUnits(currentHull.Surface) > Collider.SimPosition.Y)
|
||||
ConvertUnits.ToSimUnits(currentHull.Surface) > Collider.SimPosition.Y;
|
||||
#if CLIENT
|
||||
if (Screen.Selected is CharacterEditor.CharacterEditorScreen &&
|
||||
this is AnimController animController)
|
||||
{
|
||||
applyWaterForces = animController.CurrentAnimationParams is SwimParams;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (applyWaterForces)
|
||||
{
|
||||
Collider.ApplyWaterForces();
|
||||
}
|
||||
@@ -1473,10 +1504,10 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
// Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck.
|
||||
if (Collider.LinearVelocity == Vector2.Zero && !character.IsRemotePlayer)
|
||||
if (Collider.LinearVelocity == Vector2.Zero && GameMain.NetworkMember is not { IsClient: true })
|
||||
{
|
||||
character.IsRagdolled = true;
|
||||
if (character.IsBot)
|
||||
if (!character.IsPlayer)
|
||||
{
|
||||
// Seems to work without this on player controlled characters -> not sure if we should call it always or just for the bots.
|
||||
character.SetInput(InputType.Ragdoll, hit: false, held: true);
|
||||
@@ -1836,7 +1867,13 @@ namespace Barotrauma
|
||||
{
|
||||
floorFixture = standOnFloorFixture;
|
||||
standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction;
|
||||
if (rayStart.Y - standOnFloorY < Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor * 1.2f)
|
||||
|
||||
//allow the floor to be just a bit below the bottom of the collider for the character to be "on ground"
|
||||
//there is some inaccuracy in the physics simulation (and floats), the collider isn't usually precisely ColliderHeightFromFloor above the floor
|
||||
const float Tolerance = 0.1f;
|
||||
float standHeight = Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor;
|
||||
|
||||
if (rayStart.Y - standOnFloorY <= standHeight + Tolerance)
|
||||
{
|
||||
onGround = true;
|
||||
if (standOnFloorFixture.CollisionCategories == Physics.CollisionStairs)
|
||||
|
||||
@@ -190,6 +190,11 @@ namespace Barotrauma
|
||||
set => Params.Health.DoesBleed = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can this character be contained inside a controller?
|
||||
/// </summary>
|
||||
public bool IsContainable { get; set; }
|
||||
|
||||
public readonly Dictionary<Identifier, SerializableProperty> Properties;
|
||||
public Dictionary<Identifier, SerializableProperty> SerializableProperties
|
||||
{
|
||||
@@ -686,6 +691,11 @@ namespace Barotrauma
|
||||
get { return AnimController.Mass; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The position the character was at when we previously set the transforms of the items in the character's inventory.
|
||||
/// </summary>
|
||||
private Vector2 lastInventoryItemSetTransformPosition;
|
||||
|
||||
public CharacterInventory Inventory { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -791,7 +801,24 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
if (value == selectedCharacter) { return; }
|
||||
if (selectedCharacter != null) { selectedCharacter.selectedBy = null; }
|
||||
//deselect the currently selected character
|
||||
if (selectedCharacter != null)
|
||||
{
|
||||
selectedCharacter.selectedBy = null;
|
||||
//check if some other character has selected the currently selected character too,
|
||||
//and set selectedBy to that other character (otherwise the currently selected character would be unaware they're still being dragged by someone)
|
||||
foreach (var otherCharacter in CharacterList)
|
||||
{
|
||||
if (otherCharacter != this && otherCharacter.selectedCharacter == selectedCharacter)
|
||||
{
|
||||
selectedCharacter.selectedBy = otherCharacter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CharacterHUD.RecreateHudTextsIfControlling(this);
|
||||
|
||||
selectedCharacter = value;
|
||||
if (selectedCharacter != null) { selectedCharacter.selectedBy = this; }
|
||||
#if CLIENT
|
||||
@@ -1433,8 +1460,6 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
|
||||
GameMain.LuaCs.Hook.Call("character.created", new object[] { newCharacter });
|
||||
|
||||
return newCharacter;
|
||||
}
|
||||
|
||||
@@ -1648,8 +1673,10 @@ namespace Barotrauma
|
||||
AnimController.FindHull(setInWater: true);
|
||||
if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; }
|
||||
|
||||
IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass <= 30.0f);
|
||||
|
||||
CharacterList.Add(this);
|
||||
|
||||
|
||||
Enabled = GameMain.NetworkMember == null;
|
||||
|
||||
if (info != null)
|
||||
@@ -1889,7 +1916,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
info.Job?.GiveJobItems(this, isPvPMode, spawnPoint);
|
||||
GameMain.LuaCs.Hook.Call("character.giveJobItems", this, spawnPoint, isPvPMode);
|
||||
}
|
||||
|
||||
public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false)
|
||||
@@ -2275,6 +2301,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
// Try to detach from the controller if we are currently attached to something that is dangerous for our character
|
||||
if (aiControlled && Stun <= 0f && !IsKnockedDownOrRagdolled && !LockHands && ShouldAvoidStayingAttachedToController())
|
||||
{
|
||||
SelectedItem = null;
|
||||
}
|
||||
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
if (GameMain.NetworkMember.IsServer)
|
||||
@@ -2323,7 +2355,7 @@ namespace Barotrauma
|
||||
{
|
||||
attackCoolDown -= deltaTime;
|
||||
}
|
||||
else if (IsKeyDown(InputType.Attack))
|
||||
else if (IsKeyDown(InputType.Attack) && !IsAttachedToController())
|
||||
{
|
||||
//normally the attack target, where to aim the attack and such is handled by EnemyAIController,
|
||||
//but in the case of player-controlled monsters, we handle it here
|
||||
@@ -2850,14 +2882,14 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; }
|
||||
#endif
|
||||
if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; }
|
||||
|
||||
Controller controller = item.GetComponent<Controller>();
|
||||
if (controller != null && IsAnySelectedItem(item) && controller.IsAttachedUser(this))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; }
|
||||
|
||||
if (item.ParentInventory != null)
|
||||
{
|
||||
return CanAccessInventory(item.ParentInventory);
|
||||
@@ -2979,7 +3011,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger)
|
||||
//note that the distance to item should be set to 0 above if the character is within the item's bounding box
|
||||
bool closeEnoughToIgnoreVisibilityCheck = distanceToItem <= 0.1f;
|
||||
if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger && !closeEnoughToIgnoreVisibilityCheck)
|
||||
{
|
||||
var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true);
|
||||
bool itemCenterVisible = CheckBody(body, item);
|
||||
@@ -3008,7 +3042,6 @@ namespace Barotrauma
|
||||
{
|
||||
return itemCenterVisible;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -3098,7 +3131,11 @@ namespace Barotrauma
|
||||
|
||||
if (!CanInteract)
|
||||
{
|
||||
SelectedItem = SelectedSecondaryItem = null;
|
||||
if (!IsAttachedToController())
|
||||
{
|
||||
SelectedItem = null;
|
||||
}
|
||||
SelectedSecondaryItem = null;
|
||||
focusedItem = null;
|
||||
if (!AllowInput)
|
||||
{
|
||||
@@ -3117,8 +3154,16 @@ namespace Barotrauma
|
||||
{
|
||||
if (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld)
|
||||
{
|
||||
FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
|
||||
if (FocusedCharacter != null && !CanSeeTarget(FocusedCharacter)) { FocusedCharacter = null; }
|
||||
//don't allow focusing on anyone when the health window is open (avoids accidentally selecting someone when closing the window)
|
||||
if (CharacterHealth.OpenHealthWindow != null)
|
||||
{
|
||||
FocusedCharacter = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
|
||||
if (FocusedCharacter != null && !CanSeeTarget(FocusedCharacter)) { FocusedCharacter = null; }
|
||||
}
|
||||
float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
|
||||
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
|
||||
{
|
||||
@@ -3443,7 +3488,7 @@ namespace Barotrauma
|
||||
|
||||
obstructVisionAmount = Math.Max(obstructVisionAmount - deltaTime, 0.0f);
|
||||
|
||||
if (Inventory != null)
|
||||
if (Inventory != null && Vector2.DistanceSquared(lastInventoryItemSetTransformPosition, Position) > 0.1f)
|
||||
{
|
||||
//do not check for duplicates: this is code is called very frequently, and duplicates don't matter here,
|
||||
//so it's better just to avoid the relatively expensive duplicate check
|
||||
@@ -3452,6 +3497,7 @@ namespace Barotrauma
|
||||
if (item.body == null || item.body.Enabled) { continue; }
|
||||
item.SetTransform(SimPosition, 0.0f, forceSubmarine: Submarine);
|
||||
}
|
||||
lastInventoryItemSetTransformPosition = Position;
|
||||
}
|
||||
|
||||
HideFace = false;
|
||||
@@ -3578,7 +3624,7 @@ namespace Barotrauma
|
||||
{
|
||||
wasRagdolled = IsRagdolled;
|
||||
IsRagdolled = IsKeyDown(InputType.Ragdoll);
|
||||
if (IsRagdolled && IsBot && GameMain.NetworkMember is not { IsClient: true })
|
||||
if (IsRagdolled && !IsPlayer && GameMain.NetworkMember is not { IsClient: true })
|
||||
{
|
||||
ClearInput(InputType.Ragdoll);
|
||||
}
|
||||
@@ -3630,7 +3676,19 @@ namespace Barotrauma
|
||||
AnimController.IgnorePlatforms = true;
|
||||
}
|
||||
AnimController.ResetPullJoints();
|
||||
SelectedItem = SelectedSecondaryItem = null;
|
||||
|
||||
// Prevent us from detaching from the controller if we are attached to it OR detach if we
|
||||
// manually ragdoll, in this case it should be similar to us deselecting the controller
|
||||
if (!IsAttachedToController() ||
|
||||
(IsKeyDown(InputType.Ragdoll)
|
||||
// Let only the server do this check since the Ragdoll input for other clients is set to be held
|
||||
// for stunned characters even if a character isn't manually ragdolling
|
||||
&& (GameMain.NetworkMember == null || GameMain.NetworkMember is { IsServer: true } )))
|
||||
{
|
||||
SelectedItem = null;
|
||||
}
|
||||
|
||||
SelectedSecondaryItem = null;
|
||||
SelectedCharacter = null;
|
||||
return;
|
||||
}
|
||||
@@ -3659,6 +3717,13 @@ namespace Barotrauma
|
||||
bool MustDeselect(Item item)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
|
||||
// Prevent creatures from deselecting the controller if they are attached to it
|
||||
if (IsAIControlled && !CanInteract && IsAttachedToController())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CanInteractWith(item)) { return true; }
|
||||
bool hasSelectableComponent = false;
|
||||
foreach (var component in item.Components)
|
||||
@@ -4384,6 +4449,41 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void ForceSay(LocalizedString messageToSay, bool sayInRadio, bool removeQuotes = false, float delay = 0.0f)
|
||||
{
|
||||
if (messageToSay.IsNullOrEmpty() || SpeechImpediment >= 100.0f || IsDead)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (removeQuotes)
|
||||
{
|
||||
messageToSay = new TrimLString(messageToSay,
|
||||
TrimLString.Mode.Both, ['"', '”', '“', ' ']);
|
||||
}
|
||||
|
||||
ChatMessageType messageType = ChatMessageType.Default;
|
||||
bool canUseRadio = ChatMessage.CanUseRadio(this, out WifiComponent radio);
|
||||
if (canUseRadio && sayInRadio)
|
||||
{
|
||||
messageType = ChatMessageType.Radio;
|
||||
}
|
||||
|
||||
CoroutineManager.Invoke(() =>
|
||||
{
|
||||
#if SERVER
|
||||
GameMain.Server?.SendChatMessage(messageToSay.Value, messageType, senderClient: null, this);
|
||||
#elif CLIENT
|
||||
// no need to create the message when playing as a client, the server will send it to us
|
||||
if (GameMain.Client == null)
|
||||
{
|
||||
AIChatMessage message = new AIChatMessage(messageToSay.Value, messageType);
|
||||
SendSinglePlayerMessage(message, canUseRadio, radio);
|
||||
}
|
||||
#endif
|
||||
}, delay);
|
||||
}
|
||||
|
||||
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
|
||||
{
|
||||
CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount);
|
||||
@@ -4596,12 +4696,6 @@ namespace Barotrauma
|
||||
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false, bool ignoreDamageOverlay = false, bool recalculateVitality = true)
|
||||
{
|
||||
if (Removed) { return new AttackResult(); }
|
||||
|
||||
AttackResult? retAttackResult = GameMain.LuaCs.Hook.Call<AttackResult?>("character.damageLimb", this, worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker, damageMultiplier, allowStacking, penetration, shouldImplode);
|
||||
if (retAttackResult != null)
|
||||
{
|
||||
return retAttackResult.Value;
|
||||
}
|
||||
|
||||
SetStun(stun);
|
||||
|
||||
@@ -4774,6 +4868,10 @@ namespace Barotrauma
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
|
||||
if (Screen.Selected != GameMain.GameScreen) { return; }
|
||||
//don't allow stunning for less than one frame
|
||||
//fixes monsters/enemies that take some minuscule amount of stun from a weapon still being noticeable affected by the stun,
|
||||
//because even a one-frame stun briefly disables the animations and makes the character stop
|
||||
if (newStun < Timing.Step && Stun <= 0.0f) { return; }
|
||||
if (GodMode)
|
||||
{
|
||||
CharacterHealth.Stun = 0;
|
||||
@@ -4801,7 +4899,12 @@ namespace Barotrauma
|
||||
CharacterHealth.Stun = newStun;
|
||||
if (newStun > 0.0f)
|
||||
{
|
||||
SelectedItem = SelectedSecondaryItem = null;
|
||||
if (!IsAttachedToController())
|
||||
{
|
||||
SelectedItem = null;
|
||||
}
|
||||
|
||||
SelectedSecondaryItem = null;
|
||||
if (SelectedCharacter != null) { DeselectCharacter(); }
|
||||
}
|
||||
HealthUpdateInterval = 0.0f;
|
||||
@@ -4990,6 +5093,37 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAttachedToController()
|
||||
{
|
||||
if (SelectedItem == null) { return false; }
|
||||
|
||||
var controller = SelectedItem.GetComponent<Controller>();
|
||||
if (controller == null) { return false; }
|
||||
|
||||
return controller.IsAttachedUser(this);
|
||||
}
|
||||
|
||||
public bool ShouldAvoidStayingAttachedToController()
|
||||
{
|
||||
if (!IsAttachedToController()) { return false; }
|
||||
|
||||
var deconstructor = SelectedItem.GetComponent<Deconstructor>();
|
||||
if (deconstructor != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Character is being carried by an enemy!
|
||||
if (IsHuman &&
|
||||
SelectedItem.GetRootInventoryOwner() is Character carryingCharacter &&
|
||||
TeamID != carryingCharacter.TeamID)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true)
|
||||
{
|
||||
if (IsDead || CharacterHealth.Unkillable || GodMode || Removed) { return; }
|
||||
@@ -5117,7 +5251,6 @@ namespace Barotrauma
|
||||
AchievementManager.OnCharacterKilled(this, CauseOfDeath);
|
||||
}
|
||||
|
||||
GameMain.LuaCs.Hook.Call("character.death", this, causeOfDeathAffliction);
|
||||
KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log);
|
||||
|
||||
if (info != null)
|
||||
@@ -5128,7 +5261,7 @@ namespace Barotrauma
|
||||
AnimController.movement = Vector2.Zero;
|
||||
AnimController.TargetMovement = Vector2.Zero;
|
||||
|
||||
if (!LockHands)
|
||||
if (!LockHands && causeOfDeath != CauseOfDeathType.Disconnected)
|
||||
{
|
||||
foreach (Item heldItem in HeldItems.ToList())
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
@@ -482,7 +482,6 @@ namespace Barotrauma
|
||||
{
|
||||
GrainEffectStrength -= amount;
|
||||
}
|
||||
GameMain.LuaCs.Hook.Call("afflictionUpdate", new object[] { this, characterHealth, targetLimb, deltaTime });
|
||||
}
|
||||
|
||||
public void ApplyStatusEffects(ActionType type, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
|
||||
|
||||
+3
-2
@@ -4,6 +4,7 @@ using System.Xml.Linq;
|
||||
using System;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -337,13 +338,13 @@ namespace Barotrauma
|
||||
|
||||
if (Prefab is AfflictionPrefabHusk huskPrefab)
|
||||
{
|
||||
if (huskPrefab.ControlHusk || GameMain.LuaCs.Game.enableControlHusk)
|
||||
if (huskPrefab.ControlHusk || LuaCsSetup.Instance.Game.enableControlHusk)
|
||||
{
|
||||
#if SERVER
|
||||
if (client != null)
|
||||
{
|
||||
GameMain.Server.SetClientCharacter(client, husk);
|
||||
GameMain.LuaCs.Hook.Call("husk.clientControlHusk", new object[] { client, husk });
|
||||
LuaCsSetup.Instance.EventService.PublishEvent<IEventClientControlHusk>(x => x.OnClientControlHusk(client, husk));
|
||||
}
|
||||
#else
|
||||
if (!character.IsRemotelyControlled && character == Character.Controlled)
|
||||
|
||||
+1
-1
@@ -629,7 +629,7 @@ namespace Barotrauma
|
||||
public static readonly Identifier StunType = "stun".ToIdentifier();
|
||||
public static readonly Identifier EMPType = "emp".ToIdentifier();
|
||||
public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier();
|
||||
public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier();
|
||||
public static readonly Identifier AlienInfectionType = "alieninfection".ToIdentifier();
|
||||
public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier();
|
||||
public static readonly Identifier DisguisedAsHuskType = "disguiseashusk".ToIdentifier();
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
using Barotrauma.Abilities;
|
||||
using Barotrauma.Abilities;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoonSharp.Interpreter;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Globalization;
|
||||
using MoonSharp.Interpreter;
|
||||
using Barotrauma.Abilities;
|
||||
using static OneOf.Types.TrueFalseOrNull;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -657,7 +659,8 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
var should = GameMain.LuaCs.Hook.Call<bool?>("character.applyDamage", this, attackResult, hitLimb, allowStacking);
|
||||
bool? should = null;
|
||||
LuaCsSetup.Instance.EventService.PublishEvent<IEventCharacterApplyDamage>(x => should = x.OnCharacterApplyDamage(this, attackResult, hitLimb, allowStacking) ?? should);
|
||||
if (should != null && should.Value) { return; }
|
||||
|
||||
foreach (Affliction newAffliction in attackResult.Afflictions)
|
||||
@@ -828,10 +831,9 @@ namespace Barotrauma
|
||||
if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; }
|
||||
if (Character.Params.Health.ImmunityIdentifiers.Contains(newAffliction.Identifier)) { return; }
|
||||
|
||||
var should = GameMain.LuaCs.Hook.Call<bool?>("character.applyAffliction", this, limbHealth, newAffliction, allowStacking);
|
||||
|
||||
if (should != null && should.Value)
|
||||
return;
|
||||
bool? should = null;
|
||||
LuaCsSetup.Instance.EventService.PublishEvent<IEventCharacterApplyAffliction>(x => should = x.OnCharacterApplyAffliction(this, limbHealth, newAffliction, allowStacking) ?? should);
|
||||
if (should != null && should.Value) { return; }
|
||||
|
||||
Affliction existingAffliction = null;
|
||||
foreach ((Affliction affliction, LimbHealth value) in afflictions)
|
||||
@@ -843,9 +845,21 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
float modifiedStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType));
|
||||
if (newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType)
|
||||
{
|
||||
//don't allow stunning for less than one frame
|
||||
//fixes monsters/enemies that take some minuscule amount of stun from a weapon still being noticeable affected by the stun,
|
||||
//because even a one-frame stun briefly disables the animations and makes the character stop
|
||||
if (modifiedStrength < Timing.Step && Stun <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingAffliction != null)
|
||||
{
|
||||
float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab, limbType));
|
||||
float newStrength = modifiedStrength;
|
||||
if (allowStacking)
|
||||
{
|
||||
// Add the existing strength
|
||||
@@ -867,7 +881,7 @@ namespace Barotrauma
|
||||
//create a new instance of the affliction to make sure we don't use the same instance for multiple characters
|
||||
//or modify the affliction instance of an Attack or a StatusEffect
|
||||
var copyAffliction = newAffliction.Prefab.Instantiate(
|
||||
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType))),
|
||||
Math.Min(newAffliction.Prefab.MaxStrength, modifiedStrength),
|
||||
newAffliction.Source);
|
||||
afflictions.Add(copyAffliction, limbHealth);
|
||||
AchievementManager.OnAfflictionReceived(copyAffliction, Character);
|
||||
|
||||
@@ -190,7 +190,7 @@ namespace Barotrauma
|
||||
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
|
||||
}
|
||||
}
|
||||
humanAI.ReportRange = Hearing;
|
||||
humanAI.Hearing = Hearing;
|
||||
humanAI.ReportRange = ReportRange;
|
||||
humanAI.FindWeaponsRange = FindWeaponsRange;
|
||||
humanAI.AimSpeed = AimSpeed;
|
||||
|
||||
@@ -1293,7 +1293,7 @@ namespace Barotrauma
|
||||
if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; }
|
||||
foreach (StatusEffect statusEffect in statusEffectList)
|
||||
{
|
||||
if (statusEffect.ShouldWaitForInterval(character, deltaTime)) { return; }
|
||||
if (statusEffect.ShouldWaitForInterval(character, deltaTime)) { continue; }
|
||||
|
||||
statusEffect.sourceBody = body;
|
||||
if (statusEffect.type == ActionType.OnDamaged)
|
||||
|
||||
@@ -728,7 +728,9 @@ namespace Barotrauma
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should the character target or ignore walls when it's outside the submarine."), Editable]
|
||||
public bool TargetOuterWalls { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable]
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "If disabled (default), the character selects the limb based on a formula where the parameters are a) the priority of the attack b) the distance to the target, and c) the range of the attack" +
|
||||
"If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random. The distance to the target is in this case ignored."
|
||||
), Editable]
|
||||
public bool RandomAttack { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable]
|
||||
|
||||
@@ -77,8 +77,8 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
conn.SetLabel(conn.Connection.DisplayName, this);
|
||||
conn.Connection.DisplayNameOverride = null;
|
||||
conn.SetLabel(conn.Connection.DisplayName, this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
+3
-2
@@ -106,9 +106,10 @@ namespace Barotrauma
|
||||
void AddTexturePath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) { return; }
|
||||
var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, ragdollParams.Texture);
|
||||
//if the path contains a gender variable, we can't load it yet because we don't know which gender we need
|
||||
if (path.Contains("[GENDER]")) { return; }
|
||||
texturePaths.Add(ContentPath.FromRaw(characterPrefab.ContentPackage, ragdollParams.Texture));
|
||||
if (contentPath.FullPath.Contains("[GENDER]")) { return; }
|
||||
texturePaths.Add(contentPath);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
+9
-2
@@ -199,9 +199,16 @@ namespace Barotrauma
|
||||
|
||||
try
|
||||
{
|
||||
return success(doc.Root.GetAttributeBool("corepackage", false)
|
||||
ContentPackage contentPackage = doc.Root.GetAttributeBool("corepackage", false)
|
||||
? new CorePackage(doc, path)
|
||||
: new RegularPackage(doc, path));
|
||||
: new RegularPackage(doc, path);
|
||||
|
||||
if (System.IO.Path.GetFileNameWithoutExtension(path)?.Any(char.IsUpper) is true)
|
||||
{
|
||||
DebugConsole.ThrowError($"Invalid filename casing. Please rename \"filelist.xml\" so it is entirely lowercase.", contentPackage: contentPackage);
|
||||
}
|
||||
|
||||
return success(contentPackage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
+15
-3
@@ -9,8 +9,10 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.IO;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.Steam;
|
||||
using Microsoft.Xna.Framework;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -48,7 +50,10 @@ namespace Barotrauma
|
||||
public static ImmutableArray<RegularPackage>? Regular;
|
||||
}
|
||||
|
||||
public static void SetCore(CorePackage newCore) => SetCoreEnumerable(newCore).Consume();
|
||||
public static void SetCore(CorePackage newCore)
|
||||
{
|
||||
SetCoreEnumerable(newCore).Consume();
|
||||
}
|
||||
|
||||
public static IEnumerable<LoadProgress> SetCoreEnumerable(CorePackage newCore)
|
||||
{
|
||||
@@ -85,7 +90,9 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public static void SetRegular(IReadOnlyList<RegularPackage> newRegular)
|
||||
=> SetRegularEnumerable(newRegular).Consume();
|
||||
{
|
||||
SetRegularEnumerable(newRegular).Consume();
|
||||
}
|
||||
|
||||
public static IEnumerable<LoadProgress> SetRegularEnumerable(IReadOnlyList<RegularPackage> inNewRegular)
|
||||
{
|
||||
@@ -583,6 +590,11 @@ namespace Barotrauma
|
||||
package.UgcId.TryUnwrap(out var ugcId) && ugcId is SteamWorkshopId workshopId && workshopId.Value == childUgcItemId.Value));
|
||||
foreach (var missingChild in missingChildren)
|
||||
{
|
||||
if (missingChild.ToString() == "2559634234" ||
|
||||
missingChild.ToString() == "2795927223")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
enabledPackage.AddMissingDependency(missingChild);
|
||||
}
|
||||
});
|
||||
@@ -597,4 +609,4 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,12 +99,13 @@ namespace Barotrauma
|
||||
public static ContentPath FromRaw(ContentPackage? contentPackage, string? rawValue)
|
||||
{
|
||||
var newRaw = new ContentPath(contentPackage, rawValue);
|
||||
if (prevCreatedRaw is not null && prevCreatedRaw.ContentPackage == contentPackage &&
|
||||
// Removed as this almost never happens but makes the constructor not thread-safe.
|
||||
/*if (prevCreatedRaw is not null && prevCreatedRaw.ContentPackage == contentPackage &&
|
||||
prevCreatedRaw.RawValue == rawValue)
|
||||
{
|
||||
newRaw.cachedValue = prevCreatedRaw.Value;
|
||||
}
|
||||
prevCreatedRaw = newRaw;
|
||||
prevCreatedRaw = newRaw;*/
|
||||
return newRaw;
|
||||
}
|
||||
|
||||
@@ -158,4 +159,4 @@ namespace Barotrauma
|
||||
|
||||
public override string? ToString() => Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2311,6 +2311,8 @@ namespace Barotrauma
|
||||
NewMessage($"Start item set changed to \"{AutoItemPlacer.DefaultStartItemSet}\"");
|
||||
}, isCheat: false));
|
||||
|
||||
|
||||
|
||||
//"dummy commands" that only exist so that the server can give clients permissions to use them
|
||||
//TODO: alphabetical order?
|
||||
commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () =>
|
||||
@@ -3020,7 +3022,10 @@ namespace Barotrauma
|
||||
switch (args[argIndex].ToLowerInvariant())
|
||||
{
|
||||
case "inside":
|
||||
spawnPoint = WayPoint.GetRandom(SpawnType.Human, job, Submarine.MainSub);
|
||||
spawnPoint =
|
||||
WayPoint.GetRandom(SpawnType.Human, job, Submarine.MainSub) ??
|
||||
//try a non-job-specific spawnpoint if a job-specific one can't be found
|
||||
WayPoint.GetRandom(SpawnType.Human, assignedJob: null, Submarine.MainSub);
|
||||
break;
|
||||
case "outside":
|
||||
spawnPoint = WayPoint.GetRandom(SpawnType.Enemy);
|
||||
|
||||
@@ -34,11 +34,12 @@ namespace Barotrauma
|
||||
get { return Prefab.LifeTime; }
|
||||
}
|
||||
|
||||
private float baseAlpha = 1.0f;
|
||||
public float BaseAlpha
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = 1.0f;
|
||||
get => baseAlpha;
|
||||
set => baseAlpha = MathHelper.Clamp(value, 0f, 1f);
|
||||
}
|
||||
|
||||
public Color Color
|
||||
{
|
||||
|
||||
@@ -131,6 +131,14 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
OnRemoved = 25,
|
||||
/// <summary>
|
||||
/// Executes continuously while the item/character is being deconstructed.
|
||||
/// </summary>
|
||||
OnDeconstructing = 26,
|
||||
/// <summary>
|
||||
/// Executed once when the item/character is deconstructed.
|
||||
/// </summary>
|
||||
OnDeconstructed = 27,
|
||||
/// <summary>
|
||||
/// Executes when the character dies. Only valid for characters.
|
||||
/// </summary>
|
||||
OnDeath = OnBroken
|
||||
|
||||
@@ -12,7 +12,11 @@ namespace Barotrauma
|
||||
public readonly int RandomSeed;
|
||||
|
||||
protected readonly EventPrefab prefab;
|
||||
|
||||
|
||||
#nullable enable
|
||||
public Mission? TriggeringMission;
|
||||
#nullable restore
|
||||
|
||||
public EventPrefab Prefab => prefab;
|
||||
|
||||
public EventSet ParentSet { get; private set; }
|
||||
|
||||
+5
-3
@@ -29,6 +29,9 @@ namespace Barotrauma
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the target (or all targets if there's multiple) when the check succeeds.")]
|
||||
public Identifier ApplyTagToTarget { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should the check fail if no targets matching the specified tag are found?")]
|
||||
public bool FailIfTargetNotFound { get; set; }
|
||||
|
||||
public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
if (TargetTag.IsEmpty)
|
||||
@@ -79,11 +82,10 @@ namespace Barotrauma
|
||||
|
||||
if (targets.None())
|
||||
{
|
||||
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);
|
||||
return !FailIfTargetNotFound;
|
||||
}
|
||||
|
||||
if (targets.None() || Conditionals.None())
|
||||
if (Conditionals.None())
|
||||
{
|
||||
foreach (var target in targets)
|
||||
{
|
||||
|
||||
@@ -14,6 +14,33 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
partial class ConversationAction : EventAction
|
||||
{
|
||||
public class OptionActionGroup : SubactionGroup
|
||||
{
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "The text to display in the option.")]
|
||||
public string Text { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should this option end the conversation (closing the conversation prompt?). " +
|
||||
"By default, options that don't have any actions inside them, or that only have a GoTo action, end the conversation. " +
|
||||
"But if there are other actions inside the option, the game assumes there may be some kind of a follow-up coming to the conversation, " +
|
||||
"and by default leaves it open.")]
|
||||
public bool EndConversation { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: $"If enabled, the player will send the {nameof(Text)} in chat when selecting the option, or if {nameof(ForceSayText)} is not empty, will send that instead.")]
|
||||
public bool ForceSay { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the message sent in chat will be sent in radio chat instead.")]
|
||||
public bool ForceSayInRadio { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: $"Message sent in chat, if empty, {nameof(Text)} is used instead.")]
|
||||
public string ForceSayText { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should the chat message be stripped of any quotation mark characters?")]
|
||||
public bool ForceSayRemoveQuotes { get; set; }
|
||||
|
||||
public OptionActionGroup(ScriptedEvent scriptedEvent, ContentXElement element) : base(scriptedEvent, element)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public enum DialogTypes
|
||||
{
|
||||
@@ -33,6 +60,18 @@ namespace Barotrauma
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "The text to display in the prompt. Can be the text as-is, or a tag referring to a line in a text file.")]
|
||||
public string Text { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: $"If enabled, the speaker will send the {nameof(Text)} in chat, or if {nameof(ForceSayText)} is not empty, will send that instead. Note: requires a valid SpeakerTag to be defined.")]
|
||||
public bool ForceSay { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the message sent in chat by the speaker will be sent in radio chat instead.")]
|
||||
public bool ForceSayInRadio { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: $"Message sent in chat by the speaker, if empty, {nameof(Text)} is used instead.")]
|
||||
public string ForceSayText { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should the chat message be stripped of any quotation mark characters?")]
|
||||
public bool ForceSayRemoveQuotes { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the character who's speaking. Makes a speech bubble icon appear above the character to indicate you can speak with them, and stops the character in place when the conversation triggers. Also allows the conversation to be interrupted if the speaker dies or becomes incapacitated mid-conversation.")]
|
||||
public Identifier SpeakerTag { get; set; }
|
||||
|
||||
@@ -75,7 +114,7 @@ namespace Barotrauma
|
||||
private AIObjective prevIdleObjective, prevGotoObjective;
|
||||
private AIObjective npcWaitObjective;
|
||||
|
||||
public List<SubactionGroup> Options { get; private set; }
|
||||
public List<OptionActionGroup> Options { get; private set; }
|
||||
|
||||
public SubactionGroup Interrupted { get; private set; }
|
||||
|
||||
@@ -99,12 +138,12 @@ namespace Barotrauma
|
||||
{
|
||||
actionCount++;
|
||||
Identifier = actionCount;
|
||||
Options = new List<SubactionGroup>();
|
||||
Options = new List<OptionActionGroup>();
|
||||
foreach (var elem in element.Elements())
|
||||
{
|
||||
if (elem.Name.LocalName.Equals("option", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Options.Add(new SubactionGroup(ParentEvent, elem));
|
||||
Options.Add(new OptionActionGroup(ParentEvent, elem));
|
||||
}
|
||||
else if (elem.Name.LocalName.Equals("interrupt", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -215,6 +254,10 @@ namespace Barotrauma
|
||||
interrupt = false;
|
||||
dialogOpened = false;
|
||||
Speaker = null;
|
||||
#if CLIENT
|
||||
dialogBox?.Close();
|
||||
dialogBox = null;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -292,6 +335,7 @@ namespace Barotrauma
|
||||
if (dialogOpened)
|
||||
{
|
||||
lastActiveTime = Timing.TotalTime;
|
||||
|
||||
#if CLIENT
|
||||
if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction"))
|
||||
{
|
||||
@@ -350,7 +394,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
TryStartConversation(null);
|
||||
TryStartConversation(Speaker);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -467,11 +511,26 @@ namespace Barotrauma
|
||||
ParentEvent.AddTarget(InvokerTag, targetCharacter);
|
||||
}
|
||||
|
||||
ShowDialog(speaker, targetCharacter);
|
||||
if (ForceSay)
|
||||
{
|
||||
speaker?.ForceSay(
|
||||
ForceSayText.IsNullOrEmpty() ? TextManager.Get(Text).Fallback(Text) : TextManager.Get(ForceSayText).Fallback(ForceSayText),
|
||||
ForceSayInRadio,
|
||||
ForceSayRemoveQuotes,
|
||||
// Small delay so the speaking character doesn't talk at the same time as the player
|
||||
delay: 0.7f);
|
||||
}
|
||||
|
||||
ShowDialog(Speaker, targetCharacter);
|
||||
|
||||
dialogOpened = true;
|
||||
if (speaker != null)
|
||||
if (Speaker != null)
|
||||
{
|
||||
Speaker = speaker;
|
||||
|
||||
// Set the Speaker of the child conversation actions so they know which character is speaking
|
||||
Options.SelectMany(static op => op.Actions).OfType<ConversationAction>().ForEach(action => action.Speaker = speaker);
|
||||
|
||||
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
|
||||
speaker.SetCustomInteract(null, null);
|
||||
#if SERVER
|
||||
|
||||
@@ -99,6 +99,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
int compareToTargetCount = ParentEvent.GetTargets(CompareToTarget).Count();
|
||||
if (compareToTargetCount == 0) { return false; }
|
||||
float percentage = MathUtils.Percentage(targetCount, compareToTargetCount);
|
||||
if (MinPercentageRelativeToTarget > -1 && percentage < MinPercentageRelativeToTarget) { return false; }
|
||||
if (MaxPercentageRelativeToTarget > -1 && percentage > MaxPercentageRelativeToTarget) { return false; }
|
||||
|
||||
@@ -9,14 +9,7 @@ namespace Barotrauma
|
||||
{
|
||||
public class SubactionGroup
|
||||
{
|
||||
public string Text;
|
||||
public List<EventAction> Actions;
|
||||
/// <summary>
|
||||
/// Should this option end the conversation (closing the conversation prompt?). By default, options that don't have any actions inside them, or that only have a GoTo action, end the conversation.
|
||||
/// But if there are other actions inside the option, the game assumes there may be some kind of a follow-up coming to the conversation, and by default leaves it open.
|
||||
/// </summary>
|
||||
public bool EndConversation;
|
||||
|
||||
private int currentSubAction = 0;
|
||||
|
||||
public EventAction CurrentSubAction
|
||||
@@ -31,17 +24,17 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement elem)
|
||||
public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement element)
|
||||
{
|
||||
Text = elem.GetAttribute("text")?.Value ?? "";
|
||||
SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
Actions = new List<EventAction>();
|
||||
EndConversation = elem.GetAttributeBool("endconversation", false);
|
||||
foreach (var e in elem.Elements())
|
||||
foreach (var e in element.Elements())
|
||||
{
|
||||
if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction.",
|
||||
contentPackage: elem.ContentPackage);
|
||||
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action. Please configure status effects as child elements of a StatusEffectAction.",
|
||||
contentPackage: element.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
var action = Instantiate(scriptedEvent, e);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Forces a specific character to say a message in chat.
|
||||
/// </summary>
|
||||
class ForceSayAction : EventAction
|
||||
{
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the character that should say the message.")]
|
||||
public Identifier TargetTag { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "The message that the character should say. Can be the text as-is, or a tag referring to a line in a text file.")]
|
||||
public string Message { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should the message that the character says be sent in radio?")]
|
||||
public bool SayInRadio { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should the message be stripped of any quotation mark characters?")]
|
||||
public bool RemoveQuotes { get; set; }
|
||||
|
||||
public ForceSayAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
|
||||
|
||||
private bool isFinished = false;
|
||||
|
||||
public override bool IsFinished(ref string goTo)
|
||||
{
|
||||
return isFinished;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
isFinished = false;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
|
||||
var targets = ParentEvent.GetTargets(TargetTag);
|
||||
|
||||
LocalizedString messageToSay = TextManager.Get(Message).Fallback(Message);
|
||||
foreach (var target in targets)
|
||||
{
|
||||
if (target != null && target is Character character)
|
||||
{
|
||||
character.ForceSay(messageToSay, SayInRadio, RemoveQuotes);
|
||||
}
|
||||
}
|
||||
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
public override string ToDebugString()
|
||||
{
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(ForceSayAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
|
||||
$"Message: {Message})";
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
-65
@@ -1,84 +1,77 @@
|
||||
namespace Barotrauma
|
||||
#nullable enable
|
||||
namespace Barotrauma;
|
||||
|
||||
/// <summary>Changes the state of missions. The way the states are used depends on the type of mission.</summary>
|
||||
internal sealed class MissionStateAction : EventAction
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Changes the state of a specific active mission. The way the states are used depends on the type of mission.
|
||||
/// </summary>
|
||||
class MissionStateAction : EventAction
|
||||
/// <summary>The operation to perform on missions' states.</summary>
|
||||
public enum OperationType
|
||||
{
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the mission whose state to change.")]
|
||||
public Identifier MissionIdentifier { get; set; }
|
||||
/// <summary>Sets the missions' states to <see cref="State"/>.</summary>
|
||||
Set,
|
||||
/// <summary>Adds <see cref="State"/> to the missions' states.</summary>
|
||||
Add
|
||||
}
|
||||
|
||||
public enum OperationType
|
||||
[Serialize("", IsPropertySaveable.Yes, "Identifiers of the missions whose states to change. Leave blank to only set the state of the mission that triggered the parent event.")]
|
||||
public Identifier MissionIdentifier { get; set; }
|
||||
|
||||
[Serialize(OperationType.Set, IsPropertySaveable.Yes, "The operation to perform on missions' states.")]
|
||||
public OperationType Operation { get; set; }
|
||||
|
||||
[Serialize(0, IsPropertySaveable.Yes, "The value to apply to missions' states.")]
|
||||
public int State { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, "If set to true, missions are forced to fail without a chance of retrying them.")]
|
||||
public bool ForceFailure { get; set; }
|
||||
|
||||
public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
State = element.GetAttributeInt("value", State);
|
||||
if (Operation == OperationType.Add && State == 0 && !ForceFailure)
|
||||
{
|
||||
Set,
|
||||
Add
|
||||
DebugConsole.AddWarning($"Potential error in event \"{parentEvent.Prefab.Identifier}\": {nameof(MissionStateAction)} is set to only add 0 to the mission state, which will do nothing.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(OperationType.Set, IsPropertySaveable.Yes, description: "Should the value be added to the state of the mission, or should the state be set to the specified value.")]
|
||||
public OperationType Operation { get; set; }
|
||||
private bool isFinished;
|
||||
public override bool IsFinished(ref string goTo) => isFinished;
|
||||
public override void Reset() => isFinished = false;
|
||||
|
||||
[Serialize(0, IsPropertySaveable.Yes, description: "The state to set the mission to, or how much to add to the state of the mission.")]
|
||||
public int State { get; set; }
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the mission is forced to fail without a chance of retrying it.")]
|
||||
public bool ForceFailure { get; set; }
|
||||
|
||||
private bool isFinished;
|
||||
|
||||
public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
if (!MissionIdentifier.IsEmpty)
|
||||
{
|
||||
State = element.GetAttributeInt("value", State);
|
||||
if (MissionIdentifier.IsEmpty)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
if (Operation == OperationType.Add && State == 0 && !ForceFailure)
|
||||
{
|
||||
DebugConsole.AddWarning($"Potential error in event \"{parentEvent.Prefab.Identifier}\": {nameof(MissionStateAction)} is set to add 0 to the mission state, which will do nothing.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsFinished(ref string goTo)
|
||||
{
|
||||
return isFinished;
|
||||
}
|
||||
public override void Reset()
|
||||
{
|
||||
isFinished = false;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
|
||||
foreach (Mission mission in GameMain.GameSession.Missions)
|
||||
{
|
||||
if (mission.Prefab.Identifier != MissionIdentifier) { continue; }
|
||||
if (ForceFailure)
|
||||
{
|
||||
mission.ForceFailure = true;
|
||||
}
|
||||
|
||||
switch (Operation)
|
||||
{
|
||||
case OperationType.Set:
|
||||
mission.State = State;
|
||||
break;
|
||||
case OperationType.Add:
|
||||
mission.State += State;
|
||||
break;
|
||||
}
|
||||
SetMissionState(mission);
|
||||
}
|
||||
|
||||
isFinished = true;
|
||||
}
|
||||
else if (ParentEvent.TriggeringMission != null)
|
||||
{
|
||||
SetMissionState(ParentEvent.TriggeringMission);
|
||||
}
|
||||
|
||||
public override string ToDebugString()
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
private void SetMissionState(Mission mission)
|
||||
{
|
||||
if (ForceFailure) { mission.ForceFailure = true; }
|
||||
switch (Operation)
|
||||
{
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionStateAction)} -> ({(Operation == OperationType.Set ? State : '+' + State)})";
|
||||
case OperationType.Set:
|
||||
mission.State = State;
|
||||
break;
|
||||
case OperationType.Add:
|
||||
mission.State += State;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToDebugString() => $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionStateAction)} -> ({(Operation == OperationType.Set ? State : '+' + State)})";
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace Barotrauma
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should the NPC start or stop following the target?")]
|
||||
public bool Follow { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should the NPC be forced to walk towards the target?")]
|
||||
public bool ForceWalk { get; set; }
|
||||
|
||||
[Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of NPCs to target (e.g. you could choose to only make a specific number of security officers follow the player.)")]
|
||||
public int MaxTargets { get; set; }
|
||||
|
||||
@@ -65,7 +68,8 @@ namespace Barotrauma
|
||||
var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true)
|
||||
{
|
||||
OverridePriority = Priority,
|
||||
IsFollowOrder = true
|
||||
IsFollowOrder = true,
|
||||
ForceWalkPermanently = ForceWalk
|
||||
};
|
||||
humanAiController.ObjectiveManager.AddObjective(newObjective);
|
||||
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
|
||||
|
||||
@@ -271,6 +271,10 @@ namespace Barotrauma
|
||||
ParentEvent.AddTarget(TargetTag, newCharacter);
|
||||
}
|
||||
spawnedEntity = newCharacter;
|
||||
if (newCharacter is { AIController: EnemyAIController enemyAi, Submarine: Submarine ownSub })
|
||||
{
|
||||
enemyAi.SetUnattackableSubmarines(ownSub);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,42 +240,45 @@ namespace Barotrauma
|
||||
CreateEvents(eventSet);
|
||||
}
|
||||
|
||||
if (level?.LevelData != null)
|
||||
bool isOutpostLevel = level?.LevelData is { Type: LevelData.LevelType.Outpost } ||
|
||||
(GameMain.GameSession?.GameMode is TestGameMode && Submarine.MainSub?.Info?.Type == SubmarineType.Outpost);
|
||||
if (isOutpostLevel)
|
||||
{
|
||||
if (level.LevelData.Type == LevelData.LevelType.Outpost)
|
||||
//if the outpost is connected to a locked connection, create an event to unlock it
|
||||
if (level?.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false)
|
||||
{
|
||||
//if the outpost is connected to a locked connection, create an event to unlock it
|
||||
if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false)
|
||||
var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction);
|
||||
if (unlockPathEventPrefab != null)
|
||||
{
|
||||
var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction);
|
||||
if (unlockPathEventPrefab != null)
|
||||
var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed);
|
||||
activeEvents.Add(newEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
//if no event that unlocks the path can be found, unlock it automatically
|
||||
level.StartLocation.Connections.ForEach(c => c.Locked = false);
|
||||
}
|
||||
}
|
||||
Submarine outpost = level?.StartOutpost ?? Submarine.MainSub;
|
||||
if (GameMain.NetworkMember is not { IsClient: true } && outpost != null)
|
||||
{
|
||||
foreach (var eventTag in outpost.Info.TriggerOutpostMissionEvents)
|
||||
{
|
||||
EventPrefab eventPrefab = EventPrefab.FindEventPrefab(identifier: Identifier.Empty, tag: eventTag, outpost.ContentPackage);
|
||||
if (eventPrefab == null)
|
||||
{
|
||||
var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed);
|
||||
activeEvents.Add(newEvent);
|
||||
DebugConsole.ThrowError($"Outpost {outpost.Info.DisplayName} failed to trigger an event (tag: {eventTag}).", contentPackage: outpost.ContentPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
//if no event that unlocks the path can be found, unlock it automatically
|
||||
level.StartLocation.Connections.ForEach(c => c.Locked = false);
|
||||
var newEvent = eventPrefab.CreateInstance(RandomSeed);
|
||||
ActivateEvent(newEvent);
|
||||
}
|
||||
}
|
||||
if (GameMain.NetworkMember is not { IsClient: true } && level.StartOutpost != null)
|
||||
{
|
||||
foreach (var eventTag in level.StartOutpost.Info.TriggerOutpostMissionEvents)
|
||||
{
|
||||
EventPrefab eventPrefab = EventPrefab.FindEventPrefab(identifier: Identifier.Empty, tag: eventTag, level.StartOutpost.ContentPackage);
|
||||
if (eventPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Outpost {level.StartOutpost.Info.DisplayName} failed to trigger an event (tag: {eventTag}).", contentPackage: level.StartOutpost.ContentPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newEvent = eventPrefab.CreateInstance(RandomSeed);
|
||||
ActivateEvent(newEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (level?.LevelData != null)
|
||||
{
|
||||
RegisterNonRepeatableChildEvents(initialEventSet);
|
||||
void RegisterNonRepeatableChildEvents(EventSet eventSet)
|
||||
{
|
||||
|
||||
@@ -233,7 +233,7 @@ namespace Barotrauma
|
||||
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
return State > 0 && State != HostagesKilledState;
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
return level.CheckBeaconActive();
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
|
||||
{
|
||||
|
||||
@@ -204,7 +204,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
return Winner != CharacterTeamType.None;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#nullable enable
|
||||
namespace Barotrauma;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a mission where the success and failure are determined solely by its state.
|
||||
/// Intended to be used alongside <see cref="MissionStateAction"/>.
|
||||
/// </summary>
|
||||
internal sealed partial class CustomMission(MissionPrefab prefab, Location[] locations, Submarine sub) : Mission(prefab, locations, sub)
|
||||
{
|
||||
public readonly int SuccessState = prefab.ConfigElement.GetAttributeInt(nameof(SuccessState), +1);
|
||||
public readonly int FailureState = prefab.ConfigElement.GetAttributeInt(nameof(FailureState), -1);
|
||||
|
||||
public bool RequireDestinationReached = prefab.ConfigElement.GetAttributeBool(nameof(RequireDestinationReached), false);
|
||||
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) =>
|
||||
State == SuccessState &&
|
||||
(!RequireDestinationReached || transitionType is CampaignMode.TransitionType.ProgressToNextLocation or CampaignMode.TransitionType.ProgressToNextEmptyLocation);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.RuinGeneration;
|
||||
using Microsoft.Xna.Framework;
|
||||
@@ -199,7 +199,7 @@ namespace Barotrauma
|
||||
|
||||
private static bool IsEnemyDefeated(Character enemy) => enemy == null ||enemy.Removed || enemy.IsDead;
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
bool exitingLevel = GameMain.GameSession?.GameMode is CampaignMode campaign ?
|
||||
campaign.GetAvailableTransition() != CampaignMode.TransitionType.None :
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
@@ -301,7 +301,7 @@ namespace Barotrauma
|
||||
|
||||
partial void OnStateChangedProjSpecific();
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
return Phase == MissionPhase.BossKilled;
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ namespace Barotrauma
|
||||
return character != null && !character.Removed && !character.IsDead;
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
if (Level.Loaded?.Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
@@ -25,7 +25,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
return Submarine.MainSub is { AtEndExit: true };
|
||||
return transitionType == CampaignMode.TransitionType.ProgressToNextLocation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
@@ -47,6 +47,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minerals spawned by the mission. Note that minerals that were already present in the level may have also been used as targets.
|
||||
/// Each list of items represents a separate cluster of minerals.
|
||||
/// </summary>
|
||||
public IEnumerable<List<Item>> SpawnedResources => spawnedResources.Values;
|
||||
|
||||
public override LocalizedString SuccessMessage => ModifyMessage(base.SuccessMessage);
|
||||
public override LocalizedString FailureMessage => ModifyMessage(base.FailureMessage);
|
||||
public override LocalizedString Description => ModifyMessage(description);
|
||||
@@ -169,7 +175,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
return EnoughHaveBeenCollected();
|
||||
}
|
||||
|
||||
@@ -401,14 +401,9 @@ namespace Barotrauma
|
||||
{
|
||||
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
|
||||
}
|
||||
if (submarine != null && spawnedCharacter.AIController is EnemyAIController enemyAi)
|
||||
if (spawnedCharacter.AIController is EnemyAIController enemyAi && submarine != null)
|
||||
{
|
||||
enemyAi.UnattackableSubmarines.Add(submarine);
|
||||
enemyAi.UnattackableSubmarines.Add(Submarine.MainSub);
|
||||
foreach (Submarine sub in Submarine.MainSub.DockedTo)
|
||||
{
|
||||
enemyAi.UnattackableSubmarines.Add(sub);
|
||||
}
|
||||
enemyAi.SetUnattackableSubmarines(submarine);
|
||||
}
|
||||
InitCharacter(spawnedCharacter, element);
|
||||
return spawnedCharacter;
|
||||
@@ -532,6 +527,7 @@ namespace Barotrauma
|
||||
if (GameMain.GameSession?.EventManager != null)
|
||||
{
|
||||
var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed);
|
||||
newEvent.TriggeringMission = this;
|
||||
GameMain.GameSession.EventManager.ActivateEvent(newEvent);
|
||||
}
|
||||
}
|
||||
@@ -539,13 +535,13 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// End the mission and give a reward if it was completed successfully
|
||||
/// </summary>
|
||||
public void End()
|
||||
public void End(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
if (GameMain.NetworkMember is not { IsClient: true })
|
||||
{
|
||||
completed =
|
||||
!ForceFailure &&
|
||||
DetermineCompleted() &&
|
||||
DetermineCompleted(transitionType) &&
|
||||
(completeCheckDataAction == null || completeCheckDataAction.GetSuccess());
|
||||
}
|
||||
if (completed)
|
||||
@@ -578,7 +574,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract bool DetermineCompleted();
|
||||
protected abstract bool DetermineCompleted(CampaignMode.TransitionType transitionType);
|
||||
|
||||
protected virtual void EndMissionSpecific(bool completed) { }
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ namespace Barotrauma
|
||||
{ "GoTo".ToIdentifier(), typeof(GoToMission) },
|
||||
{ "ScanAlienRuins".ToIdentifier(), typeof(ScanMission) },
|
||||
{ "EliminateTargets".ToIdentifier(), typeof(EliminateTargetsMission) },
|
||||
{ "End".ToIdentifier(), typeof(EndMission) }
|
||||
{ "End".ToIdentifier(), typeof(EndMission) },
|
||||
{ "Custom".ToIdentifier(), typeof(CustomMission) }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -64,6 +65,7 @@ namespace Barotrauma
|
||||
|
||||
public Type MissionClass { get; private set; }
|
||||
|
||||
public bool CampaignOnly { get; private set; }
|
||||
public bool MultiplayerOnly { get; private set; }
|
||||
public bool SingleplayerOnly { get; private set; }
|
||||
|
||||
@@ -319,8 +321,9 @@ namespace Barotrauma
|
||||
|
||||
SonarIconIdentifier = ConfigElement.GetAttributeIdentifier("sonaricon", "");
|
||||
|
||||
MultiplayerOnly = ConfigElement.GetAttributeBool("multiplayeronly", false);
|
||||
SingleplayerOnly = ConfigElement.GetAttributeBool("singleplayeronly", false);
|
||||
CampaignOnly = ConfigElement.GetAttributeBool(nameof(CampaignOnly), false);
|
||||
MultiplayerOnly = ConfigElement.GetAttributeBool(nameof(MultiplayerOnly), false);
|
||||
SingleplayerOnly = ConfigElement.GetAttributeBool(nameof(SingleplayerOnly), false);
|
||||
|
||||
AchievementIdentifier = ConfigElement.GetAttributeIdentifier("achievementidentifier", "");
|
||||
|
||||
@@ -543,7 +546,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all mission types that can be selected e.g. in the server lobby, excluding any special, hidden ones like EndMission
|
||||
/// Returns all mission types that can be selected in the server lobby, excluding any special, hidden ones like EndMission
|
||||
/// (the mission at the end of the campaign)
|
||||
/// </summary>
|
||||
public static IEnumerable<Identifier> GetAllMultiplayerSelectableMissionTypes()
|
||||
@@ -552,6 +555,7 @@ namespace Barotrauma
|
||||
foreach (var missionPrefab in Prefabs)
|
||||
{
|
||||
if (missionPrefab.Commonness <= 0.0f) { continue; }
|
||||
if (missionPrefab.CampaignOnly) { continue; }
|
||||
if (missionPrefab.SingleplayerOnly) { continue; }
|
||||
if (HiddenMissionTypes.Contains(missionPrefab.Type))
|
||||
{
|
||||
|
||||
@@ -242,7 +242,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
return state > 0;
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
return AllItemsDestroyedOrRetrieved();
|
||||
}
|
||||
|
||||
@@ -547,7 +547,7 @@ namespace Barotrauma
|
||||
return character == null || character.Removed || character.Submarine == null || (character.LockHands && character.Submarine == Submarine.MainSub) || character.IsIncapacitated;
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
return state == 2;
|
||||
}
|
||||
|
||||
@@ -715,7 +715,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
if (requiredDeliveryAmount < 1.0f)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.RuinGeneration;
|
||||
@@ -17,7 +17,7 @@ namespace Barotrauma
|
||||
private readonly Dictionary<Item, ushort> parentInventoryIDs = new Dictionary<Item, ushort>();
|
||||
private readonly Dictionary<Item, int> inventorySlotIndices = new Dictionary<Item, int>();
|
||||
private readonly Dictionary<Item, byte> parentItemContainerIndices = new Dictionary<Item, byte>();
|
||||
private readonly int targetsToScan;
|
||||
private readonly int totalTargetsToScan;
|
||||
private readonly Dictionary<WayPoint, bool> scanTargets = new Dictionary<WayPoint, bool>();
|
||||
private readonly HashSet<WayPoint> newTargetsScanned = new HashSet<WayPoint>();
|
||||
private readonly float minTargetDistance;
|
||||
@@ -44,7 +44,7 @@ namespace Barotrauma
|
||||
public ScanMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
{
|
||||
itemConfig = prefab.ConfigElement.GetChildElement("Items");
|
||||
targetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1);
|
||||
totalTargetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1);
|
||||
minTargetDistance = prefab.ConfigElement.GetAttributeFloat("mintargetdistance", 0.0f);
|
||||
}
|
||||
|
||||
@@ -77,57 +77,60 @@ namespace Barotrauma
|
||||
|
||||
var ruinWaypoints = TargetRuin.Submarine.GetWaypoints(false);
|
||||
ruinWaypoints.RemoveAll(wp => wp.CurrentHull == null);
|
||||
if (ruinWaypoints.Count < targetsToScan)
|
||||
if (ruinWaypoints.Count < totalTargetsToScan)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {targetsToScan})",
|
||||
DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {totalTargetsToScan})",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
return;
|
||||
}
|
||||
|
||||
//the distance we'll use if we otherwise fail to place the targets far enough from each other
|
||||
//(smallest extent should be large enough to fit the targets and one extra to be safe)
|
||||
float guaranteedDistance = Math.Min(TargetRuin.Area.Width, TargetRuin.Area.Height) / (totalTargetsToScan + 1);
|
||||
|
||||
var availableWaypoints = new List<WayPoint>();
|
||||
float minTargetDistanceSquared = minTargetDistance * minTargetDistance;
|
||||
for (int tries = 0; tries < 15; tries++)
|
||||
const int MaxTries = 15;
|
||||
for (int tries = 0; tries < MaxTries; tries++)
|
||||
{
|
||||
float triesNormalized = tries / (float)(MaxTries - 1); // 0.0 -> 1.0
|
||||
float desperationFactor = MathF.Pow(triesNormalized, 2);
|
||||
//try placing the targets the desired minimum distance apart, gradually lowering the distance requirement on each try
|
||||
float currentMinDistance = MathHelper.Lerp(minTargetDistance, guaranteedDistance, desperationFactor);
|
||||
float currentMinDistanceSquared = currentMinDistance * currentMinDistance;
|
||||
|
||||
scanTargets.Clear();
|
||||
availableWaypoints.Clear();
|
||||
availableWaypoints.AddRange(ruinWaypoints);
|
||||
for (int i = 0; i < targetsToScan; i++)
|
||||
for (int i = 0; i < totalTargetsToScan; i++)
|
||||
{
|
||||
var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.ServerAndClient);
|
||||
scanTargets.Add(selectedWaypoint, false);
|
||||
availableWaypoints.Remove(selectedWaypoint);
|
||||
if (i < (targetsToScan - 1))
|
||||
if (i < (totalTargetsToScan - 1))
|
||||
{
|
||||
availableWaypoints.RemoveAll(wp => wp.CurrentHull == selectedWaypoint.CurrentHull);
|
||||
availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < minTargetDistanceSquared);
|
||||
availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < currentMinDistanceSquared);
|
||||
if (availableWaypoints.None())
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})",
|
||||
DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {totalTargetsToScan})",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scanTargets.Count >= targetsToScan)
|
||||
if (scanTargets.Count >= totalTargetsToScan)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"Successfully initialized a Scan mission: targets set on try #{tries + 1}", Color.Green);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
if ((tries + 1) % 5 == 0)
|
||||
{
|
||||
float reducedMinTargetDistance = (1.0f - (((tries + 1) / 5) * 0.1f)) * minTargetDistance;
|
||||
minTargetDistanceSquared = reducedMinTargetDistance * reducedMinTargetDistance;
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"Reducing minimum distance between Scan mission targets (new min: {reducedMinTargetDistance}) to reach the required target count", Color.Yellow);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (scanTargets.Count < targetsToScan)
|
||||
if (scanTargets.Count < totalTargetsToScan)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {targetsToScan})",
|
||||
DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {totalTargetsToScan})",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
}
|
||||
}
|
||||
@@ -241,9 +244,9 @@ namespace Barotrauma
|
||||
State = Math.Max(State, scanTargets.Count(kvp => kvp.Value));
|
||||
}
|
||||
|
||||
private bool AllTargetsScanned() => State >= targetsToScan;
|
||||
private bool AllTargetsScanned() => State >= totalTargetsToScan;
|
||||
|
||||
protected override bool DetermineCompleted() => AllTargetsScanned();
|
||||
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => AllTargetsScanned();
|
||||
|
||||
protected override void EndMissionSpecific(bool completed)
|
||||
{
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace Barotrauma
|
||||
|
||||
return authTicket.TryUnwrap(out var ticketUnwrapped) && ticketUnwrapped.Data is { Length: > 0 }
|
||||
? new AuthTicket(ToolBoxCore.ByteArrayToHexString(ticketUnwrapped.Data), Platform.Steam) //convert byte array to hex
|
||||
: throw new Exception("Could not retrieve Steamworks authentication ticket for GameAnalytics");
|
||||
: throw new Exception("Could not retrieve Steam authentication ticket, possibly due to connection issues. GameAnalytics logging will be disabled.");
|
||||
}
|
||||
|
||||
private static async Task<AuthTicket> GetEOSAuthTicket()
|
||||
@@ -215,9 +215,8 @@ namespace Barotrauma
|
||||
IRestResponse response;
|
||||
try
|
||||
{
|
||||
var client = new RestClient(consentServerUrl);
|
||||
|
||||
var request = new RestRequest(consentServerFile, Method.GET);
|
||||
var client = RestFactory.CreateClient(consentServerUrl);
|
||||
var request = RestFactory.CreateRequest(consentServerFile);
|
||||
request.AddParameter("authticket", authTicket.Token);
|
||||
if (consent == Consent.Ask)
|
||||
{
|
||||
@@ -321,7 +320,7 @@ namespace Barotrauma
|
||||
RestClient client;
|
||||
try
|
||||
{
|
||||
client = new RestClient(consentServerUrl);
|
||||
client = RestFactory.CreateClient(consentServerUrl);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -329,7 +328,7 @@ namespace Barotrauma
|
||||
return Consent.Error;
|
||||
}
|
||||
|
||||
var request = new RestRequest(consentServerFile, Method.GET);
|
||||
var request = RestFactory.CreateRequest(consentServerFile);
|
||||
request.AddParameter("authticket", authTicket.Token);
|
||||
request.AddParameter("action", "getconsent");
|
||||
request.AddParameter("request_version", RemoteRequestVersion);
|
||||
|
||||
@@ -1017,7 +1017,7 @@ namespace Barotrauma
|
||||
UpdateStoreStock();
|
||||
}
|
||||
|
||||
GameMain.GameSession.EndMissions();
|
||||
GameMain.GameSession.EndMissions(TransitionType.None);
|
||||
GameMain.GameSession.EventManager?.StoreEventDataAtRoundEnd(registerFinishedOnly: true);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (MissionPrefab missionPrefab in missionPrefabs)
|
||||
{
|
||||
if (missionPrefab.CampaignOnly) { continue; }
|
||||
if (!missionClasses.ContainsValue(missionPrefab.MissionClass))
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot start gamemode with a {missionPrefab.MissionClass} mission.");
|
||||
@@ -68,7 +69,7 @@ namespace Barotrauma
|
||||
{
|
||||
return missionTypes.Where(type =>
|
||||
MissionPrefab.Prefabs.OrderBy(missionPrefab => missionPrefab.UintIdentifier)
|
||||
.Any(missionPrefab => missionPrefab.Type == type && missionClasses.ContainsValue(missionPrefab.MissionClass)));
|
||||
.Any(missionPrefab => missionPrefab.Type == type && !missionPrefab.CampaignOnly && missionClasses.ContainsValue(missionPrefab.MissionClass)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
#nullable enable
|
||||
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.IO;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.PerkBehaviors;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.PerkBehaviors;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -761,7 +762,6 @@ namespace Barotrauma
|
||||
GUI.PreventPauseMenuToggle = false;
|
||||
HintManager.OnRoundStarted();
|
||||
|
||||
GameMain.LuaCs.Hook.Call("roundStart");
|
||||
EnableEventLogNotificationIcon(enabled: false);
|
||||
|
||||
LogStartRoundStats();
|
||||
@@ -955,14 +955,6 @@ namespace Barotrauma
|
||||
sub.SetPosition(spawnPos);
|
||||
myPort.Dock(outPostPort);
|
||||
myPort.Lock(isNetworkMessage: true, applyEffects: false);
|
||||
foreach (var item in sub.GetItems(alsoFromConnectedSubs: true))
|
||||
{
|
||||
//need to refresh position to maintain since the sub was moved to the docking port
|
||||
if (item.GetComponent<Steering>() is { MaintainPos: true } steering)
|
||||
{
|
||||
steering.RefreshPosToMaintain();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -985,6 +977,16 @@ namespace Barotrauma
|
||||
sub.EnableMaintainPosition();
|
||||
}
|
||||
|
||||
foreach (var item in sub.GetItems(alsoFromConnectedSubs: true))
|
||||
{
|
||||
// Refresh pos to maintain in all steering components maintaining
|
||||
// position, including ones in shuttles, since the submarines moved
|
||||
if (item.GetComponent<Steering>() is { MaintainPos: true } steering)
|
||||
{
|
||||
steering.RefreshPosToMaintain();
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that linked subs which are NOT docked to the main sub
|
||||
// (but still close enough to NOT be considered as 'left behind')
|
||||
// are also moved to keep their relative position to the main sub
|
||||
@@ -1048,9 +1050,6 @@ namespace Barotrauma
|
||||
/// </remarks>
|
||||
public static ImmutableHashSet<Character> GetSessionCrewCharacters(CharacterType type)
|
||||
{
|
||||
var result = GameMain.LuaCs.Hook.Call<Character[]?>("getSessionCrewCharacters", type);
|
||||
if (result != null) return ImmutableHashSet.Create(result);
|
||||
|
||||
if (GameMain.GameSession?.CrewManager is not { } crewManager) { return ImmutableHashSet<Character>.Empty; }
|
||||
|
||||
IEnumerable<Character> players;
|
||||
@@ -1089,9 +1088,6 @@ namespace Barotrauma
|
||||
{
|
||||
RoundEnding = true;
|
||||
|
||||
#if CLIENT
|
||||
GameMain.LuaCs.Hook.Call("roundEnd");
|
||||
#endif
|
||||
//Clear the grids to allow for garbage collection
|
||||
Powered.Grids.Clear();
|
||||
Powered.ChangedConnections.Clear();
|
||||
@@ -1103,15 +1099,13 @@ namespace Barotrauma
|
||||
ImmutableHashSet<Character> crewCharacters = GetSessionCrewCharacters(CharacterType.Both);
|
||||
int prevMoney = GetAmountOfMoney(crewCharacters);
|
||||
|
||||
EndMissions();
|
||||
EndMissions(transitionType);
|
||||
|
||||
foreach (Character character in crewCharacters)
|
||||
{
|
||||
character.CheckTalents(AbilityEffectType.OnRoundEnd);
|
||||
}
|
||||
|
||||
GameMain.LuaCs.Hook.Call("missionsEnded", missions);
|
||||
|
||||
#if CLIENT
|
||||
if (GUI.PauseMenuOpen)
|
||||
{
|
||||
@@ -1208,12 +1202,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void EndMissions()
|
||||
public void EndMissions(CampaignMode.TransitionType transitionType)
|
||||
{
|
||||
ImmutableHashSet<Character> crewCharacters = GetSessionCrewCharacters(CharacterType.Both);
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
mission.End();
|
||||
mission.End(transitionType);
|
||||
}
|
||||
|
||||
if (missions.Any())
|
||||
|
||||
@@ -43,8 +43,12 @@ namespace Barotrauma
|
||||
public InvSlotType[] SlotTypes
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimization for fast access of <see cref="slots"/> by <see cref="SlotTypes"/>.
|
||||
/// </summary>
|
||||
private readonly Dictionary<InvSlotType, List<ItemSlot>> slotsByType = [];
|
||||
|
||||
public static readonly List<InvSlotType> AnySlot = new List<InvSlotType> { InvSlotType.Any };
|
||||
public static readonly List<InvSlotType> BagSlot = new List<InvSlotType> { InvSlotType.Bag };
|
||||
@@ -106,9 +110,20 @@ namespace Barotrauma
|
||||
case InvSlotType.RightHand:
|
||||
slots[i].HideIfEmpty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
InvSlotType slotType = SlotTypes[i];
|
||||
if (!slotsByType.TryGetValue(slotType, out List<ItemSlot> slotList))
|
||||
{
|
||||
slotList = [];
|
||||
slotsByType[SlotTypes[i]] = slotList;
|
||||
}
|
||||
slotList.Add(slots[i]);
|
||||
}
|
||||
|
||||
InitProjSpecific(element);
|
||||
|
||||
var itemElements = element.Elements().Where(e => e.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase));
|
||||
@@ -198,39 +213,55 @@ namespace Barotrauma
|
||||
|
||||
public Item GetItemInLimbSlot(InvSlotType limbSlot)
|
||||
{
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
if (slotsByType.TryGetValue(limbSlot, out List<ItemSlot> slotList))
|
||||
{
|
||||
if (SlotTypes[i] == limbSlot) { return slots[i].FirstOrDefault(); }
|
||||
return slotList.First().FirstOrDefault();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<Item> GetItemsInLimbSlot(InvSlotType limbSlot)
|
||||
{
|
||||
if (slotsByType.TryGetValue(limbSlot, out List<ItemSlot> slotList))
|
||||
{
|
||||
foreach (var slot in slotList)
|
||||
{
|
||||
foreach (Item item in slot.Items)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsInLimbSlot(Item item, InvSlotType limbSlot)
|
||||
{
|
||||
if (limbSlot == (InvSlotType.LeftHand | InvSlotType.RightHand))
|
||||
{
|
||||
int rightHandSlot = FindLimbSlot(InvSlotType.RightHand);
|
||||
int leftHandSlot = FindLimbSlot(InvSlotType.LeftHand);
|
||||
if (rightHandSlot > -1 && slots[rightHandSlot].Contains(item) &&
|
||||
leftHandSlot > -1 && slots[leftHandSlot].Contains(item))
|
||||
if (GetItemsInLimbSlot(InvSlotType.RightHand).Contains(item) &&
|
||||
GetItemsInLimbSlot(InvSlotType.LeftHand).Contains(item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
else if (slotsByType.TryGetValue(limbSlot, out List<ItemSlot> slotList))
|
||||
{
|
||||
if (SlotTypes[i] == limbSlot && slots[i].Contains(item)) { return true; }
|
||||
foreach (ItemSlot slot in slotList)
|
||||
{
|
||||
if (slot.Contains(item)) { return true; }
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsSlotEmpty(InvSlotType limbSlot)
|
||||
{
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
if (slotsByType.TryGetValue(limbSlot, out List<ItemSlot> slotList))
|
||||
{
|
||||
if (SlotTypes[i] == limbSlot && slots[i].Empty()) { return true; }
|
||||
foreach (ItemSlot slot in slotList)
|
||||
{
|
||||
if (slot.Empty()) { return true; }
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -370,7 +401,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// If there is room, puts the item in the inventory and returns true, otherwise returns false
|
||||
/// </summary>
|
||||
public override bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false)
|
||||
public override bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true)
|
||||
{
|
||||
if (allowedSlots == null || !allowedSlots.Any()) { return false; }
|
||||
if (item == null)
|
||||
@@ -494,8 +525,6 @@ namespace Barotrauma
|
||||
return placedInSlot > -1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public bool IsAnySlotAvailable(Item item) => GetFreeAnySlot(item, inWrongSlot: false) > -1;
|
||||
|
||||
private int GetFreeAnySlot(Item item, bool inWrongSlot)
|
||||
@@ -542,7 +571,7 @@ namespace Barotrauma
|
||||
return -1;
|
||||
}
|
||||
|
||||
public override bool TryPutItem(Item item, int index, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false)
|
||||
public override bool TryPutItem(Item item, int index, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true)
|
||||
{
|
||||
if (index < 0 || index >= slots.Length)
|
||||
{
|
||||
@@ -590,9 +619,9 @@ namespace Barotrauma
|
||||
return TryPutItem(item, user, new List<InvSlotType>() { placeToSlots }, createNetworkEvent, ignoreCondition);
|
||||
}
|
||||
|
||||
protected override void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true)
|
||||
protected override void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true, bool triggerOnInsertedEffects = true)
|
||||
{
|
||||
base.PutItem(item, i, user, removeItem, createNetworkEvent);
|
||||
base.PutItem(item, i, user, removeItem, createNetworkEvent, triggerOnInsertedEffects);
|
||||
#if CLIENT
|
||||
CreateSlots();
|
||||
if (character == Character.Controlled)
|
||||
|
||||
@@ -739,6 +739,12 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
picker.Inventory.FlashAllowedSlots(item, Color.Red);
|
||||
}
|
||||
else
|
||||
{
|
||||
//normally this would be done in the base.OnPicked method, but clients don't call it,
|
||||
//but instead rely on the server telling them to put the item in the inventory
|
||||
SoundPlayer.PlayUISound(GUISoundType.PickItem);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using FarseerPhysics;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using FarseerPhysics.Dynamics.Contacts;
|
||||
using Microsoft.Xna.Framework;
|
||||
@@ -347,18 +348,9 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else if (f2.Body.UserData is Character targetCharacter)
|
||||
{
|
||||
if (targetCharacter == picker || targetCharacter == User) { return false; }
|
||||
if (targetCharacter.IgnoreMeleeWeapons) { return false; }
|
||||
if (HitFriendlyTarget(targetCharacter)) { return false; }
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetCharacter)) { return false; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitTargets.Any(t => t is Character)) { return false; }
|
||||
}
|
||||
hitTargets.Add(targetCharacter);
|
||||
//only allow hitting limbs, not the main collider
|
||||
//otherwise it's difficult to make certain parts of the ragdoll not take hits by making them ignore collisions or melee weapons
|
||||
return false;
|
||||
}
|
||||
else if (!HitOnlyCharacters)
|
||||
{
|
||||
@@ -435,7 +427,7 @@ namespace Barotrauma.Items.Components
|
||||
Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
|
||||
Item targetItem = target.UserData is Holdable h ? h.Item : target.UserData as Item ?? targetFixture.UserData as Item;
|
||||
Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
|
||||
GameMain.LuaCs.Hook.Call("meleeWeapon.handleImpact", this, target);
|
||||
LuaCsSetup.Instance.EventService.PublishEvent<IEventMeleeWeaponHandleImpact>(x => x.OnMeleeWeaponHandleImpact(this, target));
|
||||
|
||||
if (Attack != null)
|
||||
{
|
||||
|
||||
@@ -380,7 +380,7 @@ namespace Barotrauma.Items.Components
|
||||
break;
|
||||
case "requireditem":
|
||||
case "requireditems":
|
||||
SetRequiredItems(subElement);
|
||||
SetRequiredItems(subElement, allowEmpty: true);
|
||||
break;
|
||||
case "requiredskill":
|
||||
case "requiredskills":
|
||||
@@ -1102,6 +1102,9 @@ namespace Barotrauma.Items.Components
|
||||
foreach (RelatedItem ri in DisabledRequiredItems)
|
||||
{
|
||||
XElement newElement = new XElement("requireditem");
|
||||
//if we have some actual requirements, no need to keep the empty requirement
|
||||
//as a "placeholder" for the user to add requirements in the sub editor
|
||||
if (ri.Identifiers.IsEmpty && RequiredItems.Any()) { continue; }
|
||||
ri.Save(newElement);
|
||||
componentElement.Add(newElement);
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
partial void InitProjSpecific(ContentXElement element);
|
||||
|
||||
public void OnItemContained(Item containedItem)
|
||||
public void OnItemContained(Item containedItem, bool triggerOnInsertedEffects = true)
|
||||
{
|
||||
int index = Inventory.FindIndex(containedItem);
|
||||
RelatedItem relatedItem = null;
|
||||
@@ -444,14 +444,20 @@ namespace Barotrauma.Items.Components
|
||||
ActiveContainedItem activeContainedItem = new(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition, containableItem.BlameEquipperForDeath);
|
||||
activeContainedItems.Add(activeContainedItem);
|
||||
|
||||
if (!ShouldApplyEffects(activeContainedItem) || item.Submarine is { Loading: true} || initializingLoadedItems ||
|
||||
containedItem.OnInsertedEffectsApplied)
|
||||
{
|
||||
continue;
|
||||
if (triggerOnInsertedEffects)
|
||||
{
|
||||
if (!ShouldApplyEffects(activeContainedItem) || item.Submarine is { Loading: true} || initializingLoadedItems ||
|
||||
containedItem.OnInsertedEffectsApplied)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
activeContainedItem.StatusEffect.Apply(ActionType.OnInserted, deltaTime: 1, item, targets);
|
||||
}
|
||||
activeContainedItem.StatusEffect.Apply(ActionType.OnInserted, deltaTime: 1, item, targets);
|
||||
}
|
||||
containedItem.OnInsertedEffectsApplied = true;
|
||||
if (triggerOnInsertedEffects)
|
||||
{
|
||||
containedItem.OnInsertedEffectsApplied = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1119,6 +1125,16 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
if (item.GetComponent<Holdable>() is { Attachable: true })
|
||||
{
|
||||
//if the item is attachable to walls, we need a bit of special logic because the item can either
|
||||
//have or not have a body depending on whether it's attached.
|
||||
|
||||
//since it seems previously the contained item positions have always been configured as if the item had no body (using the top-left corner as the origin),
|
||||
//let's modify the position here to position the items correctly even when the body is active (moving the origin from the center of the body to the top-left corner)
|
||||
transformedItemPos -= item.Rect.Size.FlipY().ToVector2() / 2;
|
||||
}
|
||||
|
||||
Matrix transform = Matrix.CreateRotationZ(drawPosition ? item.body.DrawRotation : item.body.Rotation);
|
||||
if (bodyFlipped)
|
||||
{
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
#nullable enable
|
||||
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Item component used by <see cref="Controller.SpawnItemOnSelected"/> for keeping a reference to the character that is currently
|
||||
/// selecting the controller. Also provides functionality for changing the inventory sprite of the item based on the linked character.
|
||||
/// </summary>
|
||||
partial class LinkedControllerCharacterComponent : ItemComponent, IServerSerializable
|
||||
{
|
||||
#if CLIENT
|
||||
private class SpriteOverride
|
||||
{
|
||||
public readonly Sprite? Sprite;
|
||||
public readonly Identifier SpeciesName;
|
||||
public readonly Identifier SpeciesGroup;
|
||||
public SpriteOverride(ContentXElement element)
|
||||
{
|
||||
if (element.GetChildElement("Sprite") is ContentXElement spriteElement)
|
||||
{
|
||||
Sprite = new Sprite(spriteElement);
|
||||
}
|
||||
SpeciesName = element.GetAttributeIdentifier("speciesname", Identifier.Empty);
|
||||
SpeciesGroup = element.GetAttributeIdentifier("speciesgroup", Identifier.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ImmutableArray<SpriteOverride> spriteOverrides;
|
||||
#endif
|
||||
|
||||
[Serialize(0.5f, IsPropertySaveable.No, description: $"Maximum value which {nameof(DeconstructTimeMultiplier)} can be.")]
|
||||
public float MaxDeconstructTimeMultiplier
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Character? Character { get; private set; }
|
||||
|
||||
public bool DoesBleed => Character?.DoesBleed == true;
|
||||
|
||||
public float DeconstructTimeMultiplier { get; private set; } = 1f;
|
||||
|
||||
public LinkedControllerCharacterComponent(Item item, ContentXElement element) : base(item, element)
|
||||
{
|
||||
#if CLIENT
|
||||
spriteOverrides = element.Elements()
|
||||
.Where(static e => e.Name.LocalName.ToLowerInvariant() == "spriteoverride")
|
||||
.Select(static e => new SpriteOverride(e))
|
||||
.ToImmutableArray();
|
||||
#endif
|
||||
}
|
||||
|
||||
public void UpdateLinkedCharacter(Character? character)
|
||||
{
|
||||
Character = character;
|
||||
|
||||
if (character != null)
|
||||
{
|
||||
var animController = character.AnimController;
|
||||
float totalLimbs = animController.Limbs.Length;
|
||||
float nonSeveredLimbs = animController.Limbs.Count(static l => !l.IsSevered);
|
||||
|
||||
// Decrease deconstruction time if the character is missing some limbs
|
||||
DeconstructTimeMultiplier *= MathF.Max(MaxDeconstructTimeMultiplier, nonSeveredLimbs / totalLimbs);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (character != null)
|
||||
{
|
||||
SpriteOverride? spriteOverride =
|
||||
spriteOverrides.Where(s => s.SpeciesName == character.SpeciesName).FirstOrDefault()
|
||||
?? spriteOverrides.Where(s => s.SpeciesGroup == character.Group).FirstOrDefault();
|
||||
|
||||
if (spriteOverride != null)
|
||||
{
|
||||
item.OverrideInventorySprite = spriteOverride.Sprite;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
item.OverrideInventorySprite = null;
|
||||
}
|
||||
#elif SERVER
|
||||
Item.CreateServerEvent(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void ClientEventRead(IReadMessage msg, float sendingTime)
|
||||
{
|
||||
UInt16 characterId = msg.ReadUInt16();
|
||||
if (characterId == Entity.NullEntityID)
|
||||
{
|
||||
UpdateLinkedCharacter(null);
|
||||
}
|
||||
else if (Entity.FindEntityByID(characterId) is Character character)
|
||||
{
|
||||
UpdateLinkedCharacter(character);
|
||||
}
|
||||
}
|
||||
|
||||
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData? extraData = null)
|
||||
{
|
||||
if (Character != null)
|
||||
{
|
||||
msg.WriteUInt16(Character.ID);
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.WriteUInt16(Entity.NullEntityID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
using FarseerPhysics;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
@@ -45,6 +48,39 @@ namespace Barotrauma.Items.Components
|
||||
private Camera cam;
|
||||
|
||||
private Character user;
|
||||
public Character User
|
||||
{
|
||||
get { return user; }
|
||||
private set
|
||||
{
|
||||
if (user == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
user = value;
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
teleportTransition = 0f;
|
||||
teleportStartPosition = user.WorldPosition;
|
||||
}
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
|
||||
#if CLIENT
|
||||
UpdateMsg();
|
||||
|
||||
if (HideAllItemComponentHUDs && Character.Controlled == user)
|
||||
{
|
||||
// Prevents any UIs in this item from briefly showing up when you select this controller, since
|
||||
// activeHUDs would take a single frame to be updated to not contain any other item component HUD
|
||||
Item.ClearActiveHUDs();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private Item focusTarget;
|
||||
private float targetRotation;
|
||||
@@ -55,11 +91,6 @@ namespace Barotrauma.Items.Components
|
||||
set { userPos = value; }
|
||||
}
|
||||
|
||||
public Character User
|
||||
{
|
||||
get { return user; }
|
||||
}
|
||||
|
||||
public IEnumerable<LimbPos> LimbPositions { get { return limbPositions; } }
|
||||
|
||||
[Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out a signal when interacted with.", alwaysUseInstanceValues: true)]
|
||||
@@ -123,6 +154,13 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Should the HUDs of all item components in this item be hidden when a character is using this controller.")]
|
||||
public bool HideAllItemComponentHUDs
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public enum UseEnvironment
|
||||
{
|
||||
Air, Water, Both
|
||||
@@ -152,6 +190,49 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can a character put another character into this controller by dragging them and selecting this controller?")]
|
||||
public bool AllowPuttingInOtherCharacters
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Can a character select this controller by themselves?")]
|
||||
public bool CanBeSelectedByCharacters
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "If a character selects this controller, but another character already has it selected, should it be kicked out?")]
|
||||
public bool SelectingKicksCharacterOut
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("", IsPropertySaveable.No, description: "Message displayed when there's a character inside this controller.")]
|
||||
public string KickOutCharacterMsg
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("", IsPropertySaveable.No, description: "Message displayed when you are putting a character into the controller.")]
|
||||
public string PutOtherCharacterMsg
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
[Serialize("", IsPropertySaveable.No, description: "Spawns this item in the first available item container slot when a character selects this controller, if the item container is full, the character will not be able to select the controller.")]
|
||||
public Identifier SpawnItemOnSelected
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool ControlCharacterPose
|
||||
{
|
||||
get { return limbPositions.Count > 0; }
|
||||
@@ -204,6 +285,23 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to determine how fast the character is teleported
|
||||
/// to the item when they first select the controller.
|
||||
/// Only relevant for <see cref="ForceUserToStayAttached" />
|
||||
/// </summary>
|
||||
private const float TeleportTransitionSpeed = 8f;
|
||||
private float teleportTransition = 0f;
|
||||
private Vector2 teleportStartPosition;
|
||||
|
||||
private readonly ItemPrefab spawnItemOnSelectedPrefab;
|
||||
private readonly ItemContainer containerToSpawnOnSelectedItem;
|
||||
|
||||
/// <summary>
|
||||
/// Item spawned by <see cref="spawnItemOnSelectedPrefab"/>
|
||||
/// </summary>
|
||||
private Item spawnedItemOnSelected = null;
|
||||
|
||||
public Controller(Item item, ContentXElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -211,6 +309,18 @@ namespace Barotrauma.Items.Components
|
||||
Enum.TryParse(element.GetAttributeString("direction", "None"), out dir);
|
||||
LoadLimbPositions(element);
|
||||
IsActive = true;
|
||||
|
||||
containerToSpawnOnSelectedItem = item.GetComponent<ItemContainer>();
|
||||
|
||||
if (!SpawnItemOnSelected.IsEmpty && !ItemPrefab.Prefabs.TryGet(SpawnItemOnSelected, out spawnItemOnSelectedPrefab))
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find item prefab \"{SpawnItemOnSelected}\"");
|
||||
}
|
||||
|
||||
if (containerToSpawnOnSelectedItem == null && !SpawnItemOnSelected.IsEmpty)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error - Controller has a {nameof(SpawnItemOnSelected)} but no ItemContainer defined");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -236,58 +346,77 @@ namespace Barotrauma.Items.Components
|
||||
item.SendSignal(signal, "trigger_out");
|
||||
}
|
||||
|
||||
if (forceSelectNextFrame && user != null)
|
||||
if (forceSelectNextFrame && User != null)
|
||||
{
|
||||
user.SelectedItem = item;
|
||||
User.SelectedItem = item;
|
||||
}
|
||||
forceSelectNextFrame = false;
|
||||
|
||||
userCanInteractCheckTimer -= deltaTime;
|
||||
|
||||
if (user == null
|
||||
|| user.Removed
|
||||
|| !user.IsAnySelectedItem(item)
|
||||
|| (item.ParentInventory != null && !IsAttachedUser(user))
|
||||
|| (UsableIn == UseEnvironment.Water && !user.AnimController.InWater)
|
||||
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater)
|
||||
|| !CheckUserCanInteract())
|
||||
if (User == null
|
||||
|| User.Removed
|
||||
|| (((User.Stun <= 0f && !User.IsKnockedDownOrRagdolled && !User.LockHands) || !ForceUserToStayAttached) && (!User.IsAnySelectedItem(item) || !CheckUserCanInteract()))
|
||||
|| (item.ParentInventory != null && !IsAttachedUser(User))
|
||||
|| (UsableIn == UseEnvironment.Water && !User.AnimController.InWater)
|
||||
|| (UsableIn == UseEnvironment.Air && User.AnimController.InWater)
|
||||
|| !CheckSpawnItem()
|
||||
)
|
||||
{
|
||||
if (user != null)
|
||||
if (User != null)
|
||||
{
|
||||
CancelUsing(user);
|
||||
user = null;
|
||||
CancelUsing(User);
|
||||
User = null;
|
||||
}
|
||||
if (item.Connections == null || !IsToggle || string.IsNullOrEmpty(signal)) { IsActive = false; }
|
||||
return;
|
||||
}
|
||||
|
||||
if (ForceUserToStayAttached && Vector2.DistanceSquared(item.WorldPosition, user.WorldPosition) > 0.1f)
|
||||
if (ForceUserToStayAttached)
|
||||
{
|
||||
user.TeleportTo(item.WorldPosition);
|
||||
user.AnimController.Collider.ResetDynamics();
|
||||
foreach (var limb in user.AnimController.Limbs)
|
||||
teleportTransition = MathF.Min(teleportTransition + deltaTime * TeleportTransitionSpeed, 1f);
|
||||
|
||||
if (teleportTransition >= 1f)
|
||||
{
|
||||
if (limb.Removed || limb.IsSevered) { continue; }
|
||||
limb.body?.ResetDynamics();
|
||||
// Transition is complete, if someone was holding this character, force them to deselect
|
||||
// so they aren't holding the character that is now attached to the controller
|
||||
if (User.SelectedBy != null)
|
||||
{
|
||||
User.SelectedBy.SelectedCharacter = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (User == Character.Controlled
|
||||
|| teleportTransition < 1f
|
||||
|| Vector2.DistanceSquared(item.WorldPosition, User.WorldPosition) > 0.1f)
|
||||
{
|
||||
var targetPosition = Vector2.Lerp(teleportStartPosition, item.WorldPosition, teleportTransition);
|
||||
User.TeleportTo(targetPosition);
|
||||
User.AnimController.Collider.ResetDynamics();
|
||||
foreach (var limb in User.AnimController.Limbs)
|
||||
{
|
||||
if (limb.Removed || limb.IsSevered) { continue; }
|
||||
limb.body?.ResetDynamics();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user.AnimController.StartUsingItem();
|
||||
User.AnimController.StartUsingItem();
|
||||
|
||||
if (userPos != Vector2.Zero)
|
||||
{
|
||||
Vector2 diff = (item.WorldPosition + userPos) - user.WorldPosition;
|
||||
Vector2 diff = (item.WorldPosition + userPos) - User.WorldPosition;
|
||||
|
||||
if (user.AnimController.InWater)
|
||||
if (User.AnimController.InWater)
|
||||
{
|
||||
if (diff.LengthSquared() > 30.0f * 30.0f)
|
||||
{
|
||||
user.AnimController.TargetMovement = Vector2.Clamp(diff * 0.01f, -Vector2.One, Vector2.One);
|
||||
user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
|
||||
User.AnimController.TargetMovement = Vector2.Clamp(diff * 0.01f, -Vector2.One, Vector2.One);
|
||||
User.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
|
||||
}
|
||||
else
|
||||
{
|
||||
user.AnimController.TargetMovement = Vector2.Zero;
|
||||
User.AnimController.TargetMovement = Vector2.Zero;
|
||||
UserInCorrectPosition = true;
|
||||
}
|
||||
}
|
||||
@@ -295,10 +424,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
// Secondary items (like ladders or chairs) will control the character position over primary items
|
||||
// Only control the character position if the character doesn't have another secondary item already controlling it
|
||||
if (!user.HasSelectedAnotherSecondaryItem(Item))
|
||||
if (!User.HasSelectedAnotherSecondaryItem(Item))
|
||||
{
|
||||
diff.Y = 0.0f;
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && user != Character.Controlled)
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && User != Character.Controlled)
|
||||
{
|
||||
if (Math.Abs(diff.X) > 20.0f)
|
||||
{
|
||||
@@ -308,48 +437,48 @@ namespace Barotrauma.Items.Components
|
||||
else if (Math.Abs(diff.X) > 0.1f)
|
||||
{
|
||||
//aim to keep the collider at the correct position once close enough
|
||||
user.AnimController.Collider.LinearVelocity = new Vector2(
|
||||
User.AnimController.Collider.LinearVelocity = new Vector2(
|
||||
diff.X * 0.1f,
|
||||
user.AnimController.Collider.LinearVelocity.Y);
|
||||
User.AnimController.Collider.LinearVelocity.Y);
|
||||
}
|
||||
}
|
||||
else if (Math.Abs(diff.X) > 10.0f)
|
||||
{
|
||||
user.AnimController.TargetMovement = Vector2.Normalize(diff);
|
||||
user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
|
||||
User.AnimController.TargetMovement = Vector2.Normalize(diff);
|
||||
User.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
|
||||
return;
|
||||
}
|
||||
user.AnimController.TargetMovement = Vector2.Zero;
|
||||
User.AnimController.TargetMovement = Vector2.Zero;
|
||||
}
|
||||
UserInCorrectPosition = true;
|
||||
}
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, user);
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, User);
|
||||
|
||||
if (limbPositions.Count == 0) { return; }
|
||||
|
||||
user.AnimController.StartUsingItem();
|
||||
User.AnimController.StartUsingItem();
|
||||
|
||||
if (user.SelectedItem != null)
|
||||
if (User.SelectedItem != null)
|
||||
{
|
||||
user.AnimController.ResetPullJoints(l => l.IsLowerBody);
|
||||
User.AnimController.ResetPullJoints(l => l.IsLowerBody);
|
||||
}
|
||||
else
|
||||
{
|
||||
user.AnimController.ResetPullJoints();
|
||||
User.AnimController.ResetPullJoints();
|
||||
}
|
||||
|
||||
if (dir != 0) { user.AnimController.TargetDir = dir; }
|
||||
if (dir != 0) { User.AnimController.TargetDir = dir; }
|
||||
|
||||
foreach (LimbPos lb in limbPositions)
|
||||
{
|
||||
Limb limb = user.AnimController.GetLimb(lb.LimbType);
|
||||
Limb limb = User.AnimController.GetLimb(lb.LimbType);
|
||||
if (limb == null || !limb.body.Enabled) { continue; }
|
||||
// Don't move lower body limbs if there's another selected secondary item that should control them
|
||||
if (limb.IsLowerBody && user.HasSelectedAnotherSecondaryItem(Item)) { continue; }
|
||||
if (limb.IsLowerBody && User.HasSelectedAnotherSecondaryItem(Item)) { continue; }
|
||||
// Don't move hands if there's a selected primary item that should control them
|
||||
if (limb.IsArm && Item == user.SelectedSecondaryItem && user.SelectedItem != null) { continue; }
|
||||
if (limb.IsArm && Item == User.SelectedSecondaryItem && User.SelectedItem != null) { continue; }
|
||||
if (lb.AllowUsingLimb)
|
||||
{
|
||||
switch (lb.LimbType)
|
||||
@@ -357,12 +486,12 @@ namespace Barotrauma.Items.Components
|
||||
case LimbType.RightHand:
|
||||
case LimbType.RightForearm:
|
||||
case LimbType.RightArm:
|
||||
if (user.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) != null) { continue; }
|
||||
if (User.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) != null) { continue; }
|
||||
break;
|
||||
case LimbType.LeftHand:
|
||||
case LimbType.LeftForearm:
|
||||
case LimbType.LeftArm:
|
||||
if (user.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) != null) { continue; }
|
||||
if (User.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) != null) { continue; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -374,15 +503,77 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsSpawnContainerFull()
|
||||
{
|
||||
if (spawnItemOnSelectedPrefab == null || containerToSpawnOnSelectedItem == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (containerToSpawnOnSelectedItem.Inventory.IsFull())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool CheckSpawnItem()
|
||||
{
|
||||
if (spawnItemOnSelectedPrefab == null || containerToSpawnOnSelectedItem == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (containerToSpawnOnSelectedItem.Inventory.AllItems.Any(x => x.Prefab == spawnItemOnSelectedPrefab))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (spawnedItemOnSelected != null && !spawnedItemOnSelected.Removed)
|
||||
{
|
||||
if (spawnedItemOnSelected.ParentInventory != containerToSpawnOnSelectedItem.Inventory)
|
||||
{
|
||||
// Item was moved or dropped, force the user in this controller out
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (IsSpawnContainerFull())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
Entity.Spawner.AddItemToSpawnQueue(spawnItemOnSelectedPrefab, containerToSpawnOnSelectedItem.Inventory, onSpawned: spawnedItem =>
|
||||
{
|
||||
spawnedItemOnSelected = spawnedItem;
|
||||
|
||||
var linkedCharacterComponent = spawnedItem.GetComponent<LinkedControllerCharacterComponent>();
|
||||
if (linkedCharacterComponent != null)
|
||||
{
|
||||
linkedCharacterComponent.UpdateLinkedCharacter(User);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool CheckUserCanInteract()
|
||||
{
|
||||
//optimization: CanInteractWith is relatively heavy (can involve visibility checks for example), let's not do it every frame
|
||||
if (user != null)
|
||||
if (User != null)
|
||||
{
|
||||
if (userCanInteractCheckTimer <= 0.0f)
|
||||
{
|
||||
userCanInteractCheckTimer = UserCanInteractCheckInterval;
|
||||
return user.CanInteractWith(item);
|
||||
return User.CanInteractWith(item);
|
||||
}
|
||||
}
|
||||
//we only do the actual check every UserCanInteractCheckInterval seconds
|
||||
@@ -394,13 +585,13 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override bool Use(float deltaTime, Character activator = null)
|
||||
{
|
||||
if (activator != user)
|
||||
if (activator != User)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (user == null || user.Removed || !user.IsAnySelectedItem(item) || !user.CanInteractWith(item))
|
||||
if (User == null || User.Removed || !User.IsAnySelectedItem(item) || !User.CanInteractWith(item))
|
||||
{
|
||||
user = null;
|
||||
User = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -419,7 +610,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(output))
|
||||
{
|
||||
item.SendSignal(new Signal(output, sender: user), "trigger_out");
|
||||
item.SendSignal(new Signal(output, sender: User), "trigger_out");
|
||||
}
|
||||
|
||||
lastUsed = Timing.TotalTime;
|
||||
@@ -428,13 +619,13 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override bool SecondaryUse(float deltaTime, Character character = null)
|
||||
{
|
||||
if (user != character)
|
||||
if (User != character)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (user == null || character.Removed || !user.IsAnySelectedItem(item) || !character.CanInteractWith(item))
|
||||
if (User == null || character.Removed || !User.IsAnySelectedItem(item) || !character.CanInteractWith(item))
|
||||
{
|
||||
user = null;
|
||||
User = null;
|
||||
return false;
|
||||
}
|
||||
if (character == null)
|
||||
@@ -495,7 +686,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (IsOutOfPower()) { return null; }
|
||||
|
||||
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut);
|
||||
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: User), positionOut);
|
||||
|
||||
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
|
||||
{
|
||||
@@ -547,8 +738,20 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private void CancelUsing(Character character)
|
||||
{
|
||||
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
if (spawnedItemOnSelected != null)
|
||||
{
|
||||
Entity.Spawner.AddEntityToRemoveQueue(spawnedItemOnSelected);
|
||||
spawnedItemOnSelected = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (character == null || character.Removed) { return; }
|
||||
|
||||
// Wake character's colliders so they don't just float in the air when taken out of the controller
|
||||
character.AnimController.BodyInRest = false;
|
||||
|
||||
foreach (LimbPos lb in limbPositions)
|
||||
{
|
||||
Limb limb = character.AnimController.GetLimb(lb.LimbType);
|
||||
@@ -588,31 +791,80 @@ namespace Barotrauma.Items.Components
|
||||
return false;
|
||||
}
|
||||
|
||||
//someone already using the item
|
||||
if (user != null && !user.Removed)
|
||||
// Someone already using the item
|
||||
if (User != null && !User.Removed)
|
||||
{
|
||||
if (user == activator)
|
||||
// Let the server handle the logic here
|
||||
if (GameMain.NetworkMember is { IsClient: true })
|
||||
{
|
||||
IsActive = false;
|
||||
CancelUsing(user);
|
||||
user = null;
|
||||
return false;
|
||||
}
|
||||
else if (user.IsBot && !activator.IsBot)
|
||||
|
||||
// Prevent user from kicking character out if they are holding another character
|
||||
if (AllowPuttingInOtherCharacters && CanPutSelectedCharacter(activator.SelectedCharacter))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (User == activator || SelectingKicksCharacterOut)
|
||||
{
|
||||
#if SERVER
|
||||
if (User != activator)
|
||||
{
|
||||
GameServer.Log($"{GameServer.CharacterLogName(activator)} removed {GameServer.CharacterLogName(User)} from {item.Name}",
|
||||
ServerLog.MessageType.Attack);
|
||||
}
|
||||
#endif
|
||||
|
||||
IsActive = false;
|
||||
CancelUsing(User);
|
||||
User = null;
|
||||
return false;
|
||||
}
|
||||
else if (User.IsBot && !activator.IsBot)
|
||||
{
|
||||
if (AllowSelectingWhenSelectedByBot)
|
||||
{
|
||||
CancelUsing(user);
|
||||
user = activator;
|
||||
CancelUsing(User);
|
||||
User = activator;
|
||||
IsActive = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return AllowSelectingWhenSelectedByOther;
|
||||
}
|
||||
else
|
||||
else if (AllowPuttingInOtherCharacters && CanPutSelectedCharacter(activator.SelectedCharacter))
|
||||
{
|
||||
user = activator;
|
||||
// Stun pets longer so they don't immediately get out of the controller
|
||||
if (activator.SelectedCharacter.IsPet)
|
||||
{
|
||||
activator.SelectedCharacter.SetStun(MathF.Max(activator.SelectedCharacter.Stun, 4f), isNetworkMessage: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Small amount of stun since non-ragdolled characters behave weirdly when syncing the periodic teleportation in multiplayer
|
||||
activator.SelectedCharacter.SetStun(MathF.Max(activator.SelectedCharacter.Stun, 1f), isNetworkMessage: true);
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
GameServer.Log($"{GameServer.CharacterLogName(activator)} forced {GameServer.CharacterLogName(activator.SelectedCharacter)} into {item.Name}",
|
||||
ServerLog.MessageType.Attack);
|
||||
#endif
|
||||
|
||||
User = activator.SelectedCharacter;
|
||||
User.SelectedItem = this.Item;
|
||||
IsActive = true;
|
||||
if (ForceUserToStayAttached && item.Container != null)
|
||||
{
|
||||
forceSelectNextFrame = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else if (CanBeSelectedByCharacters)
|
||||
{
|
||||
activator.DeselectCharacter();
|
||||
|
||||
User = activator;
|
||||
IsActive = true;
|
||||
if (ForceUserToStayAttached && item.Container != null)
|
||||
{
|
||||
@@ -621,15 +873,12 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
//allow the selection logic above to run when out of power, but allow sending signals
|
||||
//allow the selection logic above to run when out of power, but disallow sending signals
|
||||
if (IsOutOfPower()) { return false; }
|
||||
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
|
||||
if (!string.IsNullOrEmpty(output))
|
||||
{
|
||||
item.SendSignal(new Signal(output, sender: user), "signal_out");
|
||||
item.SendSignal(new Signal(output, sender: User), "signal_out");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -639,7 +888,7 @@ namespace Barotrauma.Items.Components
|
||||
/// </summary>
|
||||
public bool IsAttachedUser(Character character)
|
||||
{
|
||||
return character != null && character == user && ForceUserToStayAttached;
|
||||
return character != null && character == User && ForceUserToStayAttached;
|
||||
}
|
||||
|
||||
public override void FlipX(bool relativeToSub)
|
||||
@@ -668,12 +917,87 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
|
||||
{
|
||||
#if CLIENT
|
||||
UpdateMsg();
|
||||
#endif
|
||||
|
||||
bool canPutCharacter = AllowPuttingInOtherCharacters && CanPutSelectedCharacter(character.SelectedCharacter, addMessage);
|
||||
bool canKickCharacter = SelectingKicksCharacterOut && User != null && !User.Removed;
|
||||
bool canUseController = CanBeSelectedByCharacters;
|
||||
|
||||
// Prevent kicking a character out when the user is holding another character to put into the controller.
|
||||
// This avoids accidentally taking out a character (e.g. from a deconstructor).
|
||||
if (canPutCharacter && canKickCharacter)
|
||||
{
|
||||
#if CLIENT
|
||||
if (addMessage)
|
||||
{
|
||||
GUI.AddMessage(TextManager.Get("ItemMsgAlreadyHasCharacterFail"), Color.Red, playSound: false);
|
||||
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
|
||||
}
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!canKickCharacter && !canPutCharacter && !canUseController)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsSpawnContainerFull())
|
||||
{
|
||||
#if CLIENT
|
||||
if (addMessage)
|
||||
{
|
||||
GUI.AddMessage(TextManager.Get("ItemMsgNotEnoughSpaceCharacterFail"), Color.Red, playSound: false);
|
||||
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
|
||||
}
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.HasRequiredItems(character, addMessage, msg);
|
||||
}
|
||||
|
||||
public override bool HasAccess(Character character)
|
||||
{
|
||||
if (!item.IsInteractable(character)) { return false; }
|
||||
return base.HasAccess(character);
|
||||
}
|
||||
|
||||
private bool CanPutSelectedCharacter(Character character, bool showMessage = false)
|
||||
{
|
||||
if (character == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!character.IsContainable)
|
||||
{
|
||||
#if CLIENT
|
||||
if (showMessage)
|
||||
{
|
||||
GUI.AddMessage(TextManager.Get("ItemMsgPutCharacterFail"), Color.Red);
|
||||
}
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (character.IsKnockedDownOrRagdolled) { return true; }
|
||||
if (character.LockHands) { return true; }
|
||||
if (character.IsPet)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
partial void HideHUDs(bool value);
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
|
||||
+131
-28
@@ -1,11 +1,13 @@
|
||||
using Barotrauma.Abilities;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using static OneOf.Types.TrueFalseOrNull;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
@@ -114,7 +116,19 @@ namespace Barotrauma.Items.Components
|
||||
float deconstructTime = 0.0f;
|
||||
foreach (Item targetItem in inputContainer.Inventory.AllItems)
|
||||
{
|
||||
deconstructTime += targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier);
|
||||
float itemDeconstructTime = item.Submarine is { Info.Type: SubmarineType.Outpost }
|
||||
? targetItem.Prefab.DeconstructTimeInOutposts : targetItem.Prefab.DeconstructTime;
|
||||
float targetDeconstructTime = itemDeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier);
|
||||
|
||||
var linkedCharacter = targetItem.GetComponent<LinkedControllerCharacterComponent>();
|
||||
if (linkedCharacter != null)
|
||||
{
|
||||
targetDeconstructTime *= linkedCharacter.DeconstructTimeMultiplier;
|
||||
}
|
||||
|
||||
deconstructTime += targetDeconstructTime;
|
||||
|
||||
ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructing, deltaTime);
|
||||
}
|
||||
|
||||
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
|
||||
@@ -143,8 +157,21 @@ namespace Barotrauma.Items.Components
|
||||
var targetItem = inputContainer.Inventory.LastOrDefault();
|
||||
if (targetItem == null) { return; }
|
||||
|
||||
ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructing, deltaTime);
|
||||
|
||||
var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => it.IsValidDeconstructor(item)).ToList();
|
||||
float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier) : 1.0f;
|
||||
|
||||
float itemDeconstructTime = item.Submarine is { Info.Type: SubmarineType.Outpost }
|
||||
? targetItem.Prefab.DeconstructTimeInOutposts : targetItem.Prefab.DeconstructTime;
|
||||
|
||||
float deconstructTime = !targetItem.Prefab.DeconstructItems.Any() || validDeconstructItems.Any()
|
||||
? itemDeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier) : 1.0f;
|
||||
|
||||
var linkedCharacter = targetItem.GetComponent<LinkedControllerCharacterComponent>();
|
||||
if (linkedCharacter != null)
|
||||
{
|
||||
deconstructTime *= linkedCharacter.DeconstructTimeMultiplier;
|
||||
}
|
||||
|
||||
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
|
||||
if (progressTimer > deconstructTime)
|
||||
@@ -183,6 +210,8 @@ namespace Barotrauma.Items.Components
|
||||
amountMultiplier = (int)itemCreationMultiplier.Value;
|
||||
}
|
||||
|
||||
ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructed, 1f);
|
||||
|
||||
if (targetItem.Prefab.RandomDeconstructionOutput)
|
||||
{
|
||||
int amount = targetItem.Prefab.RandomDeconstructionOutputAmount;
|
||||
@@ -297,30 +326,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
humanAi.HandleRelocation(spawnedItem);
|
||||
}
|
||||
for (int i = 0; i < outputContainer.Capacity; i++)
|
||||
{
|
||||
var containedItem = outputContainer.Inventory.GetItemAt(i);
|
||||
bool combined = false;
|
||||
if (containedItem?.OwnInventory != null)
|
||||
{
|
||||
foreach (Item subItem in containedItem.ContainedItems.ToList())
|
||||
{
|
||||
if (subItem.Combine(spawnedItem, null))
|
||||
{
|
||||
combined = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!combined)
|
||||
{
|
||||
if (containedItem?.Combine(spawnedItem, null) ?? false)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
PutItemsToLinkedContainer();
|
||||
|
||||
TryMoveItemToOutputContainers(spawnedItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -332,8 +339,9 @@ namespace Barotrauma.Items.Components
|
||||
GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + targetItem.Prefab.Identifier);
|
||||
}
|
||||
|
||||
bool? result = GameMain.LuaCs.Hook.Call<bool?>("item.deconstructed", targetItem, this, user, allowRemove);
|
||||
if (result == true) { return; }
|
||||
bool? should = null;
|
||||
LuaCsSetup.Instance.EventService.PublishEvent<IEventItemDeconstructed>(x => should = x.OnItemDeconstructed(targetItem, this, user, allowRemove) ?? should);
|
||||
if (should == true) { return; }
|
||||
|
||||
if (targetItem.AllowDeconstruct && allowRemove)
|
||||
{
|
||||
@@ -350,6 +358,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inputContainer.Inventory.RemoveItem(targetItem);
|
||||
Entity.Spawner.AddItemToRemoveQueue(targetItem);
|
||||
MoveInputQueue();
|
||||
@@ -384,6 +393,34 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private void TryMoveItemToOutputContainers(Item spawnedItem)
|
||||
{
|
||||
for (int i = 0; i < outputContainer.Capacity; i++)
|
||||
{
|
||||
var containedItem = outputContainer.Inventory.GetItemAt(i);
|
||||
bool combined = false;
|
||||
if (containedItem?.OwnInventory != null)
|
||||
{
|
||||
foreach (Item subItem in containedItem.ContainedItems.ToList())
|
||||
{
|
||||
if (subItem.Combine(spawnedItem, null))
|
||||
{
|
||||
combined = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!combined)
|
||||
{
|
||||
if (containedItem?.Combine(spawnedItem, null) ?? false)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
PutItemsToLinkedContainer();
|
||||
}
|
||||
|
||||
private void PutItemsToLinkedContainer()
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
||||
@@ -402,6 +439,72 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyDeconstructionStatusEffects(Item targetItem, ActionType type, float deltaTime)
|
||||
{
|
||||
var linkedCharacterComponent = targetItem.GetComponent<LinkedControllerCharacterComponent>();
|
||||
Character character = null;
|
||||
if (linkedCharacterComponent is { Character.Removed: false })
|
||||
{
|
||||
character = linkedCharacterComponent.Character;
|
||||
}
|
||||
|
||||
Limb limb = character?.AnimController.Limbs.GetRandomUnsynced();
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
item.GetStatusEffectsOfType(type).ForEach(statusEffect => statusEffect.SetUser(user));
|
||||
targetItem.GetStatusEffectsOfType(type).ForEach(statusEffect => statusEffect.SetUser(user));
|
||||
}
|
||||
|
||||
// Apply OnDeconstruct/OnDeconstructing to the Deconstructor/item being deconstructed
|
||||
item.ApplyStatusEffects(type, deltaTime, character, limb, useTarget: targetItem);
|
||||
targetItem.ApplyStatusEffects(type, deltaTime, character, limb);
|
||||
|
||||
if (character != null)
|
||||
{
|
||||
if (type == ActionType.OnDeconstructed)
|
||||
{
|
||||
// Move whatever was on the character inventory to free up space for status effects that spawn items
|
||||
MoveItemsFromCharacterToOutput();
|
||||
}
|
||||
|
||||
character.ApplyStatusEffects(type, deltaTime);
|
||||
|
||||
if (type == ActionType.OnDeconstructed)
|
||||
{
|
||||
// This needs to run next frame because the status effect might enqueue items to be spawned next frame
|
||||
CoroutineManager.Invoke(() =>
|
||||
{
|
||||
if (character.Removed) { return; }
|
||||
|
||||
// Move items again since the status effect could have spawned additional items in the character inventory
|
||||
MoveItemsFromCharacterToOutput();
|
||||
|
||||
Entity.Spawner?.AddEntityToRemoveQueue(character);
|
||||
}, 0.1f);
|
||||
}
|
||||
|
||||
void MoveItemsFromCharacterToOutput()
|
||||
{
|
||||
if (character.Inventory != null)
|
||||
{
|
||||
foreach (var item in character.Inventory.AllItemsMod)
|
||||
{
|
||||
if (item.Removed) { continue; }
|
||||
if (!item.IsPlayerTeamInteractable) { continue; }
|
||||
|
||||
if (!outputContainer.Inventory.TryPutItem(item, user: null))
|
||||
{
|
||||
item.Drop(dropper: null);
|
||||
}
|
||||
|
||||
TryMoveItemToOutputContainers(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move items towards the last slot in the inventory if there's free slots
|
||||
/// </summary>
|
||||
|
||||
@@ -388,6 +388,13 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
Attack.DamageMultiplier = damageMultiplier;
|
||||
}
|
||||
foreach (var statusEffect in Item.GetStatusEffectsOfType(ActionType.OnImpact))
|
||||
{
|
||||
foreach (var explosion in statusEffect.Explosions)
|
||||
{
|
||||
explosion.Attack.DamageMultiplier = damageMultiplier;
|
||||
}
|
||||
}
|
||||
// Set user for hitscan projectiles to work properly.
|
||||
User = user;
|
||||
// Need to set null for non-characterusable items.
|
||||
@@ -460,6 +467,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
initialRotation -= MathHelper.Pi;
|
||||
}
|
||||
Submarine initialSubmarine = item.Submarine;
|
||||
for (int i = 0; i < HitScanCount; i++)
|
||||
{
|
||||
float launchAngle;
|
||||
@@ -476,6 +484,8 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
|
||||
Vector2 prevSimpos = item.SimPosition;
|
||||
item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle);
|
||||
//when launching multiple projectiles, ensure each raycast starts from the same sub
|
||||
item.Submarine = initialSubmarine;
|
||||
if (Hitscan)
|
||||
{
|
||||
DoHitscan(launchDir);
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components
|
||||
FiringRateMultiplier
|
||||
}
|
||||
|
||||
private readonly Dictionary<StatType, float> statValues = new Dictionary<StatType, float>();
|
||||
public readonly Dictionary<StatType, float> statValues = new Dictionary<StatType, float>();
|
||||
|
||||
private int qualityLevel;
|
||||
|
||||
|
||||
@@ -448,9 +448,10 @@ namespace Barotrauma.Items.Components
|
||||
UpdateProjSpecific(deltaTime);
|
||||
IsTinkering = false;
|
||||
|
||||
if (prevSentConditionValue != (int)item.ConditionPercentage || conditionSignal == null)
|
||||
int condition = (int)(item.Condition / (item.MaxCondition / item.MaxRepairConditionMultiplier) * 100f);
|
||||
if (prevSentConditionValue != condition || conditionSignal == null)
|
||||
{
|
||||
prevSentConditionValue = (int)item.ConditionPercentage;
|
||||
prevSentConditionValue = condition;
|
||||
conditionSignal = prevSentConditionValue.ToString();
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ namespace Barotrauma.Items.Components
|
||||
private init => _displayName = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display name ignoring <see cref="DisplayNameOverride"/>
|
||||
/// </summary>
|
||||
public LocalizedString DefaultDisplayName => _displayName;
|
||||
|
||||
public LocalizedString DisplayNameOverride;
|
||||
|
||||
private readonly HashSet<Wire> wires;
|
||||
@@ -350,15 +355,11 @@ namespace Barotrauma.Items.Components
|
||||
wire.RegisterSignal(signal, source: this);
|
||||
#endif
|
||||
SendSignalIntoConnection(signal, recipient);
|
||||
GameMain.LuaCs.Hook.Call("signalReceived", signal, recipient);
|
||||
GameMain.LuaCs.Hook.Call("signalReceived." + recipient.item.Prefab.Identifier, signal, recipient);
|
||||
}
|
||||
|
||||
foreach (CircuitBoxConnection connection in CircuitBoxConnections)
|
||||
{
|
||||
connection.ReceiveSignal(signal);
|
||||
GameMain.LuaCs.Hook.Call("signalReceived", signal, connection.Connection);
|
||||
GameMain.LuaCs.Hook.Call("signalReceived." + connection.Connection.Item.Prefab.Identifier, signal, connection);
|
||||
}
|
||||
enumeratingWires = false;
|
||||
foreach (var removedWire in removedWires)
|
||||
|
||||
@@ -296,6 +296,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
#endif
|
||||
CheckIfNeedsUpdate();
|
||||
SetLightSourceTransformProjSpecific();
|
||||
}
|
||||
|
||||
public void CheckIfNeedsUpdate()
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using static Barotrauma.CharacterHealth;
|
||||
using static Barotrauma.MedicalClinic;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
@@ -228,8 +231,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public void TransmitSignal(Signal signal, bool sentFromChat)
|
||||
{
|
||||
var should = GameMain.LuaCs.Hook.Call<bool?>("wifiSignalTransmitted", this, signal, sentFromChat);
|
||||
|
||||
bool? should = null;
|
||||
LuaCsSetup.Instance.EventService.PublishEvent<IEventWifiSignalTransmitted>(x => should = x.OnWifiSignalTransmitted(this, signal, sentFromChat) ?? should);
|
||||
if (should != null && should.Value) { return; }
|
||||
|
||||
bool chatMsgSent = false;
|
||||
|
||||
@@ -42,6 +42,10 @@ namespace Barotrauma.Items.Components
|
||||
private float currentChargeTime;
|
||||
private bool tryingToCharge;
|
||||
|
||||
private const float LineOfSightCheckInterval = 0.5f;
|
||||
private (Body WorldTarget, Body TransformedTarget, double Time) lastLineOfSightCheck;
|
||||
private (Character Target, bool CanSee, double Time) lastCanSeeTargetCheck;
|
||||
|
||||
private enum ChargingState
|
||||
{
|
||||
Inactive,
|
||||
@@ -1088,6 +1092,7 @@ namespace Barotrauma.Items.Components
|
||||
foreach (Submarine sub in Submarine.Loaded)
|
||||
{
|
||||
if (sub == Item.Submarine) { continue; }
|
||||
if (sub.IsRespawnShuttle) { continue; }
|
||||
if (item.Submarine != null)
|
||||
{
|
||||
if (Character.IsOnFriendlyTeam(item.Submarine.TeamID, sub.TeamID)) { continue; }
|
||||
@@ -1164,15 +1169,28 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
|
||||
Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition);
|
||||
|
||||
bool doLineOfSightCheck = lastLineOfSightCheck.Time < Timing.TotalTimeUnpaused - LineOfSightCheckInterval;
|
||||
if (doLineOfSightCheck)
|
||||
{
|
||||
lastLineOfSightCheck.WorldTarget = CheckLineOfSight(start, end);
|
||||
lastLineOfSightCheck.Time = Timing.TotalTime;
|
||||
}
|
||||
|
||||
// Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target.
|
||||
Body worldTarget = CheckLineOfSight(start, end);
|
||||
Body worldTarget = lastLineOfSightCheck.WorldTarget;
|
||||
bool shoot;
|
||||
if (target.Submarine != null)
|
||||
{
|
||||
start -= target.Submarine.SimPosition;
|
||||
end -= target.Submarine.SimPosition;
|
||||
Body transformedTarget = CheckLineOfSight(start, end);
|
||||
shoot = CanShoot(transformedTarget, user: null, friendlyTag, TargetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines));
|
||||
if (doLineOfSightCheck)
|
||||
{
|
||||
start -= target.Submarine.SimPosition;
|
||||
end -= target.Submarine.SimPosition;
|
||||
lastLineOfSightCheck.TransformedTarget = CheckLineOfSight(start, end);
|
||||
}
|
||||
shoot =
|
||||
(worldTarget == null || CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines)) &&
|
||||
CanShoot(lastLineOfSightCheck.TransformedTarget, user: null, friendlyTag, TargetSubmarines);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1437,8 +1455,20 @@ namespace Barotrauma.Items.Components
|
||||
// Adjust the target character position (limb or submarine)
|
||||
if (currentTarget is Character targetCharacter)
|
||||
{
|
||||
bool enemyInAnotherSub = targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine;
|
||||
bool canSeeTarget = true;
|
||||
if (enemyInAnotherSub)
|
||||
{
|
||||
if (lastCanSeeTargetCheck.Time < Timing.TotalTime - LineOfSightCheckInterval ||
|
||||
targetCharacter != lastCanSeeTargetCheck.Target)
|
||||
{
|
||||
canSeeTarget = targetCharacter.CanSeeTarget(Item);
|
||||
lastCanSeeTargetCheck = (targetCharacter, canSeeTarget, Timing.TotalTime);
|
||||
}
|
||||
}
|
||||
|
||||
//if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is
|
||||
if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item))
|
||||
if (enemyInAnotherSub && !canSeeTarget)
|
||||
{
|
||||
targetPos = targetCharacter.CurrentHull.WorldPosition;
|
||||
if (closestDistance > maxDistance * maxDistance)
|
||||
|
||||
@@ -569,16 +569,16 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// If there is room, puts the item in the inventory and returns true, otherwise returns false
|
||||
/// </summary>
|
||||
public virtual bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false)
|
||||
public virtual bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true)
|
||||
{
|
||||
int slot = FindAllowedSlot(item, ignoreCondition);
|
||||
if (slot < 0) { return false; }
|
||||
|
||||
PutItem(item, slot, user, true, createNetworkEvent);
|
||||
PutItem(item, slot, user, true, createNetworkEvent, triggerOnInsertedEffects);
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false)
|
||||
public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true)
|
||||
{
|
||||
if (!IsIndexInRange(i))
|
||||
{
|
||||
@@ -609,14 +609,14 @@ namespace Barotrauma
|
||||
//item in the slot removed as a result of combining -> put this item in the now free slot
|
||||
if (!slots[i].Any())
|
||||
{
|
||||
return TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition);
|
||||
return TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition, triggerOnInsertedEffects);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (CanBePutInSlot(item, i, ignoreCondition))
|
||||
{
|
||||
PutItem(item, i, user, true, createNetworkEvent);
|
||||
PutItem(item, i, user, true, createNetworkEvent, triggerOnInsertedEffects);
|
||||
return true;
|
||||
}
|
||||
else if (slots[i].Any() && item.ParentInventory != null && allowSwapping)
|
||||
@@ -642,7 +642,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true)
|
||||
protected virtual void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true, bool triggerOnInsertedEffects = true)
|
||||
{
|
||||
if (!IsIndexInRange(i))
|
||||
{
|
||||
@@ -651,11 +651,6 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
var should = GameMain.LuaCs.Hook.Call<bool?>("inventoryPutItem", this, item, user, i, removeItem);
|
||||
|
||||
if (should != null && should.Value)
|
||||
return;
|
||||
|
||||
if (Owner == null) { return; }
|
||||
|
||||
Inventory prevInventory = item.ParentInventory;
|
||||
@@ -765,11 +760,6 @@ namespace Barotrauma
|
||||
if (slots[index].Items.Any(it => !it.IsInteractable(user))) { return false; }
|
||||
if (!AllowSwappingContainedItems) { return false; }
|
||||
|
||||
var should = GameMain.LuaCs.Hook.Call<bool?>("inventoryItemSwap", this, item, user, index, swapWholeStack);
|
||||
|
||||
if (should != null)
|
||||
return should.Value;
|
||||
|
||||
//swap to InvSlotType.Any if possible
|
||||
Inventory otherInventory = item.ParentInventory;
|
||||
bool otherIsEquipped = false;
|
||||
@@ -951,7 +941,8 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!inventory.TryPutItem(item, slotIndex, false, false, user, createNetworkEvent, ignoreCondition: true) &&
|
||||
//don't trigger OnInserted effects: we're not really "inserting" but just putting it back where it was because swapping failed
|
||||
if (!inventory.TryPutItem(item, slotIndex, false, false, user, createNetworkEvent, ignoreCondition: true, triggerOnInsertedEffects: false) &&
|
||||
!inventory.GetItemsAt(slotIndex).Contains(item))
|
||||
{
|
||||
inventory.ForceToSlot(item, slotIndex);
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Abilities;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.MapCreatures.Behavior;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using FarseerPhysics.Dynamics.Contacts;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoonSharp.Interpreter;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.MapCreatures.Behavior;
|
||||
using MoonSharp.Interpreter;
|
||||
using System.Collections.Immutable;
|
||||
using Barotrauma.Abilities;
|
||||
using static Barotrauma.CharacterHealth;
|
||||
using static Barotrauma.MedicalClinic;
|
||||
|
||||
#if CLIENT
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
@@ -263,6 +266,8 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public Character Equipper;
|
||||
|
||||
public Inventory PreviousParentInventory { get; set; }
|
||||
|
||||
//the inventory in which the item is contained in
|
||||
public Inventory ParentInventory
|
||||
{
|
||||
@@ -278,9 +283,7 @@ namespace Barotrauma
|
||||
Container = parentInventory.Owner as Item;
|
||||
RemoveFromDroppedStack(allowClientExecute: false);
|
||||
}
|
||||
#if SERVER
|
||||
PreviousParentInventory = value;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,6 +564,8 @@ namespace Barotrauma
|
||||
set;
|
||||
} = float.PositiveInfinity;
|
||||
|
||||
public Sprite OverrideInventorySprite { get; set; }
|
||||
|
||||
protected Color spriteColor;
|
||||
[Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes)]
|
||||
public Color SpriteColor
|
||||
@@ -1103,7 +1108,7 @@ namespace Barotrauma
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name + " (ID: " + ID + ")";
|
||||
return (Name.IsNullOrEmpty() ? Prefab.Identifier : Name) + " (ID: " + ID + ")";
|
||||
}
|
||||
|
||||
private readonly List<ISerializableEntity> allPropertyObjects = new List<ISerializableEntity>();
|
||||
@@ -1411,8 +1416,6 @@ namespace Barotrauma
|
||||
if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; }
|
||||
if (HasTag(Barotrauma.Tags.LogicItem)) { isLogic = true; }
|
||||
|
||||
GameMain.LuaCs.Hook.Call("item.created", this);
|
||||
|
||||
ApplyStatusEffects(ActionType.OnSpawn, 1.0f);
|
||||
|
||||
// Set max condition multipliers from campaign settings for RecalculateConditionValues()
|
||||
@@ -1793,7 +1796,8 @@ namespace Barotrauma
|
||||
ic.Move(amount, ignoreContacts);
|
||||
}
|
||||
|
||||
if (body != null && (Submarine == null || !Submarine.Loading) || Screen.Selected is { IsEditor: true }) { FindHull(); }
|
||||
// Refresh items without a body in editors so vents (or other static items that do something with hulls) know which hull they are in after being moved
|
||||
if ((body != null || Screen.Selected is { IsEditor: true }) && Submarine is not { Loading: true }) { FindHull(); }
|
||||
}
|
||||
|
||||
public Rectangle TransformTrigger(Rectangle trigger, bool world = false)
|
||||
@@ -2073,6 +2077,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<StatusEffect> GetStatusEffectsOfType(ActionType type)
|
||||
{
|
||||
if (!hasStatusEffectsOfType[(int)type]) { return Enumerable.Empty<StatusEffect>(); }
|
||||
return statusEffectLists[type];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes all StatusEffects of the specified type. Note that condition checks are ignored here: that should be handled by the code calling the method.
|
||||
/// </summary>
|
||||
@@ -3258,7 +3268,9 @@ namespace Barotrauma
|
||||
bool showUiMsg = false;
|
||||
#if CLIENT
|
||||
if (!ic.HasRequiredSkills(user, out Skill tempRequiredSkill)) { hasRequiredSkills = false; skillMultiplier = ic.GetSkillMultiplier(); }
|
||||
showUiMsg = user == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen;
|
||||
showUiMsg = user == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen &&
|
||||
// Only show the UI message of the component that we actually want to interact with
|
||||
(pickHit && ic.CanBePicked || selectHit && ic.CanBeSelected);
|
||||
#endif
|
||||
if (!ignoreRequiredItems && !ic.HasRequiredItems(user, showUiMsg)) { continue; }
|
||||
if ((ic.CanBePicked && pickHit && ic.Pick(user)) ||
|
||||
@@ -3364,10 +3376,6 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
if (condition <= 0.0f) { return; }
|
||||
|
||||
var should = GameMain.LuaCs.Hook.Call<bool?>("item.use", new object[] { this, user, targetLimb, useTarget });
|
||||
|
||||
if (should != null && should.Value) { return; }
|
||||
|
||||
bool remove = false;
|
||||
|
||||
@@ -3400,11 +3408,6 @@ namespace Barotrauma
|
||||
{
|
||||
if (condition <= 0.0f) { return; }
|
||||
|
||||
var should = GameMain.LuaCs.Hook.Call<bool?>("item.secondaryUse", this, character);
|
||||
|
||||
if (should != null && should.Value)
|
||||
return;
|
||||
|
||||
bool remove = false;
|
||||
|
||||
foreach (ItemComponent ic in components)
|
||||
@@ -3902,9 +3905,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
var result = GameMain.LuaCs.Hook.Call<bool?>("item.readPropertyChange", this, property, parentObject, allowEditing, sender);
|
||||
if (result != null && result.Value)
|
||||
return;
|
||||
bool? should = null;
|
||||
LuaCsSetup.Instance.EventService.PublishEvent<IEventItemReadPropertyChange>(x => should = x.OnItemReadPropertyChange(this, property, parentObject, allowEditing, sender) ?? should);
|
||||
if (should != null && should.Value) { return; }
|
||||
|
||||
Type type = property.PropertyType;
|
||||
string logValue = "";
|
||||
@@ -4370,6 +4373,9 @@ namespace Barotrauma
|
||||
Rotation = Rotation
|
||||
};
|
||||
|
||||
if (FlippedX) { newItem.FlipX(relativeToSub: false); }
|
||||
if (FlippedY) { newItem.FlipY(relativeToSub: false); }
|
||||
|
||||
float scaleRelativeToPrefab = Scale / Prefab.Scale;
|
||||
newItem.Scale *= scaleRelativeToPrefab;
|
||||
|
||||
@@ -4621,8 +4627,6 @@ namespace Barotrauma
|
||||
body.Remove();
|
||||
body = null;
|
||||
}
|
||||
|
||||
GameMain.LuaCs.Hook.Call("item.removed", this);
|
||||
}
|
||||
|
||||
public override void Remove()
|
||||
@@ -4707,8 +4711,6 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
RemoveProjSpecific();
|
||||
|
||||
GameMain.LuaCs.Hook.Call("item.removed", this);
|
||||
}
|
||||
|
||||
private void RemoveFromLists()
|
||||
|
||||
@@ -88,9 +88,9 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false)
|
||||
public override bool TryPutItem(Item item, Character user, IEnumerable<InvSlotType> allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true)
|
||||
{
|
||||
bool wasPut = base.TryPutItem(item, user, allowedSlots, createNetworkEvent, ignoreCondition);
|
||||
bool wasPut = base.TryPutItem(item, user, allowedSlots, createNetworkEvent, ignoreCondition, triggerOnInsertedEffects);
|
||||
|
||||
if (wasPut)
|
||||
{
|
||||
@@ -111,9 +111,9 @@ namespace Barotrauma
|
||||
return wasPut;
|
||||
}
|
||||
|
||||
public override bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false)
|
||||
public override bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true)
|
||||
{
|
||||
bool wasPut = base.TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition);
|
||||
bool wasPut = base.TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition, triggerOnInsertedEffects);
|
||||
if (wasPut && item.ParentInventory == this)
|
||||
{
|
||||
foreach (Character c in Character.CharacterList)
|
||||
@@ -124,7 +124,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
container.IsActive = true;
|
||||
container.OnItemContained(item);
|
||||
container.OnItemContained(item, triggerOnInsertedEffects);
|
||||
#if SERVER
|
||||
GameMain.Server?.KarmaManager?.OnItemContained(item, container.Item, user);
|
||||
#endif
|
||||
|
||||
@@ -548,6 +548,8 @@ namespace Barotrauma
|
||||
|
||||
public float DeconstructTime { get; private set; }
|
||||
|
||||
public float DeconstructTimeInOutposts { get; private set; }
|
||||
|
||||
public bool AllowDeconstruct { get; private set; }
|
||||
|
||||
//Containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced.
|
||||
@@ -1074,6 +1076,7 @@ namespace Barotrauma
|
||||
var storePrices = new Dictionary<Identifier, PriceInfo>();
|
||||
var preferredContainers = new List<PreferredContainer>();
|
||||
DeconstructTime = 1.0f;
|
||||
DeconstructTimeInOutposts = DeconstructTime;
|
||||
|
||||
if (ConfigElement.GetAttribute("allowasextracargo") != null)
|
||||
{
|
||||
@@ -1174,6 +1177,7 @@ namespace Barotrauma
|
||||
break;
|
||||
case "deconstruct":
|
||||
DeconstructTime = subElement.GetAttributeFloat("time", 1.0f);
|
||||
DeconstructTimeInOutposts = subElement.GetAttributeFloat("timeinoutposts", DeconstructTime);
|
||||
AllowDeconstruct = true;
|
||||
RandomDeconstructionOutput = subElement.GetAttributeBool("chooserandom", false);
|
||||
RandomDeconstructionOutputAmount = subElement.GetAttributeInt("amount", 1);
|
||||
|
||||
@@ -398,7 +398,7 @@ namespace Barotrauma
|
||||
}
|
||||
foreach (Item contained in container.Inventory.AllItems)
|
||||
{
|
||||
if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; }
|
||||
if (TargetSlot > -1 && container.Inventory.FindIndex(contained) != TargetSlot) { continue; }
|
||||
if ((!ExcludeBroken || contained.Condition > 0.0f) && (!ExcludeFullCondition || !contained.IsFullCondition) && MatchesItem(contained)) { return true; }
|
||||
if (CheckContained(contained)) { return true; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
|
||||
// taken from: <https://gist.github.com/cajuncoding/a88f0d00847dcfc241ae80d1c7bafb1e?permalink_comment_id=4498792>
|
||||
public sealed class AsyncReaderWriterLock : IDisposable
|
||||
{
|
||||
readonly SemaphoreSlim _readSemaphore = new SemaphoreSlim(1, 1);
|
||||
readonly SemaphoreSlim _writeSemaphore = new SemaphoreSlim(1, 1);
|
||||
int _readerCount;
|
||||
|
||||
public async Task<IDisposable> AcquireWriterLock(CancellationToken token = default)
|
||||
{
|
||||
await _writeSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await _readSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_writeSemaphore.Release();
|
||||
throw;
|
||||
}
|
||||
|
||||
return new LockToken(ReleaseWriterLock);
|
||||
}
|
||||
|
||||
private void ReleaseWriterLock()
|
||||
{
|
||||
_readSemaphore.Release();
|
||||
_writeSemaphore.Release();
|
||||
}
|
||||
|
||||
public async Task<IDisposable> AcquireReaderLock(CancellationToken token = default)
|
||||
{
|
||||
await _writeSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
if (Interlocked.Increment(ref _readerCount) == 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _readSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Interlocked.Decrement(ref _readerCount);
|
||||
_writeSemaphore.Release();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
_writeSemaphore.Release();
|
||||
return new LockToken(ReleaseReaderLock);
|
||||
}
|
||||
|
||||
private void ReleaseReaderLock()
|
||||
{
|
||||
if (Interlocked.Decrement(ref _readerCount) == 0)
|
||||
_readSemaphore.Release();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_writeSemaphore.Dispose();
|
||||
_readSemaphore.Dispose();
|
||||
}
|
||||
|
||||
private sealed class LockToken : IDisposable
|
||||
{
|
||||
private readonly Action _action;
|
||||
public LockToken(Action action) => _action = action;
|
||||
public void Dispose() => _action?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using Barotrauma;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using static Barotrauma.Items.Components.Quality;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
static class MapEntityExtensions
|
||||
{
|
||||
public static void AddLinked(this MapEntity entity, MapEntity other)
|
||||
{
|
||||
entity.linkedTo.Add(other);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class ClientExtensions
|
||||
{
|
||||
#if SERVER
|
||||
public static void SetClientCharacter(this Client client, Character character)
|
||||
{
|
||||
GameMain.Server.SetClientCharacter(client, character);
|
||||
}
|
||||
|
||||
public static void Kick(this Client client, string reason = "")
|
||||
{
|
||||
GameMain.Server.KickClient(client.Connection, reason);
|
||||
}
|
||||
|
||||
public static void Ban(this Client client, string reason = "", float seconds = -1)
|
||||
{
|
||||
if (seconds == -1)
|
||||
{
|
||||
GameMain.Server.BanClient(client, reason, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMain.Server.BanClient(client, reason, TimeSpan.FromSeconds(seconds));
|
||||
}
|
||||
}
|
||||
|
||||
public static bool CheckPermission(this Client client, ClientPermissions permissions)
|
||||
{
|
||||
return client.Permissions.HasFlag(permissions);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static class ItemExtensions
|
||||
{
|
||||
public static object GetComponentString(this Item item, string component)
|
||||
{
|
||||
Type type = LuaCsSetup.Instance.PluginManagementService
|
||||
.GetType("Barotrauma.Items.Components." + component);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
MethodInfo method = typeof(Item).GetMethod(nameof(Item.GetComponent));
|
||||
MethodInfo generic = method.MakeGenericMethod(type);
|
||||
return generic.Invoke(item, null);
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
public static object CreateServerEventString(this Item item, string component)
|
||||
{
|
||||
var comp = item.GetComponentString(component);
|
||||
|
||||
if (comp == null)
|
||||
return null;
|
||||
|
||||
MethodInfo method = typeof(Item).GetMethod(
|
||||
nameof(Item.CreateServerEvent),
|
||||
new Type[] { Type.MakeGenericMethodParameter(0) });
|
||||
|
||||
MethodInfo generic = method.MakeGenericMethod(comp.GetType());
|
||||
return generic.Invoke(item, new object[] { comp });
|
||||
}
|
||||
|
||||
public static object CreateServerEventString(this Item item, string component, object[] extraData)
|
||||
{
|
||||
var comp = item.GetComponentString(component);
|
||||
|
||||
if (comp == null)
|
||||
return null;
|
||||
|
||||
MethodInfo method = typeof(Item).GetMethod(
|
||||
nameof(Item.CreateServerEvent),
|
||||
new Type[] { Type.MakeGenericMethodParameter(0), typeof(object[]) });
|
||||
|
||||
MethodInfo generic = method.MakeGenericMethod(comp.GetType());
|
||||
return generic.Invoke(item, new object[] { comp, extraData });
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static class QualityExtensions
|
||||
{
|
||||
public static void SetValue(this Quality quality, StatType statType, float value)
|
||||
{
|
||||
quality.statValues[statType] = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Barotrauma.LuaCs;
|
||||
|
||||
namespace Barotrauma.LuaCs.Compatibility;
|
||||
|
||||
public interface ILuaCsHook : ILuaPatcher, ILuaCsShim
|
||||
{
|
||||
// Event Services
|
||||
[Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")]
|
||||
void Add(string eventName, string identifier, LuaCsFunc callback, object owner = null);
|
||||
[Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")]
|
||||
void Add(string eventName, LuaCsFunc callback, object owner = null);
|
||||
void Remove(string eventName, string identifier);
|
||||
// Does anyone use this? TODO: Analyze old Lua mods for usage scenarios.
|
||||
//bool Exists(string eventName, string identifier);
|
||||
object Call(string eventName, params object[] args);
|
||||
T Call<T>(string eventName, params object[] args);
|
||||
|
||||
// Needs to be here instead of ILuaPatcher for compatiility purposes
|
||||
public enum HookMethodType
|
||||
{
|
||||
Before, After
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Barotrauma.LuaCs.Compatibility;
|
||||
|
||||
public interface ILuaCsLogger : ILuaCsShim
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Barotrauma.Networking;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma.LuaCs.Compatibility;
|
||||
|
||||
internal interface ILuaCsNetworking : ILuaCsShim
|
||||
{
|
||||
void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData);
|
||||
ushort LastClientListUpdateID { get; set; }
|
||||
void HttpRequest(string url, LuaCsAction callback, string data = null, string method = "POST", string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null);
|
||||
void HttpPost(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null);
|
||||
void HttpGet(string url, LuaCsAction callback, Dictionary<string, string> headers = null, string savePath = null);
|
||||
void RequestGetHTTP(string url, LuaCsAction callback, Dictionary<string, string> headers = null, string savePath = null);
|
||||
void RequestPostHTTP(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null);
|
||||
|
||||
void Receive(string netId, LuaCsAction action);
|
||||
#if SERVER
|
||||
int FileSenderMaxPacketsPerUpdate { get; set; }
|
||||
void ClientWriteLobby(Client client);
|
||||
void UpdateClientPermissions(Client client);
|
||||
IWriteMessage Start();
|
||||
void Send(IWriteMessage mesage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
|
||||
#elif CLIENT
|
||||
void Send(IWriteMessage mesage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Barotrauma.LuaCs;
|
||||
|
||||
namespace Barotrauma.LuaCs.Compatibility;
|
||||
|
||||
public interface ILuaCsShim : IService
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma.LuaCs.Compatibility;
|
||||
|
||||
internal partial interface ILuaCsTimer : IReusableService, ILuaCsShim
|
||||
{
|
||||
public static double Time => Timing.TotalTime;
|
||||
public static double GetTime() => Time;
|
||||
public static double AccumulatorMax { get; set; }
|
||||
|
||||
public void Clear();
|
||||
public void Wait(LuaCsAction action, int millisecondDelay);
|
||||
public void NextFrame(LuaCsAction action);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user