Unstable 0.1500.5.0 (almost forgor edition 💀)

This commit is contained in:
Markus Isberg
2021-10-01 23:56:14 +09:00
parent 3043a9a7bc
commit 08bdfc6cea
150 changed files with 5669 additions and 4403 deletions
@@ -104,6 +104,8 @@ namespace Barotrauma
(!requireNonDirty || !pathSteering.IsPathDirty);
protected readonly float colliderWidth;
protected readonly float minGapSize;
protected readonly float minHullSize;
protected readonly float colliderLength;
protected readonly float avoidLookAheadDistance;
@@ -116,6 +118,8 @@ namespace Barotrauma
colliderWidth = size.X;
colliderLength = size.Y;
avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f);
minGapSize = ConvertUnits.ToDisplayUnits(Math.Min(colliderWidth, colliderLength));
minHullSize = ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth) * 1.1f);
}
public virtual void OnAttacked(Character attacker, AttackResult attackResult) { }
@@ -390,8 +394,7 @@ namespace Barotrauma
else
{
if (gap.Open < 1) { continue; }
bool canGetThrough = ConvertUnits.ToDisplayUnits(colliderWidth) < gap.Size;
if (!canGetThrough) { continue; }
if (gap.Size < minGapSize) { continue; }
}
if (gap.FlowTargetHull == Character.CurrentHull)
{
@@ -92,7 +92,16 @@ namespace Barotrauma
public string SonarLabel;
public string SonarIconIdentifier;
public bool Enabled => SoundRange > 0 || SightRange > 0;
private bool inDetectable;
/// <summary>
/// Should be reset to false each frame and kept indetectable by e.g. a status effect.
/// </summary>
public bool InDetectable
{
get => inDetectable || (SoundRange <= 0 && SightRange <= 0);
set => inDetectable = value;
}
public float MinSoundRange, MinSightRange;
public float MaxSoundRange = 100000, MaxSightRange = 100000;
@@ -181,14 +190,15 @@ namespace Barotrauma
public void Update(float deltaTime)
{
if (Enabled && !Static && FadeOutTime > 0)
InDetectable = false;
if (!Static && FadeOutTime > 0)
{
// The aitarget goes silent/invisible if the components don't keep it active
if (!StaticSight)
if (!StaticSight && SightRange > 0)
{
DecreaseSightRange(deltaTime);
}
if (!StaticSound)
if (!StaticSound && SoundRange > 0)
{
DecreaseSoundRange(deltaTime);
}
File diff suppressed because it is too large Load Diff
@@ -1036,16 +1036,19 @@ namespace Barotrauma
}
if (previousAttackResults.ContainsKey(attacker))
{
foreach (Affliction newAffliction in attackResult.Afflictions)
if (attackResult.Afflictions != null)
{
var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.Prefab && a.Source == newAffliction.Source);
if (matchingAffliction == null)
foreach (Affliction newAffliction in attackResult.Afflictions)
{
previousAttackResults[attacker].Afflictions.Add(newAffliction);
}
else
{
matchingAffliction.Strength += newAffliction.Strength;
var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.Prefab && a.Source == newAffliction.Source);
if (matchingAffliction == null)
{
previousAttackResults[attacker].Afflictions.Add(newAffliction);
}
else
{
matchingAffliction.Strength += newAffliction.Strength;
}
}
}
previousAttackResults[attacker] = new AttackResult(previousAttackResults[attacker].Afflictions, previousAttackResults[attacker].HitLimb);
@@ -1062,9 +1065,12 @@ namespace Barotrauma
float realDamage = attackResult.Damage;
// including poisons etc
float totalDamage = realDamage;
foreach (Affliction affliction in attackResult.Afflictions)
if (attackResult.Afflictions != null)
{
totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
foreach (Affliction affliction in attackResult.Afflictions)
{
totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
}
}
if (totalDamage <= 0.01f) { return; }
if (Character.IsBot)
@@ -1255,7 +1261,7 @@ namespace Barotrauma
// Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2;
}
if (attackResult.Afflictions.Any(a => a is AfflictionHusk))
if (attackResult.Afflictions != null && attackResult.Afflictions.Any(a => a is AfflictionHusk))
{
cumulativeDamage = 100;
}
@@ -2,7 +2,6 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using FarseerPhysics;
namespace Barotrauma
@@ -15,6 +14,11 @@ namespace Barotrauma
private bool canOpenDoors;
public bool CanBreakDoors { get; set; }
private bool ShouldBreakDoor(Door door) =>
CanBreakDoors &&
!door.Item.Indestructible && !door.Item.InvulnerableToDamage &&
(door.Item.Submarine == null || door.Item.Submarine.TeamID != character.TeamID);
private Character character;
private Vector2 currentTarget;
@@ -23,7 +27,7 @@ namespace Barotrauma
private float buttonPressCooldown;
const float ButtonPressInterval = 0.5f;
const float ButtonPressInterval = 0.25f;
public SteeringPath CurrentPath
{
@@ -111,9 +115,9 @@ namespace Barotrauma
IsPathDirty = true;
}
public void SteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisiblity = true)
public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisiblity = true)
{
steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
steering += CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
}
/// <summary>
@@ -158,7 +162,7 @@ namespace Barotrauma
}
}
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
{
bool needsNewPath = currentPath == null || currentPath.Unreachable;
if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f)
@@ -200,7 +204,7 @@ namespace Barotrauma
}
pathFinder.InsideSubmarine = character.Submarine != null;
pathFinder.ApplyPenaltyToOutsideNodes = character.PressureProtection <= 0;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.X) <= 0;
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
@@ -241,6 +245,10 @@ namespace Barotrauma
}
if (useNewPath)
{
if (currentPath != null)
{
CheckDoorsInPath();
}
currentPath = newPath;
}
float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority);
@@ -306,10 +314,12 @@ namespace Barotrauma
pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition;
}
return currentTarget - pos2;
}
if (canOpenDoors && !character.LockHands && buttonPressCooldown <= 0.0f)
}
bool doorsChecked = false;
if (!character.LockHands && buttonPressCooldown <= 0.0f)
{
CheckDoorsInPath();
doorsChecked = true;
}
Vector2 pos = host.SimPosition;
if (character != null && CurrentPath.CurrentNode?.Submarine != null)
@@ -394,7 +404,7 @@ namespace Barotrauma
}
if (isAboveFloor || nextLadderSameAsCurrent)
{
currentPath.SkipToNextNode();
NextNode(!doorsChecked);
}
}
else if (nextLadder != null)
@@ -404,7 +414,7 @@ namespace Barotrauma
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
currentPath.SkipToNextNode();
NextNode(!doorsChecked);
}
}
return diff;
@@ -426,7 +436,7 @@ namespace Barotrauma
float distance = horizontalDistance + verticalDistance;
if (ConvertUnits.ToSimUnits(distance) < targetDistance)
{
currentPath.SkipToNextNode();
NextNode(!doorsChecked);
}
}
}
@@ -451,7 +461,7 @@ namespace Barotrauma
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed))
{
currentPath.SkipToNextNode();
NextNode(!doorsChecked);
}
}
if (currentPath.CurrentNode == null)
@@ -461,28 +471,51 @@ namespace Barotrauma
return currentPath.CurrentNode.SimPosition - pos;
}
private void NextNode(bool checkDoors)
{
if (checkDoors)
{
CheckDoorsInPath();
}
currentPath.SkipToNextNode();
}
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsOpen || door.IsBroken) { return true; }
if (!door.Item.IsInteractable(character)) { return false; }
if (!CanBreakDoors)
if (door.IsBroken) { return true; }
if (!door.IsOpen)
{
if (door.IsStuck || door.IsJammed) { return false; }
if (!canOpenDoors || character.LockHands) { return false; }
if (!door.Item.IsInteractable(character)) { return false; }
if (!ShouldBreakDoor(door))
{
if (door.IsStuck || door.IsJammed) { return false; }
if (!canOpenDoors || character.LockHands) { return false; }
}
}
if (door.HasIntegratedButtons)
{
return door.HasAccess(character) || CanBreakDoors;
return door.IsOpen || door.HasAccess(character) || ShouldBreakDoor(door);
}
else
{
return door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))) || CanBreakDoors;
// We'll want this to run each time, because the delegate is used to find a valid button component.
bool canAccessButtons = door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b)));
return canAccessButtons || door.IsOpen || ShouldBreakDoor(door);
}
}
private Vector2 GetColliderSize() => ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize());
private float GetColliderLength()
{
Vector2 colliderSize = character.AnimController.Collider.GetSize();
return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y));
}
private void CheckDoorsInPath()
{
for (int i = 0; i < 2; i++)
if (!canOpenDoors) { return; }
for (int i = 0; i < 5; i++)
{
WayPoint currentWaypoint = null;
WayPoint nextWaypoint = null;
@@ -493,17 +526,21 @@ namespace Barotrauma
{
door = currentPath.Nodes.First().ConnectedDoor;
shouldBeOpen = door != null;
if (i > 0) { break; }
}
else
{
if (i == 0)
bool closeDoors = character.IsBot && character.IsInFriendlySub || character.Params.AI != null && character.Params.AI.KeepDoorsClosed;
if (i == 0 || !closeDoors)
{
currentWaypoint = currentPath.CurrentNode;
nextWaypoint = currentPath.NextNode;
}
else
{
currentWaypoint = currentPath.PrevNode;
int previousIndex = currentPath.CurrentIndex - i;
if (previousIndex < 0) { break; }
currentWaypoint = currentPath.Nodes[previousIndex];
nextWaypoint = currentPath.CurrentNode;
}
if (currentWaypoint?.ConnectedDoor == null) { continue; }
@@ -520,16 +557,18 @@ namespace Barotrauma
}
else
{
float colliderLength = GetColliderLength();
door = currentWaypoint.ConnectedDoor;
if (door.LinkedGap.IsHorizontal)
{
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -50.0f;
float size = character.AnimController.InWater ? colliderLength : GetColliderSize().X;
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -size;
}
else
{
int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -80.0f;
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -colliderLength;
}
}
}
@@ -573,7 +612,7 @@ namespace Barotrauma
}
else if (closestButton != null)
{
if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance * 2, 2))
if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance + GetColliderLength(), 2))
{
closestButton.Item.TryInteract(character, false, true);
buttonPressCooldown = ButtonPressInterval;
@@ -19,16 +19,19 @@ namespace Barotrauma
private Body targetBody;
private Vector2 attachSurfaceNormal;
private Submarine targetSubmarine;
private Character targetCharacter;
private readonly Character character;
public bool AttachToSub { get; private set; }
public bool AttachToWalls { get; private set; }
public bool AttachToCharacters { get; private set; }
private readonly float minDeattachSpeed, maxDeattachSpeed;
private readonly float minDeattachSpeed, maxDeattachSpeed, maxAttachDuration;
private readonly float damageOnDetach, detachStun;
private float deattachTimer;
private readonly bool weld;
private float deattachCheckTimer;
private Vector2 wallAttachPos;
private Vector2 _attachPos;
private float attachCooldown;
@@ -38,9 +41,9 @@ namespace Barotrauma
private float jointDir;
public List<WeldJoint> AttachJoints { get; } = new List<WeldJoint>();
public List<Joint> AttachJoints { get; } = new List<Joint>();
public Vector2? WallAttachPos
public Vector2? AttachPos
{
get;
private set;
@@ -48,18 +51,21 @@ namespace Barotrauma
public bool IsAttached => AttachJoints.Count > 0;
public bool IsAttachedToSub => IsAttached && targetSubmarine != null;
public bool IsAttachedToSub => IsAttached && targetSubmarine != null && targetCharacter == null;
public LatchOntoAI(XElement element, EnemyAIController enemyAI)
{
AttachToWalls = element.GetAttributeBool("attachtowalls", false);
AttachToSub = element.GetAttributeBool("attachtosub", false);
AttachToCharacters = element.GetAttributeBool("attachtocharacters", false);
minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 5.0f);
maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 8.0f));
maxAttachDuration = element.GetAttributeFloat("maxattachduration", -1.0f);
damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f);
detachStun = element.GetAttributeFloat("detachstun", 0.0f);
localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero));
attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f));
weld = element.GetAttributeBool("weld", true);
string limbString = element.GetAttributeString("attachlimb", null);
attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase));
@@ -81,30 +87,54 @@ namespace Barotrauma
public void SetAttachTarget(Structure wall, Vector2 attachPos, Vector2 attachSurfaceNormal)
{
if (!AttachToSub) { return; }
if (wall == null) { return; }
var sub = wall.Submarine;
if (sub == null) { return; }
Reset();
targetWall = wall;
targetSubmarine = sub;
targetBody = targetSubmarine.PhysicsBody.FarseerBody;
this.attachSurfaceNormal = attachSurfaceNormal;
wallAttachPos = attachPos;
_attachPos = attachPos;
}
public void SetAttachTarget(Character target)
{
if (!AttachToCharacters) { return; }
Reset();
targetCharacter = target;
targetSubmarine = target.Submarine;
targetBody = target.AnimController.Collider.FarseerBody;
attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition);
}
public void Update(EnemyAIController enemyAI, float deltaTime)
{
if (character.Submarine != null)
{
DeattachFromBody(reset: true);
return;
if (targetCharacter != null && targetCharacter.Submarine != targetSubmarine ||
character.Submarine != null && targetSubmarine != null && targetCharacter == null)
{
DeattachFromBody(reset: true);
return;
}
}
if (AttachJoints.Count > 0)
if (IsAttached)
{
if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir))
{
AttachJoints[0].LocalAnchorA =
new Vector2(-AttachJoints[0].LocalAnchorA.X, AttachJoints[0].LocalAnchorA.Y);
AttachJoints[0].ReferenceAngle = -AttachJoints[0].ReferenceAngle;
var attachJoint = AttachJoints[0];
if (attachJoint is WeldJoint weldJoint)
{
weldJoint.LocalAnchorA = new Vector2(-weldJoint.LocalAnchorA.X, weldJoint.LocalAnchorA.Y);
weldJoint.ReferenceAngle = -weldJoint.ReferenceAngle;
}
else if (attachJoint is RevoluteJoint revoluteJoint)
{
revoluteJoint.LocalAnchorA = new Vector2(-revoluteJoint.LocalAnchorA.X, revoluteJoint.LocalAnchorA.Y);
revoluteJoint.ReferenceAngle = -revoluteJoint.ReferenceAngle;
}
jointDir = attachLimb.Dir;
}
for (int i = 0; i < AttachJoints.Count; i++)
@@ -113,31 +143,51 @@ namespace Barotrauma
if (Vector2.DistanceSquared(AttachJoints[i].WorldAnchorB, AttachJoints[i].BodyA.Position) > 10.0f * 10.0f)
{
#if DEBUG
DebugConsole.ThrowError("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach");
DebugConsole.Log("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach");
#endif
DeattachFromBody(reset: true);
return;
}
}
if (targetCharacter != null)
{
if (enemyAI.AttackingLimb?.attack == null)
{
DeattachFromBody(reset: true, cooldown: 1);
}
else
{
float range = enemyAI.AttackingLimb.attack.DamageRange * 2f;
if (Vector2.DistanceSquared(targetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range)
{
DeattachFromBody(reset: true, cooldown: 1);
}
}
}
}
if (attachCooldown > 0)
{
attachCooldown -= deltaTime;
}
if (deattachTimer > 0)
if (deattachCheckTimer > 0)
{
deattachTimer -= deltaTime;
deattachCheckTimer -= deltaTime;
}
Vector2 transformedAttachPos = wallAttachPos;
if (targetCharacter != null)
{
// Own sim pos -> target where we are
_attachPos = character.SimPosition;
}
Vector2 transformedAttachPos = _attachPos;
if (character.Submarine == null && targetSubmarine != null)
{
transformedAttachPos += ConvertUnits.ToSimUnits(targetSubmarine.Position);
}
if (transformedAttachPos != Vector2.Zero)
{
WallAttachPos = transformedAttachPos;
AttachPos = transformedAttachPos;
}
switch (enemyAI.State)
@@ -151,7 +201,7 @@ namespace Barotrauma
//check if there are any walls nearby the character could attach to
if (raycastTimer < 0.0f)
{
wallAttachPos = Vector2.Zero;
_attachPos = Vector2.Zero;
var cells = Level.Loaded.GetCells(character.WorldPosition, 1);
if (cells.Count > 0)
@@ -169,7 +219,7 @@ namespace Barotrauma
{
attachSurfaceNormal = edge.GetNormal(cell);
targetBody = cell.Body;
wallAttachPos = potentialAttachPos;
_attachPos = potentialAttachPos;
closestDist = distSqr;
}
break;
@@ -183,21 +233,20 @@ namespace Barotrauma
}
else
{
wallAttachPos = Vector2.Zero;
_attachPos = Vector2.Zero;
}
if (wallAttachPos == Vector2.Zero || targetBody == null)
if (_attachPos == Vector2.Zero || targetBody == null)
{
DeattachFromBody(reset: false);
}
else
{
float squaredDistance = Vector2.DistanceSquared(character.SimPosition, wallAttachPos);
float squaredDistance = Vector2.DistanceSquared(character.SimPosition, _attachPos);
float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f;
if (squaredDistance < targetDistance * targetDistance)
{
//close enough to a wall -> attach
AttachToBody(wallAttachPos);
AttachToBody(_attachPos);
enemyAI.SteeringManager.Reset();
}
else
@@ -205,25 +254,22 @@ namespace Barotrauma
//move closer to the wall
DeattachFromBody(reset: false);
enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, 0.1f);
enemyAI.SteeringManager.SteeringSeek(wallAttachPos);
enemyAI.SteeringManager.SteeringSeek(_attachPos);
}
}
break;
case AIState.Attack:
case AIState.Aggressive:
if (enemyAI.AttackingLimb != null)
if (enemyAI.IsSteeringThroughGap) { break; }
if (_attachPos == Vector2.Zero) { break; }
if (!AttachToSub && !AttachToCharacters) { break; }
if (enemyAI.AttackingLimb == null) { break; }
if (targetBody == null) { break; }
if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; }
Vector2 referencePos = targetCharacter != null ? targetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos);
if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
{
if (AttachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && targetBody != null)
{
// is not attached or is attached to something else
if (!IsAttached || IsAttached && AttachJoints[0].BodyB != targetBody)
{
if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
{
AttachToBody(transformedAttachPos);
}
}
}
AttachToBody(transformedAttachPos);
}
break;
default:
@@ -231,43 +277,50 @@ namespace Barotrauma
break;
}
if (IsAttached && targetBody != null && targetWall != null && targetSubmarine != null && deattachTimer <= 0.0f)
if (IsAttached && targetBody != null && deattachCheckTimer <= 0.0f)
{
bool deattach = false;
// Deattach if the wall is broken enough where we are attached to
int targetSection = targetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true);
if (enemyAI.CanPassThroughHole(targetWall, targetSection))
if (maxAttachDuration > 0)
{
deattach = true;
attachCooldown = 2;
}
if (!deattach)
if (!deattach && targetWall != null && targetSubmarine != null)
{
// Deattach if the velocity is high
float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length();
deattach = velocity > maxDeattachSpeed;
// Deattach if the wall is broken enough where we are attached to
int targetSection = targetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true);
if (enemyAI.CanPassThroughHole(targetWall, targetSection))
{
deattach = true;
attachCooldown = 2;
}
if (!deattach)
{
if (velocity > minDeattachSpeed)
// Deattach if the velocity is high
float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length();
deattach = velocity > maxDeattachSpeed;
if (!deattach)
{
float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ?
Math.Sign(Math.Abs(velocity) - minDeattachSpeed) :
(Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed);
if (Rand.Range(0.0f, 1.0f) < velocityFactor)
if (velocity > minDeattachSpeed)
{
deattach = true;
character.AddDamage(character.WorldPosition, new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true);
attachCooldown = detachStun * 2;
float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ?
Math.Sign(Math.Abs(velocity) - minDeattachSpeed) :
(Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed);
if (Rand.Range(0.0f, 1.0f) < velocityFactor)
{
deattach = true;
character.AddDamage(character.WorldPosition, new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true);
attachCooldown = detachStun * 2;
}
}
}
}
deattachCheckTimer = 5.0f;
}
if (deattach)
{
DeattachFromBody(reset: true);
}
deattachTimer = 5.0f;
}
}
@@ -315,16 +368,30 @@ namespace Barotrauma
}
collider.SetTransform(attachPos + attachSurfaceNormal * colliderFront.Length(), MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2);
var colliderJoint = new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false)
{
FrequencyHz = 10.0f,
DampingRatio = 0.5f,
KinematicBodyB = true,
CollideConnected = false,
//Length = 0.1f
};
Joint colliderJoint = weld ?
new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false)
{
FrequencyHz = 10.0f,
DampingRatio = 0.5f,
KinematicBodyB = true,
CollideConnected = false,
} :
new RevoluteJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false)
{
MotorEnabled = true,
MaxMotorTorque = 0.25f
} as Joint;
GameMain.World.Add(colliderJoint);
AttachJoints.Add(colliderJoint);
AttachJoints.Add(colliderJoint);
if (targetCharacter != null)
{
targetCharacter.Latchers.Add(this);
}
if (maxAttachDuration > 0)
{
deattachCheckTimer = maxAttachDuration;
}
}
public void DeattachFromBody(bool reset, float cooldown = 0)
@@ -342,14 +409,23 @@ namespace Barotrauma
{
Reset();
}
if (targetCharacter != null)
{
targetCharacter.Latchers.Remove(this);
}
}
private void Reset()
{
if (targetCharacter != null)
{
targetCharacter.Latchers.Remove(this);
}
targetCharacter = null;
targetWall = null;
targetSubmarine = null;
targetBody = null;
WallAttachPos = null;
AttachPos = null;
}
private void OnCharacterDeath(Character character, CauseOfDeath causeOfDeath)
@@ -96,6 +96,7 @@ namespace Barotrauma
#if DEBUG
if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() && !objectiveManager.IsCurrentOrder<AIObjectiveReturn>())
{
// TODO: dismiss
throw new Exception("Order abandoned!");
}
#endif
@@ -111,7 +111,7 @@ namespace Barotrauma
public CombatMode Mode { get; private set; }
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious;
private bool TargetEliminated => IsEnemyDisabled || (Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f);
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
private float AimSpeed => HumanAIController.AimSpeed;
@@ -56,7 +56,8 @@ namespace Barotrauma
public static bool IsValidTarget(Character target, Character character)
{
if (target == null || target.Removed) { return false; }
if (target.IsDead || target.IsUnconscious) { return false; }
if (target.IsDead) { return false; }
if (target.IsUnconscious && target.Params.Health.ConstantHealthRegeneration <= 0.0f) { return false; }
if (target == character) { return false; }
if (target.Submarine == null) { return false; }
if (character.Submarine == null) { return false; }
@@ -307,6 +307,8 @@ namespace Barotrauma
foreach (Hull hull in Hull.hullList.OrderByDescending(h => EstimateHullSuitability(h)))
{
if (hull.Submarine == null) { continue; }
// Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it.
if (hull.Submarine.Info.IsRuin) { continue; }
if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; }
if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; }
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
@@ -461,11 +461,11 @@ namespace Barotrauma
nodeFilter = n => n.Waypoint.Tunnel != null;
}
PathSteering.SteeringSeek(targetPos, 1,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
endNodeFilter,
nodeFilter,
CheckVisibility);
PathSteering.SteeringSeek(targetPos, weight: 1,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
endNodeFilter: endNodeFilter,
nodeFilter: nodeFilter,
checkVisiblity: CheckVisibility);
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
{
@@ -167,7 +167,7 @@ namespace Barotrauma
private readonly List<PathNode> sortedNodes = new List<PathNode>();
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
{
foreach (PathNode node in nodes)
{
@@ -233,6 +233,10 @@ namespace Barotrauma
// Always check the visibility for the start node
if (!IsWaypointVisible(node, start)) { continue; }
if (node.IsBlocked()) { continue; }
if (node.Waypoint.ConnectedGap != null)
{
if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; }
}
startNode = node;
}
}
@@ -282,6 +286,10 @@ namespace Barotrauma
// Only check the visibility for the end node when allowed (fix leaks)
if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { continue; }
if (node.IsBlocked()) { continue; }
if (node.Waypoint.ConnectedGap != null)
{
if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; }
}
endNode = node;
}
}
@@ -294,40 +302,12 @@ namespace Barotrauma
return new SteeringPath(true);
}
var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr);
var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize);
return path;
}
public SteeringPath FindPath(WayPoint start, WayPoint end)
{
PathNode startNode = null, endNode = null;
foreach (PathNode node in nodes)
{
if (node.Waypoint == start)
{
startNode = node;
if (endNode != null) { break; }
}
if (node.Waypoint == end)
{
endNode = node;
if (startNode != null) { break; }
}
}
if (startNode == null || endNode == null)
{
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find matching pathnodes to waypoints.", Color.DarkRed);
#endif
return new SteeringPath(true);
}
return FindPath(startNode, endNode);
}
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null, string errorMsgStr = "")
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null, string errorMsgStr = "", float minGapSize = 0)
{
if (start == end)
{
@@ -356,7 +336,10 @@ namespace Barotrauma
if (isCharacter && node.Waypoint.isObstructed) { continue; }
if (filter != null && !filter(node)) { continue; }
if (node.IsBlocked()) { continue; }
if (node.Waypoint.ConnectedGap != null)
{
if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; }
}
dist = node.F;
currNode = node;
}
@@ -460,6 +443,8 @@ namespace Barotrauma
return path;
}
private bool CanFitThroughGap(Gap gap, float minWidth) => gap.IsHorizontal ? gap.RectHeight > minWidth : gap.RectWidth > minWidth;
}
}
@@ -1,12 +1,31 @@
using FarseerPhysics;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
abstract class AnimController : Ragdoll
{
public Vector2 RightHandIKPos { get; protected set; }
public Vector2 LeftHandIKPos { get; protected set; }
protected LimbJoint rightShoulder, leftShoulder;
protected float upperArmLength, forearmLength;
protected float useItemTimer;
protected bool aiming;
protected bool wasAiming;
protected bool aimingMelee;
protected bool wasAimingMelee;
public bool IsAiming => wasAiming;
public bool IsAimingMelee => wasAimingMelee;
public float ArmLength => upperArmLength + forearmLength;
public abstract GroundedMovementParams WalkParams { get; set; }
public abstract GroundedMovementParams RunParams { get; set; }
public abstract SwimParams SwimSlowParams { get; set; }
@@ -60,14 +79,14 @@ namespace Barotrauma
}
else
{
return IsMovingFast? SwimFastParams : SwimSlowParams;
return IsMovingFast ? SwimFastParams : SwimSlowParams;
}
}
}
public bool CanWalk => RagdollParams.CanWalk;
public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir);
// TODO: define death anim duration in XML
protected float deathAnimTimer, deathAnimDuration = 5.0f;
@@ -155,13 +174,9 @@ namespace Barotrauma
public AnimController(Character character, string seed, RagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { }
public virtual void UpdateAnim(float deltaTime) { }
public abstract void UpdateAnim(float deltaTime);
public virtual void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimingMelee = false) { }
public virtual void DragCharacter(Character target, float deltaTime) { }
public virtual void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { }
public abstract void DragCharacter(Character target, float deltaTime);
public virtual float GetSpeed(AnimationType type)
{
@@ -253,5 +268,437 @@ namespace Barotrauma
throw new NotImplementedException(type.ToString());
}
}
public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos)
{
useItemTimer = 0.5f;
Anim = Animation.UsingConstruction;
if (!allowMovement)
{
TargetMovement = Vector2.Zero;
TargetDir = handWorldPos.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos);
if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2))
{
TargetMovement = Vector2.Normalize(handWorldPos - character.WorldPosition) * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1);
}
}
if (!character.Enabled) { return; }
Vector2 handSimPos = ConvertUnits.ToSimUnits(handWorldPos);
if (character.Submarine != null)
{
handSimPos -= character.Submarine.SimPosition;
}
var leftHand = GetLimb(LimbType.LeftHand);
if (leftHand != null)
{
leftHand.Disabled = true;
leftHand.PullJointEnabled = true;
leftHand.PullJointWorldAnchorB = handSimPos;
}
var rightHand = GetLimb(LimbType.RightHand);
if (rightHand != null)
{
rightHand.Disabled = true;
rightHand.PullJointEnabled = true;
rightHand.PullJointWorldAnchorB = handSimPos;
}
}
public void Grab(Vector2 rightHandPos, Vector2 leftHandPos)
{
for (int i = 0; i < 2; i++)
{
Limb pullLimb = (i == 0) ? GetLimb(LimbType.LeftHand) : GetLimb(LimbType.RightHand);
pullLimb.Disabled = true;
pullLimb.PullJointEnabled = true;
pullLimb.PullJointWorldAnchorB = (i == 0) ? rightHandPos : leftHandPos;
pullLimb.PullJointMaxForce = 500.0f;
}
}
private Direction previousDirection;
private readonly Vector2[] transformedHandlePos = new Vector2[2];
//TODO: refactor this method, it's way too convoluted
public void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimMelee = false)
{
aimingMelee = aimMelee;
if (character.Stun > 0.0f || character.IsIncapacitated)
{
aim = false;
}
//calculate the handle positions
Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation);
float horizontalOffset = ConvertUnits.ToSimUnits((item.Sprite.size.X / 2 - item.Sprite.Origin.X) * item.Scale);
//handlePos[0] = ConvertUnits.ToSimUnits(new Vector2(-45,25) * 0.5f);
//handlePos[1] = ConvertUnits.ToSimUnits(new Vector2(-65,30) * 0.5f);
transformedHandlePos[0] = Vector2.Transform(new Vector2(handlePos[0].X + horizontalOffset, handlePos[0].Y), itemTransfrom);
transformedHandlePos[1] = Vector2.Transform(new Vector2(handlePos[1].X + horizontalOffset, handlePos[1].Y), itemTransfrom);
Limb torso = GetLimb(LimbType.Torso) ?? MainLimb;
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
Vector2 itemPos = aim ? aimPos : holdPos;
var controller = character.SelectedConstruction?.GetComponent<Controller>();
bool usingController = controller != null && !controller.AllowAiming;
bool isClimbing = character.IsClimbing && Math.Abs(character.AnimController.TargetMovement.Y) > 0.01f;
float itemAngle;
Holdable holdable = item.GetComponent<Holdable>();
float torsoRotation = torso.Rotation;
bool equippedInRightHand = character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item && rightHand != null && !rightHand.IsSevered;
bool equippedInLefthand = character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item && leftHand != null && !leftHand.IsSevered;
if (aim && !isClimbing && !usingController && character.Stun <= 0.0f && itemPos != Vector2.Zero && !character.IsIncapacitated)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition);
Vector2 diff = holdable.Aimable ? (mousePos - AimSourceSimPos) * Dir : Vector2.UnitX;
holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torsoRotation * Dir;
holdAngle += GetAimWobble(rightHand, leftHand, item);
itemAngle = torsoRotation + holdAngle * Dir;
if (holdable.ControlPose)
{
var head = GetLimb(LimbType.Head);
if (head != null)
{
head.body.SmoothRotate(itemAngle, force: 30 * head.Mass);
}
if (TargetMovement == Vector2.Zero && inWater)
{
torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f;
torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f);
}
aiming = true;
}
}
else
{
if (holdable.UseHandRotationForHoldAngle)
{
if (equippedInRightHand)
{
itemAngle = rightHand.Rotation + holdAngle * Dir;
}
else if (equippedInLefthand)
{
itemAngle = leftHand.Rotation + holdAngle * Dir;
}
else
{
itemAngle = torsoRotation + holdAngle * Dir;
}
}
else
{
itemAngle = torsoRotation + holdAngle * Dir;
}
}
if (rightShoulder == null) { return; }
Vector2 transformedHoldPos = rightShoulder.WorldAnchorA;
if (itemPos == Vector2.Zero || isClimbing || usingController)
{
if (equippedInRightHand)
{
transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0];
itemAngle = rightHand.Rotation + (holdAngle - rightHand.Params.GetSpriteOrientation() + MathHelper.PiOver2) * Dir;
}
else if (equippedInLefthand)
{
transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1];
itemAngle = leftHand.Rotation + (holdAngle - leftHand.Params.GetSpriteOrientation() + MathHelper.PiOver2) * Dir;
}
}
else
{
if (equippedInRightHand)
{
transformedHoldPos = rightShoulder.WorldAnchorA;
rightHand.Disabled = true;
}
if (equippedInLefthand)
{
if (leftShoulder == null) { return; }
transformedHoldPos = leftShoulder.WorldAnchorA;
leftHand.Disabled = true;
}
itemPos.X *= Dir;
transformedHoldPos += Vector2.Transform(itemPos, Matrix.CreateRotationZ(itemAngle));
}
item.body.ResetDynamics();
Vector2 currItemPos = equippedInRightHand ?
rightHand.PullJointWorldAnchorA - transformedHandlePos[0] :
leftHand.PullJointWorldAnchorA - transformedHandlePos[1];
if (!MathUtils.IsValid(currItemPos))
{
string errorMsg = "Attempted to move the item \"" + item + "\" to an invalid position in HumanidAnimController.HoldItem: " +
currItemPos + ", rightHandPos: " + rightHand.PullJointWorldAnchorA + ", leftHandPos: " + leftHand.PullJointWorldAnchorA +
", handlePos[0]: " + handlePos[0] + ", handlePos[1]: " + handlePos[1] +
", transformedHandlePos[0]: " + transformedHandlePos[0] + ", transformedHandlePos[1]:" + transformedHandlePos[1] +
", item pos: " + item.SimPosition + ", itemAngle: " + itemAngle +
", collider pos: " + character.SimPosition;
DebugConsole.Log(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"HumanoidAnimController.HoldItem:InvalidPos:" + character.Name + item.Name,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg);
return;
}
if (holdable.Pusher != null)
{
if (character.Stun > 0.0f || character.IsIncapacitated)
{
holdable.Pusher.Enabled = false;
}
else
{
if (!holdable.Pusher.Enabled)
{
holdable.Pusher.Enabled = true;
holdable.Pusher.ResetDynamics();
holdable.Pusher.SetTransform(currItemPos, itemAngle);
}
else
{
holdable.Pusher.TargetPosition = currItemPos;
holdable.Pusher.TargetRotation = holdAngle * Dir;
holdable.Pusher.MoveToTargetPosition(true);
currItemPos = holdable.Pusher.SimPosition;
itemAngle = holdable.Pusher.Rotation;
}
}
}
float targetAngle = MathUtils.WrapAngleTwoPi(itemAngle + itemAngleRelativeToHoldAngle * Dir);
float currentRotation = MathUtils.WrapAngleTwoPi(item.body.Rotation);
float itemRotation = MathHelper.SmoothStep(currentRotation, targetAngle, deltaTime * 25);
if (previousDirection != dir || Math.Abs(targetAngle - currentRotation) > MathHelper.Pi)
{
itemRotation = targetAngle;
}
item.SetTransform(currItemPos, itemRotation, setPrevTransform: false);
previousDirection = dir;
if (!isClimbing && !character.IsIncapacitated && itemPos != Vector2.Zero && (aim || !holdable.UseHandRotationForHoldAngle))
{
for (int i = 0; i < 2; i++)
{
if (!character.Inventory.IsInLimbSlot(item, i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand)) { continue; }
#if DEBUG
if (handlePos[i].LengthSquared() > ArmLength)
{
DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)");
}
#endif
HandIK(i == 0 ? rightHand : leftHand, transformedHoldPos + transformedHandlePos[i], CurrentAnimationParams.ArmIKStrength, CurrentAnimationParams.HandIKStrength);
}
}
}
private float GetAimWobble(Limb rightHand, Limb leftHand, Item heldItem)
{
float wobbleStrength = 0.0f;
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == heldItem)
{
wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: "damage");
}
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == heldItem)
{
wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: "damage");
}
if (wobbleStrength <= 0.1f) { return 0.0f; }
wobbleStrength = (float)Math.Min(wobbleStrength, 1.0f);
float lowFreqNoise = PerlinNoise.GetPerlin((float)Timing.TotalTime / 320.0f, (float)Timing.TotalTime / 240.0f) - 0.5f;
float highFreqNoise = PerlinNoise.GetPerlin((float)Timing.TotalTime / 40.0f, (float)Timing.TotalTime / 50.0f) - 0.5f;
return (lowFreqNoise * 1.0f + highFreqNoise * 0.1f) * wobbleStrength;
}
public void HandIK(Limb hand, Vector2 pos, float armTorque = 1.0f, float handTorque = 1.0f)
{
Vector2 shoulderPos;
Limb arm, forearm;
if (hand.type == LimbType.LeftHand)
{
if (leftShoulder == null) { return; }
shoulderPos = leftShoulder.WorldAnchorA;
arm = GetLimb(LimbType.LeftArm);
forearm = GetLimb(LimbType.LeftForearm);
LeftHandIKPos = pos;
}
else
{
if (rightShoulder == null) { return; }
shoulderPos = rightShoulder.WorldAnchorA;
arm = GetLimb(LimbType.RightArm);
forearm = GetLimb(LimbType.RightForearm);
RightHandIKPos = pos;
}
if (arm == null) { return; }
//distance from shoulder to holdpos
float c = Vector2.Distance(pos, shoulderPos);
c = MathHelper.Clamp(c, Math.Abs(upperArmLength - forearmLength), forearmLength + upperArmLength - 0.01f);
float armAngle = MathUtils.VectorToAngle(pos - shoulderPos) + arm.Params.GetSpriteOrientation() - MathHelper.PiOver2;
float upperArmAngle = MathUtils.SolveTriangleSSS(forearmLength, upperArmLength, c) * Dir;
float lowerArmAngle = MathUtils.SolveTriangleSSS(upperArmLength, forearmLength, c) * Dir;
//make sure the arm angle "has the same number of revolutions" as the arm
while (arm.Rotation - armAngle > MathHelper.Pi)
{
armAngle += MathHelper.TwoPi;
}
while (arm.Rotation - armAngle < -MathHelper.Pi)
{
armAngle -= MathHelper.TwoPi;
}
arm?.body.SmoothRotate(armAngle - upperArmAngle, 100.0f * armTorque * arm.Mass, wrapAngle: false);
float forearmAngle = armAngle + lowerArmAngle;
forearm?.body.SmoothRotate(forearmAngle, 100.0f * handTorque * forearm.Mass, wrapAngle: false);
float handAngle = forearm != null ? forearmAngle : armAngle;
hand?.body.SmoothRotate(handAngle, 10.0f * handTorque * hand.Mass, wrapAngle: false);
}
public void ApplyPose(Vector2 leftHandPos, Vector2 rightHandPos, Vector2 leftFootPos, Vector2 rightFootPos, float footMoveForce = 10)
{
var leftHand = GetLimb(LimbType.LeftHand);
var rightHand = GetLimb(LimbType.RightHand);
var waist = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso);
if (waist == null) { return; }
Vector2 midPos = waist.SimPosition;
if (leftHand != null)
{
leftHand.Disabled = true;
leftHandPos.X *= Dir;
leftHandPos += midPos;
HandIK(leftHand, leftHandPos);
}
if (rightHand != null)
{
rightHand.Disabled = true;
rightHandPos.X *= Dir;
rightHandPos += midPos;
HandIK(rightHand, rightHandPos);
}
var leftFoot = GetLimb(LimbType.LeftFoot);
if (leftFoot != null)
{
leftFoot.Disabled = true;
leftFootPos = new Vector2(waist.SimPosition.X + leftFootPos.X * Dir, GetColliderBottom().Y + leftFootPos.Y);
MoveLimb(leftFoot, leftFootPos, Math.Abs(leftFoot.SimPosition.X - leftFootPos.X) * footMoveForce * leftFoot.Mass, true);
}
var rightFoot = GetLimb(LimbType.RightFoot);
if (rightFoot != null)
{
rightFoot.Disabled = true;
rightFootPos = new Vector2(waist.SimPosition.X + rightFootPos.X * Dir, GetColliderBottom().Y + rightFootPos.Y);
MoveLimb(rightFoot, rightFootPos, Math.Abs(rightFoot.SimPosition.X - rightFootPos.X) * footMoveForce * rightFoot.Mass, true);
}
}
public void ApplyTestPose()
{
var waist = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso);
if (waist != null)
{
ApplyPose(
new Vector2(-0.75f, -0.2f),
new Vector2(0.75f, -0.2f),
new Vector2(-WalkParams.StepSize.X * 0.5f, -0.1f * RagdollParams.JointScale),
new Vector2(WalkParams.StepSize.X * 0.5f, -0.1f * RagdollParams.JointScale));
}
}
protected void CalculateArmLengths()
{
//calculate arm and forearm length (atm this assumes that both arms are the same size)
Limb rightForearm = GetLimb(LimbType.RightForearm);
Limb rightHand = GetLimb(LimbType.RightHand);
if (rightHand == null) { return; }
rightShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm) ?? GetJointBetweenLimbs(LimbType.Head, LimbType.RightArm) ?? GetJoint(LimbType.RightArm, new LimbType[] { LimbType.RightHand, LimbType.RightForearm });
leftShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftArm) ?? GetJointBetweenLimbs(LimbType.Head, LimbType.LeftArm) ?? GetJoint(LimbType.LeftArm, new LimbType[] { LimbType.LeftHand, LimbType.LeftForearm });
Vector2 localAnchorShoulder = Vector2.Zero;
Vector2 localAnchorElbow = Vector2.Zero;
if (rightShoulder != null)
{
localAnchorShoulder = rightShoulder.LimbA.type == LimbType.RightArm ? rightShoulder.LocalAnchorA : rightShoulder.LocalAnchorB;
}
LimbJoint rightElbow = rightForearm == null ?
GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightHand) :
GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightForearm);
if (rightElbow != null)
{
localAnchorElbow = rightElbow.LimbA.type == LimbType.RightArm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB;
}
upperArmLength = Vector2.Distance(localAnchorShoulder, localAnchorElbow);
if (rightElbow != null)
{
if (rightForearm == null)
{
forearmLength = Vector2.Distance(
rightHand.PullJointLocalAnchorA,
rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB);
}
else
{
LimbJoint rightWrist = GetJointBetweenLimbs(LimbType.RightForearm, LimbType.RightHand);
if (rightWrist != null)
{
forearmLength = Vector2.Distance(
rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB,
rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB);
forearmLength += Vector2.Distance(
rightHand.PullJointLocalAnchorA,
rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB);
}
}
}
}
protected LimbJoint GetJointBetweenLimbs(LimbType limbTypeA, LimbType limbTypeB)
{
return LimbJoints.FirstOrDefault(lj =>
(lj.LimbA.type == limbTypeA && lj.LimbB.type == limbTypeB) ||
(lj.LimbB.type == limbTypeA && lj.LimbA.type == limbTypeB));
}
protected LimbJoint GetJoint(LimbType matchingType, IEnumerable<LimbType> ignoredTypes)
{
return LimbJoints.FirstOrDefault(lj =>
lj.LimbA.type == matchingType && ignoredTypes.None(t => lj.LimbB.type == t) ||
lj.LimbB.type == matchingType && ignoredTypes.None(t => lj.LimbB.type == t));
}
public override void Recreate(RagdollParams ragdollParams = null)
{
base.Recreate(ragdollParams);
if (Character.Params.CanInteract)
{
CalculateArmLengths();
}
}
}
}
@@ -139,7 +139,7 @@ namespace Barotrauma
if (MainLimb == null) { return; }
var mainLimb = MainLimb;
levitatingCollider = true;
levitatingCollider = !IsHanging;
if (!character.CanMove)
{
@@ -192,6 +192,11 @@ namespace Barotrauma
strongestImpact = 0.0f;
}
if (aiming)
{
TargetMovement = TargetMovement.ClampLength(2);
}
if (inWater && !forceStanding)
{
Collider.FarseerBody.FixedRotation = false;
@@ -202,7 +207,7 @@ namespace Barotrauma
if (CurrentGroundedParams != null)
{
//rotate collider back upright
float standAngle = dir == Direction.Right ? CurrentGroundedParams.ColliderStandAngleInRadians : -CurrentGroundedParams.ColliderStandAngleInRadians;
float standAngle = CurrentGroundedParams.ColliderStandAngleInRadians * Dir;
if (Math.Abs(MathUtils.GetShortestAngle(Collider.Rotation, standAngle)) > 0.001f)
{
Collider.AngularVelocity = MathUtils.GetShortestAngle(Collider.Rotation, standAngle) * 60.0f;
@@ -215,17 +220,19 @@ namespace Barotrauma
}
UpdateWalkAnim(deltaTime);
}
if (character.SelectedCharacter != null)
{
DragCharacter(character.SelectedCharacter, deltaTime);
return;
}
if (character.AnimController.AnimationTestPose)
{
ApplyTestPose();
}
//don't flip when simply physics is enabled
if (SimplePhysicsEnabled) { return; }
if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip))
if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !aiming)
{
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
{
@@ -290,6 +297,10 @@ namespace Barotrauma
{
flipTimer = 0.0f;
}
wasAiming = aiming;
aiming = false;
wasAimingMelee = aimingMelee;
aimingMelee = false;
}
private bool CanDrag(Character target)
@@ -449,6 +460,16 @@ namespace Barotrauma
//limbs are disabled when simple physics is enabled, no need to move them
if (SimplePhysicsEnabled) { return; }
mainLimb.PullJointEnabled = true;
if (aiming && movement.Length() <= 0.1f)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir;
TargetMovement = new Vector2(0.0f, -0.1f);
float newRotation = MathUtils.VectorToAngle(diff);
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
if (!isMoving)
{
WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5);
@@ -146,34 +146,8 @@ namespace Barotrauma
public bool Crouching;
private float upperArmLength = 0.0f, forearmLength = 0.0f;
public float ArmLength => upperArmLength + forearmLength;
public Vector2 RightHandIKPos
{
get;
private set;
}
public Vector2 LeftHandIKPos
{
get;
private set;
}
private LimbJoint rightShoulder, leftShoulder;
private float upperLegLength = 0.0f, lowerLegLength = 0.0f;
private bool aiming;
private bool wasAiming;
private bool aimingMelee;
private bool wasAimingMelee;
public bool IsAiming => wasAiming;
public bool IsAimingMelee => wasAimingMelee;
private readonly float movementLerp;
private float cprAnimTimer;
@@ -184,7 +158,6 @@ namespace Barotrauma
//prevents rapid switches between swimming/walking if the water level is fluctuating around the minimum swimming depth
private float swimmingStateLockTimer;
private float useItemTimer;
public float HeadLeanAmount => CurrentGroundedParams.HeadLeanAmount;
public float TorsoLeanAmount => CurrentGroundedParams.TorsoLeanAmount;
public Vector2 FootMoveOffset => CurrentGroundedParams.FootMoveOffset * RagdollParams.JointScale;
@@ -219,61 +192,12 @@ namespace Barotrauma
movementLerp = RagdollParams.MainElement.GetAttributeFloat("movementlerp", 0.4f);
}
public override void Recreate(RagdollParams ragdollParams)
public override void Recreate(RagdollParams ragdollParams = null)
{
base.Recreate(ragdollParams);
CalculateArmLengths();
CalculateLegLengths();
}
private void CalculateArmLengths()
{
//calculate arm and forearm length (atm this assumes that both arms are the same size)
Limb rightForearm = GetLimb(LimbType.RightForearm);
Limb rightHand = GetLimb(LimbType.RightHand);
if (rightHand == null) { return; }
rightShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm);
leftShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftArm);
Vector2 localAnchorShoulder = Vector2.Zero;
Vector2 localAnchorElbow = Vector2.Zero;
if (rightShoulder != null)
{
localAnchorShoulder = rightShoulder.LimbA.type == LimbType.RightArm ? rightShoulder.LocalAnchorA : rightShoulder.LocalAnchorB;
}
LimbJoint rightElbow = rightForearm == null ?
GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightHand) :
GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightForearm);
if (rightElbow != null)
{
localAnchorElbow = rightElbow.LimbA.type == LimbType.RightArm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB;
}
upperArmLength = Vector2.Distance(localAnchorShoulder, localAnchorElbow);
if (rightElbow != null)
{
if (rightForearm == null)
{
forearmLength = Vector2.Distance(
rightHand.PullJointLocalAnchorA,
rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB);
}
else
{
LimbJoint rightWrist = GetJointBetweenLimbs(LimbType.RightForearm, LimbType.RightHand);
if (rightWrist != null)
{
forearmLength = Vector2.Distance(
rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB,
rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB);
forearmLength += Vector2.Distance(
rightHand.PullJointLocalAnchorA,
rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB);
}
}
}
}
private void CalculateLegLengths()
{
//calculate upper and lower leg length (atm this assumes that both legs are the same size)
@@ -304,21 +228,16 @@ namespace Barotrauma
ankleJoint.LimbA.type == footType ? ankleJoint.LocalAnchorA : ankleJoint.LocalAnchorB,
GetLimb(footType).PullJointLocalAnchorA);
}
private LimbJoint GetJointBetweenLimbs(LimbType limbTypeA, LimbType limbTypeB)
{
return LimbJoints.FirstOrDefault(lj =>
(lj.LimbA.type == limbTypeA && lj.LimbB.type == limbTypeB) ||
(lj.LimbB.type == limbTypeA && lj.LimbA.type == limbTypeB));
}
public override void UpdateAnim(float deltaTime)
{
if (Frozen) return;
if (MainLimb == null) { return; }
levitatingCollider = true;
levitatingCollider = !IsHanging;
ColliderIndex = Crouching && !swimming ? 1 : 0;
if (character.SelectedConstruction?.GetComponent<Controller>()?.ControlCharacterPose ?? false ||
character.SelectedConstruction?.GetComponent<Ladder>() != null ||
(ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined))
{
Crouching = false;
@@ -422,41 +341,25 @@ namespace Barotrauma
midPos += Vector2.Transform(new Vector2(-0.3f * Dir, -0.2f), torsoTransform);
if (rightHand.PullJointEnabled) midPos = (midPos + rightHand.PullJointWorldAnchorB) / 2.0f;
HandIK(rightHand, midPos, CurrentHumanAnimParams.ArmIKStrength, CurrentHumanAnimParams.HandIKStrength);
HandIK(leftHand, midPos, CurrentHumanAnimParams.ArmIKStrength, CurrentHumanAnimParams.HandIKStrength);
HandIK(rightHand, midPos, CurrentAnimationParams.ArmIKStrength, CurrentAnimationParams.HandIKStrength);
HandIK(leftHand, midPos, CurrentAnimationParams.ArmIKStrength, CurrentAnimationParams.HandIKStrength);
}
else if (character.AnimController.AnimationTestPose)
{
var leftHand = GetLimb(LimbType.LeftHand);
var rightHand = GetLimb(LimbType.RightHand);
var waist = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso);
rightHand.Disabled = true;
leftHand.Disabled = true;
Vector2 midPos = waist.SimPosition;
HandIK(rightHand, midPos + new Vector2(-1, -0.2f) * Dir);
HandIK(leftHand, midPos + new Vector2(1, -0.2f) * Dir);
var leftFoot = GetLimb(LimbType.LeftFoot);
var rightFoot = GetLimb(LimbType.RightFoot);
rightFoot.Disabled = true;
leftFoot.Disabled = true;
// The code here is a bit obscure, but it's pretty much copy-pasted from the block that is used for crouching.
for (int i = -1; i < 2; i += 2)
{
Vector2 footPos = GetColliderBottom();
footPos = new Vector2(waist.SimPosition.X + Math.Sign(WalkParams.StepSize.X * i) * Dir * 0.3f, footPos.Y - 0.1f * RagdollParams.JointScale);
var foot = i == -1 ? rightFoot : leftFoot;
MoveLimb(foot, footPos, Math.Abs(foot.SimPosition.X - footPos.X) * 100.0f, true);
}
ApplyTestPose();
}
else
{
if (Anim != Animation.UsingConstruction) ResetPullJoints();
if (Anim != Animation.UsingConstruction)
{
ResetPullJoints();
}
}
if (SimplePhysicsEnabled)
{
UpdateStandingSimple();
IsHanging = false;
return;
}
@@ -520,12 +423,11 @@ namespace Barotrauma
{
limb.Disabled = false;
}
wasAiming = aiming;
aiming = false;
wasAimingMelee = aimingMelee;
aimingMelee = false;
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) return;
IsHanging = false;
}
void UpdateStanding()
@@ -844,16 +746,18 @@ namespace Barotrauma
var arm = GetLimb(armType);
if (arm != null && Math.Abs(arm.body.AngularVelocity) < 10.0f)
{
arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.1f, 0.1f), arm.Mass * 10.0f);
arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f);
}
//get the elbow to a neutral rotation
if (Math.Abs(hand.body.AngularVelocity) < 10.0f)
{
LimbJoint elbow = GetJointBetweenLimbs(armType, hand.type) ?? GetJointBetweenLimbs(armType, foreArmType);
var forearm = GetLimb(foreArmType) ?? hand;
LimbJoint elbow = GetJointBetweenLimbs(armType, foreArmType) ?? GetJointBetweenLimbs(armType, hand.type);
if (elbow != null)
{
hand.body.ApplyTorque(MathHelper.Clamp(-elbow.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 10.0f);
float diff = elbow.JointAngle - (Dir > 0 ? elbow.LowerLimit : elbow.UpperLimit);
forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f);
}
}
}
@@ -1099,6 +1003,7 @@ namespace Barotrauma
rightHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, rightHandPos.X) : Math.Min(-0.3f, rightHandPos.X);
rightHandPos = Vector2.Transform(rightHandPos, rotationMatrix);
float speedMultiplier = character.SpeedMultiplier * (1 - Character.GetRightHandPenalty());
// Limb hand, Vector2 pos, float force = 1.0f
HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.ArmMoveStrength * speedMultiplier, CurrentSwimParams.HandMoveStrength * speedMultiplier);
}
@@ -1390,6 +1295,8 @@ namespace Barotrauma
{
target.Oxygen += deltaTime * 0.5f; //Stabilize them
}
bool powerfulCPR = character.HasAbilityFlag(AbilityFlags.PowerfulCPR);
int skill = (int)character.GetSkillLevel("medical");
//pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17)
@@ -1406,13 +1313,19 @@ namespace Barotrauma
{
if (target.Oxygen < -10.0f)
{
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.StabilizationMin, CPRSettings.StabilizationMax);
character.Oxygen -= (1.0f / stabilizationAmount) * deltaTime; //Worse skill = more oxygen required
if (character.Oxygen > 0.0f) target.Oxygen += stabilizationAmount * deltaTime; //we didn't suffocate yet did we
//DebugConsole.NewMessage("CPR Us: " + character.Oxygen + " Them: " + target.Oxygen + " How good we are: restore " + cpr + " use " + (30.0f - cpr), Color.Aqua);
if (powerfulCPR)
{
//prevent the patient from suffocating no matter how fast their oxygen level is dropping
target.Oxygen = Math.Max(target.Oxygen, -10.0f);
}
else
{
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.StabilizationMin, CPRSettings.StabilizationMax);
character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required
if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we
}
}
}
}
@@ -1447,6 +1360,8 @@ namespace Barotrauma
reviveChance = (float)Math.Pow(reviveChance, CPRSettings.ReviveChanceExponent);
reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.ReviveChanceMin, CPRSettings.ReviveChanceMax);
if (powerfulCPR) { reviveChance *= 2.0f; }
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) <= reviveChance)
{
//increase oxygen and clamp it above zero
@@ -1706,248 +1621,6 @@ namespace Barotrauma
}
}
public void Grab(Vector2 rightHandPos, Vector2 leftHandPos)
{
for (int i = 0; i < 2; i++)
{
Limb pullLimb = (i == 0) ? GetLimb(LimbType.LeftHand) : GetLimb(LimbType.RightHand);
pullLimb.Disabled = true;
pullLimb.PullJointEnabled = true;
pullLimb.PullJointWorldAnchorB = (i == 0) ? rightHandPos : leftHandPos;
pullLimb.PullJointMaxForce = 500.0f;
}
}
//TODO: refactor this method, it's way too convoluted
public override void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimingMelee = false)
{
if (character.Stun > 0.0f || character.IsIncapacitated)
{
aim = false;
}
//calculate the handle positions
Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation);
// TODO: don't create new arrays, reuse
Vector2[] transformedHandlePos = new Vector2[2];
transformedHandlePos[0] = Vector2.Transform(handlePos[0], itemTransfrom);
transformedHandlePos[1] = Vector2.Transform(handlePos[1], itemTransfrom);
Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso);
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
// TODO: Remove this. Provide the position in params.
Vector2 itemPos = aim ? aimPos : holdPos;
var controller = character.SelectedConstruction?.GetComponent<Controller>();
bool usingController = controller != null && !controller.AllowAiming;
bool isClimbing = character.IsClimbing && Math.Abs(character.AnimController.TargetMovement.Y) > 0.01f;
float itemAngle;
Holdable holdable = item.GetComponent<Holdable>();
this.aimingMelee = aimingMelee;
if (!isClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero && !character.IsIncapacitated)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition);
Vector2 diff = holdable.Aimable ? (mousePos - AimSourceSimPos) * Dir : Vector2.UnitX;
holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torso.body.Rotation * Dir;
holdAngle += GetAimWobble(rightHand, leftHand, item);
itemAngle = torso.body.Rotation + holdAngle * Dir;
if (holdable.ControlPose)
{
head?.body.SmoothRotate(itemAngle);
if (TargetMovement == Vector2.Zero && inWater)
{
torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f;
torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f);
}
aiming = true;
}
}
else
{
itemAngle = torso.body.Rotation + holdAngle * Dir;
}
Vector2 transformedHoldPos = rightShoulder.WorldAnchorA;
if (itemPos == Vector2.Zero || isClimbing || usingController)
{
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item)
{
if (rightHand == null || rightHand.IsSevered) { return; }
transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0];
itemAngle = (rightHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir);
}
else if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item)
{
if (leftHand == null || leftHand.IsSevered) { return; }
transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1];
itemAngle = (leftHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir);
}
}
else
{
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item)
{
if (rightHand == null || rightHand.IsSevered) { return; }
transformedHoldPos = rightShoulder.WorldAnchorA;
rightHand.Disabled = true;
}
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item)
{
if (leftHand == null || leftHand.IsSevered) { return; }
transformedHoldPos = leftShoulder.WorldAnchorA;
leftHand.Disabled = true;
}
itemPos.X *= Dir;
transformedHoldPos += Vector2.Transform(itemPos, Matrix.CreateRotationZ(itemAngle));
}
item.body.ResetDynamics();
Vector2 currItemPos = (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item) ?
rightHand.PullJointWorldAnchorA - transformedHandlePos[0] :
leftHand.PullJointWorldAnchorA - transformedHandlePos[1];
if (!MathUtils.IsValid(currItemPos))
{
string errorMsg = "Attempted to move the item \"" + item + "\" to an invalid position in HumanidAnimController.HoldItem: " +
currItemPos + ", rightHandPos: " + rightHand.PullJointWorldAnchorA + ", leftHandPos: " + leftHand.PullJointWorldAnchorA +
", handlePos[0]: " + handlePos[0] + ", handlePos[1]: " + handlePos[1] +
", transformedHandlePos[0]: " + transformedHandlePos[0] + ", transformedHandlePos[1]:" + transformedHandlePos[1] +
", item pos: " + item.SimPosition + ", itemAngle: " + itemAngle +
", collider pos: " + character.SimPosition;
DebugConsole.Log(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"HumanoidAnimController.HoldItem:InvalidPos:" + character.Name + item.Name,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg);
return;
}
if (holdable.Pusher != null)
{
if (character.Stun > 0.0f || character.IsIncapacitated)
{
holdable.Pusher.Enabled = false;
}
else
{
if (!holdable.Pusher.Enabled)
{
holdable.Pusher.Enabled = true;
holdable.Pusher.ResetDynamics();
holdable.Pusher.SetTransform(currItemPos, itemAngle);
}
else
{
holdable.Pusher.TargetPosition = currItemPos;
holdable.Pusher.TargetRotation = holdAngle * Dir;
holdable.Pusher.MoveToTargetPosition(true);
currItemPos = holdable.Pusher.SimPosition;
itemAngle = holdable.Pusher.Rotation;
}
}
}
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false);
if (!isClimbing && !character.IsIncapacitated && itemPos != Vector2.Zero)
{
for (int i = 0; i < 2; i++)
{
if (!character.Inventory.IsInLimbSlot(item, i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand)) { continue; }
HandIK(i == 0 ? rightHand : leftHand, transformedHoldPos + transformedHandlePos[i], CurrentHumanAnimParams.ArmIKStrength, CurrentHumanAnimParams.HandIKStrength);
}
}
}
private float GetAimWobble(Limb rightHand, Limb leftHand, Item heldItem)
{
float wobbleStrength = 0.0f;
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == heldItem)
{
wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: "damage");
}
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == heldItem)
{
wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: "damage");
}
if (wobbleStrength <= 0.1f) { return 0.0f; }
wobbleStrength = (float)Math.Min(wobbleStrength, 1.0f);
float lowFreqNoise = PerlinNoise.GetPerlin((float)Timing.TotalTime / 320.0f, (float)Timing.TotalTime / 240.0f) - 0.5f;
float highFreqNoise = PerlinNoise.GetPerlin((float)Timing.TotalTime / 40.0f, (float)Timing.TotalTime / 50.0f) - 0.5f;
return (lowFreqNoise * 1.0f + highFreqNoise * 0.1f) * wobbleStrength;
}
private void HandIK(Limb hand, Vector2 pos, float armTorque = 1.0f, float handTorque = 1.0f)
{
Vector2 shoulderPos;
Limb arm, forearm;
if (hand.type == LimbType.LeftHand)
{
if (leftShoulder == null) { return; }
shoulderPos = leftShoulder.WorldAnchorA;
arm = GetLimb(LimbType.LeftArm);
forearm = GetLimb(LimbType.LeftForearm);
LeftHandIKPos = pos;
}
else
{
if (rightShoulder == null) { return; }
shoulderPos = rightShoulder.WorldAnchorA;
arm = GetLimb(LimbType.RightArm);
forearm = GetLimb(LimbType.RightForearm);
RightHandIKPos = pos;
}
if (arm == null) { return; }
//distance from shoulder to holdpos
float c = Vector2.Distance(pos, shoulderPos);
c = MathHelper.Clamp(c, Math.Abs(upperArmLength - forearmLength), forearmLength + upperArmLength - 0.01f);
float armAngle = MathUtils.VectorToAngle(pos - shoulderPos) + MathHelper.PiOver2;
float upperArmAngle = MathUtils.SolveTriangleSSS(forearmLength, upperArmLength, c) * Dir;
float lowerArmAngle = MathUtils.SolveTriangleSSS(upperArmLength, forearmLength, c) * Dir;
//make sure the arm angle "has the same number of revolutions" as the arm
while (arm.Rotation - armAngle > MathHelper.Pi)
{
armAngle += MathHelper.TwoPi;
}
while (arm.Rotation - armAngle < -MathHelper.Pi)
{
armAngle -= MathHelper.TwoPi;
}
arm?.body.SmoothRotate(armAngle - upperArmAngle, 100.0f * armTorque * arm.Mass, wrapAngle: false);
float forearmAngle = armAngle + lowerArmAngle;
forearm?.body.SmoothRotate(forearmAngle, 100.0f * handTorque * forearm.Mass, wrapAngle: false);
float handAngle = forearm != null ? armAngle : forearmAngle;
hand?.body.SmoothRotate(handAngle, 100.0f * handTorque * hand.Mass, wrapAngle: false);
}
private void FootIK(Limb foot, Vector2 pos, float legTorque, float footTorque, float footAngle)
{
if (!MathUtils.IsValid(pos))
@@ -2014,47 +1687,6 @@ namespace Barotrauma
foot.body.SmoothRotate((legAngle - (lowerLegAngle + footAngle) * Dir), foot.Mass * footTorque, wrapAngle: false);
}
public override void UpdateUseItem(bool allowMovement, Vector2 handWorldPos)
{
useItemTimer = 0.5f;
Anim = Animation.UsingConstruction;
if (!allowMovement)
{
TargetMovement = Vector2.Zero;
TargetDir = handWorldPos.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos);
if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2))
{
TargetMovement = Vector2.Normalize(handWorldPos - character.WorldPosition) * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1);
}
}
if (!character.Enabled) { return; }
Vector2 handSimPos = ConvertUnits.ToSimUnits(handWorldPos);
if (character.Submarine != null)
{
handSimPos -= character.Submarine.SimPosition;
}
var leftHand = GetLimb(LimbType.LeftHand);
if (leftHand != null)
{
leftHand.Disabled = true;
leftHand.PullJointEnabled = true;
leftHand.PullJointWorldAnchorB = handSimPos;
}
var rightHand = GetLimb(LimbType.RightHand);
if (rightHand != null)
{
rightHand.Disabled = true;
rightHand.PullJointEnabled = true;
rightHand.PullJointWorldAnchorB = handSimPos;
}
}
public override void Flip()
{
base.Flip();
@@ -2073,7 +1705,8 @@ namespace Barotrauma
{
heldItem.FlipX(relativeToSub: false);
}
heldItem.FlipX(relativeToSub: false);
// TODO: was this added by a mistake?
//heldItem.FlipX(relativeToSub: false);
}
foreach (Limb limb in Limbs)
@@ -125,7 +125,8 @@ namespace Barotrauma
protected float surfaceY;
protected bool inWater, headInWater;
public bool onGround;
protected bool onGround;
public bool OnGround => onGround;
private Vector2 lastFloorCheckPos;
private bool lastFloorCheckIgnoreStairs, lastFloorCheckIgnorePlatforms;
@@ -887,7 +888,7 @@ namespace Barotrauma
/// <param name="pullFromCenter">if false, force is applied to the position of pullJoint</param>
protected void MoveLimb(Limb limb, Vector2 pos, float amount, bool pullFromCenter = false)
public void MoveLimb(Limb limb, Vector2 pos, float amount, bool pullFromCenter = false)
{
limb.MoveToPos(pos, amount, pullFromCenter);
}
@@ -974,8 +975,7 @@ namespace Barotrauma
Vector2 newSubPos = newHull.Submarine == null ? Vector2.Zero : newHull.Submarine.Position;
Vector2 prevSubPos = currentHull.Submarine == null ? Vector2.Zero : currentHull.Submarine.Position;
Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos),
Vector2.Zero);
Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero);
}
}
@@ -1099,6 +1099,7 @@ namespace Barotrauma
}
public bool forceStanding;
public bool forceNotStanding;
public void Update(float deltaTime, Camera cam)
{
@@ -1270,6 +1271,7 @@ namespace Barotrauma
}
}
UpdateProjSpecific(deltaTime, cam);
forceNotStanding = false;
}
private void CheckBodyInRest(float deltaTime)
@@ -1569,7 +1571,7 @@ namespace Barotrauma
return closestFraction;
}, rayStart, rayEnd, Physics.CollisionStairs | Physics.CollisionPlatform | Physics.CollisionWall | Physics.CollisionLevel);
if (standOnFloorFixture != null)
if (standOnFloorFixture != null && !IsHanging)
{
standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction;
if (rayStart.Y - standOnFloorY < Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f)
@@ -1606,6 +1608,13 @@ namespace Barotrauma
}
if (MainLimb == null) { return; }
if (Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached)
{
enemyAI.LatchOntoAI.DeattachFromBody(reset: true);
}
Character.Latchers.ForEachMod(l => l.DeattachFromBody(reset: true));
Character.Latchers.Clear();
Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition;
if (lerp)
{
@@ -1629,6 +1638,16 @@ namespace Barotrauma
}
}
public bool IsHanging { get; protected set; }
public void Hang()
{
ResetPullJoints();
onGround = false;
levitatingCollider = false;
IsHanging = true;
}
protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true)
{
Vector2 movePos = simPosition;
@@ -129,6 +129,8 @@ namespace Barotrauma
}
}
public readonly HashSet<LatchOntoAI> Latchers = new HashSet<LatchOntoAI>();
protected readonly Dictionary<string, ActiveTeamChange> activeTeamChanges = new Dictionary<string, ActiveTeamChange>();
protected ActiveTeamChange currentTeamChange;
const string OriginalTeamIdentifier = "original";
@@ -264,6 +266,7 @@ namespace Barotrauma
public readonly CharacterParams Params;
public string SpeciesName => Params.SpeciesName;
public string Group => Params.Group;
public bool IsHumanoid => Params.Humanoid;
public bool IsHusk => Params.Husk;
@@ -473,8 +476,7 @@ namespace Barotrauma
return true;
}
}
public bool CanInteract => AllowInput && IsHumanoid && !LockHands;
public bool CanInteract => AllowInput && Params.CanInteract && !LockHands;
// Eating is not implemented for humanoids. If we implement that at some point, we could remove this restriction.
public bool CanEat => !IsHumanoid && Params.CanEat && AllowInput && AnimController.GetLimb(LimbType.Head) != null;
@@ -592,6 +594,7 @@ namespace Barotrauma
get
{
if (IsUnconscious) { return true; }
if (IsDead) { return true; }
return CharacterHealth.Afflictions.Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
}
}
@@ -626,7 +629,7 @@ namespace Barotrauma
public float Stun
{
get { return IsRagdolled ? 1.0f : CharacterHealth.Stun; }
get { return IsRagdolled && !AnimController.IsHanging ? 1.0f : CharacterHealth.Stun; }
set
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
@@ -1172,6 +1175,7 @@ namespace Barotrauma
{
LoadHeadAttachments();
}
ApplyStatusEffects(ActionType.OnSpawn, 1.0f);
}
partial void InitProjSpecific(XElement mainElement);
@@ -1669,7 +1673,7 @@ namespace Barotrauma
}
if (!aiControlled &&
AnimController.onGround &&
AnimController.OnGround &&
!AnimController.InWater &&
AnimController.Anim != AnimController.Animation.UsingConstruction &&
AnimController.Anim != AnimController.Animation.CPR &&
@@ -2697,6 +2701,11 @@ namespace Barotrauma
ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
if (aiTarget != null)
{
aiTarget.InDetectable = false;
}
UpdateControlled(deltaTime, cam);
//Health effects
@@ -2720,7 +2729,7 @@ namespace Barotrauma
//Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us!
bool allowRagdoll = GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true;
bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 2.5f * 2.5f;
bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 8.0f * 8.0f;
bool wasRagdolled = false;
bool selfRagdolled = false;
@@ -2838,7 +2847,7 @@ namespace Barotrauma
// If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage)
reduction *= 0.5f;
}
enemy.Damage = Math.Max(0.0f, enemy.Damage-reduction);
enemy.Damage = Math.Max(0.0f, enemy.Damage - reduction);
}
}
}
@@ -3514,7 +3523,7 @@ namespace Barotrauma
if (Removed) { return new AttackResult(); }
if (attacker != null && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
{
if (attacker.TeamID == TeamID) { return new AttackResult(); }
}
@@ -3676,6 +3685,7 @@ namespace Barotrauma
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
if (Screen.Selected != GameMain.GameScreen) { return; }
if (newStun > 0 && Params.Health.StunImmunity) { return; }
if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; }
if (Math.Sign(newStun) != Math.Sign(Stun))
{
@@ -3876,9 +3886,12 @@ namespace Barotrauma
AnimController.movement = Vector2.Zero;
AnimController.TargetMovement = Vector2.Zero;
foreach (Item heldItem in HeldItems.ToList())
if (!LockHands)
{
heldItem.Drop(this);
foreach (Item heldItem in HeldItems.ToList())
{
heldItem.Drop(this);
}
}
SelectedConstruction = null;
@@ -4435,6 +4448,11 @@ namespace Barotrauma
/// </summary>
private readonly Dictionary<StatTypes, float> statValues = new Dictionary<StatTypes, float>();
/// <summary>
/// A dictionary with temporary values, updated when the character equips/unequips wearables. Used to reduce unnecessary inventory checking.
/// </summary>
private readonly Dictionary<StatTypes, float> wearableStatValues = new Dictionary<StatTypes, float>();
public float GetStatValue(StatTypes statType)
{
if (!IsHuman) { return 0f; }
@@ -4453,23 +4471,37 @@ namespace Barotrauma
// could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change
statValue += Info.GetSavedStatValue(statType);
}
//replace by updating the character wearable stat values when equipping or unequipping wearables
for (int i = 0; i < Inventory.Capacity; i++)
if (wearableStatValues.TryGetValue(statType, out float wearableValue))
{
if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.SlotTypes[i] != InvSlotType.LeftHand && Inventory.SlotTypes[i] != InvSlotType.RightHand
&& Inventory.GetItemAt(i)?.GetComponent<Wearable>() is Wearable wearable)
{
if (wearable.WearableStatValues.TryGetValue(statType, out float wearableValue))
{
statValue += wearableValue;
}
}
statValue += wearableValue;
}
return statValue;
}
public void OnWearablesChanged()
{
wearableStatValues.Clear();
for (int i = 0; i < Inventory.Capacity; i++)
{
if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.SlotTypes[i] != InvSlotType.LeftHand && Inventory.SlotTypes[i] != InvSlotType.RightHand
&& Inventory.GetItemAt(i)?.GetComponent<Wearable>() is Wearable wearable)
{
foreach (var statValuePair in wearable.WearableStatValues)
{
if (wearableStatValues.ContainsKey(statValuePair.Key))
{
wearableStatValues[statValuePair.Key] += statValuePair.Value;
}
else
{
wearableStatValues.Add(statValuePair.Key, statValuePair.Value);
}
}
}
}
}
public void ChangeStat(StatTypes statType, float value)
{
if (statValues.ContainsKey(statType))
@@ -4516,7 +4548,7 @@ namespace Barotrauma
public bool HasAbilityFlag(AbilityFlags abilityFlag)
{
return abilityFlags.Contains(abilityFlag);
return abilityFlags.Contains(abilityFlag) || CharacterHealth.HasFlag(abilityFlag);
}
private readonly Dictionary<string, float> abilityResistances = new Dictionary<string, float>();
@@ -218,30 +218,30 @@ namespace Barotrauma
public int AdditionalTalentPoints { get; set; }
private Sprite headSprite;
private Sprite _headSprite;
public Sprite HeadSprite
{
get
{
if (headSprite == null)
if (_headSprite == null)
{
LoadHeadSprite();
}
#if CLIENT
if (headSprite != null)
if (_headSprite != null)
{
CalculateHeadPosition(headSprite);
CalculateHeadPosition(_headSprite);
}
#endif
return headSprite;
return _headSprite;
}
private set
{
if (headSprite != null)
if (_headSprite != null)
{
headSprite.Remove();
_headSprite.Remove();
}
headSprite = value;
_headSprite = value;
}
}
@@ -287,6 +287,7 @@ namespace Barotrauma
Character.CharacterHealth.ApplyAffliction(Character.AnimController.GetLimb(LimbType.Head), AfflictionPrefab.List.FirstOrDefault(a => a.Identifier.Equals("disguised", StringComparison.OrdinalIgnoreCase)).Instantiate(100f));
}
idCard ??= Character.Inventory?.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent<IdCard>();
if (idCard != null)
{
#if CLIENT
@@ -294,19 +295,6 @@ namespace Barotrauma
#endif
return;
}
if (Character.Inventory != null)
{
idCard = Character.Inventory.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent<IdCard>();
if (idCard != null)
{
#if CLIENT
GetDisguisedSprites(idCard);
#endif
return;
}
}
}
#if CLIENT
@@ -1085,7 +1073,7 @@ namespace Barotrauma
}
}
private static List<XElement> AddEmpty(IEnumerable<XElement> elements, WearableType type, float commonness = 1)
public static List<XElement> AddEmpty(IEnumerable<XElement> elements, WearableType type, float commonness = 1)
{
// Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example.
var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness));
@@ -1094,9 +1082,9 @@ namespace Barotrauma
return list;
}
private XElement GetRandomElement(IEnumerable<XElement> elements)
public XElement GetRandomElement(IEnumerable<XElement> elements)
{
var filtered = elements.Where(e => IsWearableAllowed(e));
var filtered = elements.Where(IsWearableAllowed);
if (filtered.Count() == 0) { return null; }
var element = ToolBox.SelectWeightedRandom(filtered.ToList(), GetWeights(filtered).ToList(), Rand.RandSync.Unsynced);
return element == null || element.Name == "Empty" ? null : element;
@@ -1121,7 +1109,7 @@ namespace Barotrauma
return true;
}
private static bool IsValidIndex(int index, List<XElement> list) => index >= 0 && index < list.Count;
public static bool IsValidIndex(int index, List<XElement> list) => index >= 0 && index < list.Count;
private static IEnumerable<float> GetWeights(IEnumerable<XElement> elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f));
@@ -1219,8 +1207,8 @@ namespace Barotrauma
OnExperienceChanged(prevAmount, ExperiencePoints, Character.Position + Vector2.UnitY * 150.0f);
}
const int BaseExperienceRequired = 150;
const int AddedExperienceRequiredPerLevel = 350;
const int BaseExperienceRequired = 50;
const int AddedExperienceRequiredPerLevel = 450;
public int GetTotalTalentPoints()
{
@@ -191,6 +191,30 @@ namespace Barotrauma
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public Color GetFaceTint()
{
if (Strength < Prefab.ActivationThreshold) { return Color.TransparentBlack; }
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return Color.TransparentBlack; }
return Color.Lerp(
currentEffect.MinFaceTint,
currentEffect.MaxFaceTint,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public Color GetBodyTint()
{
if (Strength < Prefab.ActivationThreshold) { return Color.TransparentBlack; }
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return Color.TransparentBlack; }
return Color.Lerp(
currentEffect.MinBodyTint,
currentEffect.MaxBodyTint,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public float GetScreenBlurStrength()
{
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
@@ -277,6 +301,13 @@ namespace Barotrauma
return 0.0f;
}
public bool HasFlag(AbilityFlags flagType)
{
if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; }
return currentEffect.AfflictionAbilityFlags.Contains(flagType);
}
private AfflictionPrefab.Effect GetViableEffect()
{
if (Strength < Prefab.ActivationThreshold) { return null; }
@@ -3,6 +3,7 @@ using System.Linq;
using System.Xml.Linq;
using System;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -216,6 +217,11 @@ namespace Barotrauma
XElement parentElement = new XElement("CharacterInfo");
XElement infoElement = character.Info?.Save(parentElement);
CharacterInfo huskCharacterInfo = infoElement == null ? null : new CharacterInfo(infoElement);
var bodyTint = GetBodyTint();
huskCharacterInfo.SkinColor =
Color.Lerp(huskCharacterInfo.SkinColor, bodyTint.Opaque(), bodyTint.A / 255.0f);
var husk = Character.Create(huskedSpeciesName, character.WorldPosition, ToolBox.RandomSeed(8), huskCharacterInfo, isRemotePlayer: false, hasAi: true);
if (husk.Info != null)
{
@@ -1,11 +1,10 @@
using Microsoft.Xna.Framework;
using Barotrauma.Abilities;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using System.Linq;
using System.Security.Cryptography;
using Barotrauma.Abilities;
namespace Barotrauma
{
@@ -223,7 +222,20 @@ namespace Barotrauma
[Serialize("", false)]
public string DialogFlag { get; private set; }
[Serialize("0,0,0,0", false)]
public Color MinFaceTint { get; private set; }
[Serialize("0,0,0,0", false)]
public Color MaxFaceTint { get; private set; }
[Serialize("0,0,0,0", false)]
public Color MinBodyTint { get; private set; }
[Serialize("0,0,0,0", false)]
public Color MaxBodyTint { get; private set; }
public readonly Dictionary<StatTypes, (float minValue, float maxValue)> AfflictionStatValues = new Dictionary<StatTypes, (float minValue, float maxValue)>();
public readonly HashSet<AbilityFlags> AfflictionAbilityFlags = new HashSet<AbilityFlags>();
//statuseffects applied on the character when the affliction is active
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
@@ -250,6 +262,10 @@ namespace Barotrauma
AfflictionStatValues.TryAdd(statType, (minValue, maxValue));
break;
case "abilityflag":
var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName);
AfflictionAbilityFlags.Add(flagType);
break;
}
}
}
@@ -133,7 +133,7 @@ namespace Barotrauma
public bool IsUnconscious
{
get { return Vitality <= 0.0f || Character.IsDead; }
get { return (Vitality <= 0.0f || Character.IsDead) && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious); }
}
public float PressureKillDelay { get; private set; } = 5.0f;
@@ -169,6 +169,20 @@ namespace Barotrauma
}
}
public Color DefaultFaceTint = Color.TransparentBlack;
public Color FaceTint
{
get;
private set;
}
public Color BodyTint
{
get;
private set;
}
public float OxygenAmount
{
get
@@ -409,7 +423,7 @@ namespace Barotrauma
return strength;
}
public void ApplyAffliction(Limb targetLimb, Affliction affliction)
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true)
{
if (!affliction.Prefab.IsBuff && Unkillable || Character.GodMode) { return; }
if (affliction.Prefab.LimbSpecific)
@@ -419,17 +433,17 @@ namespace Barotrauma
//if a limb-specific affliction is applied to no specific limb, apply to all limbs
foreach (LimbHealth limbHealth in limbHealths)
{
AddLimbAffliction(limbHealth, affliction);
AddLimbAffliction(limbHealth, affliction, allowStacking: allowStacking);
}
}
else
{
AddLimbAffliction(targetLimb, affliction);
AddLimbAffliction(targetLimb, affliction, allowStacking: allowStacking);
}
}
else
{
AddAffliction(affliction);
AddAffliction(affliction, allowStacking: allowStacking);
}
}
@@ -453,6 +467,15 @@ namespace Barotrauma
return value;
}
public bool HasFlag(AbilityFlags flagType)
{
for (int i = 0; i < afflictions.Count; i++)
{
if (afflictions[i].HasFlag(flagType)) { return true; }
}
return false;
}
private readonly List<Affliction> matchingAfflictions = new List<Affliction>();
public void ReduceAffliction(Limb targetLimb, string affliction, float amount, ActionType? treatmentAction = null)
{
@@ -671,6 +694,7 @@ namespace Barotrauma
private void AddAffliction(Affliction newAffliction, bool allowStacking = true)
{
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab)
{
@@ -725,6 +749,8 @@ namespace Barotrauma
StunTimer = Stun > 0 ? StunTimer + deltaTime : 0;
FaceTint = DefaultFaceTint;
for (int i = 0; i < limbHealths.Count; i++)
{
for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--)
@@ -749,10 +775,14 @@ namespace Barotrauma
{
UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime);
}
Color faceTint = affliction.GetFaceTint();
if (faceTint.A > FaceTint.A) { FaceTint = faceTint; }
Color bodyTint = affliction.GetBodyTint();
if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; }
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
}
for (int i = afflictions.Count - 1; i >= 0; i--)
{
var affliction = afflictions[i];
@@ -768,6 +798,10 @@ namespace Barotrauma
var affliction = afflictions[i];
affliction.Update(this, null, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
Color faceTint = affliction.GetFaceTint();
if (faceTint.A > FaceTint.A) { FaceTint = faceTint; }
Color bodyTint = affliction.GetBodyTint();
if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; }
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
@@ -543,6 +543,8 @@ namespace Barotrauma
get
{
if (character.IsHumanoid) { return false; }
// TODO: We might need this or solve the cases where a limb is severed while holding on to an item
//if (character.Params.CanInteract) { return false; }
if (this == character.AnimController.MainLimb) { return false; }
if (character.AnimController.CanWalk)
{
@@ -129,6 +129,12 @@ namespace Barotrauma
[Serialize(AnimationType.NotDefined, true), Editable]
public virtual AnimationType AnimationType { get; protected set; }
[Serialize(1f, true, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float ArmIKStrength { get; set; }
[Serialize(1f, true, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandIKStrength { get; set; }
public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType}";
public static string GetDefaultFile(string speciesName, AnimationType animType) => Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml");
@@ -94,12 +94,6 @@ namespace Barotrauma
[Serialize(1f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandMoveStrength { get; set; }
[Serialize(1f, true, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float ArmIKStrength { get; set; }
[Serialize(1f, true, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandIKStrength { get; set; }
}
abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation
@@ -153,12 +147,6 @@ namespace Barotrauma
[Serialize(1f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandMoveStrength { get; set; }
[Serialize(1f, true, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float ArmIKStrength { get; set; }
[Serialize(1f, true, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandIKStrength { get; set; }
}
public interface IHumanAnimation
@@ -169,9 +157,5 @@ namespace Barotrauma
float ArmMoveStrength { get; set; }
float HandMoveStrength { get; set; }
float ArmIKStrength { get; set; }
float HandIKStrength { get; set; }
}
}
@@ -34,6 +34,9 @@ namespace Barotrauma
[Serialize(false, true), Editable(ReadOnly = true)]
public bool HasInfo { get; private set; }
[Serialize(false, true, description: "Can the creature interact with items?"), Editable]
public bool CanInteract { get; private set; }
[Serialize(false, true), Editable]
public bool Husk { get; private set; }
@@ -454,6 +457,9 @@ namespace Barotrauma
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HealthRegenerationWhenEating { get; private set; }
[Serialize(false, true), Editable]
public bool StunImmunity { get; set; }
// TODO: limbhealths, sprite?
public HealthParams(XElement element, CharacterParams character) : base(element, character) { }
@@ -553,15 +559,24 @@ namespace Barotrauma
[Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable]
public bool RandomAttack { get; private set; }
[Serialize(false, true, description:"Does the creature know how to open doors (still requires a proper ID card). Only applies on humanoids. Humans can always open doors (They don't use this AI definition)."), Editable]
[Serialize(false, true, 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]
public bool CanOpenDoors { get; private set; }
[Serialize(false, true, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable]
public bool KeepDoorsClosed { get; private set; }
[Serialize(true, true, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable]
public bool AvoidAbyss { get; set; }
[Serialize(false, true, "Does the creature try to keep in the abyss? Has effect only when AvoidAbyss is false."), Editable]
public bool StayInAbyss { get; set; }
[Serialize(false, true, "Does the creature patrol the flooded hulls while idling inside a friendly submarine?"), Editable]
public bool PatrolFlooded { get; set; }
[Serialize(false, true, "Does the creature patrol the dry hulls while idling inside a friendly submarine?"), Editable]
public bool PatrolDry { get; set; }
[Serialize(0f, true, description: ""), Editable]
public float StartAggression { get; private set; }
@@ -682,8 +697,14 @@ namespace Barotrauma
[Serialize(false, true), Editable]
public bool IgnoreIncapacitated { get; set; }
[Serialize(0f, true, description: "How much damage the protected target should take from an attacker before the creature starts defending it."), Editable]
public float DamageThreshold { get; private set; }
[Serialize(0f, true, description: "A generic threshold. For example, how much damage the protected target should take from an attacker before the creature starts defending it."), Editable]
public float Threshold { get; private set; }
[Serialize(-1f, true, description: "A generic min threshold. Not used if set to negative."), Editable]
public float ThresholdMin { get; private set; }
[Serialize(-1f, true, description: "A generic max threshold. Not used if set to negative."), Editable]
public float ThresholdMax { get; private set; }
[Serialize(AttackPattern.Straight, true), Editable]
public AttackPattern AttackPattern { get; set; }
@@ -37,7 +37,7 @@ namespace Barotrauma
[Serialize("1.0,1.0,1.0,1.0", true), Editable()]
public Color Color { get; set; }
[Serialize(0.0f, true, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'. Used mainly for animations and widgets."), Editable(-360, 360)]
[Serialize(0.0f, true, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)]
public float SpritesheetOrientation { get; set; }
public bool IsSpritesheetOrientationHorizontal
@@ -572,7 +572,9 @@ namespace Barotrauma
/// <summary>
/// The orientation of the sprite as drawn on the sprite sheet (in radians).
/// </summary>
public float GetSpriteOrientation() => MathHelper.ToRadians(float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation);
public float GetSpriteOrientation() => MathHelper.ToRadians(GetSpriteOrientationInDegrees());
public float GetSpriteOrientationInDegrees() => float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation;
[Serialize("", true), Editable]
public string Notes { get; set; }
@@ -592,7 +594,7 @@ namespace Barotrauma
[Serialize(false, true, description: "Disable drawing for this limb."), Editable()]
public bool Hide { get; set; }
[Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings. Used mainly for animations and widgets."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)]
[Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)]
public float SpriteOrientation { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
@@ -23,7 +23,7 @@ namespace Barotrauma.Abilities
if (afflictions.Any())
{
if (!afflictions.Any(a => attackResult.Afflictions.Select(c => c.Identifier).Contains(a))) { return false; }
if (attackResult.Afflictions == null || !afflictions.Any(a => attackResult.Afflictions.Select(c => c.Identifier).Contains(a))) { return false; }
}
return true;
@@ -2,10 +2,10 @@
namespace Barotrauma.Abilities
{
class AbilityConditionScavenger : AbilityConditionData
class AbilityConditionItemOutsideSubmarine : AbilityConditionData
{
public AbilityConditionScavenger(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
public AbilityConditionItemOutsideSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
@@ -2,10 +2,10 @@
namespace Barotrauma.Abilities
{
class AbilityConditionScrounger : AbilityConditionData
class AbilityConditionItemWreck : AbilityConditionData
{
public AbilityConditionScrounger(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
public AbilityConditionItemWreck(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
@@ -5,19 +5,25 @@ namespace Barotrauma.Abilities
{
class AbilityConditionHasPermanentStat : AbilityConditionDataless
{
private readonly string statIdentifier;
private readonly StatTypes statType;
private readonly float min;
public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
statType = CharacterAbilityGroup.ParseStatType(conditionElement.GetAttributeString("stattype", ""), characterTalent.DebugIdentifier);
statIdentifier = conditionElement.GetAttributeString("statidentifier", string.Empty);
if (string.IsNullOrEmpty(statIdentifier))
{
DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!");
}
string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier);
min = conditionElement.GetAttributeFloat("min", 0f);
}
protected override bool MatchesConditionSpecific()
{
// should consider decoupling this from stat values entirely
return character.Info.GetSavedStatValue(statType) >= min;
return character.Info.GetSavedStatValue(statType, statIdentifier) >= min;
}
}
}
@@ -0,0 +1,24 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionHasSkill : AbilityConditionDataless
{
private readonly string skillIdentifier;
private readonly float minValue;
public AbilityConditionHasSkill(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
skillIdentifier = conditionElement.GetAttributeString("skillidentifier", string.Empty);
minValue = conditionElement.GetAttributeFloat("minvalue", 0f);
}
protected override bool MatchesConditionSpecific()
{
return character.GetSkillLevel(skillIdentifier) >= minValue;
}
}
}
@@ -2,7 +2,7 @@
namespace Barotrauma.Abilities
{
class AbilityObject
abstract class AbilityObject
{
// kept as blank for now, as we are using a composition and only using this object to enforce parameter types
}
@@ -160,6 +160,22 @@ namespace Barotrauma.Abilities
}
}
class AbilityApplyTreatment : AbilityObject, IAbilityCharacter, IAbilityItem
{
public Character Character { get; set; }
public Character User { get; set; }
public Item Item { get; set; }
public AbilityApplyTreatment(Character user, Character target, Item item)
{
Character = target;
User = user;
Item = item;
}
}
class AbilityAttackResult : AbilityObject, IAbilityAttackResult
{
public AttackResult AttackResult { get; set; }
@@ -62,7 +62,7 @@ namespace Barotrauma.Abilities
protected virtual void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
DebugConsole.ThrowError($"Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups.");
}
public void ApplyAbilityEffect(AbilityObject abilityObject)
@@ -128,14 +128,5 @@ namespace Barotrauma.Abilities
DebugConsole.AddWarning("Instantiated " + characterAbility + " for talent " + characterAbilityGroup.CharacterTalent.DebugIdentifier);
return characterAbility;
}
public static AbilityFlags ParseFlagType(string flagTypeString, string debugIdentifier)
{
AbilityFlags flagType = AbilityFlags.None;
if (!Enum.TryParse(flagTypeString, true, out flagType))
{
DebugConsole.ThrowError("Invalid flag type type \"" + flagTypeString + "\" in CharacterTalent (" + debugIdentifier + ")");
}
return flagType;
}
}
}
@@ -0,0 +1,44 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveAffliction : CharacterAbility
{
private readonly string afflictionId;
private readonly float strength;
private readonly string multiplyStrengthBySkill;
private readonly bool setValue;
public CharacterAbilityGiveAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
afflictionId = abilityElement.GetAttributeString("afflictionid", abilityElement.GetAttributeString("affliction", string.Empty));
strength = abilityElement.GetAttributeFloat("strength", 0f);
multiplyStrengthBySkill = abilityElement.GetAttributeString("multiplystrengthbyskill", string.Empty);
setValue = abilityElement.GetAttributeBool("setvalue", false);
if (string.IsNullOrEmpty(afflictionId))
{
DebugConsole.ThrowError("Error in CharacterAbilityGiveAffliction - affliction identifier not set.");
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is IAbilityCharacter character)
{
var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier.Equals(afflictionId, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\".");
return;
}
float strength = this.strength;
if (!string.IsNullOrEmpty(multiplyStrengthBySkill))
{
strength *= Character.GetSkillLevel(multiplyStrengthBySkill);
}
character.Character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(strength), allowStacking: !setValue);
}
}
}
}
@@ -4,12 +4,12 @@ namespace Barotrauma.Abilities
{
class CharacterAbilityGiveFlag : CharacterAbility
{
private AbilityFlags abilityFlag;
private readonly AbilityFlags abilityFlag;
// this and resistance giving should probably be moved directly to charactertalent attributes, as they don't need to interact with either ability group types
public CharacterAbilityGiveFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
abilityFlag = ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier);
abilityFlag = CharacterAbilityGroup.ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier);
}
public override void InitializeAbility(bool addingFirstTime)
@@ -7,20 +7,20 @@ namespace Barotrauma.Abilities
public override bool AppliesEffectOnIntervalUpdate => true;
private readonly int amount;
private readonly StatTypes scalingStatType;
private readonly string scalingStatIdentifier;
public CharacterAbilityGiveMoney(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
scalingStatType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("scalingstattype", "None"), CharacterTalent.DebugIdentifier);
scalingStatIdentifier = abilityElement.GetAttributeString("scalingstatidentifier", string.Empty);
}
private void ApplyEffectSpecific(Character targetCharacter)
{
float multiplier = 1f;
if (scalingStatType != StatTypes.None)
if (!string.IsNullOrEmpty(scalingStatIdentifier))
{
multiplier = 0 + Character.Info.GetSavedStatValue(scalingStatType);
multiplier = 0 + Character.Info.GetSavedStatValue(StatTypes.None, scalingStatIdentifier);
}
targetCharacter.GiveMoney((int)(multiplier * amount));
@@ -11,7 +11,6 @@ namespace Barotrauma.Abilities
private readonly float maxValue;
private readonly bool targetAllies;
private readonly bool removeOnDeath;
private readonly bool removeAfterRound;
private readonly bool giveOnAddingFirstTime;
private readonly bool setValue;
@@ -22,12 +21,12 @@ namespace Barotrauma.Abilities
public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant();
statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier);
string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier);
value = abilityElement.GetAttributeFloat("value", 0f);
maxValue = abilityElement.GetAttributeFloat("maxvalue", float.MaxValue);
targetAllies = abilityElement.GetAttributeBool("targetallies", false);
removeOnDeath = abilityElement.GetAttributeBool("removeondeath", true);
removeAfterRound = abilityElement.GetAttributeBool("removeafterround", false);
giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None);
setValue = abilityElement.GetAttributeBool("setvalue", false);
}
@@ -54,11 +53,11 @@ namespace Barotrauma.Abilities
{
if (targetAllies)
{
Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, removeAfterRound, maxValue, setValue));
Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue));
}
else
{
Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, removeAfterRound, maxValue, setValue);
Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
}
}
}
@@ -5,13 +5,13 @@ namespace Barotrauma.Abilities
{
class CharacterAbilityModifyFlag : CharacterAbility
{
private AbilityFlags abilityFlag;
private readonly AbilityFlags abilityFlag;
private bool lastState;
public CharacterAbilityModifyFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
abilityFlag = ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier);
abilityFlag = CharacterAbilityGroup.ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
@@ -208,5 +208,15 @@ namespace Barotrauma.Abilities
return afflictions;
}
public static AbilityFlags ParseFlagType(string flagTypeString, string debugIdentifier)
{
AbilityFlags flagType = AbilityFlags.None;
if (!Enum.TryParse(flagTypeString, true, out flagType))
{
DebugConsole.ThrowError("Invalid flag type type \"" + flagTypeString + "\" in CharacterTalent (" + debugIdentifier + ")");
}
return flagType;
}
}
}
@@ -30,7 +30,7 @@ namespace Barotrauma
{
ConfigElement = element;
string jobIdentifier = element.GetAttributeString("jobidentifier", "");
string jobIdentifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant();
if (string.IsNullOrEmpty(jobIdentifier))
{
@@ -863,6 +863,60 @@ namespace Barotrauma
};
}, isCheat: true));
commands.Add(new Command("unlocktalents", "unlocktalents [all/[jobname]]: give the controlled characters all the talents of the specified class", (string[] args) =>
{
if (Character.Controlled == null) { return; }
if (args.Length == 0 || args[0].Equals("all", StringComparison.OrdinalIgnoreCase))
{
foreach (var talentTree in TalentTree.JobTalentTrees)
{
foreach (var subTree in talentTree.Value.TalentSubTrees)
{
foreach (var option in subTree.TalentOptionStages)
{
foreach (var talent in option.Talents)
{
Character.Controlled.GiveTalent(talent);
}
}
}
}
}
else
{
var job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase));
if (job == null)
{
ThrowError($"Failed to find the job \"{args[0]}\".");
return;
}
if (!TalentTree.JobTalentTrees.TryGetValue(job.Identifier, out TalentTree talentTree))
{
ThrowError($"No talents configured for the job \"{args[0]}\".");
return;
}
foreach (var subTree in talentTree.TalentSubTrees)
{
foreach (var option in subTree.TalentOptionStages)
{
foreach (var talent in option.Talents)
{
Character.Controlled.GiveTalent(talent);
}
}
}
}
},
() =>
{
List<string> availableArgs = new List<string>() { "All" };
availableArgs.AddRange(JobPrefab.Prefabs.Select(j => j.Name));
return new string[][]
{
availableArgs.ToArray()
};
}, isCheat: true));
commands.Add(new Command("giveexperience", "giveexperience [amount] [character]: Give experience to character.", (string[] args) =>
{
if (args.Length < 1)
@@ -892,7 +946,7 @@ namespace Barotrauma
}, isCheat: true, getValidArgs: () =>
{
return new[]
{
{
new string[] { "100" },
Character.CharacterList.Select(c => c.Name).Distinct().ToArray(),
};
@@ -2041,6 +2095,7 @@ namespace Barotrauma
{
spawnLocation = args[^2];
if (!int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; }
amount = Math.Min(amount, 100);
}
switch (spawnLocation)
@@ -22,9 +22,10 @@
OnSevered = 17,
OnProduceSpawned = 18,
OnOpen = 19, OnClose = 20,
OnDeath = OnBroken,
OnSuccess,
OnAbility,
OnSpawn = 21,
OnSuccess = 22,
OnAbility = 23,
OnDeath = OnBroken
}
public enum AbilityEffectType
@@ -62,7 +63,10 @@
OnItemDeconstructedInventory,
OnStopTinkering,
OnItemPicked,
OnGeneticMaterialCombinedOrRefined,
OnCrewGeneticMaterialCombinedOrRefined,
AfterSubmarineAttacked,
OnApplyTreatment,
}
public enum StatTypes
@@ -100,7 +104,8 @@
RepairToolStructureRepairMultiplier,
RepairToolStructureDamageMultiplier,
RepairToolDeattachTimeMultiplier,
MaxRepairConditionMultiplier,
MaxRepairConditionMultiplierMechanical,
MaxRepairConditionMultiplierElectrical,
IncreaseFabricationQuality,
GeneticMaterialRefineBonus,
GeneticMaterialTaintedProbabilityReductionOnCombine,
@@ -114,11 +119,9 @@
MissionMoneyGainMultiplier,
ExperienceGainMultiplier,
MissionExperienceGainMultiplier,
// these should be deprecated and moved to their own implementation, no sense making them share space with stat values
Coauthor,
WarriorPoetMissionRuns,
WarriorPoetEnemiesKilled,
QuickfixRepairCount,
ExtraSpecialSalesCount,
ApplyTreatmentsOnSelfFraction,
MaxAttachableCount,
}
public enum AbilityFlags
@@ -134,6 +137,8 @@
GainSkillPastMaximum,
RetainExperienceForNewCharacter,
AllowSecondOrderedTarget,
PowerfulCPR,
AlwaysStayConscious,
}
}
@@ -226,11 +226,11 @@ namespace Barotrauma
List<Item> potentialItems = SpawnLocation switch
{
SpawnLocationType.MainSub => Item.ItemList.FindAll(it => it.Submarine == Submarine.MainSub),
SpawnLocationType.MainPath => Item.ItemList.FindAll(it => it.Submarine == null && it.ParentRuin == null),
SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsOutpost),
SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsWreck),
SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.ParentRuin != null),
SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsBeacon),
SpawnLocationType.MainPath => Item.ItemList.FindAll(it => it.Submarine == null),
SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsOutpost),
SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsWreck),
SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsRuin),
SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsBeacon),
_ => throw new NotImplementedException()
};
@@ -252,11 +252,11 @@ namespace Barotrauma
List<WayPoint> potentialSpawnPoints = spawnLocation switch
{
SpawnLocationType.MainSub => WayPoint.WayPointList.FindAll(wp => wp.Submarine == Submarine.MainSub && wp.CurrentHull != null),
SpawnLocationType.MainPath => WayPoint.WayPointList.FindAll(wp => wp.Submarine == null && wp.ParentRuin == null),
SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost),
SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.Submarine.Info.IsWreck),
SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.ParentRuin != null),
SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.Submarine.Info.IsBeacon),
SpawnLocationType.MainPath => WayPoint.WayPointList.FindAll(wp => wp.Submarine == null),
SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost),
SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsWreck),
SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsRuin),
SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsBeacon),
_ => throw new NotImplementedException()
};
@@ -981,6 +981,7 @@ namespace Barotrauma
return false;
case SubmarineType.Wreck:
case SubmarineType.BeaconStation:
case SubmarineType.Ruin:
return true;
}
}
@@ -0,0 +1,176 @@
using Barotrauma.Extensions;
using Barotrauma.RuinGeneration;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class AlienRuinMission : Mission
{
private readonly string[] targetItemIdentifiers;
private readonly string[] targetEnemyIdentifiers;
private readonly int minEnemyCount;
private readonly HashSet<Entity> existingTargets = new HashSet<Entity>();
private readonly HashSet<Character> spawnedTargets = new HashSet<Character>();
private readonly HashSet<Entity> allTargets = new HashSet<Entity>();
private Ruin TargetRuin { get; set; }
public override IEnumerable<Vector2> SonarPositions
{
get
{
if (State == 0)
{
return allTargets.Where(t => (t is Item i && !IsItemDestroyed(i)) || (t is Character c && !IsEnemyDefeated(c))).Select(t => t.WorldPosition);
}
else
{
return Enumerable.Empty<Vector2>();
}
}
}
public AlienRuinMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
targetItemIdentifiers = prefab.ConfigElement.GetAttributeStringArray("targetitems", new string[0], convertToLowerInvariant: true);
targetEnemyIdentifiers = prefab.ConfigElement.GetAttributeStringArray("targetenemies", new string[0], convertToLowerInvariant: true);
minEnemyCount = prefab.ConfigElement.GetAttributeInt("minenemycount", 0);
}
protected override void StartMissionSpecific(Level level)
{
existingTargets.Clear();
spawnedTargets.Clear();
allTargets.Clear();
if (IsClient) { return; }
TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.Server);
if (TargetRuin == null)
{
DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): level contains no alien ruins");
return;
}
if (targetItemIdentifiers.Length < 1 && targetEnemyIdentifiers.Length < 1)
{
DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): no target identifiers set in the mission definition");
return;
}
foreach (var item in Item.ItemList)
{
if (!targetItemIdentifiers.Contains(item.Prefab.Identifier)) { continue; }
if (item.Submarine != TargetRuin.Submarine) { continue; }
existingTargets.Add(item);
allTargets.Add(item);
}
int existingEnemyCount = 0;
foreach (var character in Character.CharacterList)
{
if (string.IsNullOrEmpty(character.SpeciesName)) { continue; }
if (!targetEnemyIdentifiers.Contains(character.SpeciesName.ToLowerInvariant())) { continue; }
if (character.Submarine != TargetRuin.Submarine) { continue; }
existingTargets.Add(character);
allTargets.Add(character);
existingEnemyCount++;
}
if (existingEnemyCount < minEnemyCount)
{
var enemyPrefabs = new HashSet<CharacterPrefab>();
foreach (string identifier in targetEnemyIdentifiers)
{
var prefab = CharacterPrefab.FindBySpeciesName(identifier);
if (prefab != null)
{
enemyPrefabs.Add(prefab);
}
else
{
DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): could not find a character prefab with the species \"{identifier}\"");
}
}
if (enemyPrefabs.None())
{
DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no enemy species defined that could be used to spawn more ({minEnemyCount - existingEnemyCount}) enemies");
return;
}
for (int i = 0; i < (minEnemyCount - existingEnemyCount); i++)
{
var prefab = enemyPrefabs.GetRandom();
var spawnPos = TargetRuin.Submarine.GetWaypoints(false).GetRandom(w => w.CurrentHull != null)?.WorldPosition;
if (!spawnPos.HasValue)
{
DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no valid spawn positions could be found for the additional ({minEnemyCount - existingEnemyCount}) enemies to be spawned");
return;
}
var newEnemy = Character.Create(prefab.Identifier, spawnPos.Value, ToolBox.RandomSeed(8), createNetworkEvent: false);
spawnedTargets.Add(newEnemy);
allTargets.Add(newEnemy);
}
}
#if DEBUG
DebugConsole.NewMessage("********** CLEAR RUIN MISSION INFO **********");
DebugConsole.NewMessage($"Existing item targets: {existingTargets.Count - existingEnemyCount}");
DebugConsole.NewMessage($"Existing enemy targets: {existingEnemyCount}");
DebugConsole.NewMessage($"Spawned enemy targets: {spawnedTargets.Count}");
#endif
}
protected override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient) { return; }
switch (State)
{
case 0:
if (!AllTargetsEliminated()) { return; }
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
State = 2;
break;
}
}
private bool AllTargetsEliminated()
{
foreach (var target in allTargets)
{
if (target is Item targetItem)
{
if (!IsItemDestroyed(targetItem))
{
return false;
}
}
else if (target is Character targetEnemy)
{
if (!IsEnemyDefeated(targetEnemy))
{
return false;
}
}
#if DEBUG
else
{
DebugConsole.ThrowError($"Error in Alien Ruin mission (\"{Prefab.Identifier}\"): unexpected target of type {target?.GetType()?.ToString()}");
}
#endif
}
return true;
}
private bool IsItemDestroyed(Item item) => item == null || item.Removed || item.Condition <= 0.0f;
private bool IsEnemyDefeated(Character enemy) => enemy == null ||enemy.Removed || enemy.IsDead;
public override void End()
{
if (AllTargetsEliminated())
{
GiveReward();
completed = true;
}
failed = !completed && state > 0;
}
}
}
@@ -198,55 +198,14 @@ namespace Barotrauma
if (requiredDeliveryAmount <= 0.0f) { requiredDeliveryAmount = 1.0f; }
}
private ItemPrefab FindItemPrefab(XElement element)
{
ItemPrefab itemPrefab;
if (element.Attribute("name") != null)
{
DebugConsole.ThrowError("Error in cargo mission \"" + Name + "\" - use item identifiers instead of names to configure the items.");
string itemName = element.GetAttributeString("name", "");
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemName + "\" not found");
}
}
else
{
string itemIdentifier = element.GetAttributeString("identifier", "");
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemIdentifier + "\" not found");
}
}
return itemPrefab;
}
private void LoadItemAsChild(XElement element, Item parent)
{
ItemPrefab itemPrefab = FindItemPrefab(element);
WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true);
if (cargoSpawnPos == null)
{
DebugConsole.ThrowError("Couldn't spawn items for cargo mission, cargo spawnpoint not found");
return;
}
Vector2? position = GetCargoSpawnPosition(itemPrefab, out Submarine cargoRoomSub);
if (!position.HasValue) { return; }
var cargoRoom = cargoSpawnPos.CurrentHull;
if (cargoRoom == null)
{
DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!");
return;
}
Vector2 position = new Vector2(
cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.Server),
cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2);
var item = new Item(itemPrefab, position, cargoRoom.Submarine)
var item = new Item(itemPrefab, position.Value, cargoRoomSub)
{
SpawnedInOutpost = true,
AllowStealing = false
@@ -27,7 +27,7 @@ namespace Barotrauma
state = value;
TryTriggerEvents(state);
#if SERVER
GameMain.Server?.UpdateMissionState(this, state);
GameMain.Server?.UpdateMissionState(this);
#endif
ShowMessage(State);
}
@@ -347,7 +347,10 @@ namespace Barotrauma
if (!(GameMain.GameSession.GameMode is CampaignMode campaign)) { return; }
int reward = GetReward(Submarine.MainSub);
float baseExperienceGain = reward * 0.15f;
float baseExperienceGain = reward * 0.1f;
float difficultyMultiplier = 1 + level.Difficulty / 100f;
baseExperienceGain *= difficultyMultiplier;
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters();
@@ -471,5 +474,55 @@ namespace Barotrauma
return spawnedCharacter;
}
protected ItemPrefab FindItemPrefab(XElement element)
{
ItemPrefab itemPrefab;
if (element.Attribute("name") != null)
{
DebugConsole.ThrowError($"Error in mission \"{Name}\" - use item identifiers instead of names to configure the items");
string itemName = element.GetAttributeString("name", "");
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemName}\" not found");
}
}
else
{
string itemIdentifier = element.GetAttributeString("identifier", "");
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemIdentifier}\" not found");
}
}
return itemPrefab;
}
protected Vector2? GetCargoSpawnPosition(ItemPrefab itemPrefab, out Submarine cargoRoomSub)
{
cargoRoomSub = null;
WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true);
if (cargoSpawnPos == null)
{
DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": no waypoints marked as Cargo were found");
return null;
}
var cargoRoom = cargoSpawnPos.CurrentHull;
if (cargoRoom == null)
{
DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": waypoints marked as Cargo must be placed inside a room");
return null;
}
cargoRoomSub = cargoRoom.Submarine;
return new Vector2(
cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.Server),
cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2);
}
}
}
@@ -22,7 +22,9 @@ namespace Barotrauma
Escort = 0x100,
Pirate = 0x200,
GoTo = 0x400,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo
ScanAlienRuins = 0x800,
ClearAlienRuins = 0x1000,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins
}
partial class MissionPrefab
@@ -40,7 +42,9 @@ namespace Barotrauma
{ MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) },
{ MissionType.Escort, typeof(EscortMission) },
{ MissionType.Pirate, typeof(PirateMission) },
{ MissionType.GoTo, typeof(GoToMission) }
{ MissionType.GoTo, typeof(GoToMission) },
{ MissionType.ScanAlienRuins, typeof(ScanMission) },
{ MissionType.ClearAlienRuins, typeof(AlienRuinMission) }
};
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
{
@@ -372,6 +376,11 @@ namespace Barotrauma
var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to));
if (connection?.LevelData == null || !connection.LevelData.HasBeaconStation || connection.LevelData.IsBeaconActive) { return false; }
}
else if (Type == MissionType.ScanAlienRuins || Type == MissionType.ClearAlienRuins)
{
var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to));
if (connection?.LevelData == null || connection.LevelData.GenerationParams.RuinCount < 1) { return false; }
}
return false;
}
@@ -126,12 +126,12 @@ namespace Barotrauma
item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f);
break;
case Level.PositionType.Ruin:
item = suitableItems.FirstOrDefault(it => it.ParentRuin != null && it.ParentRuin.Area.Contains(position));
break;
case Level.PositionType.Wreck:
foreach (Item it in suitableItems)
{
if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
if (it.Submarine?.Info == null) { continue; }
if (spawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; }
if (spawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
Rectangle worldBorders = it.Submarine.Borders;
worldBorders.Location += it.Submarine.WorldPosition.ToPoint();
if (Submarine.RectContains(worldBorders, it.WorldPosition))
@@ -178,10 +178,10 @@ namespace Barotrauma
{
case Level.PositionType.Cave:
case Level.PositionType.MainPath:
if (it.Submarine != null || it.ParentRuin != null) { continue; }
if (it.Submarine != null) { continue; }
break;
case Level.PositionType.Ruin:
if (it.ParentRuin == null) { continue; }
if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; }
break;
case Level.PositionType.Wreck:
if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
@@ -0,0 +1,258 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.RuinGeneration;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class ScanMission : Mission
{
private readonly XElement itemConfig;
private readonly List<Item> startingItems = new List<Item>();
private readonly List<Scanner> scanners = new List<Scanner>();
private readonly Dictionary<Item, ushort> parentInventoryIDs = new Dictionary<Item, ushort>();
private readonly Dictionary<Item, byte> parentItemContainerIndices = new Dictionary<Item, byte>();
private readonly int targetsToScan;
private readonly Dictionary<WayPoint, bool> scanTargets = new Dictionary<WayPoint, bool>();
private readonly HashSet<WayPoint> newTargetsScanned = new HashSet<WayPoint>();
private readonly float minTargetDistance, minTargetDistanceSquared;
private Ruin TargetRuin { get; set; }
private bool AllTargetsScanned
{
get
{
return scanTargets.Any() && scanTargets.All(kvp => kvp.Value);
}
}
public override IEnumerable<Vector2> SonarPositions
{
get
{
if (State > 0)
{
return Enumerable.Empty<Vector2>();
}
else if (scanTargets.Any())
{
return scanTargets
.Where(kvp => !kvp.Value)
.Select(kvp => kvp.Key.WorldPosition);
}
else
{
return Enumerable.Empty<Vector2>();
}
}
}
public ScanMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
itemConfig = prefab.ConfigElement.Element("Items");
targetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1);
minTargetDistance = prefab.ConfigElement.GetAttributeFloat("mintargetdistance", 0.0f);
minTargetDistanceSquared = minTargetDistance * minTargetDistance;
}
protected override void StartMissionSpecific(Level level)
{
Reset();
if (IsClient) { return; }
if (itemConfig == null)
{
DebugConsole.ThrowError("Failed to initialize a Scan mission: item config is not set");
return;
}
foreach (var element in itemConfig.Elements())
{
LoadItem(element, null);
}
GetScanners();
TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.Server);
if (TargetRuin == null)
{
DebugConsole.ThrowError("Failed to initialize a Scan mission: level contains no alien ruins");
return;
}
var availableWaypoints = TargetRuin.Submarine.GetWaypoints(false);
availableWaypoints.RemoveAll(wp => wp.CurrentHull == null);
if (availableWaypoints.Count < targetsToScan)
{
DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({availableWaypoints.Count} < {targetsToScan})");
return;
}
for (int i = 0; i < targetsToScan; i++)
{
var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.Server);
scanTargets.Add(selectedWaypoint, false);
availableWaypoints.Remove(selectedWaypoint);
if (i < (targetsToScan - 1))
{
availableWaypoints.RemoveAll(wp => wp.CurrentHull == selectedWaypoint.CurrentHull);
availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < minTargetDistanceSquared);
if (availableWaypoints.None())
{
DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})");
break;
}
}
}
}
private void Reset()
{
startingItems.Clear();
parentInventoryIDs.Clear();
parentItemContainerIndices.Clear();
scanners.Clear();
TargetRuin = null;
scanTargets.Clear();
}
private void LoadItem(XElement element, Item parent)
{
var itemPrefab = FindItemPrefab(element);
Vector2? position = GetCargoSpawnPosition(itemPrefab, out Submarine cargoRoomSub);
if (!position.HasValue) { return; }
var item = new Item(itemPrefab, position.Value, cargoRoomSub);
item.FindHull();
startingItems.Add(item);
if (parent?.GetComponent<ItemContainer>() is ItemContainer itemContainer)
{
parentInventoryIDs.Add(item, parent.ID);
parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(itemContainer));
parent.Combine(item, user: null);
}
foreach (XElement subElement in element.Elements())
{
int amount = subElement.GetAttributeInt("amount", 1);
for (int i = 0; i < amount; i++)
{
LoadItem(subElement, item);
}
}
}
private void GetScanners()
{
foreach (var startingItem in startingItems)
{
if (startingItem.GetComponent<Scanner>() is Scanner scanner)
{
scanner.OnScanStarted += OnScanStarted;
if (!IsClient)
{
scanner.OnScanCompleted += OnScanCompleted;
}
scanners.Add(scanner);
}
}
}
private void OnScanStarted(Scanner scanner)
{
float scanRadiusSquared = scanner.ScanRadius * scanner.ScanRadius;
foreach (var kvp in scanTargets)
{
if (!IsValidScanPosition(scanner, kvp, scanRadiusSquared)) { continue; }
scanner.DisplayProgressBar = true;
break;
}
}
private void OnScanCompleted(Scanner scanner)
{
if (IsClient) { return; }
newTargetsScanned.Clear();
float scanRadiusSquared = scanner.ScanRadius * scanner.ScanRadius;
foreach (var kvp in scanTargets)
{
if (!IsValidScanPosition(scanner, kvp, scanRadiusSquared)) { continue; }
newTargetsScanned.Add(kvp.Key);
}
foreach (var wp in newTargetsScanned)
{
scanTargets[wp] = true;
}
#if SERVER
// Server should make sure that the clients' scan target status is in-sync
GameMain.Server?.UpdateMissionState(this);
#endif
}
private bool IsValidScanPosition(Scanner scanner, KeyValuePair<WayPoint, bool> scanStatus, float scanRadiusSquared)
{
if (scanStatus.Value) { return false; }
if (scanStatus.Key.Submarine != scanner.Item.Submarine) { return false; }
if (Vector2.DistanceSquared(scanStatus.Key.WorldPosition, scanner.Item.WorldPosition) > scanRadiusSquared) { return false; }
return true;
}
protected override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient) { return; }
switch (State)
{
case 0:
if (!AllTargetsScanned) { return; }
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
State = 2;
break;
}
}
public override void End()
{
if (AllTargetsScanned && AllScannersReturned())
{
GiveReward();
completed = true;
}
foreach (var scanner in scanners)
{
if (scanner.Item != null && !scanner.Item.Removed)
{
scanner.OnScanStarted -= OnScanStarted;
scanner.OnScanCompleted -= OnScanCompleted;
scanner.Item.Remove();
}
}
Reset();
failed = !completed && state > 0;
bool AllScannersReturned()
{
foreach (var scanner in scanners)
{
if (scanner?.Item == null || scanner.Item.Removed) { return false; }
var owner = scanner.Item.GetRootInventoryOwner();
if (owner.Submarine != null && owner.Submarine.Info.Type == SubmarineType.Player)
{
continue;
}
else if (owner is Character c && c.Info != null && GameMain.GameSession.CrewManager.CharacterInfos.Contains(c.Info))
{
continue;
}
return false;
}
return true;
}
}
}
}
@@ -285,11 +285,10 @@ namespace Barotrauma
spawnPos = chosenPosition.Position.ToVector2();
if (chosenPosition.Submarine != null || chosenPosition.Ruin != null)
{
var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine, ruin: chosenPosition.Ruin, useSyncedRand: false);
var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine, useSyncedRand: false);
if (spawnPoint != null)
{
System.Diagnostics.Debug.Assert(spawnPoint.Submarine == chosenPosition.Submarine);
System.Diagnostics.Debug.Assert(spawnPoint.ParentRuin == chosenPosition.Ruin);
System.Diagnostics.Debug.Assert(spawnPoint.Submarine == (chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine));
spawnPos = spawnPoint.WorldPosition;
}
else
@@ -22,14 +22,17 @@ namespace Barotrauma
Place(subs);
subs.ForEach(s => s.Info.InitialSuppliesSpawned = true);
}
foreach (var sub in Submarine.Loaded)
{
if (sub.Info.Type == SubmarineType.Wreck ||
sub.Info.Type == SubmarineType.BeaconStation)
if (sub.Info.Type == SubmarineType.Player ||
sub.Info.Type == SubmarineType.Outpost ||
sub.Info.Type == SubmarineType.OutpostModule ||
sub.Info.Type == SubmarineType.EnemySubmarine)
{
Place(sub.ToEnumerable());
continue;
}
Place(sub.ToEnumerable());
}
if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost)
@@ -1139,8 +1139,8 @@ namespace Barotrauma
if (PlayerCharacterCustomization != null)
{
playerElement.SetAttributeValue("headindex", PlayerCharacterCustomization.HeadSpriteId);
playerElement.SetAttributeValue("gender", PlayerCharacterCustomization.gender);
playerElement.SetAttributeValue("race", PlayerCharacterCustomization.race);
if (PlayerCharacterCustomization.gender != Gender.None) { playerElement.SetAttributeValue("gender", PlayerCharacterCustomization.gender); }
if (PlayerCharacterCustomization.race != Race.None) { playerElement.SetAttributeValue("race", PlayerCharacterCustomization.race); }
playerElement.SetAttributeValue("hairindex", PlayerCharacterCustomization.HairIndex);
playerElement.SetAttributeValue("beardindex", PlayerCharacterCustomization.BeardIndex);
playerElement.SetAttributeValue("moustacheindex", PlayerCharacterCustomization.MoustacheIndex);
@@ -1269,7 +1269,7 @@ namespace Barotrauma
playerName = playerElement.GetAttributeString("name", playerName);
int head = playerElement.GetAttributeInt("headindex", -1);
Enum.TryParse(playerElement.GetAttributeString("gender", "none"), true, out Gender gender);
Enum.TryParse(playerElement.GetAttributeString("race", "white"), true, out Race race);
Enum.TryParse(playerElement.GetAttributeString("race", "none"), true, out Race race);
int hair = playerElement.GetAttributeInt("hairindex", -1);
int beard = playerElement.GetAttributeInt("beardindex", -1);
int moustache = playerElement.GetAttributeInt("moustacheindex", -1);
@@ -87,7 +87,9 @@ namespace Barotrauma
continue;
}
Entity.Spawner?.AddToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false));
string slotString = subElement.GetAttributeString("slot", "None");
InvSlotType slot = Enum.TryParse(slotString, ignoreCase: true, out InvSlotType s) ? s : InvSlotType.None;
Entity.Spawner?.AddToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false), slot: slot);
}
}
@@ -79,6 +79,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, false, description: "Use the hand rotation instead of torso rotation for the item hold angle. Enable this if you want the item just to follow with the arm when not aiming instead of forcing the arm to a hold pose.")]
public bool UseHandRotationForHoldAngle
{
get;
set;
}
[Serialize(false, false, description: "Can the item be attached to walls.")]
public bool Attachable
{
@@ -93,6 +100,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, false, description: "Can the item only be attached in limited amount? Uses permanent stat values to check for legibility.")]
public bool LimitedAttachable
{
get;
set;
}
[Serialize(false, false, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")]
public bool AttachedByDefault
{
@@ -154,7 +168,8 @@ namespace Barotrauma.Items.Components
BodyType = BodyType.Dynamic,
CollidesWith = Physics.CollisionCharacter,
CollisionCategories = Physics.CollisionItemBlocking,
Enabled = false
Enabled = false,
UserData = "Holdable.Pusher"
};
Pusher.FarseerBody.OnCollision += OnPusherCollision;
Pusher.FarseerBody.FixedRotation = false;
@@ -205,7 +220,6 @@ namespace Barotrauma.Items.Components
}
}
}
characterUsable = element.GetAttributeBool("characterusable", true);
}
@@ -247,6 +261,7 @@ namespace Barotrauma.Items.Components
private void Drop(bool dropConnectedWires, Character dropper)
{
GetRope()?.Snap();
if (dropConnectedWires)
{
DropConnectedWires(dropper);
@@ -558,6 +573,15 @@ namespace Barotrauma.Items.Components
PickKey = InputType.Select;
}
public override void ParseMsg()
{
base.ParseMsg();
if (Attachable)
{
prevMsg = DisplayMsg;
}
}
public override bool Use(float deltaTime, Character character = null)
{
if (!attachable || item.body == null) { return character == null || (character.IsKeyDown(InputType.Aim) && characterUsable); }
@@ -567,6 +591,25 @@ namespace Barotrauma.Items.Components
if (!character.IsKeyDown(InputType.Aim)) { return false; }
if (!CanBeAttached(character)) { return false; }
if (LimitedAttachable)
{
if (character?.Info == null)
{
DebugConsole.AddWarning("Character without CharacterInfo attempting to attach a limited attachable item!");
return false;
}
int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
int currentlyAttachedCount = Item.ItemList.Count(
i => i.Submarine == item.Submarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier);
if (currentlyAttachedCount >= maxAttachableCount)
{
#if CLIENT
GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red);
#endif
return false;
}
}
if (GameMain.NetworkMember != null)
{
if (character != Character.Controlled)
@@ -666,6 +709,20 @@ namespace Barotrauma.Items.Components
Update(deltaTime, cam);
}
public Rope GetRope()
{
var rangedWeapon = Item.GetComponent<RangedWeapon>();
if (rangedWeapon != null)
{
var lastProjectile = rangedWeapon.LastProjectile;
if (lastProjectile != null)
{
return lastProjectile.Item.GetComponent<Rope>();
}
}
return null;
}
public override void Update(float deltaTime, Camera cam)
{
if (attachTargetCell != null)
@@ -720,9 +777,18 @@ namespace Barotrauma.Items.Components
scaledHandlePos[1] = handlePos[1] * item.Scale;
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim;
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swing, aimPos + swing, aim, holdAngle);
if (!aim)
{
var rope = GetRope();
if (rope != null && rope.SnapWhenNotAimed)
{
rope.Snap();
}
}
}
else
{
GetRope()?.Snap();
Limb equipLimb = null;
if (picker.Inventory.IsInLimbSlot(item, InvSlotType.Headset) || picker.Inventory.IsInLimbSlot(item, InvSlotType.Head))
{
@@ -792,7 +858,7 @@ namespace Barotrauma.Items.Components
attachTargetCell = null;
if (Pusher != null)
{
GameMain.World.Remove(Pusher.FarseerBody);
Pusher.Remove();
Pusher = null;
}
body = null;
@@ -31,7 +31,7 @@ namespace Barotrauma.Items.Components
public void Initialize(CharacterInfo info)
{
if (info == null) return;
if (info == null) { return; }
if (info.Job?.Prefab != null)
{
@@ -42,20 +42,22 @@ namespace Barotrauma.Items.Components
var head = info.Head;
if (info != null && head != null)
{
item.AddTag("gender:" + head.gender.ToString().ToLowerInvariant());
item.AddTag("race:" + head.race.ToString());
item.AddTag("headspriteid:" + info.HeadSpriteId.ToString());
item.AddTag("hairindex:" + head.HairIndex);
item.AddTag("beardindex:" + head.BeardIndex);
item.AddTag("moustacheindex:" + head.MoustacheIndex);
item.AddTag("faceattachmentindex:" + head.FaceAttachmentIndex);
if (head == null) { return; }
if (info.HasGenders) { item.AddTag($"gender:{head.gender.ToString().ToLowerInvariant()}"); }
if (info.HasRaces) { item.AddTag($"race:{head.race}"); }
item.AddTag($"headspriteid:{info.HeadSpriteId}");
item.AddTag($"hairindex:{head.HairIndex}");
item.AddTag($"beardindex:{head.BeardIndex}");
item.AddTag($"moustacheindex:{head.MoustacheIndex}");
item.AddTag($"faceattachmentindex:{head.FaceAttachmentIndex}");
item.AddTag($"haircolor:{head.HairColor.ToStringHex()}");
item.AddTag($"facialhaircolor:{head.FacialHairColor.ToStringHex()}");
item.AddTag($"skincolor:{head.SkinColor.ToStringHex()}");
if (head.SheetIndex != null)
{
item.AddTag("sheetindex:" + head.SheetIndex.Value.X + ";" + head.SheetIndex.Value.Y);
}
if (head.SheetIndex != null)
{
item.AddTag($"sheetindex:{head.SheetIndex.Value.X};{head.SheetIndex.Value.Y}");
}
}
@@ -50,6 +50,16 @@ namespace Barotrauma.Items.Components
set;
}
[Editable, Serialize(true, false)]
public bool Swing { get; set; }
[Editable, Serialize("2.0, 0.0", false)]
public Vector2 SwingPos { get; set; }
[Editable, Serialize("3.0, -1.0", false)]
public Vector2 SwingForce { get; set; }
/// <summary>
/// Defines items that boost the weapon functionality, like battery cell for stun batons.
/// </summary>
@@ -67,8 +77,6 @@ namespace Barotrauma.Items.Components
};
}
item.IsShootable = true;
// TODO: should define this in xml if we have melee weapons that don't require aim to use
item.RequireAimToUse = true;
PreferredContainedItems = element.GetAttributeStringArray("preferredcontaineditems", new string[0], convertToLowerInvariant: true);
}
@@ -82,6 +90,9 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || reloadTimer > 0.0f) { return false; }
#if CLIENT
if (!Item.RequireAimToUse && character.IsPlayer && (GUI.MouseOn != null || character.Inventory.visualSlots.Any(s => s.MouseOn()) || Inventory.DraggingItems.Any())) { return false; }
#endif
if (Item.RequireAimToUse && !character.IsKeyDown(InputType.Aim) || hitting) { return false; }
//don't allow hitting if the character is already hitting with another weapon
@@ -94,7 +105,7 @@ namespace Barotrauma.Items.Components
SetUser(character);
if (hitPos < MathHelper.PiOver4) { return false; }
if (Item.RequireAimToUse && hitPos < MathHelper.PiOver4) { return false; }
ActivateNearbySleepingCharacters();
reloadTimer = reload / (1 + character.GetStatValue(StatTypes.MeleeAttackSpeed));
@@ -105,20 +116,28 @@ namespace Barotrauma.Items.Components
item.body.FarseerBody.IsBullet = true;
item.body.PhysEnabled = true;
if (!character.AnimController.InWater)
if (Swing && !character.AnimController.InWater)
{
foreach (Limb l in character.AnimController.Limbs)
{
if (l.IsSevered) { continue; }
if (l.type == LimbType.LeftFoot || l.type == LimbType.LeftThigh || l.type == LimbType.LeftLeg) { continue; }
if (l.type == LimbType.Head || l.type == LimbType.Torso)
Vector2 force = new Vector2(character.AnimController.Dir * SwingForce.X, SwingForce.Y) * l.Mass;
switch (l.type)
{
l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 7.0f, -4.0f));
case LimbType.Torso:
force *= 2;
break;
case LimbType.Legs:
case LimbType.LeftFoot:
case LimbType.LeftThigh:
case LimbType.LeftLeg:
case LimbType.RightFoot:
case LimbType.RightThigh:
case LimbType.RightLeg:
force = Vector2.Zero;
break;
}
else
{
l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 5.0f, -2.0f));
}
l.body.ApplyLinearImpulse(force);
}
}
@@ -154,9 +173,16 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (!item.body.Enabled) { impactQueue.Clear(); return; }
if (picker == null && !picker.HeldItems.Contains(item)) { impactQueue.Clear(); IsActive = false; }
if (!item.body.Enabled)
{
impactQueue.Clear();
return;
}
if (picker == null && !picker.HeldItems.Contains(item))
{
impactQueue.Clear();
IsActive = false;
}
while (impactQueue.Count > 0)
{
var impact = impactQueue.Dequeue();
@@ -164,37 +190,47 @@ namespace Barotrauma.Items.Components
}
//in case handling the impact does something to the picker
if (picker == null) { return; }
reloadTimer -= deltaTime;
if (reloadTimer < 0) { reloadTimer = 0; }
if (!picker.IsKeyDown(InputType.Aim) && !hitting) { hitPos = 0.0f; }
if (reloadTimer < 0)
{
reloadTimer = 0;
}
if (!picker.IsKeyDown(InputType.Aim) && !hitting)
{
hitPos = 0.0f;
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); }
if (item.body.Dir != picker.AnimController.Dir)
{
item.FlipX(relativeToSub: false);
}
AnimController ac = picker.AnimController;
//TODO: refactor the hitting logic (get rid of the magic numbers, make it possible to use different kinds of animations for different items)
if (!hitting)
{
bool aim = picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim;
bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim;
if (aim)
{
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 5f, MathHelper.PiOver4));
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, hitPos, holdAngle + hitPos, aimingMelee: true);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
}
else
{
hitPos = 0;
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle);
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
}
}
else
{
// TODO: We might want to make this configurable
hitPos = MathUtils.WrapAnglePi(hitPos - deltaTime * 15f);
ac.HoldItem(deltaTime, item, handlePos, new Vector2(2, 0), Vector2.Zero, false, hitPos, holdAngle + hitPos); // aimPos not used -> zero (new Vector2(-0.3f, 0.2f)), holdPos new Vector2(0.6f, -0.1f)
if (Swing)
{
ac.HoldItem(deltaTime, item, handlePos, SwingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos);
}
else
{
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
}
if (hitPos < -MathHelper.PiOver2)
{
RestoreCollision();
@@ -1,7 +1,6 @@
using Barotrauma.Abilities;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Collision;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
@@ -80,6 +79,9 @@ namespace Barotrauma.Items.Components
}
}
public Projectile LastProjectile { get; private set; }
private float currentChargeTime;
private bool tryingToCharge;
@@ -196,6 +198,7 @@ namespace Barotrauma.Items.Components
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi;
float spread = GetSpread(character) * Rand.Range(-0.5f, 0.5f);
LastProjectile?.Item.GetComponent<Rope>()?.Snap();
float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.AttackMultiplier);
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false, damageMultiplier);
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
@@ -206,6 +209,7 @@ namespace Barotrauma.Items.Components
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f));
Item.RemoveContained(projectile.Item);
}
LastProjectile = projectile;
}
LaunchProjSpecific();
@@ -123,18 +123,18 @@ namespace Barotrauma.Items.Components
if (aim)
{
throwPos = MathUtils.WrapAnglePi(System.Math.Min(throwPos + deltaTime * 5.0f, MathHelper.PiOver2));
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos);
}
else
{
throwPos = 0;
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle);
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
}
}
else
{
throwPos = MathUtils.WrapAnglePi(throwPos - deltaTime * 15.0f);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos);
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos);
if (throwPos < 0)
{
@@ -169,8 +169,8 @@ namespace Barotrauma.Items.Components
item.body.FarseerBody.IsBullet = true;
midAir = true;
ac.GetLimb(LimbType.Head).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
ac.GetLimb(LimbType.Torso).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
ac.GetLimb(LimbType.Head)?.body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
ac.GetLimb(LimbType.Torso)?.body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
Limb rightHand = ac.GetLimb(LimbType.RightHand);
item.body.AngularVelocity = rightHand.body.AngularVelocity;
@@ -767,7 +767,7 @@ namespace Barotrauma.Items.Components
}
}
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null)
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float applyOnUserFraction = 0.0f)
{
if (statusEffectLists == null) { return; }
@@ -779,7 +779,13 @@ namespace Barotrauma.Items.Components
{
if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; }
if (user != null) { effect.SetUser(user); }
item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, false, false, worldPosition);
item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
if (user != null && applyOnUserFraction > 0.0f && effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.AfflictionMultiplier = applyOnUserFraction;
item.ApplyStatusEffect(effect, type, deltaTime, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), useTarget, false, false, worldPosition);
effect.AfflictionMultiplier = 1.0f;
}
reducesCondition |= effect.ReducesItemCondition();
}
//if any of the effects reduce the item's condition, set the user for OnBroken effects as well
@@ -959,7 +965,7 @@ namespace Barotrauma.Items.Components
}
}
public void ParseMsg()
public virtual void ParseMsg()
{
string msg = TextManager.Get(Msg, true);
if (msg != null)
@@ -220,6 +220,12 @@ namespace Barotrauma.Items.Components
if (targetItem == otherItem) { continue; }
if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r.Equals(otherItem.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)))
{
user.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined);
foreach (Character character in Character.GetFriendlyCrew(user))
{
character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined);
}
var geneticMaterial1 = targetItem.GetComponent<GeneticMaterial>();
var geneticMaterial2 = otherItem.GetComponent<GeneticMaterial>();
if (geneticMaterial1 != null && geneticMaterial2 != null)
@@ -344,22 +344,27 @@ namespace Barotrauma.Items.Components
var tempUser = user;
for (int i = 0; i < (int)fabricationValueItem.Value; i++)
{
float outCondition = fabricatedItem.OutCondition;
if (i < amountFittingContainer)
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition,
onSpawned: (Item spawnedItem) =>
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition,
onSpawned: (Item spawnedItem) =>
{
onItemSpawned(spawnedItem, tempUser);
spawnedItem.Quality = quality;
//reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers
spawnedItem.Condition = spawnedItem.MaxCondition * outCondition;
});
}
else
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition,
onSpawned: (Item spawnedItem) =>
{
Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition,
onSpawned: (Item spawnedItem) =>
{
onItemSpawned(spawnedItem, tempUser);
spawnedItem.Quality = quality;
//reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers
spawnedItem.Condition = spawnedItem.MaxCondition * outCondition;
});
}
}
@@ -137,6 +137,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, false, description: "Can the item stick even to deflective targets.")]
public bool StickToDeflective
{
get;
set;
}
[Serialize(false, false, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+
"It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")]
public bool Hitscan
@@ -231,7 +238,10 @@ namespace Barotrauma.Items.Components
{
Item.body.ResetDynamics();
Item.SetTransform(simPosition, rotation);
Attack.DamageMultiplier = damageMultiplier;
if (Attack != null)
{
Attack.DamageMultiplier = damageMultiplier;
}
// Set user for hitscan projectiles to work properly.
User = user;
// Need to set null for non-characterusable items.
@@ -356,6 +366,7 @@ namespace Barotrauma.Items.Components
{
float rotation = item.body.Rotation;
Vector2 simPositon = item.SimPosition;
Vector2 rayStartWorld = item.WorldPosition;
item.Drop(null);
item.body.Enabled = true;
@@ -367,7 +378,6 @@ namespace Barotrauma.Items.Components
Vector2 rayStart = simPositon;
Vector2 rayEnd = rayStart + dir * 500.0f;
Vector2 rayStartWorld = item.WorldPosition;
float worldDist = 1000.0f;
#if CLIENT
worldDist = Screen.Selected?.Cam?.WorldView.Width ?? GameMain.GraphicsWidth;
@@ -579,7 +589,8 @@ namespace Barotrauma.Items.Components
if (!removePending)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
Entity useTarget = lastTarget?.Body.UserData is Limb limb ? limb.character : lastTarget?.Body.UserData as Entity;
ApplyStatusEffects(ActionType.OnActive, deltaTime, useTarget: useTarget, user: _user);
}
if (item.body != null && item.body.FarseerBody.IsBullet)
@@ -624,7 +635,6 @@ namespace Barotrauma.Items.Components
return false;
}
private bool OnProjectileCollision(Fixture f1, Fixture target, Contact contact)
{
if (User != null && User.Removed) { User = null; return false; }
@@ -695,7 +705,8 @@ namespace Barotrauma.Items.Components
}
}
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
private Fixture lastTarget;
private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity)
{
@@ -706,6 +717,7 @@ namespace Barotrauma.Items.Components
{
return false;
}
lastTarget = target;
float projectileNewSpeed = 0.5f;
float projectileDeflectedNewSpeed = 0.1f;
@@ -836,14 +848,16 @@ namespace Barotrauma.Items.Components
}
if (attackResult.AppliedDamageModifiers != null &&
attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles))
(attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective))
{
item.body.LinearVelocity *= projectileDeflectedNewSpeed;
}
else if (Vector2.Dot(velocity, collisionNormal) < 0.0f && hits.Count() >= MaxTargetsToHit &&
else if ( // When hitting characters the collision normal seems to sometimes point into wrong direction, resulting in a failed attempt to stick
//Vector2.Dot(Vector2.Normalize(velocity), collisionNormal) < 0.0f &&
hits.Count() >= MaxTargetsToHit &&
target.Body.Mass > item.body.Mass * 0.5f &&
(DoesStick ||
(StickToCharacters && target.Body.UserData is Limb) ||
(StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) ||
(StickToStructures && target.Body.UserData is Structure) ||
(StickToItems && target.Body.UserData is Item)))
{
@@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components
private int qualityLevel;
[Serialize(0, false)]
[Serialize(0, true)]
public int QualityLevel
{
get { return qualityLevel; }
@@ -406,7 +406,15 @@ namespace Barotrauma.Items.Components
fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f;
fixDuration /= 1 + item.GetQualityModifier(Quality.StatType.RepairSpeed);
item.MaxRepairConditionMultiplier = 1 + CurrentFixer.GetStatValue(StatTypes.MaxRepairConditionMultiplier);
// kind of rough to keep this in update, but seems most robust
if (requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical", StringComparison.OrdinalIgnoreCase)))
{
item.MaxRepairConditionMultiplier = 1 + CurrentFixer.GetStatValue(StatTypes.MaxRepairConditionMultiplierMechanical);
}
if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase)))
{
item.MaxRepairConditionMultiplier = 1 + CurrentFixer.GetStatValue(StatTypes.MaxRepairConditionMultiplierElectrical);
}
if (currentFixerAction == FixActions.Repair)
{
@@ -3,7 +3,6 @@ using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -54,6 +53,27 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, false, description: "Should the rope snap when the character drops the aim?")]
public bool SnapWhenNotAimed
{
get;
set;
}
[Serialize(30.0f, false, description: "How much mass is required for the target to pull the source towards it. Static and kinematic targets are always treated heavy enough.")]
public float TargetMinMass
{
get;
set;
}
[Serialize(false, false)]
public bool LerpForces
{
get;
set;
}
private bool snapped;
public bool Snapped
{
@@ -85,6 +105,7 @@ namespace Barotrauma.Items.Components
partial void InitProjSpecific(XElement element);
public void Snap() => Snapped = true;
public void Attach(ISpatialEntity source, Item target)
{
@@ -118,14 +139,15 @@ namespace Barotrauma.Items.Components
Vector2 diff = target.WorldPosition - source.WorldPosition;
if (diff.LengthSquared() > MaxLength * MaxLength)
{
Snapped = true;
Snap();
return;
}
#if CLIENT
item.ResetCachedVisibleSize();
#endif
var projectile = target.GetComponent<Projectile>();
if (projectile == null) { return; }
if (SnapOnCollision)
{
raycastTimer += deltaTime;
@@ -135,28 +157,24 @@ namespace Barotrauma.Items.Components
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
customPredicate: (Fixture f) =>
{
var projectile = target?.GetComponent<Projectile>();
if (projectile != null)
foreach (Body body in projectile.Hits)
{
foreach (Body body in projectile.Hits)
Submarine alreadyHitSub = null;
if (body.UserData is Structure hitStructure)
{
Submarine alreadyHitSub = null;
if (body.UserData is Structure hitStructure)
{
alreadyHitSub = hitStructure.Submarine;
}
else if (body.UserData is Submarine hitSub)
{
alreadyHitSub = hitSub;
}
if (alreadyHitSub != null)
{
if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; }
if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; }
}
alreadyHitSub = hitStructure.Submarine;
}
else if (body.UserData is Submarine hitSub)
{
alreadyHitSub = hitSub;
}
if (alreadyHitSub != null)
{
if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; }
if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; }
}
}
Submarine targetSub = target?.GetComponent<Projectile>()?.StickTarget?.UserData as Submarine ?? target.Submarine;
Submarine targetSub = projectile.StickTarget?.UserData as Submarine ?? target.Submarine;
if (f.Body?.UserData is MapEntity mapEntity && mapEntity.Submarine != null)
{
@@ -175,7 +193,7 @@ namespace Barotrauma.Items.Components
return true;
}) != null)
{
Snapped = true;
Snap();
return;
}
raycastTimer = 0.0f;
@@ -183,27 +201,107 @@ namespace Barotrauma.Items.Components
}
Vector2 forceDir = diff;
if (forceDir.LengthSquared() > 0.01f)
float distance = diff.Length();
if (distance > 0.001f)
{
forceDir = Vector2.Normalize(forceDir);
}
if (Math.Abs(ProjectilePullForce) > 0.001f)
{
var projectile = target.GetComponent<Projectile>();
projectile?.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce);
projectile.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce);
}
if (Math.Abs(SourcePullForce) > 0.001f)
if (projectile.StickTarget != null)
{
var sourceBody = GetBodyToPull(source);
sourceBody?.ApplyForce(forceDir * SourcePullForce);
}
if (Math.Abs(TargetPullForce) > 0.001f)
{
var targetBody = GetBodyToPull(target);
targetBody?.ApplyForce(-forceDir * TargetPullForce);
float targetMass = float.MaxValue;
Character targetCharacter = null;
if (projectile.StickTarget.UserData is Limb targetLimb)
{
targetCharacter = targetLimb.character;
targetMass = targetLimb.ragdoll.Mass;
}
else if (projectile.StickTarget.UserData is Character character)
{
targetCharacter = character;
targetMass = character.Mass;
}
else if (projectile.StickTarget.UserData is Item item)
{
targetMass = projectile.StickTarget.Mass;
}
if (projectile.StickTarget.BodyType != BodyType.Dynamic)
{
targetMass = float.MaxValue;
}
var user = item.GetComponent<Projectile>()?.User;
if (targetMass > TargetMinMass)
{
if (Math.Abs(SourcePullForce) > 0.001f)
{
var sourceBody = GetBodyToPull(source);
if (sourceBody != null)
{
var targetBody = GetBodyToPull(target);
if (targetBody != null && !(targetBody.UserData is Character))
{
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
}
float forceMultiplier = 1;
if (user != null)
{
user.AnimController.Hang();
if (user.InWater)
{
if (user.IsRagdolled)
{
forceMultiplier = 0;
}
}
else
{
forceMultiplier = user.IsRagdolled ? 0.1f : 0.4f;
// Prevents too easy smashing to the walls
forceDir.X /= 4;
// Prevents rubberbanding up and down
if (forceDir.Y < 0)
{
forceDir.Y = 0;
}
}
if (targetCharacter != null)
{
var myCollider = user.AnimController.Collider;
var targetCollider = targetCharacter.AnimController.Collider;
if (myCollider.LinearVelocity != Vector2.Zero && targetCollider.LinearVelocity != Vector2.Zero)
{
if (Vector2.Dot(Vector2.Normalize(myCollider.LinearVelocity), Vector2.Normalize(targetCollider.LinearVelocity)) < 0)
{
myCollider.ApplyForce(targetCollider.LinearVelocity * targetCollider.Mass);
}
}
}
}
float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) * forceMultiplier : SourcePullForce * forceMultiplier;
sourceBody.ApplyForce(forceDir * force);
}
}
}
if (Math.Abs(TargetPullForce) > 0.001f)
{
var targetBody = GetBodyToPull(target);
if (user != null && targetCharacter != null && !user.AnimController.InWater)
{
// Prevents rubberbanding horizontally when dragging a corpse.
if ((forceDir.X < 0) != (user.AnimController.Dir < 0))
{
forceDir.X = Math.Clamp(forceDir.X, -0.1f, 0.1f);
}
}
float force = LerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance)) : TargetPullForce;
targetBody?.ApplyForce(-forceDir * force);
targetCharacter?.AnimController.Collider.ApplyForce(-forceDir * force * 3);
}
}
}
@@ -231,19 +329,23 @@ namespace Barotrauma.Items.Components
return ownerCharacter.AnimController.Collider;
}
var projectile = targetItem.GetComponent<Projectile>();
if (projectile != null)
if (projectile != null && projectile.StickTarget != null)
{
if (projectile.StickTarget?.UserData is Structure structure)
if (projectile.StickTarget.UserData is Structure structure)
{
return structure.Submarine?.PhysicsBody;
}
else if (projectile.StickTarget?.UserData is Submarine sub)
else if (projectile.StickTarget.UserData is Submarine sub)
{
return sub?.PhysicsBody;
return sub.PhysicsBody;
}
else if (projectile.StickTarget?.UserData is Character character)
else if (projectile.StickTarget.UserData is Item item)
{
return character.AnimController.Collider;
return item.body;
}
else if (projectile.StickTarget.UserData is Limb limb)
{
return limb.body;
}
return null;
}
@@ -0,0 +1,90 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Scanner : ItemComponent
{
[Serialize(1.0f, false, description: "How long it takes for the scan to be completed.")]
public float ScanDuration { get; set; }
[Serialize(0.0f, false, description: "How far along the scan is. When the timer goes above ScanDuration, the scan is completed.")]
public float ScanTimer
{
get
{
return scanTimer;
}
set
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (Holdable == null) { return; }
bool wasScanCompletedPreviously = IsScanCompleted;
scanTimer = Math.Max(0.0f, value);
if (!wasScanCompletedPreviously && IsScanCompleted)
{
OnScanCompleted?.Invoke(this);
}
#if SERVER
if (wasScanCompletedPreviously != IsScanCompleted || Math.Abs(LastSentScanTimer - scanTimer) > 0.1f)
{
item.CreateServerEvent(this);
LastSentScanTimer = scanTimer;
}
#endif
}
}
[Serialize(1.0f, false, description: "How far the scanner can be from the target for the scan to be successful.")]
public float ScanRadius { get; set; }
[Serialize(true, false, description: "Should the progress bar always be displayed when the item has been attached.")]
public bool AlwaysDisplayProgressBar { get; set; }
private Holdable Holdable { get; set; }
/// <summary>
/// Should the progress bar be displayed. Use when AlwaysDisplayProgressBar is set to false.
/// </summary>
public bool DisplayProgressBar { get; set; } = false;
private bool IsScanCompleted => scanTimer >= ScanDuration;
private float scanTimer;
public Action<Scanner> OnScanStarted, OnScanCompleted;
public Scanner(Item item, XElement element) : base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
if (Holdable != null && Holdable.Attachable && Holdable.Attached)
{
if (ScanTimer <= 0.0f)
{
OnScanStarted?.Invoke(this);
}
ScanTimer += deltaTime;
item.AiTarget?.IncreaseSoundRange(deltaTime, speed: 2.0f);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
}
else
{
ScanTimer = 0.0f;
DisplayProgressBar = false;
}
UpdateProjSpecific();
}
partial void UpdateProjSpecific();
public override void OnItemLoaded()
{
base.OnItemLoaded();
Holdable = item.GetComponent<Holdable>();
if (Holdable == null || !Holdable.Attachable)
{
DebugConsole.ThrowError("Error in initializing a Scanner component: an attachable Holdable component is required on the same item and none was found");
IsActive = false;
}
}
}
}
@@ -0,0 +1,119 @@
using Barotrauma.Extensions;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class ButtonTerminal : ItemComponent
{
[Editable, Serialize(new string[0], true, description: "Signals sent when the corresponding buttons are pressed.", alwaysUseInstanceValues: true)]
public string[] Signals { get; set; }
[Editable, Serialize("", true, description: "Identifiers or tags of items that, when contained, allow the terminal buttons to be used. Multiple ones should be separated by commas.", alwaysUseInstanceValues: true)]
public string ActivatingItems { get; set; }
private int RequiredSignalCount { get; set; }
private ItemContainer Container { get; set; }
private HashSet<ItemPrefab> ActivatingItemPrefabs { get; set; } = new HashSet<ItemPrefab>();
private bool AllowUsingButtons => ActivatingItemPrefabs.None() || Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab));
public ButtonTerminal(Item item, XElement element) : base(item, element)
{
IsActive = true;
RequiredSignalCount = element.GetChildElements("TerminalButton").Count(c => c.GetAttribute("style") != null);
if (RequiredSignalCount < 1)
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements defined for the ButtonTerminal component!");
}
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public override void OnItemLoaded()
{
base.OnItemLoaded();
if (Signals == null)
{
Signals = new string[RequiredSignalCount];
for (int i = 0; i < RequiredSignalCount; i++)
{
Signals[i] = string.Empty;
}
}
else if (Signals.Length != RequiredSignalCount)
{
string[] newSignals = new string[RequiredSignalCount];
if (Signals.Length < RequiredSignalCount)
{
Signals.CopyTo(newSignals, 0);
for (int i = Signals.Length; i < RequiredSignalCount; i++)
{
newSignals[i] = string.Empty;
}
}
else
{
for (int i = 0; i < RequiredSignalCount; i++)
{
newSignals[i] = Signals[i];
}
}
Signals = newSignals;
}
ActivatingItemPrefabs.Clear();
if (!string.IsNullOrEmpty(ActivatingItems))
{
foreach (var activatingItem in ActivatingItems.Split(','))
{
if (MapEntityPrefab.Find(null, identifier: activatingItem, showErrorMessages: false) is ItemPrefab prefab)
{
ActivatingItemPrefabs.Add(prefab);
}
else
{
ItemPrefab.Prefabs.Where(p => p.Tags.Any(t => t.Equals(activatingItem, StringComparison.OrdinalIgnoreCase)))
.ForEach(p => ActivatingItemPrefabs.Add(p));
}
}
if (ActivatingItemPrefabs.None())
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no activating item prefabs found with identifiers or tags \"{ActivatingItems}\"");
}
}
var containers = item.GetComponents<ItemContainer>().ToList();
if (containers.Count != 1)
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\": the ButtonTerminal component requires exactly one ItemContainer component!");
return;
}
Container = containers[0];
OnItemLoadedProjSpecific();
}
partial void OnItemLoadedProjSpecific();
private bool SendSignal(int signalIndex, bool isServerMessage = false)
{
if (!isServerMessage && !AllowUsingButtons) { return false; }
string signal = Signals[signalIndex];
string connectionName = $"signal_out{signalIndex + 1}";
item.SendSignal(signal, connectionName);
return true;
}
private void Write(IWriteMessage msg, object[] extraData)
{
if (extraData == null || extraData.Length < 3) { return; }
msg.WriteRangedInteger((int)extraData[2], 0, Signals.Length - 1);
}
}
}
@@ -101,7 +101,7 @@ namespace Barotrauma.Items.Components
foreach (XElement connectionElement in subElement.Elements())
{
string prefabConnectionName = element.GetAttributeString("name", null);
string prefabConnectionName = connectionElement.GetAttributeString("name", null);
if (prefabConnectionName == Name)
{
displayNameTag = connectionElement.GetAttributeString("displayname", "");
@@ -0,0 +1,185 @@
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class TriggerComponent : ItemComponent
{
[Editable, Serialize(0.0f, true, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
public float Force { get; set; }
public PhysicsBody PhysicsBody { get; private set; }
private float Radius { get; set; }
private float RadiusInDisplayUnits { get; set; }
private bool TriggeredOnce { get; set; }
public bool TriggerActive { get; private set; }
private readonly LevelTrigger.TriggererType triggeredBy;
private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
private readonly bool triggerOnce;
private readonly List<ISerializableEntity> statusEffectTargets = new List<ISerializableEntity>();
/// <summary>
/// Effects applied to entities inside the trigger
/// </summary>
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
/// <summary>
/// Attacks applied to entities inside the trigger
/// </summary>
private readonly List<Attack> attacks = new List<Attack>();
public TriggerComponent(Item item, XElement element) : base(item, element)
{
string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character");
if (!Enum.TryParse(triggeredByAttribute, out triggeredBy))
{
DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type.");
}
triggerOnce = element.GetAttributeBool("triggeronce", false);
string parentDebugName = $"TriggerComponent in {item.Name}";
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
LevelTrigger.LoadStatusEffect(statusEffects, subElement, parentDebugName);
break;
case "attack":
case "damage":
LevelTrigger.LoadAttack(subElement, parentDebugName, triggerOnce, attacks);
break;
}
}
IsActive = true;
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
float radiusAttribute = originalElement.GetAttributeFloat("radius", 10.0f);
Radius = ConvertUnits.ToSimUnits(radiusAttribute * item.Scale);
PhysicsBody = new PhysicsBody(0.0f, 0.0f, Radius, 1.5f)
{
BodyType = BodyType.Static,
CollidesWith = LevelTrigger.GetCollisionCategories(triggeredBy),
CollisionCategories = Physics.CollisionWall,
UserData = item
};
PhysicsBody.FarseerBody.SetIsSensor(true);
PhysicsBody.FarseerBody.OnCollision += OnCollision;
PhysicsBody.FarseerBody.OnSeparation += OnSeparation;
RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.radius);
}
public override void OnMapLoaded()
{
base.OnMapLoaded();
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
PhysicsBody.Submarine = item.Submarine;
}
private bool OnCollision(Fixture sender, Fixture other, Contact contact)
{
if (!(LevelTrigger.GetEntity(other) is Entity entity)) { return false; }
if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (true, item.Submarine))) { return false; }
triggerers.Add(entity);
return true;
}
private void OnSeparation(Fixture sender, Fixture other, Contact contact)
{
if (!(LevelTrigger.GetEntity(other) is Entity entity))
{
return;
}
if (entity is Character character && (!character.Enabled || character.Removed) && triggerers.Contains(entity))
{
triggerers.Remove(entity);
return;
}
if (LevelTrigger.CheckContactsForOtherFixtures(PhysicsBody, other, entity))
{
return;
}
triggerers.Remove(entity);
}
public override void Update(float deltaTime, Camera cam)
{
triggerers.RemoveWhere(t => t.Removed);
LevelTrigger.RemoveDistantTriggerers(PhysicsBody, triggerers, item.WorldPosition);
if (triggerOnce)
{
if (TriggeredOnce) { return; }
if (triggerers.Count > 0)
{
TriggeredOnce = true;
IsActive = false;
triggerers.Clear();
}
}
TriggerActive = triggerers.Any();
foreach (Entity triggerer in triggerers)
{
LevelTrigger.ApplyStatusEffects(statusEffects, item.WorldPosition, triggerer, deltaTime, statusEffectTargets);
if (triggerer is IDamageable damageable)
{
LevelTrigger.ApplyAttacks(attacks, damageable, item.WorldPosition, deltaTime);
}
else if (triggerer is Submarine submarine)
{
LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime);
}
if (Force < 0.01f)
{
// Just ignore very minimal forces
continue;
}
else if (triggerer is Character c)
{
ApplyForce(c.AnimController.Collider);
}
else if (triggerer is Submarine s)
{
ApplyForce(s.SubBody.Body);
}
else if (triggerer is Item i && i.body != null)
{
ApplyForce(i.body);
}
}
}
private void ApplyForce(PhysicsBody body)
{
Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition);
if (diff.LengthSquared() < 0.0001f) { return; }
float distanceFactor = LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits);
if (distanceFactor <= 0.0f) { return; }
Vector2 force = distanceFactor * Force * Vector2.Normalize(diff);
if (force.LengthSquared() < 0.01f) { return; }
body.ApplyForce(force);
}
public override void Move(Vector2 amount)
{
base.Move(amount);
if (PhysicsBody != null)
{
PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
PhysicsBody.Submarine = item.Submarine;
}
}
}
}
@@ -413,19 +413,19 @@ namespace Barotrauma.Items.Components
return i1.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(i2.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes));
});
}
#if CLIENT
equipLimb.UpdateWearableTypesToHide();
#endif
}
character.OnWearablesChanged();
}
public override void Drop(Character dropper)
{
Character previousPicker = picker;
Unequip(picker);
base.Drop(dropper);
previousPicker?.OnWearablesChanged();
picker = null;
IsActive = false;
}
@@ -11,6 +11,7 @@ using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.MapCreatures.Behavior;
using Barotrauma.Abilities;
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
@@ -36,7 +37,6 @@ namespace Barotrauma
set
{
currentHull = value;
ParentRuin = currentHull?.ParentRuin;
}
}
@@ -975,6 +975,9 @@ namespace Barotrauma
if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; }
if (HasTag("logic")) { isLogic = true; }
ApplyStatusEffects(ActionType.OnSpawn, 1.0f);
Components.ForEach(c => c.ApplyStatusEffects(ActionType.OnSpawn, 1.0f));
}
partial void InitProjSpecific();
@@ -1603,7 +1606,10 @@ namespace Barotrauma
}
}
aiTarget?.Update(deltaTime);
if (aiTarget != null)
{
aiTarget.Update(deltaTime);
}
if (!isActive) { return; }
@@ -2351,6 +2357,8 @@ namespace Barotrauma
}
#endif
float applyOnSelfFraction = user?.GetStatValue(StatTypes.ApplyTreatmentsOnSelfFraction) ?? 0.0f;
bool remove = false;
foreach (ItemComponent ic in components)
{
@@ -2363,7 +2371,19 @@ namespace Barotrauma
ic.PlaySound(actionType, user);
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user);
ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user, applyOnUserFraction: applyOnSelfFraction);
if (applyOnSelfFraction > 0.0f)
{
//hacky af
ic.statusEffectLists.TryGetValue(actionType, out var effectList);
if (effectList != null)
{
effectList.ForEach(e => e.AfflictionMultiplier = applyOnSelfFraction);
ic.ApplyStatusEffects(actionType, 1.0f, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), user: user);
effectList.ForEach(e => e.AfflictionMultiplier = 1.0f);
}
}
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
@@ -2376,6 +2396,15 @@ namespace Barotrauma
if (ic.DeleteOnUse) { remove = true; }
}
if (user != null)
{
var abilityItem = new AbilityApplyTreatment(user, character, this);
user.CheckTalents(AbilityEffectType.OnApplyTreatment, abilityItem);
}
if (remove) { Spawner?.AddToRemoveQueue(this); }
}
@@ -2549,6 +2578,14 @@ namespace Barotrauma
{
msg.Write((int)value);
}
else if (value is string[] a)
{
msg.Write(a.Length);
for (int i = 0; i < a.Length; i++)
{
msg.Write(a[i] ?? "");
}
}
else
{
throw new NotImplementedException("Serializing item properties of the type \"" + value.GetType() + "\" not supported");
@@ -2656,6 +2693,19 @@ namespace Barotrauma
logValue = XMLExtensions.RectToString(val);
if (allowEditing) { property.TrySetValue(parentObject, val); }
}
else if (type == typeof(string[]))
{
int arrayLength = msg.ReadInt32();
string[] val = new string[arrayLength];
for (int i = 0; i < arrayLength; i++)
{
val[i] = msg.ReadString();
}
if (allowEditing)
{
property.TrySetValue(parentObject, val);
}
}
else if (typeof(Enum).IsAssignableFrom(type))
{
int intVal = msg.ReadInt32();
@@ -30,55 +30,44 @@ namespace Barotrauma
private bool idFreed;
public virtual bool Removed
{
get;
private set;
}
public virtual bool Removed { get; private set; }
public bool IdFreed
{
get { return idFreed; }
}
public bool IdFreed => idFreed;
public readonly ushort ID;
public virtual Vector2 SimPosition
public virtual Vector2 SimPosition => Vector2.Zero;
public virtual Vector2 Position => Vector2.Zero;
public virtual Vector2 WorldPosition => Submarine == null ? Position : Submarine.Position + Position;
public virtual Vector2 DrawPosition => Submarine == null ? Position : Submarine.DrawPosition + Position;
public Submarine Submarine { get; set; }
public AITarget AiTarget => aiTarget;
public bool InDetectable
{
get { return Vector2.Zero; }
}
public virtual Vector2 Position
{
get { return Vector2.Zero; }
}
public virtual Vector2 WorldPosition
{
get { return Submarine == null ? Position : Submarine.Position + Position; }
}
public virtual Vector2 DrawPosition
{
get { return Submarine == null ? Position : Submarine.DrawPosition + Position; }
}
public Submarine Submarine
{
get;
set;
}
public AITarget AiTarget
{
get { return aiTarget; }
}
public double SpawnTime
{
get { return spawnTime; }
get
{
if (aiTarget != null)
{
return aiTarget.InDetectable;
}
return false;
}
set
{
if (aiTarget != null)
{
aiTarget.InDetectable = value;
}
}
}
public double SpawnTime => spawnTime;
private readonly double spawnTime;
public Entity(Submarine submarine, ushort id)
@@ -88,7 +77,7 @@ namespace Barotrauma
if (id != NullEntityID && dictionary.ContainsKey(id))
{
throw new Exception($"ID {id} is taken by {dictionary[id].ToString()}");
throw new Exception($"ID {id} is taken by {dictionary[id]}");
}
//give a unique ID
@@ -325,6 +325,7 @@ namespace Barotrauma
get { return LevelData.Seed; }
}
public static float? ForcedDifficulty;
public float Difficulty
{
@@ -527,6 +528,7 @@ namespace Barotrauma
//create a tunnel from the lowest point in the main path to the abyss
//to ensure there's a way to the abyss in all levels
Tunnel abyssTunnel = null;
if (GenerationParams.CreateHoleToAbyss)
{
Point lowestPoint = mainPath.Nodes.First();
@@ -534,7 +536,7 @@ namespace Barotrauma
{
if (pathNode.Y < lowestPoint.Y) { lowestPoint = pathNode; }
}
var abyssTunnel = new Tunnel(
abyssTunnel = new Tunnel(
TunnelType.SidePath,
new List<Point>() { lowestPoint, new Point(lowestPoint.X, 0) },
minWidth / 2, parentTunnel: mainPath);
@@ -545,7 +547,7 @@ namespace Barotrauma
for (int j = 0; j < sideTunnelCount; j++)
{
if (mainPath.Nodes.Count < 4) { break; }
var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole);
var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole && t != abyssTunnel);
Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.Server)];
if (tunnelToBranchOff == null) { tunnelToBranchOff = mainPath; }
@@ -558,7 +560,7 @@ namespace Barotrauma
Tunnels.Add(new Tunnel(TunnelType.SidePath, sidePathNodes, pathWidth, parentTunnel: tunnelToBranchOff));
}
CalculateTunnelDistanceField(density: 1000);
CalculateTunnelDistanceField(null);
GenerateSeaFloorPositions();
GenerateAbyssArea();
GenerateCaves(mainPath);
@@ -690,7 +692,10 @@ namespace Barotrauma
}
}
}
GenerateWaypoints(tunnel, parentTunnel: tunnel.ParentTunnel);
bool connectToParentTunnel = tunnel.Type != TunnelType.Cave || tunnel.ParentTunnel.Type == TunnelType.Cave;
GenerateWaypoints(tunnel, parentTunnel: connectToParentTunnel ? tunnel.ParentTunnel : null);
EnlargePath(tunnel.Cells, tunnel.MinWidth);
foreach (var pathCell in tunnel.Cells)
{
@@ -790,6 +795,15 @@ namespace Barotrauma
cells.AddRange(abyssIsland.Cells);
}
List<Point> ruinPositions = new List<Point>();
for (int i = 0; i < GenerationParams.RuinCount; i++)
{
Point ruinSize = new Point(5000);
ruinPositions.Add(FindPosAwayFromMainPath((Math.Max(ruinSize.X, ruinSize.Y) + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true,
limits: new Rectangle(new Point(ruinSize.X / 2, ruinSize.Y / 2), Size - ruinSize)));
CalculateTunnelDistanceField(ruinPositions);
}
//----------------------------------------------------------------------------------
// initialize the cells that are still left and insert them into the cell grid
//----------------------------------------------------------------------------------
@@ -812,7 +826,9 @@ namespace Barotrauma
//----------------------------------------------------------------------------------
// mirror if needed
//----------------------------------------------------------------------------------
int asdfasdf = Rand.Int(int.MaxValue, Rand.RandSync.Server);
if (mirror)
{
HashSet<GraphEdge> mirroredEdges = new HashSet<GraphEdge>();
@@ -850,6 +866,11 @@ namespace Barotrauma
island.Area = new Rectangle(borders.Width - island.Area.Right, island.Area.Y, island.Area.Width, island.Area.Height);
}
for (int i = 0; i < ruinPositions.Count; i++)
{
ruinPositions[i] = new Point(borders.Width - ruinPositions[i].X, ruinPositions[i].Y);
}
foreach (Cave cave in Caves)
{
cave.Area = new Rectangle(borders.Width - cave.Area.Right, cave.Area.Y, cave.Area.Width, cave.Area.Height);
@@ -895,7 +916,7 @@ namespace Barotrauma
startExitPosition.X = borders.Width - startExitPosition.X;
endExitPosition.X = borders.Width - endExitPosition.X;
CalculateTunnelDistanceField(density: 1000);
CalculateTunnelDistanceField(ruinPositions);
}
foreach (VoronoiCell cell in cells)
@@ -912,8 +933,23 @@ namespace Barotrauma
foreach (Cave cave in Caves)
{
if (cave.Area.Y > 0)
{
CreatePathToClosestTunnel(cave.StartPos);
{
List<VoronoiCell> cavePathCells = CreatePathToClosestTunnel(cave.StartPos);
var mainTunnel = cave.Tunnels.Find(t => t.ParentTunnel.Type != TunnelType.Cave);
WayPoint prevWp = mainTunnel.WayPoints.First();
if (prevWp != null)
{
for (int i = 0; i < cavePathCells.Count; i++)
{
var newWaypoint = new WayPoint(cavePathCells[i].Center, SpawnType.Path, submarine: null);
ConnectWaypoints(prevWp, newWaypoint, 500.0f);
prevWp = newWaypoint;
}
var closestPathPoint = FindClosestWayPoint(prevWp.WorldPosition, mainTunnel.ParentTunnel.WayPoints);
ConnectWaypoints(prevWp, closestPathPoint, 500.0f);
}
}
List<VoronoiCell> caveCells = new List<VoronoiCell>();
@@ -939,9 +975,10 @@ namespace Barotrauma
//----------------------------------------------------------------------------------
Ruins = new List<Ruin>();
for (int i = 0; i < GenerationParams.RuinCount; i++)
for (int i = 0; i < ruinPositions.Count; i++)
{
GenerateRuin(mainPath, mirror);
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed) + i);
GenerateRuin(ruinPositions[i], mirror);
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
@@ -1003,7 +1040,6 @@ namespace Barotrauma
}
}
#if CLIENT
List<(List<VoronoiCell> cells, Cave parentCave)> cellBatches = new List<(List<VoronoiCell>, Cave)>
{
@@ -1082,7 +1118,6 @@ namespace Barotrauma
}
#endif
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server));
//----------------------------------------------------------------------------------
@@ -1100,6 +1135,11 @@ namespace Barotrauma
// connect side paths and cave branches to their parents
//----------------------------------------------------------------------------------
foreach (Ruin ruin in Ruins)
{
GenerateRuinWayPoints(ruin);
}
foreach (Tunnel tunnel in Tunnels)
{
if (tunnel.ParentTunnel == null) { continue; }
@@ -1342,18 +1382,7 @@ namespace Barotrauma
if (wayPoints.Count > 1)
{
wayPoints[wayPoints.Count - 2].linkedTo.Add(newWaypoint);
newWaypoint.linkedTo.Add(wayPoints[wayPoints.Count - 2]);
}
for (int n = 0; n < wayPoints.Count; n++)
{
if (wayPoints[n].Position != newWaypoint.Position) { continue; }
wayPoints[n].linkedTo.Add(newWaypoint);
newWaypoint.linkedTo.Add(wayPoints[n]);
break;
wayPoints[wayPoints.Count - 2].ConnectTo(newWaypoint);
}
}
@@ -1362,19 +1391,17 @@ namespace Barotrauma
//connect to the tunnel we're branching off from
if (parentTunnel != null)
{
var parentStart = FindClosestWayPoint(wayPoints.First(), parentTunnel);
var parentStart = FindClosestWayPoint(wayPoints.First().WorldPosition, parentTunnel);
if (parentStart != null)
{
wayPoints.First().linkedTo.Add(parentStart);
parentStart.linkedTo.Add(wayPoints.First());
wayPoints.First().ConnectTo(parentStart);
}
if (tunnel.Type != TunnelType.Cave || tunnel.ParentTunnel.Type == TunnelType.Cave)
{
var parentEnd = FindClosestWayPoint(wayPoints.Last(), parentTunnel);
var parentEnd = FindClosestWayPoint(wayPoints.Last().WorldPosition, parentTunnel);
if (parentEnd != null)
{
wayPoints.Last().linkedTo.Add(parentEnd);
parentEnd.linkedTo.Add(wayPoints.Last());
wayPoints.Last().ConnectTo(parentEnd);
}
}
}
@@ -1384,45 +1411,58 @@ namespace Barotrauma
{
foreach (WayPoint wayPoint in tunnel.WayPoints)
{
var closestWaypoint = FindClosestWayPoint(wayPoint, parentTunnel);
var closestWaypoint = FindClosestWayPoint(wayPoint.WorldPosition, parentTunnel);
if (closestWaypoint == null) { continue; }
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(wayPoint.WorldPosition),
ConvertUnits.ToSimUnits(closestWaypoint.WorldPosition), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null)
{
Vector2 diff = closestWaypoint.WorldPosition - wayPoint.WorldPosition;
float dist = diff.Length();
float step = ConvertUnits.ToDisplayUnits(Steering.AutopilotMinDistToPathNode) * 0.8f;
WayPoint prevWaypoint = wayPoint;
for (float x = step; x < dist - step; x += step)
{
var newWaypoint = new WayPoint(wayPoint.WorldPosition + (diff / dist * x), SpawnType.Path, submarine: null)
{
Tunnel = tunnel
};
prevWaypoint.linkedTo.Add(newWaypoint);
newWaypoint.linkedTo.Add(prevWaypoint);
prevWaypoint = newWaypoint;
}
prevWaypoint.linkedTo.Add(closestWaypoint);
closestWaypoint.linkedTo.Add(prevWaypoint);
ConnectWaypoints(wayPoint, closestWaypoint, step).ForEach(wp => wp.Tunnel = tunnel);
}
}
}
private static WayPoint FindClosestWayPoint(WayPoint wayPoint, Tunnel otherTunnel)
private List<WayPoint> ConnectWaypoints(WayPoint wp1, WayPoint wp2, float interval)
{
List<WayPoint> newWaypoints = new List<WayPoint>();
Vector2 diff = wp2.WorldPosition - wp1.WorldPosition;
float dist = diff.Length();
WayPoint prevWaypoint = wp1;
for (float x = interval; x < dist - interval; x += interval)
{
var newWaypoint = new WayPoint(wp1.WorldPosition + (diff / dist * x), SpawnType.Path, submarine: null);
prevWaypoint.ConnectTo(newWaypoint);
prevWaypoint = newWaypoint;
newWaypoints.Add(newWaypoint);
}
prevWaypoint.ConnectTo(wp2);
return newWaypoints;
}
private static WayPoint FindClosestWayPoint(Vector2 worldPosition, Tunnel otherTunnel)
{
return FindClosestWayPoint(worldPosition, otherTunnel.WayPoints);
}
private static WayPoint FindClosestWayPoint(Vector2 worldPosition, IEnumerable<WayPoint> waypoints, Func<WayPoint, bool> filter = null)
{
float closestDist = float.PositiveInfinity;
WayPoint closestWayPoint = null;
foreach (WayPoint otherWayPoint in otherTunnel.WayPoints)
foreach (WayPoint otherWayPoint in waypoints)
{
float dist = Vector2.DistanceSquared(otherWayPoint.WorldPosition, wayPoint.WorldPosition);
float dist = Vector2.DistanceSquared(otherWayPoint.WorldPosition, worldPosition);
if (dist < closestDist)
{
if (filter != null)
{
if (!filter(otherWayPoint)) { continue; }
}
closestDist = dist;
closestWayPoint = otherWayPoint;
}
}
return closestWayPoint;
@@ -1705,7 +1745,7 @@ namespace Barotrauma
GenerateCave(caveParams, parentTunnel, cavePos, caveSize);
CalculateTunnelDistanceField(density: 1000);
CalculateTunnelDistanceField(null);
}
}
@@ -1787,84 +1827,145 @@ namespace Barotrauma
}
}
private void GenerateRuin(Tunnel mainPath, bool mirror)
private void GenerateRuin(Point ruinPos, bool mirror)
{
var ruinGenerationParams = RuinGenerationParams.GetRandom();
Point ruinSize = new Point(
Rand.Range(ruinGenerationParams.SizeMin.X, ruinGenerationParams.SizeMax.X, Rand.RandSync.Server),
Rand.Range(ruinGenerationParams.SizeMin.Y, ruinGenerationParams.SizeMax.Y, Rand.RandSync.Server));
int ruinRadius = Math.Max(ruinSize.X, ruinSize.Y) / 2;
Point ruinPos = FindPosAwayFromMainPath((ruinRadius + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true,
limits: new Rectangle(new Point(ruinSize.X / 2, ruinSize.Y / 2), Size - ruinSize));
VoronoiCell closestPathCell = null;
double closestDist = 0.0f;
foreach (VoronoiCell pathCell in mainPath.Cells)
LocationType locationType = StartLocation?.Type;
if (locationType == null)
{
double dist = MathUtils.DistanceSquared(pathCell.Site.Coord.X, pathCell.Site.Coord.Y, ruinPos.X, ruinPos.Y);
if (closestPathCell == null || dist < closestDist)
locationType = LocationType.List.GetRandom(Rand.RandSync.Server);
if (ruinGenerationParams.AllowedLocationTypes.Any())
{
closestPathCell = pathCell;
closestDist = dist;
locationType = LocationType.List.Where(lt =>
ruinGenerationParams.AllowedLocationTypes.Any(allowedType =>
allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom();
}
}
var ruin = new Ruin(closestPathCell, cells, ruinGenerationParams, new Rectangle(ruinPos - new Point(ruinSize.X / 2, ruinSize.Y / 2), ruinSize), mirror);
var ruin = new Ruin(this, ruinGenerationParams, locationType, ruinPos, mirror);
Ruins.Add(ruin);
ruin.RuinShapes.Sort((shape1, shape2) => shape2.DistanceFromEntrance.CompareTo(shape1.DistanceFromEntrance));
// TODO: autogenerate waypoints inside the ruins and connect them to the main path in multiple places.
// We need the waypoints for the AI navigation and we could use them for spawning the creatures too.
int waypointCount = 0;
foreach (WayPoint wp in WayPoint.WayPointList)
var tooClose = GetTooCloseCells(ruinPos.ToVector2(), Math.Max(ruin.Area.Width, ruin.Area.Height) * 4);
foreach (VoronoiCell cell in tooClose)
{
if (wp.SpawnType != SpawnType.Enemy || wp.Submarine != null) { continue; }
if (ruin.RuinShapes.Any(rs => rs.Rect.Contains(wp.WorldPosition)))
if (cell.CellType == CellType.Empty) { continue; }
if (ExtraWalls.Any(w => w.Cells.Contains(cell))) { continue; }
foreach (GraphEdge e in cell.Edges)
{
PositionsOfInterest.Add(new InterestingPosition(new Point((int)wp.WorldPosition.X, (int)wp.WorldPosition.Y), PositionType.Ruin, ruin: ruin));
waypointCount++;
}
}
//not enough waypoints inside ruins -> create some spawn positions manually
for (int i = 0; i < 4 - waypointCount && i < ruin.RuinShapes.Count; i++)
{
PositionsOfInterest.Add(new InterestingPosition(ruin.RuinShapes[i].Rect.Center, PositionType.Ruin, ruin: ruin));
}
foreach (RuinShape ruinShape in ruin.RuinShapes)
{
var tooClose = GetTooCloseCells(ruinShape.Rect.Center.ToVector2(), Math.Max(ruinShape.Rect.Width, ruinShape.Rect.Height) * 4);
foreach (VoronoiCell cell in tooClose)
{
if (cell.CellType == CellType.Empty) { continue; }
if (ExtraWalls.Any(w => w.Cells.Contains(cell))) { continue; }
foreach (GraphEdge e in cell.Edges)
if (ruin.Area.Contains(e.Point1) || ruin.Area.Contains(e.Point2) ||
MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, ruin.Area, out _))
{
Rectangle rect = ruinShape.Rect;
rect.Y += rect.Height;
if (ruinShape.Rect.Contains(e.Point1) || ruinShape.Rect.Contains(e.Point2) ||
MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, rect, out _))
cell.CellType = CellType.Removed;
for (int x = 0; x < cellGrid.GetLength(0); x++)
{
cell.CellType = CellType.Removed;
for (int x = 0; x < cellGrid.GetLength(0); x++)
for (int y = 0; y < cellGrid.GetLength(1); y++)
{
for (int y = 0; y < cellGrid.GetLength(1); y++)
{
cellGrid[x, y].Remove(cell);
}
cellGrid[x, y].Remove(cell);
}
cells.Remove(cell);
break;
}
cells.Remove(cell);
break;
}
}
}
CreatePathToClosestTunnel(ruinPos);
ruin.PathCells = CreatePathToClosestTunnel(ruin.Area.Center);
}
private void GenerateRuinWayPoints(Ruin ruin)
{
var tooClose = GetTooCloseCells(ruin.Area.Center.ToVector2(), Math.Max(ruin.Area.Width, ruin.Area.Height) * 6);
List<WayPoint> wayPoints = new List<WayPoint>();
float outSideWaypointInterval = 500.0f;
WayPoint[,] cornerWaypoint = new WayPoint[2, 2];
Rectangle waypointArea = ruin.Area;
waypointArea.Inflate(100, 100);
//generate waypoints around the ruin
for (int i = 0; i < 2; i++)
{
for (float x = waypointArea.X + outSideWaypointInterval; x < waypointArea.Right - outSideWaypointInterval; x += outSideWaypointInterval)
{
var wayPoint = new WayPoint(new Vector2(x, waypointArea.Y + waypointArea.Height * i), SpawnType.Path, null);
wayPoints.Add(wayPoint);
if (x == waypointArea.X + outSideWaypointInterval)
{
cornerWaypoint[i, 0] = wayPoint;
}
else
{
wayPoint.ConnectTo(wayPoints[wayPoints.Count - 2]);
}
}
cornerWaypoint[i, 1] = wayPoints[wayPoints.Count - 1];
}
for (int i = 0; i < 2; i++)
{
WayPoint wayPoint = null;
for (float y = waypointArea.Y; y < waypointArea.Y + waypointArea.Height; y += outSideWaypointInterval)
{
wayPoint = new WayPoint(new Vector2(waypointArea.X + waypointArea.Width * i, y), SpawnType.Path, null);
wayPoints.Add(wayPoint);
if (y == waypointArea.Y)
{
wayPoint.ConnectTo(cornerWaypoint[0, i]);
}
else
{
wayPoint.ConnectTo(wayPoints[wayPoints.Count - 2]);
}
}
wayPoint.ConnectTo(cornerWaypoint[1, i]);
}
//remove waypoints that are inside walls
for (int i = wayPoints.Count - 1; i >= 0; i--)
{
WayPoint wp = wayPoints[i];
var overlappingCell = tooClose.Find(c => c.CellType != CellType.Removed && c.IsPointInside(wp.WorldPosition));
if (overlappingCell == null) { continue; }
if (wp.linkedTo.Count > 1)
{
WayPoint linked1 = wp.linkedTo[0] as WayPoint;
WayPoint linked2 = wp.linkedTo[1] as WayPoint;
linked1.ConnectTo(linked2);
}
wp.Remove();
wayPoints.RemoveAt(i);
}
//connect ruin entrances to the outside waypoints
foreach (Gap g in Gap.GapList)
{
if (g.Submarine != ruin.Submarine || g.IsRoomToRoom) { continue; }
var gapWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == g);
if (gapWaypoint == null) { continue; }
var closestWp = FindClosestWayPoint(gapWaypoint.WorldPosition, wayPoints);
if (closestWp == null) { continue; }
gapWaypoint.ConnectTo(closestWp);
}
//create a waypoint path from the ruin to the closest tunnel
WayPoint prevWp = FindClosestWayPoint(ruin.PathCells.First().Center, wayPoints, (wp) =>
{
return Submarine.PickBody(
ConvertUnits.ToSimUnits(wp.WorldPosition),
ConvertUnits.ToSimUnits(ruin.PathCells.First().Center), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null;
});
if (prevWp != null)
{
for (int i = 0; i < ruin.PathCells.Count; i++)
{
var newWaypoint = new WayPoint(ruin.PathCells[i].Center, SpawnType.Path, submarine: null);
ConnectWaypoints(prevWp, newWaypoint, outSideWaypointInterval);
prevWp = newWaypoint;
}
var closestPathPoint = FindClosestWayPoint(prevWp.WorldPosition, Tunnels.SelectMany(t => t.WayPoints));
ConnectWaypoints(prevWp, closestPathPoint, outSideWaypointInterval);
}
}
private Point FindPosAwayFromMainPath(double minDistance, bool asCloseAsPossible, Rectangle? limits = null)
@@ -1890,8 +1991,9 @@ namespace Barotrauma
}
}
private void CalculateTunnelDistanceField(int density)
private void CalculateTunnelDistanceField(List<Point> ruinPositions)
{
int density = 1000;
distanceField = new List<(Point point, double distance)>();
if (Mirrored)
@@ -1926,6 +2028,23 @@ namespace Barotrauma
shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point));
}
}
if (ruinPositions != null)
{
int ruinSize = 10000;
foreach (Point ruinPos in ruinPositions)
{
double xDiff = Math.Abs(point.X - ruinPos.X);
double yDiff = Math.Abs(point.Y - ruinPos.Y);
if (xDiff < ruinSize || yDiff < ruinSize)
{
shortestDistSqr = 0.0f;
}
else
{
shortestDistSqr = Math.Min(xDiff * xDiff + yDiff * yDiff, shortestDistSqr);
}
}
}
shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startPosition.X, (double)startPosition.Y));
shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startExitPosition.X, (double)borders.Bottom));
shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endPosition.X, (double)endPosition.Y));
@@ -2953,7 +3072,7 @@ namespace Barotrauma
return closestCell;
}
private void CreatePathToClosestTunnel(Point pos)
private List<VoronoiCell> CreatePathToClosestTunnel(Point pos)
{
VoronoiCell closestPathCell = null;
double closestDist = 0.0f;
@@ -2973,6 +3092,7 @@ namespace Barotrauma
//cast a ray from the closest path cell towards the position and remove the cells it hits
List<VoronoiCell> validCells = cells.FindAll(c => c.CellType != CellType.Empty && c.CellType != CellType.Removed);
List<VoronoiCell> pathCells = new List<VoronoiCell>() { closestPathCell };
foreach (VoronoiCell cell in validCells)
{
foreach (GraphEdge e in cell.Edges)
@@ -2987,6 +3107,7 @@ namespace Barotrauma
cellGrid[x, y].Remove(cell);
}
}
pathCells.Add(cell);
cells.Remove(cell);
//go through the edges of this cell and find the ones that are next to a removed cell
@@ -3019,12 +3140,19 @@ namespace Barotrauma
}
}
}
break;
}
}
pathCells.Sort((c1, c2) => { return Vector2.DistanceSquared(c1.Center, pos.ToVector2()).CompareTo(Vector2.DistanceSquared(c2.Center, pos.ToVector2())); });
return pathCells;
}
public string GetWreckIDTag(string originalTag, Submarine wreck)
{
string shortSeed = ToolBox.StringToInt(LevelData.Seed + wreck?.Info.Name).ToString();
if (shortSeed.Length > 6) { shortSeed = shortSeed.Substring(0, 6); }
return originalTag + "_" + shortSeed;
}
public bool IsCloseToStart(Vector2 position, float minDist) => IsCloseToStart(position.ToPoint(), minDist);
@@ -3049,10 +3177,8 @@ namespace Barotrauma
var waypoints = WayPoint.WayPointList.Where(wp =>
wp.Submarine == null &&
wp.SpawnType == SpawnType.Path &&
wp.WorldPosition.X < EndExitPosition.X &&
!IsCloseToStart(wp.WorldPosition, minDistance) &&
!IsCloseToEnd(wp.WorldPosition, minDistance)
).ToList();
!IsCloseToEnd(wp.WorldPosition, minDistance)).ToList();
var subDoc = SubmarineInfo.OpenFile(contentFile.Path);
Rectangle subBorders = Submarine.GetBorders(subDoc.Root);
@@ -3137,7 +3263,7 @@ namespace Barotrauma
}
tempSW.Stop();
Debug.WriteLine($"Sub {sub.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)");
sub.SetPosition(spawnPoint, forceUndockFromStaticSubmarines: false);
sub.SetPosition(spawnPoint);
wreckPositions.Add(sub, positions);
blockedRects.Add(sub, rects);
return sub;
@@ -84,25 +84,42 @@ namespace Barotrauma
objectGrid = new List<LevelObject>[
level.Size.X / GridSize,
(level.Size.Y - level.BottomPos) / GridSize];
List<SpawnPosition> availableSpawnPositions = new List<SpawnPosition>();
var levelCells = level.GetAllCells();
availableSpawnPositions.AddRange(GetAvailableSpawnPositions(levelCells, LevelObjectPrefab.SpawnPosType.Wall));
availableSpawnPositions.AddRange(GetAvailableSpawnPositions(levelCells, LevelObjectPrefab.SpawnPosType.Wall));
availableSpawnPositions.AddRange(GetAvailableSpawnPositions(level.SeaFloor.Cells, LevelObjectPrefab.SpawnPosType.SeaFloor));
foreach (RuinGeneration.Ruin ruin in level.Ruins)
foreach (Structure structure in Structure.WallList)
{
foreach (var ruinShape in ruin.RuinShapes)
if (!structure.HasBody || structure.HiddenInGame) { continue; }
if (level.Ruins.Any(r => r.Submarine == structure.Submarine))
{
foreach (var wall in ruinShape.Walls)
if (structure.IsHorizontal)
{
bool topHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitY * 64) != null;
bool bottomHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitY * 64) != null;
if (topHull && bottomHull ) { continue; }
availableSpawnPositions.Add(new SpawnPosition(
new GraphEdge(wall.A, wall.B),
(wall.A + wall.B) / 2.0f - ruinShape.Center,
new GraphEdge(new Vector2(structure.WorldRect.X, structure.WorldPosition.Y), new Vector2(structure.WorldRect.Right, structure.WorldPosition.Y)),
bottomHull ? Vector2.UnitY : -Vector2.UnitY,
LevelObjectPrefab.SpawnPosType.RuinWall,
ruinShape.GetLineAlignment(wall)));
bottomHull ? Alignment.Bottom : Alignment.Top));
}
}
else
{
bool rightHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitX * 64) != null;
bool leftHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitX * 64) != null;
if (rightHull && leftHull) { continue; }
availableSpawnPositions.Add(new SpawnPosition(
new GraphEdge(new Vector2(structure.WorldPosition.X, structure.WorldRect.Y), new Vector2(structure.WorldPosition.X, structure.WorldRect.Y - structure.WorldRect.Height)),
leftHull ? Vector2.UnitX : -Vector2.UnitX,
LevelObjectPrefab.SpawnPosType.RuinWall,
leftHull ? Alignment.Left : Alignment.Right));
}
}
}
foreach (var posOfInterest in level.PositionsOfInterest)
@@ -13,7 +13,7 @@ namespace Barotrauma
partial class LevelTrigger
{
[Flags]
enum TriggererType
public enum TriggererType
{
None = 0,
Human = 1,
@@ -258,7 +258,11 @@ namespace Barotrauma
{
DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type.");
}
UpdateCollisionCategories();
if (PhysicsBody != null)
{
PhysicsBody.CollidesWith = GetCollisionCategories(triggeredBy);
}
TriggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f);
var tagsArray = element.GetAttributeStringArray("tags", new string[0]);
@@ -276,26 +280,17 @@ namespace Barotrauma
}
}
string debugName = string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : $"LevelTrigger in {parentDebugName}";
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
statusEffects.Add(StatusEffect.Load(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in "+ parentDebugName));
LoadStatusEffect(statusEffects, subElement, debugName);
break;
case "attack":
case "damage":
var attack = new Attack(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in " + parentDebugName);
if (!triggerOnce)
{
var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step);
attack.Afflictions.Clear();
foreach (Affliction affliction in multipliedAfflictions)
{
attack.Afflictions.Add(affliction, null);
}
}
attacks.Add(attack);
LoadAttack(subElement, debugName, triggerOnce, attacks);
break;
}
}
@@ -304,16 +299,13 @@ namespace Barotrauma
randomTriggerTimer = Rand.Range(0.0f, randomTriggerInterval);
}
private void UpdateCollisionCategories()
public static Category GetCollisionCategories(TriggererType triggeredBy)
{
if (PhysicsBody == null) return;
var collidesWith = Physics.CollisionNone;
if (triggeredBy.HasFlag(TriggererType.Human) || triggeredBy.HasFlag(TriggererType.Creature)) { collidesWith |= Physics.CollisionCharacter; }
if (triggeredBy.HasFlag(TriggererType.Item)) { collidesWith |= Physics.CollisionItem | Physics.CollisionProjectile; }
if (triggeredBy.HasFlag(TriggererType.Submarine)) { collidesWith |= Physics.CollisionWall; }
PhysicsBody.CollidesWith = collidesWith;
return collidesWith;
}
private void CalculateDirectionalForce()
@@ -326,33 +318,31 @@ namespace Barotrauma
-sa * unrotatedForce.X + ca * unrotatedForce.Y);
}
private bool PhysicsBody_OnCollision(Fixture fixtureA, Fixture fixtureB, FarseerPhysics.Dynamics.Contacts.Contact contact)
public static void LoadStatusEffect(List<StatusEffect> statusEffects, XElement element, string parentDebugName)
{
statusEffects.Add(StatusEffect.Load(element, parentDebugName));
}
public static void LoadAttack(XElement element, string parentDebugName, bool triggerOnce, List<Attack> attacks)
{
var attack = new Attack(element, parentDebugName);
if (!triggerOnce)
{
var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step);
attack.Afflictions.Clear();
foreach (Affliction affliction in multipliedAfflictions)
{
attack.Afflictions.Add(affliction, null);
}
}
attacks.Add(attack);
}
private bool PhysicsBody_OnCollision(Fixture fixtureA, Fixture fixtureB, Contact contact)
{
Entity entity = GetEntity(fixtureB);
if (entity == null) return false;
if (entity is Character character)
{
if (character.CurrentHull != null) return false;
if (character.IsHuman)
{
if (!triggeredBy.HasFlag(TriggererType.Human)) return false;
}
else
{
if (!triggeredBy.HasFlag(TriggererType.Creature)) return false;
}
}
else if (entity is Item item)
{
if (item.CurrentHull != null) return false;
if (!triggeredBy.HasFlag(TriggererType.Item)) return false;
}
else if (entity is Submarine)
{
if (!triggeredBy.HasFlag(TriggererType.Submarine)) return false;
}
if (entity == null) { return false; }
if (!IsTriggeredByEntity(entity, triggeredBy, mustBeOutside: true)) { return false; }
if (!triggerers.Contains(entity))
{
if (!IsTriggered)
@@ -365,6 +355,34 @@ namespace Barotrauma
return true;
}
public static bool IsTriggeredByEntity(Entity entity, TriggererType triggeredBy, bool mustBeOutside = false, (bool mustBe, Submarine sub) mustBeOnSpecificSub = default)
{
if (entity is Character character)
{
if (mustBeOutside && character.CurrentHull != null) { return false; }
if (mustBeOnSpecificSub.mustBe && character.Submarine != mustBeOnSpecificSub.sub) { return false; }
if (character.IsHuman)
{
if (!triggeredBy.HasFlag(TriggererType.Human)) { return false; }
}
else
{
if (!triggeredBy.HasFlag(TriggererType.Creature)) { return false; }
}
}
else if (entity is Item item)
{
if (mustBeOutside && item.CurrentHull != null) { return false; }
if (mustBeOnSpecificSub.mustBe && item.Submarine != mustBeOnSpecificSub.sub) { return false; }
if (!triggeredBy.HasFlag(TriggererType.Item)) { return false; }
}
else if (entity is Submarine)
{
if (!triggeredBy.HasFlag(TriggererType.Submarine)) { return false; }
}
return true;
}
private void PhysicsBody_OnSeparation(Fixture fixtureA, Fixture fixtureB, Contact contact)
{
Entity entity = GetEntity(fixtureB);
@@ -379,10 +397,21 @@ namespace Barotrauma
return;
}
if (CheckContactsForOtherFixtures(PhysicsBody, fixtureB, entity)) { return; }
if (triggerers.Contains(entity))
{
TriggererPosition.Remove(entity);
triggerers.Remove(entity);
}
}
public static bool CheckContactsForOtherFixtures(PhysicsBody triggerBody, Fixture otherFixture, Entity separatingEntity)
{
//check if there are contacts with any other fixture of the trigger
//(the OnSeparation callback happens when two fixtures separate,
//e.g. if a body stops touching the circular fixture at the end of a capsule-shaped body)
foreach (Fixture fixture in PhysicsBody.FarseerBody.FixtureList)
foreach (Fixture fixture in triggerBody.FarseerBody.FixtureList)
{
ContactEdge contactEdge = fixture.Body.ContactList;
while (contactEdge != null)
@@ -393,30 +422,24 @@ namespace Barotrauma
{
if (contactEdge.Contact.FixtureA != fixture && contactEdge.Contact.FixtureB != fixture)
{
var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ?
var otherEntity = GetEntity(contactEdge.Contact.FixtureB == otherFixture ?
contactEdge.Contact.FixtureB :
contactEdge.Contact.FixtureA);
if (otherEntity == entity) { return; }
if (otherEntity == separatingEntity) { return true; }
}
}
contactEdge = contactEdge.Next;
}
}
if (triggerers.Contains(entity))
{
TriggererPosition.Remove(entity);
triggerers.Remove(entity);
}
return false;
}
private Entity GetEntity(Fixture fixture)
public static Entity GetEntity(Fixture fixture)
{
if (fixture.Body == null || fixture.Body.UserData == null) { return null; }
if (fixture.Body.UserData is Entity entity) { return entity; }
if (fixture.Body.UserData is Limb limb) { return limb.character; }
if (fixture.Body.UserData is SubmarineBody subBody) { return subBody.Submarine; }
return null;
}
@@ -452,15 +475,7 @@ namespace Barotrauma
triggerers.RemoveWhere(t => t.Removed);
if (PhysicsBody != null)
{
//failsafe to ensure triggerers get removed when they're far from the trigger
float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(PhysicsBody.GetMaxExtent() * 5), 5000.0f);
triggerers.RemoveWhere(t =>
{
return Vector2.Distance(t.WorldPosition, WorldPosition) > maxExtent;
});
}
RemoveDistantTriggerers(PhysicsBody, triggerers, WorldPosition);
bool isNotClient = true;
#if CLIENT
@@ -525,57 +540,15 @@ namespace Barotrauma
foreach (Entity triggerer in triggerers)
{
foreach (StatusEffect effect in statusEffects)
{
if (effect.type == ActionType.OnBroken) { continue; }
Vector2? position = null;
if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = WorldPosition; }
if (triggerer is Character character)
{
effect.Apply(effect.type, deltaTime, triggerer, character, position);
if (effect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory != null)
{
foreach (Item item in character.Inventory.AllItemsMod)
{
if (item.ContainedItems == null) { continue; }
foreach (Item containedItem in item.ContainedItems)
{
effect.Apply(effect.type, deltaTime, triggerer, containedItem.AllPropertyObjects, position);
}
}
}
}
else if (triggerer is Item item)
{
effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(worldPosition, targets));
effect.Apply(effect.type, deltaTime, triggerer, targets);
}
}
ApplyStatusEffects(statusEffects, worldPosition, triggerer, deltaTime, targets);
if (triggerer is IDamageable damageable)
{
foreach (Attack attack in attacks)
{
attack.DoDamage(null, damageable, WorldPosition, deltaTime, false);
}
ApplyAttacks(attacks, damageable, worldPosition, deltaTime);
}
else if (triggerer is Submarine submarine)
{
foreach (Attack attack in attacks)
{
float structureDamage = attack.GetStructureDamage(deltaTime);
if (structureDamage > 0.0f)
{
Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f);
}
}
ApplyAttacks(attacks, worldPosition, deltaTime);
if (!string.IsNullOrWhiteSpace(InfectIdentifier))
{
submarine.AttemptBallastFloraInfection(InfectIdentifier, deltaTime, InfectionChance);
@@ -586,16 +559,16 @@ namespace Barotrauma
{
if (triggerer is Character character)
{
ApplyForce(character.AnimController.Collider, deltaTime);
ApplyForce(character.AnimController.Collider);
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
ApplyForce(limb.body, deltaTime);
ApplyForce(limb.body);
}
}
else if (triggerer is Submarine submarine)
{
ApplyForce(submarine.SubBody.Body, deltaTime);
ApplyForce(submarine.SubBody.Body);
}
}
@@ -606,12 +579,84 @@ namespace Barotrauma
}
}
private void ApplyForce(PhysicsBody body, float deltaTime)
public static void RemoveDistantTriggerers(PhysicsBody physicsBody, HashSet<Entity> triggerers, Vector2 calculateDistanceTo)
{
//failsafe to ensure triggerers get removed when they're far from the trigger
if (physicsBody == null) { return; }
float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(physicsBody.GetMaxExtent() * 5), 5000.0f);
triggerers.RemoveWhere(t =>
{
return Vector2.Distance(t.WorldPosition, calculateDistanceTo) > maxExtent;
});
}
public static void ApplyStatusEffects(List<StatusEffect> statusEffects, Vector2 worldPosition, Entity triggerer, float deltaTime, List<ISerializableEntity> targets)
{
foreach (StatusEffect effect in statusEffects)
{
if (effect.type == ActionType.OnBroken) { return; }
Vector2? position = null;
if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = worldPosition; }
if (triggerer is Character character)
{
effect.Apply(effect.type, deltaTime, triggerer, character, position);
if (effect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory != null)
{
foreach (Item item in character.Inventory.AllItemsMod)
{
if (item.ContainedItems == null) { continue; }
foreach (Item containedItem in item.ContainedItems)
{
effect.Apply(effect.type, deltaTime, triggerer, containedItem.AllPropertyObjects, position);
}
}
}
}
else if (triggerer is Item item)
{
effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(worldPosition, targets));
effect.Apply(effect.type, deltaTime, triggerer, targets);
}
}
}
/// <summary>
/// Applies attacks to a damageable.
/// </summary>
public static void ApplyAttacks(List<Attack> attacks, IDamageable damageable, Vector2 worldPosition, float deltaTime)
{
foreach (Attack attack in attacks)
{
attack.DoDamage(null, damageable, worldPosition, deltaTime, false);
}
}
/// <summary>
/// Applies attacks to structures.
/// </summary>
public static void ApplyAttacks(List<Attack> attacks, Vector2 worldPosition, float deltaTime)
{
foreach (Attack attack in attacks)
{
float structureDamage = attack.GetStructureDamage(deltaTime);
if (structureDamage > 0.0f)
{
Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f);
}
}
}
private void ApplyForce(PhysicsBody body)
{
float distFactor = 1.0f;
if (ForceFalloff)
{
distFactor = 1.0f - ConvertUnits.ToDisplayUnits(Vector2.Distance(body.SimPosition, PhysicsBody.SimPosition)) / ColliderRadius;
distFactor = GetDistanceFactor(body, PhysicsBody, ColliderRadius);
if (distFactor < 0.0f) return;
}
@@ -648,6 +693,11 @@ namespace Barotrauma
}
}
public static float GetDistanceFactor(PhysicsBody triggererBody, PhysicsBody triggerBody, float colliderRadius)
{
return 1.0f - ConvertUnits.ToDisplayUnits(Vector2.Distance(triggererBody.SimPosition, triggerBody.SimPosition)) / colliderRadius;
}
public Vector2 GetWaterFlowVelocity(Vector2 viewPosition)
{
Vector2 baseVel = GetWaterFlowVelocity();
@@ -1,190 +0,0 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.RuinGeneration
{
/// <summary>
/// nodes of a binary tree used for generating underwater "dungeons"
/// </summary>
class BTRoom : RuinShape
{
private BTRoom[] subRooms;
public BTRoom Parent
{
get;
private set;
}
public Corridor Corridor
{
get;
set;
}
public BTRoom[] SubRooms
{
get { return subRooms; }
}
public BTRoom Adjacent
{
get;
private set;
}
public BTRoom(Rectangle rect)
{
this.rect = rect;
}
public void Split(float minDivRatio, float verticalProbability = 0.5f, int minWidth = 200, int minHeight = 200)
{
bool verticalSplit = Rand.Range(0.0f, rect.Height / (float)rect.Width, Rand.RandSync.Server) < verticalProbability;
if (rect.Width * minDivRatio < minWidth && rect.Height * minDivRatio < minHeight)
{
minDivRatio = 0.5f;
}
else if (rect.Width * minDivRatio < minWidth)
{
verticalSplit = false;
}
else if (rect.Height * minDivRatio < minHeight)
{
verticalSplit = true;
}
subRooms = new BTRoom[2];
if (verticalSplit)
{
SplitVertical(minDivRatio);
}
else
{
SplitHorizontal(minDivRatio);
}
subRooms[0].Parent = this;
subRooms[1].Parent = this;
subRooms[0].Adjacent = subRooms[1];
subRooms[1].Adjacent = subRooms[0];
}
private void SplitHorizontal(float minDivRatio)
{
float div = Rand.Range(minDivRatio, 1.0f - minDivRatio, Rand.RandSync.Server);
subRooms[0] = new BTRoom(new Rectangle(rect.X, rect.Y, rect.Width, (int)(rect.Height * div)));
subRooms[1] = new BTRoom(new Rectangle(rect.X, rect.Y + subRooms[0].rect.Height, rect.Width, rect.Height - subRooms[0].rect.Height));
}
private void SplitVertical(float minDivRatio)
{
float div = Rand.Range(minDivRatio, 1.0f - minDivRatio, Rand.RandSync.Server);
subRooms[0] = new BTRoom(new Rectangle(rect.X, rect.Y, (int)(rect.Width * div), rect.Height));
subRooms[1] = new BTRoom(new Rectangle(rect.X + subRooms[0].rect.Width, rect.Y, rect.Width - subRooms[0].rect.Width, rect.Height));
}
public override void CreateWalls()
{
Walls = new List<Line>
{
new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.Right, Rect.Y)),
new Line(new Vector2(Rect.X, Rect.Bottom), new Vector2(Rect.Right, Rect.Bottom)),
new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.X, Rect.Bottom)),
new Line(new Vector2(Rect.Right, Rect.Y), new Vector2(Rect.Right, Rect.Bottom))
};
}
public void Scale(Vector2 scale)
{
rect.Inflate((scale.X - 1.0f) * 0.5f * rect.Width, (scale.Y - 1.0f) * 0.5f * rect.Height);
}
public List<BTRoom> GetLeaves()
{
return GetLeaves(new List<BTRoom>());
}
private List<BTRoom> GetLeaves(List<BTRoom> leaves)
{
if (subRooms == null)
{
leaves.Add(this);
}
else
{
subRooms[0].GetLeaves(leaves);
subRooms[1].GetLeaves(leaves);
}
return leaves;
}
public void GenerateCorridors(int minWidth, int maxWidth, List<Corridor> corridors)
{
if (Adjacent != null && Corridor == null)
{
Corridor = new Corridor(this, Rand.Range(minWidth, maxWidth, Rand.RandSync.Server), corridors);
}
if (subRooms != null)
{
subRooms[0].GenerateCorridors(minWidth, maxWidth, corridors);
subRooms[1].GenerateCorridors(minWidth, maxWidth, corridors);
}
}
public static void CalculateDistancesFromEntrance(BTRoom entrance, List<BTRoom> rooms, List<Corridor> corridors)
{
entrance.CalculateDistanceFromEntrance(0, rooms, new List<Corridor>(corridors));
}
private void CalculateDistanceFromEntrance(int currentDist, List<BTRoom> rooms, List<Corridor> corridors)
{
DistanceFromEntrance = DistanceFromEntrance == 0 ? currentDist : Math.Min(currentDist, DistanceFromEntrance);
currentDist++;
var roomRect = Rect;
roomRect.Inflate(5, 5);
foreach (var corridor in corridors)
{
var corridorRect = corridor.Rect;
corridorRect.Inflate(5, 5);
if (!corridorRect.Intersects(roomRect)) continue;
corridor.DistanceFromEntrance = corridor.DistanceFromEntrance == 0 ?
DistanceFromEntrance + 1 :
Math.Min(corridor.DistanceFromEntrance, DistanceFromEntrance + 1);
List<BTRoom> connectedRooms = new List<BTRoom>();
foreach (var otherRoom in rooms)
{
if (otherRoom == this) continue;
if (otherRoom.DistanceFromEntrance > 0 && otherRoom.DistanceFromEntrance < currentDist) continue;
var otherRoomRect = otherRoom.Rect;
otherRoomRect.Inflate(5, 5);
if (corridorRect.Intersects(otherRoomRect)) { connectedRooms.Add(otherRoom); }
}
connectedRooms.Sort((r1, r2) =>
{
return
(Math.Abs(r1.Rect.Center.X - Rect.Center.X) + Math.Abs(r1.Rect.Center.Y - Rect.Center.Y)) -
(Math.Abs(r2.Rect.Center.X - Rect.Center.X) + Math.Abs(r2.Rect.Center.Y - Rect.Center.Y));
});
for (int i = 0; i < connectedRooms.Count; i++)
{
connectedRooms[i].CalculateDistanceFromEntrance(currentDist + 1 + i, rooms, corridors);
}
}
}
}
}
@@ -1,210 +0,0 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma.RuinGeneration
{
class Corridor : RuinShape
{
private readonly bool isHorizontal;
public bool IsHorizontal
{
get { return isHorizontal; }
}
public BTRoom[] ConnectedRooms
{
get;
private set;
}
public Corridor(Rectangle rect)
{
this.rect = rect;
isHorizontal = rect.Width > rect.Height;
}
public Corridor(BTRoom room, int width, List<Corridor> corridors)
{
System.Diagnostics.Debug.Assert(room.Adjacent != null);
ConnectedRooms = new BTRoom[2];
ConnectedRooms[0] = room;
ConnectedRooms[1] = room.Adjacent;
Rectangle room1, room2;
room1 = room.Rect;
room2 = room.Adjacent.Rect;
isHorizontal = (room1.Right <= room2.X || room2.Right <= room1.X);
//use the leaves as starting points for the corridor
if (room.SubRooms != null)
{
var leaves1 = room.GetLeaves();
var leaves2 = room.Adjacent.GetLeaves();
var suitableLeaves = GetSuitableLeafRooms(leaves1, leaves2, width, isHorizontal);
if (suitableLeaves == null || suitableLeaves.Length < 2)
{
// No suitable leaves found due to intersections
//DebugConsole.ThrowError("Error while generating ruins. Could not find a suitable position for a corridor. The width of the corridors may be too large compared to the sizes of the rooms.");
return;
}
else
{
ConnectedRooms[0] = suitableLeaves[0];
ConnectedRooms[1] = suitableLeaves[1];
}
}
else
{
rect = CalculateRectangle(room1, room2, width, isHorizontal);
if (rect.Width <= 0 || rect.Height <= 0)
{
DebugConsole.ThrowError("Error while generating ruins. Attempted to create a corridor with a width or height of <= 0");
return;
}
}
room.Corridor = this;
room.Adjacent.Corridor = this;
for (int i = corridors.Count - 1; i >= 0; i--)
{
var corridor = corridors[i];
if (corridor.rect.Intersects(this.rect))
{
if (isHorizontal && corridor.isHorizontal)
{
if (this.rect.Width < corridor.rect.Width)
return;
else
corridors.RemoveAt(i);
}
else if (!isHorizontal && !corridor.isHorizontal)
{
if (this.rect.Height < corridor.rect.Height)
return;
else
corridors.RemoveAt(i);
}
}
}
corridors.Add(this);
}
public override void CreateWalls()
{
Walls = new List<Line>();
if (IsHorizontal)
{
Walls.Add(new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.Right, Rect.Y)));
Walls.Add(new Line(new Vector2(Rect.X, Rect.Bottom), new Vector2(Rect.Right, Rect.Bottom)));
}
else
{
Walls.Add(new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.X, Rect.Bottom)));
Walls.Add(new Line(new Vector2(Rect.Right, Rect.Y), new Vector2(Rect.Right, Rect.Bottom)));
}
}
/// <summary>
/// Find two rooms which have two face-two-face walls that we can place a corridor in between
/// </summary>
/// <returns></returns>
private BTRoom[] GetSuitableLeafRooms(List<BTRoom> leaves1, List<BTRoom> leaves2, int width, bool isHorizontal)
{
int iOffset = Rand.Int(leaves1.Count, Rand.RandSync.Server);
int jOffset = Rand.Int(leaves2.Count, Rand.RandSync.Server);
for (int iCount = 0; iCount < leaves1.Count; iCount++)
{
int i = (iCount + iOffset) % leaves1.Count;
for (int jCount = 0; jCount < leaves2.Count; jCount++)
{
int j = (jCount + jOffset) % leaves2.Count;
if (isHorizontal)
{
if (leaves1[i].Rect.Y > leaves2[j].Rect.Bottom - width) continue;
if (leaves1[i].Rect.Bottom < leaves2[j].Rect.Y + width) continue;
}
else
{
if (leaves1[i].Rect.X > leaves2[j].Rect.Right - width) continue;
if (leaves1[i].Rect.Right < leaves2[j].Rect.X + width) continue;
}
// Check if the given corridor rect would intersect over a third room
if (CheckForIntersection(leaves1[i], leaves2[j], leaves1, leaves2, width, isHorizontal)) continue;
return new BTRoom[] { leaves1[i], leaves2[j] };
}
}
return null;
}
private bool CheckForIntersection(BTRoom potential1, BTRoom potential2, List<BTRoom> leaves1, List<BTRoom> leaves2, int width, bool isHorizontal)
{
Rectangle potentialCorridorRectangle = CalculateRectangle(potential1.Rect, potential2.Rect, width, isHorizontal);
if (potentialCorridorRectangle.Width <= 0 || potentialCorridorRectangle.Height <= 0) return true; // Invalid rectangle
for (int i = 0; i < leaves1.Count; i++)
{
if (leaves1[i] == potential1) continue;
if (potentialCorridorRectangle.Intersects(leaves1[i].Rect)) return true;
}
for (int i = 0; i < leaves2.Count; i++)
{
if (leaves2[i] == potential2) continue;
if (potentialCorridorRectangle.Intersects(leaves2[i].Rect)) return true;
}
rect = potentialCorridorRectangle; // Save the rectangle that passes the test
return false;
}
private Rectangle CalculateRectangle(Rectangle rect1, Rectangle rect2, int width, bool isHorizontal)
{
if (isHorizontal)
{
int left = Math.Min(rect1.Right, rect2.Right);
int right = Math.Max(rect1.X, rect2.X);
int top = Math.Max(rect1.Y, rect2.Y);
//int bottom = Math.Min(room1.Bottom, room2.Bottom);
int yPos = top;//Rand.Range(top, bottom - width, Rand.RandSync.Server);
return new Rectangle(left, yPos, right - left, width);
}
else if (rect1.Y > rect2.Bottom || rect2.Y > rect1.Bottom)
{
int left = Math.Max(rect1.X, rect2.X);
int right = Math.Min(rect1.Right, rect2.Right);
int top = Math.Min(rect1.Bottom, rect2.Bottom);
int bottom = Math.Max(rect1.Y, rect2.Y);
int xPos = Rand.Range(left, right - width, Rand.RandSync.Server);
return new Rectangle(xPos, top, width, bottom - top);
}
else
{
DebugConsole.ThrowError("wat");
return new Rectangle();
}
}
}
}
@@ -18,9 +18,9 @@ namespace Barotrauma.RuinGeneration
Wall, Back, Door, Hatch, Prop
}
class RuinGenerationParams : ISerializableEntity
class RuinGenerationParams : OutpostGenerationParams
{
public static List<RuinGenerationParams> List
public static List<RuinGenerationParams> RuinParams
{
get
{
@@ -34,102 +34,14 @@ namespace Barotrauma.RuinGeneration
private static List<RuinGenerationParams> paramsList;
private string filePath;
private readonly List<RuinRoom> roomTypeList;
public string Name => "RuinGenerationParams";
private readonly string filePath;
public override string Name => "RuinGenerationParams";
[Serialize("5000,5000", false), Editable]
public Point SizeMin
{
get;
set;
}
[Serialize("8000,8000", false), Editable]
public Point SizeMax
{
get;
set;
}
[Serialize(3, false, description: "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the minimum number of times the split is done."), Editable(MinValueInt = 1, MaxValueInt = 10)]
public int RoomDivisionIterationsMin
private RuinGenerationParams(XElement element, string filePath) : base(element, filePath)
{
get;
set;
}
[Serialize(4, false, description: "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the maximum number of times the split is done."), Editable(MinValueInt = 1, MaxValueInt = 10)]
public int RoomDivisionIterationsMax
{
get;
set;
}
[Serialize(0.5f, false, description: "The probability for the split algorithm to split the area vertically. High values tend to create tall, vertical rooms, and low values wide, horizontal rooms."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.9f)]
public float VerticalSplitProbability
{
get;
set;
}
[Serialize(400, false, description: "The splitting algorithm attempts to keep the width of the split areas larger than this. If the width of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable]
public int MinSplitWidth
{
get;
set;
}
[Serialize(400, false, description: "The splitting algorithm attempts to keep the height of the split areas larger than this. If the height of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable]
public int MinSplitHeight
{
get;
set;
}
[Serialize("0.5,0.9", false, description: "The minimum and maximum width of a room relative to the areas created by the split algorithm."), Editable]
public Vector2 RoomWidthRange
{
get;
set;
}
[Serialize("0.5,0.9", false, description: "The minimum and maximum height of a room relative to the areas created by the split algorithm."), Editable]
public Vector2 RoomHeightRange
{
get;
set;
}
[Serialize("200,256", false, description: "The minimum and maximum width of the corridors between rooms."), Editable]
public Point CorridorWidthRange
{
get;
set;
}
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
} = new Dictionary<string, SerializableProperty>();
public IEnumerable<RuinRoom> RoomTypeList
{
get { return roomTypeList; }
}
private RuinGenerationParams(XElement element)
{
roomTypeList = new List<RuinRoom>();
if (element != null)
{
foreach (XElement subElement in element.Elements())
{
roomTypeList.Add(new RuinRoom(subElement));
}
}
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
this.filePath = filePath;
}
public static RuinGenerationParams GetRandom()
@@ -139,7 +51,7 @@ namespace Barotrauma.RuinGeneration
if (paramsList.Count == 0)
{
DebugConsole.ThrowError("No ruin configuration files found in any content package.");
return new RuinGenerationParams(null);
return new RuinGenerationParams(null, null);
}
return paramsList[Rand.Int(paramsList.Count, Rand.RandSync.Server)];
@@ -151,23 +63,24 @@ namespace Barotrauma.RuinGeneration
foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig))
{
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
if (doc == null) { continue; }
var mainElement = doc.Root;
if (doc.Root.IsOverride())
if (doc?.Root == null) { continue; }
foreach (XElement subElement in doc.Root.Elements())
{
mainElement = doc.Root.FirstElement();
paramsList.Clear();
DebugConsole.NewMessage($"Overriding all ruin generation parameters using the file {configFile.Path}.", Color.Yellow);
var mainElement = subElement;
if (subElement.IsOverride())
{
mainElement = subElement.FirstElement();
paramsList.Clear();
DebugConsole.NewMessage($"Overriding all ruin generation parameters using the file {configFile.Path}.", Color.Yellow);
}
else if (paramsList.Any())
{
DebugConsole.NewMessage($"Adding additional ruin generation parameters from file '{configFile.Path}'");
}
var newParams = new RuinGenerationParams(mainElement, configFile.Path);
paramsList.Add(newParams);
}
else if (paramsList.Any())
{
DebugConsole.NewMessage($"Adding additional ruin generation parameters from file '{configFile.Path}'");
}
var newParams = new RuinGenerationParams(mainElement)
{
filePath = configFile.Path
};
paramsList.Add(newParams);
}
}
@@ -185,11 +98,11 @@ namespace Barotrauma.RuinGeneration
NewLineOnAttributes = true
};
foreach (RuinGenerationParams generationParams in List)
foreach (RuinGenerationParams generationParams in RuinParams)
{
foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig))
{
if (configFile.Path != generationParams.filePath) continue;
if (configFile.Path != generationParams.filePath) { continue; }
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
if (doc == null) { continue; }
@@ -205,298 +118,4 @@ namespace Barotrauma.RuinGeneration
}
}
}
class RuinRoom : ISerializableEntity
{
public enum RoomPlacement
{
Any,
First,
Last
}
public string Name
{
get;
private set;
}
[Serialize(1.0f, false), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float Commonness { get; private set; }
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
} = new Dictionary<string, SerializableProperty>();
[Serialize(RoomPlacement.Any, false), Editable]
public RoomPlacement Placement
{
get;
set;
}
[Serialize(0, false), Editable]
public int PlacementOffset
{
get;
set;
}
[Serialize(false, false), Editable]
public bool IsCorridor
{
get;
set;
}
[Serialize(1.0f, false), Editable]
public float MinWaterAmount
{
get;
set;
}
[Serialize(1.0f, false), Editable]
public float MaxWaterAmount
{
get;
set;
}
private List<RuinEntityConfig> entityList = new List<RuinEntityConfig>();
public RuinRoom(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
Name = element.GetAttributeString("name", "");
if (element != null)
{
int groupIndex = 0;
LoadEntities(element, ref groupIndex);
}
void LoadEntities(XElement element2, ref int groupIndex)
{
foreach (XElement subElement in element2.Elements())
{
if (subElement.Name.ToString().Equals("chooseone", StringComparison.OrdinalIgnoreCase))
{
groupIndex++;
LoadEntities(subElement, ref groupIndex);
}
else
{
entityList.Add(new RuinEntityConfig(subElement) { SingleGroupIndex = groupIndex });
}
}
}
}
public RuinEntityConfig GetRandomEntity(RuinEntityType type, Alignment alignment)
{
var matchingEntities = entityList.FindAll(rs =>
rs.Type == type &&
rs.Alignment.HasFlag(alignment));
if (!matchingEntities.Any()) return null;
return ToolBox.SelectWeightedRandom(
matchingEntities,
matchingEntities.Select(s => s.Commonness).ToList(),
Rand.RandSync.Server);
}
public List<RuinEntityConfig> GetPropList(RuinShape room, Rand.RandSync randSync)
{
Dictionary<int, List<RuinEntityConfig>> propGroups = new Dictionary<int, List<RuinEntityConfig>>();
foreach (RuinEntityConfig entityConfig in entityList)
{
if (entityConfig.Type != RuinEntityType.Prop) { continue; }
if (room.Rect.Width < entityConfig.MinRoomSize.X || room.Rect.Height < entityConfig.MinRoomSize.Y) { continue; }
if (room.Rect.Width > entityConfig.MaxRoomSize.X || room.Rect.Height > entityConfig.MaxRoomSize.Y) { continue; }
if (!propGroups.ContainsKey(entityConfig.SingleGroupIndex))
{
propGroups[entityConfig.SingleGroupIndex] = new List<RuinEntityConfig>();
}
propGroups[entityConfig.SingleGroupIndex].Add(entityConfig);
}
List<RuinEntityConfig> props = new List<RuinEntityConfig>();
foreach (KeyValuePair<int, List<RuinEntityConfig>> propGroup in propGroups)
{
if (propGroup.Key == 0)
{
props.AddRange(propGroup.Value);
}
else
{
props.Add(propGroup.Value[Rand.Int(propGroup.Value.Count, randSync)]);
}
}
return props;
}
}
class RuinEntityConfig : ISerializableEntity
{
public readonly MapEntityPrefab Prefab;
public enum RelativePlacement
{
SameRoom,
NextRoom,
NextCorridor,
PreviousRoom,
PreviousCorridor,
FirstRoom,
FirstCorridor,
LastRoom,
LastCorridor
}
public class EntityConnection
{
//which type of room to search for the item to connect to
//sameroom, nextroom, previousroom, firstroom and lastroom are also valid
public string RoomName
{
get;
private set;
}
public string TargetEntityIdentifier
{
get;
private set;
}
//Identifier of the item to run the wire from. Only needed in item assemblies to determine which item in the assembly to use.
public string SourceEntityIdentifier
{
get;
private set;
}
//if set, the connection is done by running a wire from
//(Pair.First = the name of the connection in this item) to (Pair.Second = the name of the connection in the target item)
public Pair<string, string> WireConnection
{
get;
private set;
}
public EntityConnection(XElement element)
{
RoomName = element.GetAttributeString("roomname", "");
TargetEntityIdentifier = element.GetAttributeString("targetentity", "");
SourceEntityIdentifier = element.GetAttributeString("sourceentity", "");
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("wire", StringComparison.OrdinalIgnoreCase))
{
WireConnection = new Pair<string, string>(
subElement.GetAttributeString("from", ""),
subElement.GetAttributeString("to", ""));
}
}
}
}
[Serialize(Alignment.Bottom, false), Editable]
public Alignment Alignment { get; private set; }
[Serialize("0,0", false, description: "Minimum offset from the anchor position, relative to the size of the room." +
" For example, a value of { -0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-left corner of the room and bottom-center."), Editable]
public Vector2 MinOffset { get; private set; }
[Serialize("0,0", false, description: "Maximum offset from the anchor position, relative to the size of the room." +
" For example, a value of { 0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-right corner of the room and bottom-center."), Editable]
public Vector2 MaxOffset { get; private set; }
[Serialize(RuinEntityType.Prop, false), Editable]
public RuinEntityType Type { get; private set; }
[Serialize(false, false), Editable]
public bool Expand { get; private set; }
[Serialize(RelativePlacement.SameRoom, false), Editable]
public RelativePlacement PlacementRelativeToParent { get; private set; }
[Serialize(1.0f, false), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float Commonness { get; private set; }
[Serialize(1, false)]
public int MinAmount { get; private set; }
[Serialize(1, false)]
public int MaxAmount { get; private set; }
[Serialize("0,0", false)]
public Point MinRoomSize { get; private set; }
[Serialize("100000,100000", false)]
public Point MaxRoomSize { get; private set; }
[Serialize("", false)]
public string TargetContainer { get; private set; }
public List<EntityConnection> EntityConnections { get; private set; } = new List<EntityConnection>();
public int SingleGroupIndex;
private readonly List<RuinEntityConfig> childEntities = new List<RuinEntityConfig>();
public IEnumerable<RuinEntityConfig> ChildEntities
{
get { return childEntities; }
}
public string Name => Prefab == null ? "null" : Prefab.Name;
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
} = new Dictionary<string, SerializableProperty>();
public RuinEntityConfig(XElement element)
{
string name = element.GetAttributeString("prefab", "");
Prefab = MapEntityPrefab.Find(name: null, identifier: name);
if (Prefab == null)
{
DebugConsole.ThrowError("Loading ruin entity config failed - map entity prefab \"" + name + "\" not found.");
return;
}
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
int gIndex = 0;
LoadChildren(element, ref gIndex);
void LoadChildren(XElement element2, ref int groupIndex)
{
foreach (XElement subElement in element2.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "connection":
case "entityconnection":
EntityConnections.Add(new EntityConnection(subElement));
break;
case "chooseone":
groupIndex++;
LoadChildren(subElement, ref groupIndex);
break;
default:
childEntities.Add(new RuinEntityConfig(subElement) { SingleGroupIndex = groupIndex });
break;
}
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1006,12 +1006,22 @@ namespace Barotrauma
stockToRemove.ForEach(i => stock.Remove(i));
StoreStock = stock;
if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval)
int extraSpecialSalesCount = GetExtraSpecialSalesCount();
if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval ||
DailySpecials.Count() != DailySpecialsCount + extraSpecialSalesCount)
{
CreateStoreSpecials();
}
}
private int GetExtraSpecialSalesCount()
{
var characters = GameSession.GetSessionCrewCharacters();
if (!characters.Any()) { return 0; }
return characters.Max(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount));
}
private void GenerateRandomPriceModifier()
{
StorePriceModifier = Rand.Range(-StorePriceModifierRange, StorePriceModifierRange);
@@ -1035,7 +1045,9 @@ namespace Barotrauma
}
availableStock.Add(stockItem.ItemPrefab, weight);
}
for (int i = 0; i < DailySpecialsCount; i++)
int extraSpecialSalesCount = GetExtraSpecialSalesCount();
for (int i = 0; i < DailySpecialsCount + extraSpecialSalesCount; i++)
{
if (availableStock.None()) { break; }
var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced);
@@ -224,12 +224,6 @@ namespace Barotrauma
}
}
public RuinGeneration.Ruin ParentRuin
{
get;
set;
}
[Serialize(true, true)]
public bool RemoveIfLinkedOutpostDoorInUse
{
@@ -11,7 +11,7 @@ namespace Barotrauma
{
public static List<OutpostGenerationParams> Params { get; private set; }
public string Name { get; private set; }
public virtual string Name { get; private set; }
public string Identifier { get; private set; }
@@ -67,6 +67,34 @@ namespace Barotrauma
set;
}
[Serialize(true, isSaveable: true), Editable]
public bool LockUnusedDoors
{
get;
set;
}
[Serialize(true, isSaveable: true), Editable]
public bool RemoveUnusedGaps
{
get;
set;
}
[Serialize(0.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float MinWaterPercentage
{
get;
set;
}
[Serialize(0.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float MaxWaterPercentage
{
get;
set;
}
[Serialize("", isSaveable: true), Editable]
public string ReplaceInRadiation { get; set; }
@@ -81,12 +109,14 @@ namespace Barotrauma
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
private OutpostGenerationParams(XElement element, string filePath)
protected OutpostGenerationParams(XElement element, string filePath)
{
Identifier = element.GetAttributeString("identifier", "");
Name = element.GetAttributeString("name", Identifier);
allowedLocationTypes = element.GetAttributeStringArray("allowedlocationtypes", Array.Empty<string>()).ToList();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
if (element == null) { return; }
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -85,6 +85,7 @@ namespace Barotrauma
var subInfo = new SubmarineInfo(outpostModuleFile.Path);
if (subInfo.OutpostModuleInfo != null)
{
if (subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin") != generationParams is RuinGeneration.RuinGenerationParams) { continue; }
outpostModules.Add(subInfo);
}
}
@@ -162,7 +163,7 @@ namespace Barotrauma
selectedModules.Add(new PlacedModule(initialModule, null, OutpostModuleInfo.GapPosition.None));
selectedModules.Last().FulfilledModuleTypes.Add(initialModuleFlag);
AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType);
AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams);
if (pendingModuleFlags.Any(flag => !flag.Equals("none", StringComparison.OrdinalIgnoreCase)))
{
remainingTries--;
@@ -233,17 +234,23 @@ namespace Barotrauma
var selectedModule = selectedModules[i];
sub.Info.GameVersion = selectedModule.Info.GameVersion;
var moduleEntities = MapEntity.LoadAll(sub, selectedModule.Info.SubmarineElement, selectedModule.Info.FilePath, idOffset);
idOffset = moduleEntities.Max(e => e.ID);
MapEntity.InitializeLoadedLinks(moduleEntities);
foreach (MapEntity entity in moduleEntities)
foreach (MapEntity entity in moduleEntities.ToList())
{
entity.OriginalModuleIndex = i;
if (!(entity is Item item)) { continue; }
item.GetComponent<Door>()?.RefreshLinkedGap();
var door = item.GetComponent<Door>();
if (door != null)
{
door.RefreshLinkedGap();
if (!moduleEntities.Contains(door.LinkedGap)) { moduleEntities.Add(door.LinkedGap); }
}
item.GetComponent<ConnectionPanel>()?.InitializeLinks();
item.GetComponent<ItemContainer>()?.OnMapLoaded();
}
idOffset = moduleEntities.Max(e => e.ID);
var wallEntities = moduleEntities.Where(e => e is Structure).Cast<Structure>();
var hullEntities = moduleEntities.Where(e => e is Hull).Cast<Hull>();
@@ -345,11 +352,33 @@ namespace Barotrauma
Submarine.RepositionEntities(module.Offset + sub.HiddenSubPosition, entities[module]);
}
Gap.UpdateHulls();
allEntities.AddRange(GenerateHallways(sub, locationType, selectedModules, outpostModules, entities));
allEntities.AddRange(GenerateHallways(sub, locationType, selectedModules, outpostModules, entities, generationParams is RuinGeneration.RuinGenerationParams));
LinkOxygenGenerators(allEntities);
LockUnusedDoors(selectedModules, entities);
if (generationParams.LockUnusedDoors)
{
LockUnusedDoors(selectedModules, entities, generationParams.RemoveUnusedGaps);
}
AlignLadders(selectedModules, entities);
PowerUpOutpost(entities.SelectMany(e => e.Value));
if (generationParams.MaxWaterPercentage > 0.0f)
{
foreach (var entity in allEntities)
{
if (entity is Hull hull)
{
float diff = generationParams.MaxWaterPercentage - generationParams.MinWaterPercentage;
if (diff < 0.01f)
{
// Overfill the hulls to get rid of air pockets in the vertical hallways. Airpockets make it impossible to swim up the hallways.
hull.WaterVolume = hull.Volume * 2;
}
else
{
hull.WaterVolume = hull.Volume * Rand.Range(generationParams.MinWaterPercentage, generationParams.MaxWaterPercentage, Rand.RandSync.Server) * 0.01f;
}
}
}
}
}
return allEntities;
@@ -414,7 +443,8 @@ namespace Barotrauma
List<string> pendingModuleFlags,
List<PlacedModule> selectedModules,
LocationType locationType,
bool retry = true)
bool retry = true,
bool allowExtendBelowInitialModule = false)
{
if (pendingModuleFlags.Count == 0) { return true; }
@@ -422,8 +452,11 @@ namespace Barotrauma
foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions().Randomize(Rand.RandSync.Server))
{
if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; }
//don't continue downwards if it'd extend below the airlock
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; }
if (!allowExtendBelowInitialModule)
{
//don't continue downwards if it'd extend below the airlock
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; }
}
if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition))
{
var newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType);
@@ -438,7 +471,7 @@ namespace Barotrauma
//try to append to some other module first
foreach (PlacedModule otherModule in selectedModules)
{
if (AppendToModule(otherModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false))
if (AppendToModule(otherModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule))
{
return true;
}
@@ -454,7 +487,7 @@ namespace Barotrauma
//retry
currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType);
if (currentModule == null) { break; }
if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false))
if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule))
{
return true;
}
@@ -676,6 +709,10 @@ namespace Barotrauma
else
{
availableModules = modules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag));
if (moduleFlag != "hallwayhorizontal" && moduleFlag != "hallwayvertical")
{
availableModules = availableModules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayhorizontal") && !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayvertical"));
}
}
if (availableModules.Count() == 0) { return null; }
@@ -840,7 +877,7 @@ namespace Barotrauma
return from.AllowAttachToModules.Any(s => to.ModuleFlags.Contains(s));
}
private static List<MapEntity> GenerateHallways(Submarine sub, LocationType locationType, IEnumerable<PlacedModule> placedModules, IEnumerable<SubmarineInfo> availableModules, Dictionary<PlacedModule, List<MapEntity>> allEntities)
private static List<MapEntity> GenerateHallways(Submarine sub, LocationType locationType, IEnumerable<PlacedModule> placedModules, IEnumerable<SubmarineInfo> availableModules, Dictionary<PlacedModule, List<MapEntity>> allEntities, bool isRuin)
{
//if a hallway is shorter than this, one of the doors at the ends of the hallway is removed
const float MinTwoDoorHallwayLength = 32.0f;
@@ -1193,14 +1230,13 @@ namespace Barotrauma
}
}
private static void LockUnusedDoors(IEnumerable<PlacedModule> placedModules, Dictionary<PlacedModule, List<MapEntity>> entities)
private static void LockUnusedDoors(IEnumerable<PlacedModule> placedModules, Dictionary<PlacedModule, List<MapEntity>> entities, bool removeUnusedGaps)
{
foreach (PlacedModule module in placedModules)
{
foreach (MapEntity me in entities[module])
{
var gap = me as Gap;
if (gap == null) { continue; }
if (!(me is Gap gap)) { continue; }
var door = gap.ConnectedDoor;
if (door != null && !door.UseBetweenOutpostModules) { continue; }
if (placedModules.Any(m => m.PreviousGap == gap || m.ThisGap == gap))
@@ -1247,11 +1283,11 @@ namespace Barotrauma
if (connectionPanel != null) { connectionPanel.Locked = true; }
}
}
else
else if (removeUnusedGaps)
{
gap.Remove();
WayPoint.WayPointList.Where(wp => wp.ConnectedGap == gap).ForEachMod(wp => wp.Remove());
}
}
}
entities[module].RemoveAll(e => e.Removed);
}
@@ -89,11 +89,13 @@ namespace Barotrauma
if (newFlags.Contains("hallwayhorizontal"))
{
moduleFlags.Add("hallwayhorizontal");
if (newFlags.Contains("ruin")) { moduleFlags.Add("ruin"); }
return;
}
if (newFlags.Contains("hallwayvertical"))
{
moduleFlags.Add("hallwayvertical");
if (newFlags.Contains("ruin")) { moduleFlags.Add("ruin"); }
return;
}
if (!newFlags.Any())
@@ -23,7 +23,7 @@ namespace Barotrauma
HideInMenus = 2
}
public enum SubmarineType { Player, Outpost, OutpostModule, Wreck, BeaconStation, EnemySubmarine }
public enum SubmarineType { Player, Outpost, OutpostModule, Wreck, BeaconStation, EnemySubmarine, Ruin }
public enum SubmarineClass { Undefined, Scout, Attack, Transport, DeepDiver }
partial class SubmarineInfo : IDisposable
@@ -97,11 +97,10 @@ namespace Barotrauma
public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule;
//TODO: replace when the ruin branch is merged
public bool IsRuin => false;
public bool IsWreck => Type == SubmarineType.Wreck;
public bool IsBeacon => Type == SubmarineType.BeaconStation;
public bool IsPlayer => Type == SubmarineType.Player;
public bool IsRuin => Type == SubmarineType.Ruin;
public bool IsCampaignCompatible => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus) && SubmarineClass != SubmarineClass.Undefined;
public bool IsCampaignCompatibleIgnoreClass => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus);
@@ -6,7 +6,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.RuinGeneration;
using Barotrauma.Extensions;
namespace Barotrauma
@@ -189,61 +188,141 @@ namespace Barotrauma
door.Body.Enabled = true;
}
}
bool isFlooded = submarine.Info.IsRuin || submarine.Info.Type == SubmarineType.OutpostModule && submarine.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin");
float diffFromHullEdge = 50;
float minDist = 100.0f;
float heightFromFloor = 110.0f;
float hullMinHeight = 100;
var removals = new List<WayPoint>();
foreach (Hull hull in Hull.hullList)
{
// Ignore hulls that a human couldn't fit in.
// Doesn't take multi-hull rooms into account, but it's probably best to leave them to be setup manually.
if (hull.Rect.Height < hullMinHeight) { continue; }
// Do five raycasts to check if there's a floor. Don't create waypoints unless we can find a floor.
Body floor = null;
for (int i = 0; i < 5; i++)
if (isFlooded)
{
float horizontalOffset = 0;
switch (i)
diffFromHullEdge = 75;
var hullWaypoints = new List<WayPoint>();
float top = hull.Rect.Y;
float bottom = hull.Rect.Y - hull.Rect.Height;
if (hull.Rect.Width < 300 || hull.Rect.Height < 300)
{
case 1:
horizontalOffset = hull.RectWidth * 0.2f;
break;
case 2:
horizontalOffset = hull.RectWidth * 0.4f;
break;
case 3:
horizontalOffset = -hull.RectWidth * 0.2f;
break;
case 4:
horizontalOffset = -hull.RectWidth * 0.4f;
break;
// For narrow hulls, create one line of waypoints either horizontally or vertically
if (hull.Rect.Width > hull.Rect.Height)
{
// Horizontal
float y = hull.Rect.Y - hull.Rect.Height / 2;
for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist)
{
hullWaypoints.Add(new WayPoint(new Vector2(x, y), SpawnType.Path, submarine));
}
}
else
{
// Vertical
float x = hull.Rect.X + hull.Rect.Width / 2;
for (float y = top - diffFromHullEdge; y >= bottom + diffFromHullEdge; y -= minDist)
{
hullWaypoints.Add(new WayPoint(new Vector2(x, y), SpawnType.Path, submarine));
}
}
}
if (hullWaypoints.None())
{
// Try to create a grid-like network of waypoints
for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist)
{
for (float y = top - diffFromHullEdge; y >= bottom + diffFromHullEdge; y -= minDist)
{
hullWaypoints.Add(new WayPoint(new Vector2(x, y), SpawnType.Path, submarine));
}
}
if (hullWaypoints.None())
{
// If that fails, just create one waypoint at the center.
hullWaypoints.Add(new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height / 2), SpawnType.Path, submarine));
}
foreach (WayPoint wp in hullWaypoints)
{
foreach (Structure wall in Structure.WallList)
{
if (wall.HasBody)
{
// Remove waypoints that are too close/inside the walls.
Rectangle rect = wall.Rect;
rect.Inflate(10, 10);
if (rect.ContainsWorld(wp.Position))
{
removals.Add(wp);
}
}
}
}
}
// Connect the waypoints
foreach (var wayPoint in hullWaypoints)
{
for (int dir = -1; dir <= 1; dir += 2)
{
WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: true, new Vector2(minDist * 1.9f, minDist));
if (closest != null && closest.CurrentHull == wayPoint.CurrentHull)
{
wayPoint.ConnectTo(closest);
}
closest = wayPoint.FindClosest(dir, horizontalSearch: false, new Vector2(minDist, minDist * 1.9f));
if (closest != null && closest.CurrentHull == wayPoint.CurrentHull)
{
wayPoint.ConnectTo(closest);
}
}
}
horizontalOffset = ConvertUnits.ToSimUnits(horizontalOffset);
Vector2 floorPos = new Vector2(hull.SimPosition.X + horizontalOffset, ConvertUnits.ToSimUnits(hull.Rect.Y - hull.RectHeight - 50));
floor = Submarine.PickBody(new Vector2(hull.SimPosition.X + horizontalOffset, hull.SimPosition.Y), floorPos, collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform, customPredicate: f => !(f.Body.UserData is Submarine));
if (floor != null) { break; }
}
if (floor == null) { continue; }
float waypointHeight = hull.Rect.Height > heightFromFloor * 2 ? heightFromFloor : hull.Rect.Height / 2;
if (hull.Rect.Width < diffFromHullEdge * 3.0f)
{
new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
}
else
{
WayPoint prevWaypoint = null;
for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist)
if (hull.Rect.Height < hullMinHeight) { continue; }
// Do five raycasts to check if there's a floor. Don't create waypoints unless we can find a floor.
Body floor = null;
for (int i = 0; i < 5; i++)
{
var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
if (prevWaypoint != null) { wayPoint.ConnectTo(prevWaypoint); }
prevWaypoint = wayPoint;
float horizontalOffset = 0;
switch (i)
{
case 1:
horizontalOffset = hull.RectWidth * 0.2f;
break;
case 2:
horizontalOffset = hull.RectWidth * 0.4f;
break;
case 3:
horizontalOffset = -hull.RectWidth * 0.2f;
break;
case 4:
horizontalOffset = -hull.RectWidth * 0.4f;
break;
}
horizontalOffset = ConvertUnits.ToSimUnits(horizontalOffset);
Vector2 floorPos = new Vector2(hull.SimPosition.X + horizontalOffset, ConvertUnits.ToSimUnits(hull.Rect.Y - hull.RectHeight - 50));
floor = Submarine.PickBody(new Vector2(hull.SimPosition.X + horizontalOffset, hull.SimPosition.Y), floorPos, collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform, customPredicate: f => !(f.Body.UserData is Submarine));
if (floor != null) { break; }
}
if (prevWaypoint == null)
if (floor == null) { continue; }
float waypointHeight = hull.Rect.Height > heightFromFloor * 2 ? heightFromFloor : hull.Rect.Height / 2;
if (hull.Rect.Width < diffFromHullEdge * 3.0f)
{
// Ensure that we always create at least one waypoint per hull.
new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
}
else
{
WayPoint previousWaypoint = null;
for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist)
{
var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
if (previousWaypoint != null) { wayPoint.ConnectTo(previousWaypoint); }
previousWaypoint = wayPoint;
}
if (previousWaypoint == null)
{
// Ensure that we always create at least one waypoint per hull.
new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
}
}
}
}
@@ -278,7 +357,7 @@ namespace Barotrauma
}
float outSideWaypointInterval = 100.0f;
if (submarine.Info.Type != SubmarineType.OutpostModule)
if (!isFlooded && submarine.Info.Type != SubmarineType.OutpostModule)
{
List<(WayPoint, int)> outsideWaypoints = new List<(WayPoint, int)>();
@@ -381,7 +460,6 @@ namespace Barotrauma
}
}
// Remove unwanted points
var removals = new List<WayPoint>();
WayPoint previous = null;
float tooClose = outSideWaypointInterval / 2;
foreach (var wayPoint in outsideWaypoints)
@@ -412,7 +490,6 @@ namespace Barotrauma
foreach (WayPoint wp in removals)
{
outsideWaypoints.RemoveAll(w => w.Item1 == wp);
wp.Remove();
}
for (int i = 0; i < outsideWaypoints.Count; i++)
{
@@ -433,41 +510,35 @@ namespace Barotrauma
}
}
}
List<Structure> stairList = new List<Structure>();
foreach (MapEntity me in mapEntityList)
{
if (!(me is Structure stairs)) { continue; }
if (stairs.StairDirection != Direction.None) stairList.Add(stairs);
}
foreach (Structure stairs in stairList)
foreach (Structure wall in Structure.WallList)
{
if (wall.StairDirection == Direction.None) { continue; }
WayPoint[] stairPoints = new WayPoint[3];
stairPoints[0] = new WayPoint(
new Vector2(stairs.Rect.X - 32.0f,
stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? 80 : stairs.Rect.Height) + heightFromFloor), SpawnType.Path, submarine);
new Vector2(wall.Rect.X - 32.0f,
wall.Rect.Y - (wall.StairDirection == Direction.Left ? 80 : wall.Rect.Height) + heightFromFloor), SpawnType.Path, submarine);
stairPoints[1] = new WayPoint(
new Vector2(stairs.Rect.Right + 32.0f,
stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? stairs.Rect.Height : 80) + heightFromFloor), SpawnType.Path, submarine);
new Vector2(wall.Rect.Right + 32.0f,
wall.Rect.Y - (wall.StairDirection == Direction.Left ? wall.Rect.Height : 80) + heightFromFloor), SpawnType.Path, submarine);
for (int i = 0; i < 2; i++ )
for (int i = 0; i < 2; i++)
{
for (int dir = -1; dir <= 1; dir += 2)
{
WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(100, 70));
if (closest == null) { continue; }
stairPoints[i].ConnectTo(closest);
}
}
}
stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position) / 2, SpawnType.Path, submarine);
stairPoints[0].ConnectTo(stairPoints[2]);
stairPoints[2].ConnectTo(stairPoints[1]);
}
removals.ForEach(wp => wp.Remove());
removals.Clear();
foreach (Item item in Item.ItemList)
{
@@ -605,12 +676,25 @@ namespace Barotrauma
{
if (gap.IsHorizontal)
{
// Too small to walk through
if (gap.Rect.Height < hullMinHeight) { continue; }
if ( isFlooded)
{
// Too small to swim through
if (gap.Rect.Height < 50) { continue; }
}
else
{
// Too small to walk through
if (gap.Rect.Height < hullMinHeight) { continue; }
}
Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor);
if (isFlooded)
{
pos.Y = gap.Rect.Y - gap.Rect.Height / 2;
}
var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap);
// The closest waypoint can be quite far if the gap is at an exterior door.
Vector2 tolerance = gap.IsRoomToRoom ? new Vector2(150, 70) : new Vector2(1000, 1000);
Vector2 tolerance = gap.IsRoomToRoom && !isFlooded ? new Vector2(150, 70) : new Vector2(1000, 1000);
for (int dir = -1; dir <= 1; dir += 2)
{
WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: true, tolerance, gap.ConnectedDoor?.Body.FarseerBody);
@@ -623,7 +707,7 @@ namespace Barotrauma
else
{
// Create waypoints on vertical gaps on the outer walls, also hatches.
if (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull)) { continue; }
if (!isFlooded && (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull))) { continue; }
// Too small to swim through
if (gap.Rect.Width < 50.0f) { continue; }
Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2);
@@ -632,11 +716,20 @@ namespace Barotrauma
var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap);
Hull connectedHull = (Hull)gap.linkedTo.First(l => l is Hull);
int dir = Math.Sign(connectedHull.Position.Y - gap.Position.Y);
WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: false, new Vector2(50, 100));
WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: false, isFlooded ? new Vector2(500, 500) : new Vector2(50, 100));
if (closest != null)
{
wayPoint.ConnectTo(closest);
}
if (isFlooded)
{
closest = wayPoint.FindClosest(-dir, horizontalSearch: false, isFlooded ? new Vector2(500, 500) : new Vector2(50, 100));
if (closest != null)
{
wayPoint.ConnectTo(closest);
}
}
// Link to outside
for (dir = -1; dir <= 1; dir += 2)
{
closest = wayPoint.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), gap.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null);
@@ -656,7 +749,7 @@ namespace Barotrauma
foreach (WayPoint wp in WayPointList)
{
if (wp.CurrentHull == null && wp.Ladders == null && wp.linkedTo.Count < 2)
if (wp.SpawnType == SpawnType.Path && wp.CurrentHull == null && wp.Ladders == null && wp.linkedTo.Count < 2)
{
DebugConsole.ThrowError($"Couldn't automatically link the waypoint {wp.ID} outside of the submarine. You should do it manually. The waypoint ID is shown in red color.");
}
@@ -775,11 +868,10 @@ namespace Barotrauma
}
}
public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, Ruin ruin = null, bool useSyncedRand = false)
public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, bool useSyncedRand = false)
{
return WayPointList.GetRandom(wp =>
wp.Submarine == sub &&
wp.ParentRuin == ruin &&
wp.spawnType == spawnType &&
(assignedJob == null || (assignedJob != null && wp.AssignedJob == assignedJob)),
useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced);
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
@@ -28,6 +29,7 @@ namespace Barotrauma
public bool SpawnIfInventoryFull = true;
public bool IgnoreLimbSlots = false;
public InvSlotType Slot = InvSlotType.None;
private readonly Action<Item> onSpawned;
@@ -73,7 +75,8 @@ namespace Barotrauma
{
Condition = Condition
};
if (!Inventory.Owner.Removed && !Inventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots))
var slot = Slot != InvSlotType.None ? Slot.ToEnumerable() : spawnedItem.AllowedSlots;
if (!Inventory.Owner.Removed && !Inventory.TryPutItem(spawnedItem, null, slot))
{
if (IgnoreLimbSlots)
{
@@ -264,7 +267,7 @@ namespace Barotrauma
spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition));
}
public void AddToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, Action<Item> onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false)
public void AddToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, Action<Item> onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (itemPrefab == null)
@@ -277,7 +280,8 @@ namespace Barotrauma
spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition)
{
SpawnIfInventoryFull = spawnIfInventoryFull,
IgnoreLimbSlots = ignoreLimbSlots
IgnoreLimbSlots = ignoreLimbSlots,
Slot = slot
});
}
@@ -756,12 +756,13 @@ namespace Barotrauma
Vector2 vel = FarseerBody.LinearVelocity;
Vector2 deltaPos = simPosition - (Vector2)pullPos;
#if DEBUG
if (deltaPos.LengthSquared() > 100.0f * 100.0f)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to move a physics body to an invalid position.\n" + Environment.StackTrace.CleanupStackTrace());
}
#endif
return;
}
deltaPos *= force;
ApplyLinearImpulse((deltaPos - vel * 0.5f) * FarseerBody.Mass, (Vector2)pullPos);
}
@@ -146,6 +146,7 @@ namespace Barotrauma
{ typeof(Vector4), "vector4" },
{ typeof(Rectangle), "rectangle" },
{ typeof(Color), "color" },
{ typeof(string[]), "stringarray" }
};
private static readonly Dictionary<Type, Dictionary<string, SerializableProperty>> cachedProperties =
@@ -273,6 +274,9 @@ namespace Barotrauma
case "rectangle":
PropertyInfo.SetValue(parentObject, XMLExtensions.ParseRect(value, true));
break;
case "stringarray":
PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray(value));
break;
}
}
@@ -345,6 +349,9 @@ namespace Barotrauma
case "rectangle":
PropertyInfo.SetValue(parentObject, XMLExtensions.ParseRect((string)value, false));
return true;
case "stringarray":
PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value));
break;
default:
DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString());
DebugConsole.ThrowError("(Cannot convert a string to a " + PropertyType.ToString() + ")");
@@ -723,6 +730,10 @@ namespace Barotrauma
case "rectangle":
stringValue = XMLExtensions.RectToString((Rectangle)value);
break;
case "stringarray":
string[] stringArray = (string[])value;
stringValue = stringArray != null ? string.Join(';', stringArray) : "";
break;
default:
stringValue = value.ToString();
break;
@@ -520,10 +520,12 @@ namespace Barotrauma
vector.W.ToString(format, CultureInfo.InvariantCulture);
}
[Obsolete("Prefer XMLExtensions.ToStringHex")]
public static string ColorToString(Color color)
{
return color.R + "," + color.G + "," + color.B + "," + color.A;
}
=> $"{color.R},{color.G},{color.B},{color.A}";
public static string ToStringHex(this Color color)
=> $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
public static string RectToString(Rectangle rect)
{
@@ -718,6 +720,11 @@ namespace Barotrauma
return floatArray;
}
public static string[] ParseStringArray(string stringArrayValues)
{
return string.IsNullOrEmpty(stringArrayValues) ? new string[0] : stringArrayValues.Split(';');
}
public static bool IsOverride(this XElement element) => element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase);
public static bool IsCharacterVariant(this XElement element) => element.Name.ToString().Equals("charactervariant", StringComparison.OrdinalIgnoreCase);
@@ -151,6 +151,12 @@ namespace Barotrauma
{
sourceVector = overrideElement.GetAttributeVector4("sourcerect", Vector4.Zero);
}
if ((overrideElement ?? SourceElement).Attribute("sheetindex") != null)
{
Point sheetElementSize = (overrideElement ?? SourceElement).GetAttributePoint("sheetelementsize", Point.Zero);
Point sheetIndex = (overrideElement ?? SourceElement).GetAttributePoint("sheetindex", Point.Zero);
sourceVector = new Vector4(sheetIndex.X * sheetElementSize.X, sheetIndex.Y * sheetElementSize.Y, sheetElementSize.X, sheetElementSize.Y);
}
Compress = SourceElement.GetAttributeBool("compress", true);
bool shouldReturn = false;
if (!lazyLoad)
@@ -294,6 +300,12 @@ namespace Barotrauma
{
sourceRect = overrideElement.GetAttributeRect("sourcerect", Rectangle.Empty);
}
if ((overrideElement ?? SourceElement).Attribute("sheetindex") != null)
{
Point sheetElementSize = (overrideElement ?? SourceElement).GetAttributePoint("sheetelementsize", Point.Zero);
Point sheetIndex = (overrideElement ?? SourceElement).GetAttributePoint("sheetindex", Point.Zero);
sourceRect = new Rectangle(sheetIndex.X * sheetElementSize.X, sheetIndex.Y * sheetElementSize.Y, sheetElementSize.X, sheetElementSize.Y);
}
size = SourceElement.GetAttributeVector2("size", Vector2.One);
size.X *= sourceRect.Width;
size.Y *= sourceRect.Height;
@@ -144,6 +144,7 @@ namespace Barotrauma
public readonly float Spread;
public readonly SpawnRotationType RotationType;
public readonly float AimSpread;
public readonly bool Equip;
public ItemSpawnInfo(XElement element, string parentDebugName)
{
@@ -181,6 +182,7 @@ namespace Barotrauma
Count = element.GetAttributeInt("count", 1);
Spread = element.GetAttributeFloat("spread", 0f);
AimSpread = element.GetAttributeFloat("aimspread", 0f);
Equip = element.GetAttributeBool("equip", false);
string spawnTypeStr = element.GetAttributeString("spawnposition", "This");
if (!Enum.TryParse(spawnTypeStr, ignoreCase: true, out SpawnPosition))
@@ -308,13 +310,15 @@ namespace Barotrauma
/// </summary>
private readonly HashSet<(string affliction, float strength)> requiredAfflictions;
public float AfflictionMultiplier = 1.0f;
public List<Affliction> Afflictions
{
get;
private set;
}
private bool modifyAfflictionsByMaxVitality;
private readonly bool modifyAfflictionsByMaxVitality;
public IEnumerable<CharacterSpawnInfo> SpawnCharacters
{
@@ -571,11 +575,14 @@ namespace Barotrauma
requiredItems.Add(newRequiredItem);
break;
case "requiredaffliction":
requiredAfflictions ??= new HashSet<(string, float)>();
requiredAfflictions.Add((
subElement.GetAttributeString("identifier", string.Empty),
subElement.GetAttributeFloat("minstrength", 0.0f)));
string[] ids = subElement.GetAttributeStringArray("identifier", new string[0]);
foreach (string afflictionId in ids)
{
requiredAfflictions.Add((
afflictionId,
subElement.GetAttributeFloat("minstrength", 0.0f)));
}
break;
case "conditional":
foreach (XAttribute attribute in subElement.Attributes())
@@ -798,7 +805,16 @@ namespace Barotrauma
{
var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent);
var targetItem = target as Item ?? (target as ItemComponent)?.Item;
if (targetItem?.ParentInventory == null) { continue; }
if (targetItem?.ParentInventory == null)
{
//if we're checking for inequality, not being inside a valid container counts as success
//(not inside a container = the container doesn't have a specific tag/value)
if (pc.Operator == PropertyConditional.OperatorType.NotEquals)
{
return true;
}
continue;
}
var owner = targetItem.ParentInventory.Owner;
if (pc.TargetGrandParent && owner is Item ownerItem)
{
@@ -816,7 +832,7 @@ namespace Barotrauma
if (HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return true; }
}
}
if (owner is Character character && HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; }
if (owner is Character character && HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; }
}
else
{
@@ -841,7 +857,16 @@ namespace Barotrauma
{
var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent);
var targetItem = target as Item ?? (target as ItemComponent)?.Item;
if (targetItem?.ParentInventory == null) { return false; }
if (targetItem?.ParentInventory == null)
{
//if we're checking for inequality, not being inside a valid container counts as success
//(not inside a container = the container doesn't have a specific tag/value)
if (pc.Operator == PropertyConditional.OperatorType.NotEquals)
{
continue;
}
return false;
}
var owner = targetItem.ParentInventory.Owner;
if (pc.TargetGrandParent && owner is Item ownerItem)
{
@@ -1424,7 +1449,19 @@ namespace Barotrauma
}
if (inventory != null && (inventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull))
{
Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull);
Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: item =>
{
if (chosenItemSpawnInfo.Equip && entity is Character character && character.Inventory != null)
{
//if the item is both pickable and wearable, try to wear it instead of picking it up
List<InvSlotType> allowedSlots =
item.GetComponents<Pickable>().Count() > 1 ?
new List<InvSlotType>(item.GetComponent<Wearable>()?.AllowedSlots ?? item.GetComponent<Pickable>().AllowedSlots) :
new List<InvSlotType>(item.AllowedSlots);
allowedSlots.Remove(InvSlotType.Any);
character.Inventory.TryPutItem(item, null, allowedSlots);
}
});
}
}
break;
@@ -1616,7 +1653,7 @@ namespace Barotrauma
{
multiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier);
}
return multiplier;
return multiplier * AfflictionMultiplier;
}
private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool modifyByMaxVitality)
@@ -1636,11 +1673,11 @@ namespace Barotrauma
private void RegisterTreatmentResults(Entity entity, Limb limb, Affliction affliction, AttackResult result)
{
if (entity is Item item && item.UseInHealthInterface)
if (entity is Item item && item.UseInHealthInterface && limb != null)
{
foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions())
{
if (result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) &&
if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) &&
(!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb))
{
if (type == ActionType.OnUse)