(bcb06cc5c) Unstable v0.9.9.0

This commit is contained in:
Juan Pablo Arce
2020-03-27 15:22:59 -03:00
parent c81486a993
commit b143329701
326 changed files with 9692 additions and 4364 deletions
@@ -25,7 +25,7 @@ namespace Barotrauma
/// <summary>
/// How long does it take for the ai target to fade out if not kept alive.
/// </summary>
public float FadeOutTime { get; private set; } = 1;
public float FadeOutTime { get; private set; } = 2;
public bool Static { get; private set; }
public bool StaticSound { get; private set; }
@@ -92,7 +92,7 @@ namespace Barotrauma
public string SonarLabel;
public string SonarIconIdentifier;
public bool Enabled = true;
public bool Enabled => SoundRange > 0 || SightRange > 0;
public float MinSoundRange, MinSightRange;
public float MaxSoundRange = 100000, MaxSightRange = 100000;
@@ -177,7 +177,8 @@ namespace Barotrauma
StaticSight = true;
}
SonarDisruption = element.GetAttributeFloat("sonardisruption", 0.0f);
SonarLabel = element.GetAttributeString("sonarlabel", "");
string label = element.GetAttributeString("sonarlabel", "");
SonarLabel = TextManager.Get(label, returnNull: true) ?? label;
SonarIconIdentifier = element.GetAttributeString("sonaricon", "");
string typeString = element.GetAttributeString("type", "Any");
if (Enum.TryParse(typeString, out TargetType t))
@@ -195,7 +196,7 @@ namespace Barotrauma
public void Update(float deltaTime)
{
if (!Static && FadeOutTime > 0)
if (Enabled && !Static && FadeOutTime > 0)
{
// The aitarget goes silent/invisible if the components don't keep it active
if (!StaticSight)
@@ -89,9 +89,6 @@ namespace Barotrauma
private readonly float memoryFadeTime = 0.5f;
private readonly float avoidTime = 3;
//Has the character been attacked since the last Update.
private bool wasAttacked;
private float avoidTimer;
public LatchOntoAI LatchOntoAI { get; private set; }
@@ -234,13 +231,6 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
if (wasAttacked)
{
LatchOntoAI?.DeattachFromBody();
Character.AnimController.ReleaseStuckLimbs();
wasAttacked = false;
}
if (DisableEnemyAI) { return; }
base.Update(deltaTime);
@@ -305,7 +295,8 @@ namespace Barotrauma
FadeMemories(updateMemoriesInverval);
updateMemoriesTimer = updateMemoriesInverval;
}
if (Character.HealthPercentage <= FleeHealthThreshold)
if (Character.HealthPercentage <= FleeHealthThreshold && SelectedAiTarget != null &&
SelectedAiTarget.Entity is Character target && (target.IsPlayer || IsBeingChasedBy(target)))
{
State = AIState.Flee;
wallTarget = null;
@@ -384,9 +375,9 @@ namespace Barotrauma
State = AIState.Idle;
return;
}
float distance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
float squaredDistance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
var attackLimb = GetAttackLimb(SelectedAiTarget.WorldPosition);
if (attackLimb != null && distance <= Math.Pow(attackLimb.attack.Range, 2))
if (attackLimb != null && squaredDistance <= Math.Pow(attackLimb.attack.Range, 2))
{
run = true;
if (State == AIState.Avoid)
@@ -402,17 +393,18 @@ namespace Barotrauma
{
bool isBeingChased = IsBeingChased;
float reactDistance = !isBeingChased && selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget);
if (distance <= Math.Pow(reactDistance + escapeMargin, 2))
if (squaredDistance <= Math.Pow(reactDistance + escapeMargin, 2))
{
float halfReactDistance = reactDistance / 2;
if (State == AIState.Aggressive || State == AIState.PassiveAggressive && distance < Math.Pow(halfReactDistance, 2))
float attackDistance = selectedTargetingParams != null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDistance;
if (State == AIState.Aggressive || State == AIState.PassiveAggressive && squaredDistance < Math.Pow(attackDistance, 2))
{
run = true;
UpdateAttack(deltaTime);
}
else
{
run = isBeingChased ? true : distance < Math.Pow(halfReactDistance, 2);
run = isBeingChased ? true : squaredDistance < Math.Pow(halfReactDistance, 2);
if (escapeMargin <= 0)
{
escapeMargin = halfReactDistance;
@@ -440,13 +432,14 @@ namespace Barotrauma
SwarmBehavior.Refresh();
SwarmBehavior.UpdateSteering(deltaTime);
}
float speed = Character.AnimController.GetCurrentSpeed(run);
float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun);
steeringManager.Update(speed);
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, State == AIState.Idle && Character.AnimController.InWater ? Steering.Length() : speed);
if (Character.CurrentHull != null && Character.AnimController.InWater)
{
// Halve the swimming speed inside the sub
speed /= 2;
Character.AnimController.TargetMovement *= 0.5f;
}
steeringManager.Update(speed);
}
#region Idle
@@ -577,7 +570,7 @@ namespace Barotrauma
else if (SelectedAiTarget?.Entity is Character targetCharacter && targetCharacter.CurrentHull == Character.CurrentHull)
{
// Steer away from the target if in the same room
Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.GetTargetMovement());
Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement);
if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY;
SteeringManager.SteeringManual(deltaTime, escapeDir);
}
@@ -615,7 +608,7 @@ namespace Barotrauma
{
escapeTarget = null;
allGapsSearched = false;
Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.GetTargetMovement());
Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement);
if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY;
SteeringManager.SteeringManual(deltaTime, escapeDir);
if (Character.CurrentHull == null)
@@ -758,6 +751,10 @@ namespace Barotrauma
bool pursue = false;
if (IsCoolDownRunning)
{
if (AttackingLimb.attack.CoolDownTimer >= AttackingLimb.attack.CoolDown + AttackingLimb.attack.CurrentRandomCoolDown - AttackingLimb.attack.AfterAttackDelay)
{
return;
}
switch (AttackingLimb.attack.AfterAttack)
{
case AIBehaviorAfterAttack.Pursue:
@@ -1017,58 +1014,63 @@ namespace Barotrauma
return;
}
Vector2 offset = Character.SimPosition - steeringLimb.SimPosition;
// Offset so that we don't overshoot the movement
Vector2 steerPos = attackSimPos + offset;
if (SteeringManager is IndoorsSteeringManager pathSteering)
if (AttackingLimb != null && AttackingLimb.attack.Retreat)
{
if (pathSteering.CurrentPath != null)
UpdateFallBack(attackWorldPos, deltaTime, false);
}
else
{
Vector2 offset = Character.SimPosition - steeringLimb.SimPosition;
// Offset so that we don't overshoot the movement
Vector2 steerPos = attackSimPos + offset;
if (SteeringManager is IndoorsSteeringManager pathSteering)
{
// Attack doors
if (canAttackSub)
if (pathSteering.CurrentPath != null)
{
// If the target is in the same hull, there shouldn't be any doors blocking the path
if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull)
// Attack doors
if (canAttackSub)
{
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
// If the target is in the same hull, there shouldn't be any doors blocking the path
if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull)
{
if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget)
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
{
SelectTarget(door.Item.AiTarget, selectedTargetMemory.Priority);
return;
if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget)
{
SelectTarget(door.Item.AiTarget, selectedTargetMemory.Priority);
return;
}
}
}
}
}
// Steer towards the target if in the same room and swimming
if ((Character.AnimController.InWater || pursue) && targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull))
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition));
// Steer towards the target if in the same room and swimming
if ((Character.AnimController.InWater || pursue) && targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull))
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition));
}
else
{
SteeringManager.SteeringSeek(steerPos, 2);
// Switch to Idle when cannot reach the target and if cannot damage the walls
if ((!canAttackSub || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
{
State = AIState.Idle;
return;
}
}
}
else
{
SteeringManager.SteeringSeek(steerPos, 2);
// Switch to Idle when cannot reach the target and if cannot damage the walls
if ((!canAttackSub || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
{
State = AIState.Idle;
return;
}
SteeringManager.SteeringSeek(steerPos, 5);
}
}
else
{
SteeringManager.SteeringSeek(steerPos, 5);
SteeringManager.SteeringSeek(steerPos, 10);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
}
else
{
SteeringManager.SteeringSeek(steerPos, 10);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
if (canAttack)
{
if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb))
@@ -1110,31 +1112,11 @@ namespace Barotrauma
return false;
}
private bool CanAttack(Entity target)
{
if (target == null) { return false; }
if (target is Character c)
{
if (Character.CurrentHull == null && c.CurrentHull != null || Character.CurrentHull != null && c.CurrentHull == null)
{
return false;
}
}
else if (target is Item i && i.GetComponent<Door>() == null)
{
if (Character.CurrentHull == null && i.CurrentHull != null || Character.CurrentHull != null && i.CurrentHull == null)
{
return false;
}
}
return true;
}
private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null)
{
var currentContexts = Character.GetAttackContexts();
Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity;
if (!CanAttack(target)) { return null; }
if (target == null) { return null; }
Limb selectedLimb = null;
float currentPriority = -1;
foreach (Limb limb in Character.AnimController.Limbs)
@@ -1174,14 +1156,18 @@ namespace Barotrauma
{
wallTarget = null;
if (SelectedAiTarget == null) { return; }
if (SelectedAiTarget.Entity == null) { return; }
//check if there's a wall between the target and the Character
Vector2 rayStart = SimPosition;
Vector2 rayEnd = SelectedAiTarget.SimPosition;
bool offset = SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null;
if (offset)
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
}
else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
{
rayEnd -= Character.Submarine.SimPosition;
}
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine);
if (Submarine.LastPickedFraction != 1.0f && closestBody != null)
{
@@ -1259,9 +1245,16 @@ namespace Barotrauma
float reactionTime = Rand.Range(0.1f, 0.3f);
updateTargetsTimer = Math.Min(updateTargetsTimer, reactionTime);
wasAttacked = true;
bool wasLatched = IsLatchedOnSub;
Character.AnimController.ReleaseStuckLimbs();
LatchOntoAI?.DeattachFromBody();
if (attacker == null || attacker.AiTarget == null) { return; }
if (wasLatched)
{
avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f);
SelectTarget(attacker.AiTarget);
return;
}
if (State == AIState.Flee)
{
@@ -1272,22 +1265,30 @@ namespace Barotrauma
if (attackResult.Damage > 0.0f)
{
bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackSub;
if (Character.Params.AI.AttackWhenProvoked)
if (Character.Params.AI.AttackWhenProvoked && canAttack)
{
if (canAttack)
if (attacker.IsHusk)
{
ChangeTargetState("husk", AIState.Attack, 100);
}
else
{
ChangeTargetState(attacker, AIState.Attack, 100);
}
}
else if (!AIParams.HasTag(attacker.SpeciesName))
{
if (attacker.AIController is EnemyAIController enemyAI)
if (attacker.IsHusk)
{
ChangeTargetState("husk", canAttack ? AIState.Attack : AIState.Escape, 100);
}
else if (attacker.AIController is EnemyAIController enemyAI)
{
if (enemyAI.CombatStrength > CombatStrength)
{
if (!AIParams.HasTag("stronger"))
{
ChangeTargetState(attacker, AIState.Escape, 100);
ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100);
}
}
else if (enemyAI.CombatStrength < CombatStrength)
@@ -1305,7 +1306,14 @@ namespace Barotrauma
}
else
{
ChangeTargetState(attacker, AIState.Escape, 100);
ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100);
}
}
else if (canAttack && attacker.IsHuman && AIParams.TryGetTarget(attacker.SpeciesName, out CharacterParams.TargetParams targetingParams))
{
if (targetingParams.State == AIState.Aggressive)
{
ChangeTargetState(attacker, AIState.Attack, 100);
}
}
}
@@ -1502,7 +1510,7 @@ namespace Barotrauma
if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; }
if (!TargetOutposts)
{
if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.IsOutpost) { continue; }
if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsOutpost) { continue; }
}
Character targetCharacter = aiTarget.Entity as Character;
//ignore the aitarget if it is the Character itself
@@ -1512,29 +1520,6 @@ namespace Barotrauma
string targetingTag = null;
if (targetCharacter != null)
{
if (targetCharacter.Submarine != Character.Submarine)
{
// In a different sub or the target is outside when we are inside or vice versa.
if (State == AIState.Avoid && State == AIState.Escape & State == AIState.Flee)
{
// If we are escaping, let's not ignore the target entirely, because there can be a gaps where we or they can go freely
if (targetCharacter.Submarine != null)
{
// Target is inside -> reduce the priority
valueModifier *= 0.5f;
if (Character.Submarine != null)
{
// Both inside different submarines -> can ignore safely
continue;
}
}
}
else
{
// Don't attack targets that are not in the same submarine
continue;
}
}
if (targetCharacter.IsDead)
{
targetingTag = "dead";
@@ -1550,38 +1535,47 @@ namespace Barotrauma
// Ignore targets that are in the same group (treat them like they were of the same species)
continue;
}
if (enemy.CombatStrength > CombatStrength)
if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
{
targetingTag = "stronger";
targetingTag = "husk";
}
else if (enemy.CombatStrength < CombatStrength)
else
{
targetingTag = "weaker";
}
if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee))
{
if (SelectedAiTarget == aiTarget)
if (enemy.CombatStrength > CombatStrength)
{
// Freightened -> hold on to the target
valueModifier *= 2;
targetingTag = "stronger";
}
if (IsBeingChasedBy(targetCharacter))
else if (enemy.CombatStrength < CombatStrength)
{
valueModifier *= 2;
targetingTag = "weaker";
}
if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull))
if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee))
{
// Inside but in a different room
valueModifier /= 2;
if (SelectedAiTarget == aiTarget)
{
// Freightened -> hold on to the target
valueModifier *= 2;
}
if (IsBeingChasedBy(targetCharacter))
{
valueModifier *= 2;
}
if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull))
{
// Inside but in a different room
valueModifier /= 2;
}
}
}
}
}
else if (aiTarget.Entity != null)
{
// Ignore all structures and items inside wrecks
if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsWreck) { continue; }
// Ignore the target if it's a room and the character is already inside a sub
if (character.CurrentHull != null && aiTarget.Entity is Hull) { continue; }
Door door = null;
if (aiTarget.Entity is Item item)
{
@@ -1619,15 +1613,13 @@ namespace Barotrauma
{
continue;
}
if (character.CurrentHull != null)
bool isCharacterOutside = s.Submarine == null || character.CurrentHull == null;
bool targetInnerWalls = AIParams.TargetInnerWalls;
if (!isCharacterOutside && !targetInnerWalls)
{
// Ignore walls when inside (walltargets still work)
continue;
}
if (s.Submarine == null)
{
continue;
}
valueModifier = 1;
if (!Character.AnimController.CanEnterSubmarine && IsWallDisabled(s))
{
@@ -1640,30 +1632,37 @@ namespace Barotrauma
bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null;
if (Character.AnimController.CanEnterSubmarine)
{
if (CanPassThroughHole(s, i))
if (isCharacterOutside)
{
valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : 0;
if (CanPassThroughHole(s, i))
{
valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : (targetInnerWalls ? 1 : 0);
}
else
{
// Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in
if (!canAttackSub)
{
continue;
}
if (AggressiveBoarding && leadsInside)
{
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
valueModifier *= 1 + section.gap.Open;
}
}
}
else
else if (!canAttackSub || CanPassThroughHole(s, i))
{
// Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in
if (!canAttackSub)
{
valueModifier = 0;
break;
}
if (AggressiveBoarding)
{
// Up to 100% priority increase for every gap in the wall
valueModifier *= 1 + section.gap.Open;
}
// Already inside -> ignore holes in the walls and ignore walls if cannot attack the sub.
continue;
}
}
else if (!leadsInside)
else if (!leadsInside || !canAttackSub)
{
// Ignore inner walls
valueModifier = 0;
break;
// Can't get in, ignore inner walls
// Also ignore all walls if cannot attack the sub
continue;
}
}
}
@@ -1746,6 +1745,53 @@ namespace Barotrauma
if (valueModifier > targetValue)
{
// Don't target items that we own.
// This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive)
if (aiTarget.Entity is Item i && i.IsOwnedBy(character)) { continue; }
if (targetCharacter != null)
{
if (targetCharacter.Submarine != Character.Submarine)
{
if (targetCharacter.Submarine != null)
{
// Target is inside -> reduce the priority
valueModifier *= 0.5f;
if (Character.Submarine != null)
{
// Both inside different submarines -> can ignore safely
continue;
}
}
else if (Character.CurrentHull != null)
{
// Target outside, but we are inside -> Check if we can get to the target.
// Only check if we are not already targeting the character.
// If we are, keep the target (unless we choose another).
if (SelectedAiTarget?.Entity != targetCharacter)
{
foreach (var gap in Character.CurrentHull.ConnectedGaps)
{
var door = gap.ConnectedDoor;
if (door == null || !door.IsOpen)
{
var wall = gap.ConnectedWall;
if (wall != null)
{
for (int j = 0; j < wall.Sections.Length; j++)
{
WallSection section = wall.Sections[j];
if (!CanPassThroughHole(wall, j) && section?.gap != null)
{
continue;
}
}
}
}
}
}
}
}
}
newTarget = aiTarget;
selectedTargetMemory = targetMemory;
targetValue = valueModifier;
@@ -1867,6 +1913,39 @@ namespace Barotrauma
private readonly Dictionary<string, CharacterParams.TargetParams> modifiedParams = new Dictionary<string, CharacterParams.TargetParams>();
private readonly Dictionary<string, CharacterParams.TargetParams> tempParams = new Dictionary<string, CharacterParams.TargetParams>();
private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false)
{
if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams))
{
if (!onlyExisting && !tempParams.ContainsKey(tag))
{
if (AIParams.TryAddNewTarget(tag, state, priority ?? 100, out targetParams))
{
tempParams.Add(tag, targetParams);
}
}
}
if (targetParams != null)
{
if (priority.HasValue)
{
targetParams.Priority = priority.Value;
}
targetParams.State = state;
if (!modifiedParams.ContainsKey(tag))
{
modifiedParams.Add(tag, targetParams);
}
}
}
private void ChangeTargetState(string tag, AIState state, float? priority = null)
{
isStateChanged = true;
SetStateResetTimer();
ChangeParams(tag, state, priority);
}
/// <summary>
/// Temporarily changes the predefined state for a target. Eg. Idle -> Attack.
/// </summary>
@@ -1874,50 +1953,27 @@ namespace Barotrauma
{
isStateChanged = true;
SetStateResetTimer();
ChangeParams(target.SpeciesName);
// Target also items, because if we are blind and the target doesn't move, we can only perceive the target when it uses items
if (state == AIState.Attack || state == AIState.Escape)
ChangeParams(target.SpeciesName, state, priority);
if (target.IsHuman)
{
ChangeParams("weapon");
ChangeParams("tool");
}
if (state == AIState.Attack)
{
// If the target is shooting from the submarine, we might not perceive it because it doesn't move.
// --> Target the submarine too.
if (target.Submarine != null && canAttackSub)
// Target also items, because if we are blind and the target doesn't move, we can only perceive the target when it uses items
if (state == AIState.Attack || state == AIState.Escape)
{
ChangeParams("room");
ChangeParams("wall");
ChangeParams("door");
ChangeParams("weapon", state, priority);
ChangeParams("tool", state, priority);
}
ChangeParams("provocative", onlyExisting: true);
ChangeParams("light", onlyExisting: true);
}
void ChangeParams(string tag, bool onlyExisting = false)
{
if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams))
if (state == AIState.Attack)
{
if (!onlyExisting && !tempParams.ContainsKey(tag))
// If the target is shooting from the submarine, we might not perceive it because it doesn't move.
// --> Target the submarine too.
if (target.Submarine != null && canAttackSub)
{
if (AIParams.TryAddNewTarget(tag, state, priority ?? 100, out targetParams))
{
tempParams.Add(tag, targetParams);
}
}
}
if (targetParams != null)
{
if (priority.HasValue)
{
targetParams.Priority = priority.Value;
}
targetParams.State = state;
if (!modifiedParams.ContainsKey(tag))
{
modifiedParams.Add(tag, targetParams);
ChangeParams("room", state, priority);
ChangeParams("wall", state, priority);
ChangeParams("door", state, priority);
}
ChangeParams("provocative", state, priority, onlyExisting: true);
ChangeParams("light", state, priority, onlyExisting: true);
}
}
}
@@ -74,7 +74,7 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
if (DisableCrewAI || Character.IsUnconscious || Character.Removed) { return; }
if (DisableCrewAI || Character.IsIncapacitated || Character.Removed) { return; }
base.Update(deltaTime);
if (unreachableClearTimer > 0)
@@ -139,7 +139,10 @@ namespace Barotrauma
}
if (Character.SpeechImpediment < 100.0f)
{
ReportProblems();
if (Character.Submarine != null && Character.Submarine.TeamID == Character.TeamID && !Character.Submarine.Info.IsWreck)
{
ReportProblems();
}
UpdateSpeaking();
}
UnequipUnnecessaryItems();
@@ -170,12 +173,7 @@ namespace Barotrauma
}
}
}
if (run)
{
run = !AnimController.Crouching && !AnimController.IsMovingBackwards;
}
float currentSpeed = Character.AnimController.GetCurrentSpeed(run);
steeringManager.Update(currentSpeed);
steeringManager.Update(Character.AnimController.GetCurrentSpeed(run && Character.CanRun));
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f &&
(-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
@@ -209,17 +207,6 @@ namespace Barotrauma
targetMovement = new Vector2(Character.AnimController.TargetMovement.X, MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f));
}
float maxSpeed = Character.ApplyTemporarySpeedLimits(currentSpeed);
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
//apply speed multiplier if
// a. it's boosting the movement speed and the character is trying to move fast (= running)
// b. it's a debuff that decreases movement speed
float speedMultiplier = Character.SpeedMultiplier;
if (run || speedMultiplier <= 0.0f) targetMovement *= speedMultiplier;
Character.ResetSpeedMultiplier(); // Reset, items will set the value before the next update
if (Character.AnimController.InWater && targetMovement.LengthSquared() < 0.000001f)
{
bool isAiming = false;
@@ -243,7 +230,7 @@ namespace Barotrauma
}
}
Character.AnimController.TargetMovement = targetMovement;
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(targetMovement, AnimController.GetCurrentSpeed(run));
flipTimer -= deltaTime;
if (flipTimer <= 0.0f)
@@ -323,7 +310,7 @@ namespace Barotrauma
|| ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>()
|| ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
bool removeDivingSuit = !Character.AnimController.HeadInWater && oxygenLow;
AIObjectiveGoTo gotoObjective = ObjectiveManager.CurrentOrder as AIObjectiveGoTo;
AIObjectiveGoTo gotoObjective = ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>();
if (!removeDivingSuit)
{
bool targetHasNoSuit = gotoObjective != null && gotoObjective.mimic && !HasDivingSuit(gotoObjective.Target as Character);
@@ -536,14 +523,16 @@ namespace Barotrauma
Hull targetHull = null;
if (Character.CurrentHull != null)
{
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
bool isFleeing = ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>();
foreach (var hull in VisibleHulls)
{
foreach (Character c in Character.CharacterList)
foreach (Character target in Character.CharacterList)
{
if (c.CurrentHull != hull || !c.Enabled) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(c, Character))
if (target.CurrentHull != hull || !target.Enabled) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(target, Character))
{
if (AddTargets<AIObjectiveFightIntruders, Character>(Character, c) && newOrder == null)
if (AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
{
var orderPrefab = Order.GetPrefab("reportintruders");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
@@ -560,42 +549,48 @@ namespace Barotrauma
targetHull = hull;
}
}
foreach (Character c in Character.CharacterList)
if (!isFighting)
{
if (c.CurrentHull != hull) { continue; }
if (AIObjectiveRescueAll.IsValidTarget(c, Character))
foreach (var gap in hull.ConnectedGaps)
{
if (AddTargets<AIObjectiveRescueAll, Character>(c, Character) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
if (AIObjectiveFixLeaks.IsValidTarget(gap, Character))
{
var orderPrefab = Order.GetPrefab("requestfirstaid");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
if (AddTargets<AIObjectiveFixLeaks, Gap>(Character, gap) && newOrder == null && !gap.IsRoomToRoom)
{
var orderPrefab = Order.GetPrefab("reportbreach");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
}
}
}
}
foreach (var gap in hull.ConnectedGaps)
{
if (AIObjectiveFixLeaks.IsValidTarget(gap, Character))
if (!isFleeing)
{
if (AddTargets<AIObjectiveFixLeaks, Gap>(Character, gap) && newOrder == null && !gap.IsRoomToRoom)
foreach (Character target in Character.CharacterList)
{
var orderPrefab = Order.GetPrefab("reportbreach");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
if (target.CurrentHull != hull) { continue; }
if (AIObjectiveRescueAll.IsValidTarget(target, Character))
{
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
var orderPrefab = Order.GetPrefab("requestfirstaid");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
}
}
}
}
}
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.AIRepairThreshold)) { continue; }
if (AddTargets<AIObjectiveRepairItems, Item>(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRepairItem>())
foreach (Item item in Item.ItemList)
{
var orderPrefab = Order.GetPrefab("reportbrokendevices");
newOrder = new Order(orderPrefab, hull, item.Repairables?.FirstOrDefault(), orderGiver: Character);
targetHull = hull;
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.AIRepairThreshold)) { continue; }
if (AddTargets<AIObjectiveRepairItems, Item>(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRepairItem>())
{
var orderPrefab = Order.GetPrefab("reportbrokendevices");
newOrder = new Order(orderPrefab, hull, item.Repairables?.FirstOrDefault(), orderGiver: Character);
targetHull = hull;
}
}
}
}
}
@@ -650,7 +645,7 @@ namespace Barotrauma
// Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate).
return;
}
if (!attacker.IsRemotePlayer && Character.Controlled != attacker && attacker.AIController != null && attacker.AIController.Enabled)
if (!attacker.IsPlayer && attacker.AIController != null && attacker.AIController.Enabled)
{
// Don't retaliate on damage done by friendly ai, because we know that it's accidental
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
@@ -664,9 +659,8 @@ namespace Barotrauma
}
else
{
float currentVitality = Character.CharacterHealth.Vitality;
float dmgPercentage = damage / currentVitality * 100;
if (dmgPercentage < currentVitality / 10)
float dmgPercentage = MathUtils.Percentage(damage, Character.CharacterHealth.Vitality);
if (dmgPercentage < 10)
{
// Don't retaliate on minor (accidental) dmg done by characters that are in the same team
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
@@ -711,7 +705,6 @@ namespace Barotrauma
public void SetOrder(Order order, string option, Character orderGiver, bool speak = true)
{
SetOrderProjSpecific(order, option);
CurrentOrderOption = option;
CurrentOrder = order;
objectiveManager.SetOrder(order, option, orderGiver);
@@ -752,8 +745,6 @@ namespace Barotrauma
}
}
partial void SetOrderProjSpecific(Order order, string option);
public override void SelectTarget(AITarget target)
{
SelectedAiTarget = target;
@@ -806,11 +797,11 @@ namespace Barotrauma
/// </summary>
public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, "divingmask", "oxygensource", conditionPercentage);
public static bool HasItem(Character character, string identifier, string containedTag, float conditionPercentage = 0)
public static bool HasItem(Character character, string tagOrIdentifier, string containedTag = null, float conditionPercentage = 0)
{
if (character == null) { return false; }
if (character.Inventory == null) { return false; }
var item = character.Inventory.FindItemByIdentifier(identifier) ?? character.Inventory.FindItemByTag(identifier);
var item = character.Inventory.FindItemByIdentifier(tagOrIdentifier) ?? character.Inventory.FindItemByTag(tagOrIdentifier);
return item != null &&
item.ConditionPercentage > conditionPercentage &&
character.HasEquippedItem(item) &&
@@ -934,7 +925,7 @@ namespace Barotrauma
visibleHulls = VisibleHulls;
}
// TODO: should we calculate the visible hulls for each hull? -> could be a bit heavy.
bool ignoreFire = ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFires>() || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreFire = objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = HasDivingSuit(character);
bool ignoreOxygen = ignoreWater || HasDivingMask(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
@@ -1022,15 +1013,23 @@ namespace Barotrauma
return false;
}
public static int CountCrew(Character character, Func<HumanAIController, bool> predicate = null)
public static int CountCrew(Character character, Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false)
{
if (character == null) { return 0; }
int count = 0;
foreach (var c in Character.CharacterList)
foreach (var other in Character.CharacterList)
{
if (FilterCrewMember(character, c))
if (onlyActive && !IsActive(other))
{
if (predicate == null || predicate(c.AIController as HumanAIController))
continue;
}
if (onlyBots && other.IsPlayer)
{
continue;
}
if (FilterCrewMember(character, other))
{
if (predicate == null || predicate(other.AIController as HumanAIController))
{
count++;
}
@@ -1053,12 +1052,61 @@ namespace Barotrauma
private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self);
public static bool IsItemOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter)
{
operatingCharacter = null;
foreach (var c in Character.CharacterList)
{
if (character != null)
{
if (c == character) { continue; }
if (!IsFriendly(character, c)) { continue; }
}
if (c.SelectedConstruction != target.Item) { continue; }
operatingCharacter = c;
// If the other character is player, don't try to operate
if (c.IsRemotePlayer || Character.Controlled == c) { return true; }
if (c.AIController is HumanAIController controllingHumanAi)
{
// If the other character is ordered to operate the item, let him do it
if (controllingHumanAi.ObjectiveManager.IsCurrentOrder<AIObjectiveOperateItem>())
{
return true;
}
else
{
if (character == null)
{
return true;
}
else if (target is Steering)
{
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm");
}
else
{
return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c);
}
}
}
else
{
// Shouldn't go here, unless we allow non-humans to operate items
return false;
}
}
return false;
}
#region Wrappers
public bool IsFriendly(Character other) => IsFriendly(Character, other);
public void DoForEachCrewMember(Action<HumanAIController> action) => DoForEachCrewMember(Character, action);
public bool IsTrueForAnyCrewMember(Func<HumanAIController, bool> predicate) => IsTrueForAnyCrewMember(Character, predicate);
public bool IsTrueForAllCrewMembers(Func<HumanAIController, bool> predicate) => IsTrueForAllCrewMembers(Character, predicate);
public int CountCrew(Func<HumanAIController, bool> predicate = null) => CountCrew(Character, predicate);
public int CountCrew(Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots);
public bool IsItemOperatedByAnother(ItemComponent target, out Character operatingCharacter) => IsItemOperatedByAnother(Character, target, out operatingCharacter);
#endregion
}
}
@@ -134,7 +134,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 needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.NextNode == null || Vector2.DistanceSquared(target, currentTarget) > 1);
bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || Vector2.DistanceSquared(target, currentTarget) > 1);
//find a new path if one hasn't been found yet or the target is different from the current target
if (needsNewPath || findPathTimer < -1.0f)
{
@@ -308,7 +308,7 @@ namespace Barotrauma
currentPath.SkipToNextNode();
}
}
else
else if (!IsNextLadderSameAsCurrent)
{
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
Vector2 colliderSize = collider.GetSize();
@@ -530,7 +530,6 @@ namespace Barotrauma
if (node.Waypoint != null && node.Waypoint.CurrentHull != null)
{
var hull = node.Waypoint.CurrentHull;
if (hull.FireSources.Count > 0)
{
foreach (FireSource fs in hull.FireSources)
@@ -538,9 +537,14 @@ namespace Barotrauma
penalty += fs.Size.X * 10.0f;
}
}
if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f) penalty += 500.0f;
if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume) penalty += 1000.0f;
if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f)
{
penalty += 500.0f;
}
if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
{
penalty += 1000.0f;
}
}
return penalty;
@@ -253,7 +253,7 @@ namespace Barotrauma
jointDir = attachLimb.Dir;
Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.character.AnimController.RagdollParams.LimbScale;
Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale;
if (jointDir < 0.0f) transformedLocalAttachPos.X = -transformedLocalAttachPos.X;
//transformedLocalAttachPos = Vector2.Transform(transformedLocalAttachPos, Matrix.CreateRotationZ(attachLimb.Rotation));
@@ -136,9 +136,10 @@ namespace Barotrauma
string allowedJobsStr = element.GetAttributeString("allowedjobs", "");
foreach (string allowedJobIdentifier in allowedJobsStr.Split(','))
{
if (JobPrefab.Prefabs.ContainsKey(allowedJobIdentifier.ToLowerInvariant()))
string key = allowedJobIdentifier.ToLowerInvariant();
if (JobPrefab.Prefabs.ContainsKey(key))
{
AllowedJobs.Add(JobPrefab.Prefabs[allowedJobIdentifier.ToLowerInvariant()]);
AllowedJobs.Add(JobPrefab.Prefabs[key]);
}
}
@@ -32,6 +32,18 @@ namespace Barotrauma
public virtual bool UnequipItems => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
private float _cumulatedDevotion;
protected float CumulatedDevotion
{
get { return _cumulatedDevotion; }
set { _cumulatedDevotion = MathHelper.Clamp(value, 0, MaxDevotion); }
}
protected virtual float MaxDevotion => 10;
/// <summary>
/// Final priority value after all calculations.
/// </summary>
public float Priority { get; set; }
public float PriorityModifier { get; private set; } = 1;
public readonly Character character;
@@ -59,6 +71,7 @@ namespace Barotrauma
/// </summary>
public virtual bool IsLoop { get; set; }
public IEnumerable<AIObjective> SubObjectives => subObjectives;
public AIObjective CurrentSubObjective => subObjectives.FirstOrDefault();
private readonly List<AIObjective> all = new List<AIObjective>();
public IEnumerable<AIObjective> GetSubObjectivesRecursive(bool includingSelf = false)
@@ -86,7 +99,7 @@ namespace Barotrauma
public AIObjective GetActiveObjective()
{
var subObjective = SubObjectives.FirstOrDefault();
var subObjective = CurrentSubObjective;
return subObjective == null ? this : subObjective.GetActiveObjective();
}
@@ -157,7 +170,8 @@ namespace Barotrauma
{
if (!AllowSubObjectiveSorting) { return; }
if (subObjectives.None()) { return; }
subObjectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority()));
subObjectives.ForEach(so => so.GetPriority());
subObjectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
if (ConcurrentObjectives)
{
subObjectives.ForEach(so => so.SortSubObjectives());
@@ -168,7 +182,23 @@ namespace Barotrauma
}
}
public virtual float GetPriority() => Priority * PriorityModifier;
/// <summary>
/// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate.
/// </summary>
public virtual float GetPriority()
{
Priority = CumulatedDevotion * PriorityModifier;
return Priority;
}
private void UpdateDevotion(float deltaTime)
{
var currentObjective = objectiveManager.CurrentObjective;
if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.Any(so => so == this)))
{
CumulatedDevotion += Devotion * PriorityModifier * deltaTime;
}
}
public virtual bool IsDuplicate<T>(T otherObjective) where T : AIObjective => otherObjective.Option == Option;
@@ -180,14 +210,7 @@ namespace Barotrauma
}
else if (objectiveManager.WaitTimer <= 0)
{
if (objectiveManager.CurrentObjective != null)
{
if (objectiveManager.CurrentObjective == this || objectiveManager.CurrentObjective.subObjectives.Any(so => so == this))
{
Priority += Devotion * PriorityModifier * deltaTime;
}
}
Priority = MathHelper.Clamp(Priority, 0, 100);
UpdateDevotion(deltaTime);
}
subObjectives.ForEach(so => so.Update(deltaTime));
}
@@ -264,6 +287,7 @@ namespace Barotrauma
public virtual void OnDeselected()
{
CumulatedDevotion = 0;
Deselected?.Invoke();
}
@@ -282,6 +306,7 @@ namespace Barotrauma
isCompleted = false;
hasBeenChecked = false;
_abandon = false;
CumulatedDevotion = 0;
}
protected abstract void Act(float deltaTime);
@@ -100,7 +100,11 @@ namespace Barotrauma
}
}
public override float GetPriority() => (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100);
public override float GetPriority()
{
Priority = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100);
return Priority;
}
public override void Update(float deltaTime)
{
@@ -139,18 +143,18 @@ namespace Barotrauma
}
if (seekAmmunition == null)
{
if (TryArm() && Enemy != null && !Enemy.Removed)
if (Mode != CombatMode.Retreat && TryArm() && Enemy != null && !Enemy.Removed)
{
OperateWeapon(deltaTime);
}
if (!HoldPosition && seekAmmunition == null)
{
Move();
Move(deltaTime);
}
}
}
private void Move()
private void Move(float deltaTime)
{
switch (Mode)
{
@@ -159,7 +163,7 @@ namespace Barotrauma
break;
case CombatMode.Defensive:
case CombatMode.Retreat:
Retreat();
Retreat(deltaTime);
break;
default:
throw new NotImplementedException();
@@ -407,7 +411,10 @@ namespace Barotrauma
return true;
}
private void Retreat()
private float findHullTimer;
private readonly float findHullInterval = 1.0f;
private void Retreat(float deltaTime)
{
RemoveSubObjective(ref followTargetObjective);
RemoveSubObjective(ref seekAmmunition);
@@ -417,7 +424,15 @@ namespace Barotrauma
}
if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
{
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls);
if (findHullTimer > 0)
{
findHullTimer -= deltaTime;
}
else
{
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls);
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
if (retreatTarget != null && character.CurrentHull != retreatTarget)
{
@@ -74,15 +74,6 @@ namespace Barotrauma
}
}
public override float GetPriority()
{
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
return 1.0f;
}
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage > ConditionLevel;
protected override void Act(float deltaTime)
@@ -54,15 +54,6 @@ namespace Barotrauma
protected override bool Check() => IsCompleted;
public override float GetPriority()
{
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
return 1.0f;
}
protected override void Act(float deltaTime)
{
Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)), recursive: false);
@@ -1,5 +1,4 @@
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
@@ -29,32 +28,44 @@ namespace Barotrauma
public override float GetPriority()
{
if (!objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>()
&& Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return 0; }
float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetHull == character.CurrentHull)
&& Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
distanceFactor = 1;
Priority = 0;
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
float severityFactor = MathHelper.Lerp(0, 1, severity / 100);
float devotion = Math.Min(Priority, 10) / 100;
return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + severityFactor * distanceFactor, 0, 1));
else
{
float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetHull == character.CurrentHull)
{
distanceFactor = 1;
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
float severityFactor = MathHelper.Lerp(0, 1, severity / 100);
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severityFactor * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
protected override bool Check() => targetHull.FireSources.None();
private float sinTime;
protected override void Act(float deltaTime)
{
var extinguisherItem = character.Inventory.FindItemByIdentifier("extinguisher") ?? character.Inventory.FindItemByTag("extinguisher");
var extinguisherItem = character.Inventory.FindItemByIdentifier("fireextinguisher") ?? character.Inventory.FindItemByTag("fireextinguisher");
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
{
TryAddSubObjective(ref getExtinguisherObjective, () =>
{
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
return new AIObjectiveGetItem(character, "extinguisher", objectiveManager, equip: true);
return new AIObjectiveGetItem(character, "fireextinguisher", objectiveManager, equip: true)
{
// If the item is inside an unsafe hull, decrease the priority
GetItemPriority = i => HumanAIController.UnsafeHulls.Contains(i.CurrentHull) ? 0.1f : 1
};
});
}
else
@@ -79,8 +90,12 @@ namespace Barotrauma
{
useExtinquisherTimer = 0.0f;
}
// Aim
character.CursorPosition = fs.Position;
if (extinguisher.Item.RequireAimToUse)
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
float dist = fromCharacterToFireSource.Length();
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
if (extinguisherItem.RequireAimToUse)
{
bool isOperatingButtons = false;
if (SteeringManager == PathSteering)
@@ -95,8 +110,9 @@ namespace Barotrauma
{
character.SetInput(InputType.Aim, false, true);
}
sinTime += deltaTime * 10;
}
character.SetInput(extinguisher.Item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
character.SetInput(extinguisherItem.IsShootable ? InputType.Shoot : InputType.Use, false, true);
extinguisher.Use(deltaTime, character);
if (!targetHull.FireSources.Contains(fs))
{
@@ -110,7 +126,7 @@ namespace Barotrauma
if (move)
{
//go to the first firesource
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager)
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2)
{
DialogueIdentifier = "dialogcannotreachfire",
TargetName = fs.Hull.DisplayName
@@ -9,7 +9,6 @@ namespace Barotrauma
{
public override string DebugTag => "extinguish fires";
public override bool ForceRun => true;
public override bool IgnoreUnsafeHulls => true;
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
@@ -9,7 +9,6 @@ namespace Barotrauma
public override string DebugTag => $"find diving gear ({gearTag})";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
private readonly string gearTag;
private readonly string fallbackTag;
@@ -33,6 +33,8 @@ namespace Barotrauma
private bool resetPriority;
public override float GetPriority() => Priority;
public override void Update(float deltaTime)
{
if (resetPriority)
@@ -252,7 +254,7 @@ namespace Barotrauma
else
{
// Outside
if (hull.RoomName != null && hull.RoomName.ToLowerInvariant().Contains("airlock"))
if (hull.RoomName != null && hull.RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase))
{
hullSafety = 100;
}
@@ -29,16 +29,23 @@ namespace Barotrauma
public override float GetPriority()
{
if (Leak.Removed || Leak.Open <= 0) { return 0; }
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f));
float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90);
float devotion = Math.Min(Priority, 10) / 100;
return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + severity * distanceFactor * PriorityModifier, 0, 1));
if (Leak.Removed || Leak.Open <= 0)
{
Priority = 0;
}
else
{
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f));
float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90);
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
protected override void Act(float deltaTime)
@@ -11,7 +11,6 @@ namespace Barotrauma
public override string DebugTag => "fix leaks";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
@@ -36,7 +35,7 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>());
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>(), onlyBots: true);
int totalLeaks = Targets.Count();
if (totalLeaks == 0) { return 0; }
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
@@ -44,13 +43,13 @@ namespace Barotrauma
bool anyFixers = otherFixers > 0;
if (objectiveManager.CurrentOrder == this)
{
float ratio = anyFixers ? totalLeaks / otherFixers : 1;
float ratio = anyFixers ? totalLeaks / (float)otherFixers : 1;
return Targets.Sum(t => GetLeakSeverity(t)) * ratio;
}
else
{
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / otherFixers : 1;
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / HumanAIController.CountCrew() > 0.75f))
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
{
// Enough fixers
return 0;
@@ -31,15 +31,6 @@ namespace Barotrauma
public bool AllowToFindDivingGear { get; set; } = true;
public override float GetPriority()
{
if (objectiveManager.CurrentOrder == this)
{
return AIObjectiveManager.OrderPriority;
}
return 1.0f;
}
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
@@ -51,14 +51,19 @@ namespace Barotrauma
public override float GetPriority()
{
if (followControlledCharacter && Character.Controlled == null) { return 0.0f; }
if (Target is Entity e && e.Removed) { return 0.0f; }
if (IgnoreIfTargetDead && Target is Character character && character.IsDead) { return 0.0f; }
if (objectiveManager.CurrentOrder == this)
if (followControlledCharacter && Character.Controlled == null)
{
return AIObjectiveManager.OrderPriority;
Priority = 0;
}
return 1.0f;
if (Target is Entity e && e.Removed)
{
Priority = 0;
}
if (IgnoreIfTargetDead && Target is Character character && character.IsDead)
{
Priority = 0;
}
return objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : Priority;
}
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
@@ -45,20 +45,17 @@ namespace Barotrauma
private float randomUpdateInterval = 5;
public float Random { get; private set; }
public void SetRandom()
public void CalculatePriority()
{
Random = Rand.Range(0.5f, 1.5f);
randomTimer = randomUpdateInterval;
}
public override float GetPriority()
{
float max = Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100);
float initiative = character.GetSkillLevel("initiative");
Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random));
return Priority;
}
public override float GetPriority() => Priority;
public override void Update(float deltaTime)
{
if (objectiveManager.CurrentObjective == this)
@@ -69,7 +66,7 @@ namespace Barotrauma
}
else
{
SetRandom();
CalculatePriority();
}
}
}
@@ -182,7 +179,7 @@ namespace Barotrauma
if (!character.IsClimbing)
{
if (SteeringManager != PathSteering || (PathSteering.CurrentPath != null &&
(PathSteering.CurrentPath.NextNode == null || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
(PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
{
Wander(deltaTime);
return;
@@ -264,9 +261,9 @@ namespace Barotrauma
public static bool IsForbidden(Hull hull)
{
if (hull == null) { return true; }
string hullName = hull.RoomName?.ToLowerInvariant();
string hullName = hull.RoomName;
if (hullName == null) { return false; }
return hullName.Contains("ballast") || hullName.Contains("airlock");
return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -45,6 +45,7 @@ namespace Barotrauma
public override bool CanBeCompleted => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowSubObjectiveSorting => true;
public virtual bool InverseTargetEvaluation => false;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
@@ -107,21 +108,46 @@ namespace Barotrauma
public override float GetPriority()
{
if (character.LockHands) { return 0; }
if (character.Submarine == null) { return 0; }
if (Targets.None()) { return 0; }
// Allow the target value to be more than 100.
float targetValue = TargetEvaluation();
// If the target value is less than 1% of the max value, let's just treat it as zero.
if (targetValue < 1) { return 0; }
if (objectiveManager.CurrentOrder == this)
if (character.LockHands || character.Submarine == null || Targets.None())
{
return AIObjectiveManager.OrderPriority;
Priority = 0;
}
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
float devotion = MathHelper.Min(10, Priority);
float value = MathHelper.Clamp((devotion + targetValue * PriorityModifier) / 100, 0, 1);
return MathHelper.Lerp(0, max, value);
else
{
// Allow the target value to be more than 100.
float targetValue = TargetEvaluation();
if (InverseTargetEvaluation)
{
targetValue = 100 - targetValue;
}
var currentSubObjective = CurrentSubObjective;
if (currentSubObjective != null && currentSubObjective.Priority > targetValue)
{
// If the priority is higher than the target value, let's just use it.
// The priority calculation is more precise, but it takes into account things like distances,
// so it's better not to use it if it's lower than the rougher targetValue.
targetValue = Priority;
}
// If the target value is less than 1% of the max value, let's just treat it as zero.
if (targetValue < 1)
{
Priority = 0;
}
else
{
if (objectiveManager.CurrentOrder == this)
{
Priority = AIObjectiveManager.OrderPriority;
}
else
{
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
}
}
}
return Priority;
}
protected void UpdateTargets()
@@ -41,6 +41,15 @@ namespace Barotrauma
public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T;
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
/// <summary>
/// Returns the last active objective of the specific type.
/// </summary>
public T GetActiveObjective<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
/// <summary>
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
/// </summary>
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
@@ -146,7 +155,7 @@ namespace Barotrauma
{
var previousObjective = CurrentObjective;
var firstObjective = Objectives.FirstOrDefault();
if (CurrentOrder != null && firstObjective != null && CurrentOrder.GetPriority() > firstObjective.GetPriority())
if (CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority)
{
CurrentObjective = CurrentOrder;
}
@@ -158,14 +167,14 @@ namespace Barotrauma
{
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().SetRandom();
GetObjective<AIObjectiveIdle>().CalculatePriority();
}
return CurrentObjective;
}
public float GetCurrentPriority()
{
return CurrentObjective == null ? 0.0f : CurrentObjective.GetPriority();
return CurrentObjective == null ? 0.0f : CurrentObjective.Priority;
}
public void UpdateObjectives(float deltaTime)
@@ -205,7 +214,8 @@ namespace Barotrauma
{
if (Objectives.Any())
{
Objectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority()));
Objectives.ForEach(o => o.GetPriority());
Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
}
GetCurrentObjective()?.SortSubObjectives();
}
@@ -297,7 +307,7 @@ namespace Barotrauma
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver != null && (orderGiver == Character.Controlled || orderGiver.IsRemotePlayer)
Override = orderGiver != null && orderGiver.IsPlayer
};
break;
default:
@@ -306,7 +316,7 @@ namespace Barotrauma
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver != null && (orderGiver == Character.Controlled || orderGiver.IsRemotePlayer)
Override = orderGiver != null && orderGiver.IsPlayer
};
break;
}
@@ -3,6 +3,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -31,19 +32,33 @@ namespace Barotrauma
public override float GetPriority()
{
if (component.Item.ConditionPercentage <= 0) { return 0; }
if (objectiveManager.CurrentOrder == this)
if (component.Item.ConditionPercentage <= 0)
{
return AIObjectiveManager.OrderPriority;
Priority = 0;
}
if (component.Item.CurrentHull == null) { return 0; }
if (component.Item.CurrentHull.FireSources.Count > 0) { return 0; }
if (IsOperatedByAnother(GetTarget())) { return 0; }
if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return 0; }
float devotion = MathHelper.Min(10, Priority);
float value = devotion + AIObjectiveManager.OrderPriority * PriorityModifier;
float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90);
return MathHelper.Clamp(value, 0, max);
else
{
if (objectiveManager.CurrentOrder == this)
{
Priority = AIObjectiveManager.OrderPriority;
}
if (component.Item.CurrentHull == null || component.Item.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(GetTarget(), out _))
{
Priority = 0;
}
else if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
Priority = 0;
}
else
{
float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier);
float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90);
Priority = MathHelper.Clamp(value, 0, max);
}
}
return Priority;
}
public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1)
@@ -62,45 +77,6 @@ namespace Barotrauma
}
}
private bool IsOperatedByAnother(ItemComponent target)
{
foreach (var c in Character.CharacterList)
{
if (c == character) { continue; }
if (!HumanAIController.IsFriendly(c)) { continue; }
if (c.SelectedConstruction != target.Item) { continue; }
// If the other character is player, don't try to operate
if (c.IsRemotePlayer || Character.Controlled == c) { return true; }
if (c.AIController is HumanAIController humanAi)
{
// If the other character is ordered to operate the item, let him do it
if (humanAi.ObjectiveManager.IsCurrentOrder<AIObjectiveOperateItem>())
{
return true;
}
else
{
if (target is Steering)
{
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm");
}
else
{
return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c);
}
}
}
else
{
// Shouldn't go here, unless we allow non-humans to operate items
return false;
}
}
return false;
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
@@ -116,7 +92,7 @@ namespace Barotrauma
return;
}
// Don't allow to operate an item that someone with a better skills already operates, unless this is an order
if (objectiveManager.CurrentOrder != this && IsOperatedByAnother(target))
if (objectiveManager.CurrentOrder != this && HumanAIController.IsItemOperatedByAnother(target, out _))
{
// Don't abandon
return;
@@ -12,7 +12,6 @@ namespace Barotrauma
public override string DebugTag => "pump water";
public override bool KeepDivingGearOn => true;
public override bool UnequipItems => true;
public override bool IgnoreUnsafeHulls => true;
private IEnumerable<Pump> pumpList;
@@ -30,21 +30,28 @@ namespace Barotrauma
{
// TODO: priority list?
// Ignore items that are being repaired by someone else.
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) { return 0; }
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
if (Item.CurrentHull == character.CurrentHull)
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character))
{
distanceFactor = 1;
Priority = 0;
}
float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
float isSelected = IsRepairing ? 50 : 0;
float devotion = (Math.Min(Priority, 10) + isSelected) / 100;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + damagePriority * distanceFactor * successFactor * PriorityModifier, 0, 1));
else
{
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
if (Item.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
float isSelected = IsRepairing ? 50 : 0;
float devotion = (CumulatedDevotion + isSelected) / 100;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (damagePriority * distanceFactor * successFactor * PriorityModifier), 0, 1));
}
return Priority;
}
protected override bool Check()
@@ -158,7 +165,7 @@ namespace Barotrauma
}
repairable.StopRepairing(character);
}
else
else if (repairable.CurrentFixer != character)
{
repairable.StartRepairing(character, Repairable.FixActions.Repair);
}
@@ -81,18 +81,17 @@ namespace Barotrauma
// Don't stop fixing until done
return 100;
}
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>());
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>(), onlyBots: true);
int items = Targets.Count;
bool anyFixers = otherFixers > 0;
float ratio = anyFixers ? items / otherFixers : 1;
var result = ratio;
float ratio = anyFixers ? items / (float)otherFixers : 1;
if (objectiveManager.CurrentOrder == this)
{
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
}
else
{
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / HumanAIController.CountCrew() > 0.75f))
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
{
// Enough fixers
return 0;
@@ -14,7 +14,7 @@ namespace Barotrauma
const float TreatmentDelay = 0.5f;
const float CloseEnoughToTreat = 150.0f;
const float CloseEnoughToTreat = 100.0f;
private readonly Character targetCharacter;
@@ -22,6 +22,8 @@ namespace Barotrauma
private AIObjectiveGetItem getItemObjective;
private float treatmentTimer;
private Hull safeHull;
private float findHullTimer;
private readonly float findHullInterval = 1.0f;
public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -44,11 +46,20 @@ namespace Barotrauma
Abandon = true;
return;
}
if (targetCharacter.SelectedBy != null && targetCharacter.SelectedBy != character)
{
var otherCharacter = character.SelectedBy;
if (otherCharacter != null)
{
// Someone else is rescuing/holding the target.
Abandon = otherCharacter.IsPlayer || character.GetSkillLevel("medical") < otherCharacter.GetSkillLevel("medical");
}
}
if (targetCharacter != character)
{
// Unconcious target is not in a safe place -> Move to a safe place first
if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
// Incapacitated target is not in a safe place -> Move to a safe place first
if (targetCharacter.IsIncapacitated && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
if (character.SelectedCharacter != targetCharacter)
{
@@ -67,7 +78,11 @@ namespace Barotrauma
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () => RemoveSubObjective(ref goToObjective));
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
}
else
{
@@ -79,14 +94,26 @@ namespace Barotrauma
// Drag the character into safety
if (safeHull == null)
{
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
if (findHullTimer > 0)
{
findHullTimer -= deltaTime;
}
else
{
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
if (character.CurrentHull != safeHull)
if (safeHull != null && character.CurrentHull != safeHull)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () => RemoveSubObjective(ref goToObjective));
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
safeHull = character.CurrentHull;
});
}
}
}
@@ -105,7 +132,11 @@ namespace Barotrauma
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () => RemoveSubObjective(ref goToObjective));
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
}
else
{
@@ -127,6 +158,11 @@ namespace Barotrauma
private Dictionary<string, float> currentTreatmentSuitabilities = new Dictionary<string, float>();
private void GiveTreatment(float deltaTime)
{
if (!targetCharacter.IsPlayer)
{
// If the target is a bot, don't let it move
targetCharacter.AIController?.SteeringManager.Reset();
}
if (treatmentTimer > 0.0f)
{
treatmentTimer -= deltaTime;
@@ -137,9 +173,8 @@ namespace Barotrauma
//find which treatments are the most suitable to treat the character's current condition
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false);
var allAfflictions = GetVitalityReducingAfflictions(targetCharacter).OrderByDescending(a => a.GetVitalityDecrease(targetCharacter.CharacterHealth));
//check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in allAfflictions)
foreach (Affliction affliction in GetSortedAfflictions(targetCharacter))
{
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
@@ -200,10 +235,12 @@ namespace Barotrauma
onAbandon: () => RemoveSubObjective(ref getItemObjective));
}
}
character.AnimController.Anim = AnimController.Animation.CPR;
if (character != targetCharacter)
{
character.AnimController.Anim = AnimController.Animation.CPR;
}
}
private void ApplyTreatment(Affliction affliction, Item item)
{
var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction);
@@ -240,7 +277,7 @@ namespace Barotrauma
Abandon = true;
return false;
}
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) > AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager);
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
if (isCompleted && targetCharacter != character)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
@@ -253,20 +290,24 @@ namespace Barotrauma
{
if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
return 0;
Priority = 0;
}
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetCharacter.CurrentHull == character.CurrentHull)
else
{
distanceFactor = 1;
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetCharacter.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
}
float vitalityFactor = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter);
float devotion = Math.Min(Priority, 10) / 100;
return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + vitalityFactor * distanceFactor, 0, 1));
return Priority;
}
public static IEnumerable<Affliction> GetVitalityReducingAfflictions(Character character) => character.CharacterHealth.GetAllAfflictions(a => a.GetVitalityDecrease(character.CharacterHealth) > 0);
public static IEnumerable<Affliction> GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions());
}
}
@@ -8,11 +8,11 @@ namespace Barotrauma
{
public override string DebugTag => "rescue all";
public override bool ForceRun => true;
public override bool IgnoreUnsafeHulls => true;
public override bool InverseTargetEvaluation => true;
private const float vitalityThreshold = 80;
private const float vitalityThresholdForOrders = 95;
public static float GetVitalityThreshold(AIObjectiveManager manager)
private const float vitalityThresholdForOrders = 100;
public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
{
if (manager == null)
{
@@ -20,7 +20,7 @@ namespace Barotrauma
}
else
{
return manager.CurrentOrder is AIObjectiveRescueAll ? vitalityThresholdForOrders : vitalityThreshold;
return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? vitalityThresholdForOrders : vitalityThreshold;
}
}
@@ -31,9 +31,50 @@ namespace Barotrauma
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation() => Targets.Max(t => GetVitalityFactor(t));
protected override float TargetEvaluation()
{
int otherRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRescueAll>(), onlyBots: true);
int targetCount = Targets.Count;
bool anyRescuers = otherRescuers > 0;
float ratio = anyRescuers ? targetCount / (float)otherRescuers : 1;
if (objectiveManager.CurrentOrder == this)
{
return Targets.Min(t => GetVitalityFactor(t)) / ratio;
}
else
{
float multiplier = 1;
if (anyRescuers)
{
float mySkill = character.GetSkillLevel("medical");
int betterRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.Character.Info.Job.GetSkillLevel("medical") >= mySkill, onlyBots: true);
if (targetCount / (float)betterRescuers <= 1)
{
// Enough rescuers
return 100;
}
else
{
bool foundOtherMedics = HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.Info.Job.Prefab.Identifier == "medicaldoctor");
if (foundOtherMedics)
{
if (character.Info.Job.Prefab.Identifier != "medicaldoctor")
{
// Double the vitality factor -> less likely to take action
multiplier = 2;
}
}
}
}
return Targets.Min(t => GetVitalityFactor(t)) / ratio * multiplier;
}
}
public static float GetVitalityFactor(Character character) => Math.Min(character.HealthPercentage - character.Bleeding - character.Bloodloss - Math.Min(character.Oxygen, 0), 100);
public static float GetVitalityFactor(Character character)
{
float vitality = character.HealthPercentage - character.Bleeding - character.Bloodloss + Math.Min(character.Oxygen, 0);
return Math.Clamp(vitality, 0, 100);
}
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
@@ -47,16 +88,34 @@ namespace Barotrauma
if (!HumanAIController.IsFriendly(character, target)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (GetVitalityFactor(target) > GetVitalityThreshold(humanAI.ObjectiveManager)) { return false; }
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; }
if (!humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveRescueAll>())
{
// Ignore unsafe hulls, unless ordered
if (humanAI.UnsafeHulls.Contains(target.CurrentHull))
{
return false;
}
}
}
else
{
if (GetVitalityFactor(target) > vitalityThreshold) { return false; }
if (GetVitalityFactor(target) >= vitalityThreshold) { return false; }
}
if (target.Submarine == null || character.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
if (!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
{
// Ignore all concious targets that are currently fighting, fleeing or treating characters
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
return false;
}
}
// Don't go into rooms that have enemies
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { return false; }
return true;
@@ -291,7 +291,7 @@ namespace Barotrauma
}
for (int i = 0; i < AppropriateJobs.Length; i++)
{
if (character.Info.Job.Prefab.Identifier.ToLowerInvariant() == AppropriateJobs[i].ToLowerInvariant()) { return true; }
if (character.Info.Job.Prefab.Identifier.Equals(AppropriateJobs[i], StringComparison.OrdinalIgnoreCase)) { return true; }
}
return false;
}
@@ -0,0 +1,272 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using Barotrauma.Networking;
using System.Linq;
using System;
namespace Barotrauma
{
class WreckAI : IServerSerializable
{
public Submarine Wreck { get; private set; }
public bool IsAlive { get; private set; }
private readonly List<Item> allItems;
private readonly List<Item> thalamusItems;
private readonly List<Turret> turrets = new List<Turret>();
private readonly List<WayPoint> wayPoints = new List<WayPoint>();
private readonly List<Hull> hulls = new List<Hull>();
private readonly List<Item> spawnOrgans = new List<Item>();
private readonly Item brain;
private bool initialCellsSpawned;
public readonly WreckAIConfig Config;
private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
public WreckAI(Submarine wreck, Item brain, List<Item> items = null)
{
Config = WreckAIConfig.GetRandom();
if (Config == null)
{
DebugConsole.ThrowError("WreckAI: No wreck AI config found!");
Kill();
return;
}
allItems = items ?? wreck.GetItems(false);
thalamusItems = allItems.FindAll(i => i.Prefab.Category == MapEntityCategory.Thalamus || i.HasTag("thalamus"));
foreach (Item item in allItems)
{
if (thalamusItems.Contains(item))
{
// Ensure that thalamus items are visible
item.HiddenInGame = false;
}
else
{
// Load regular turrets
var turret = item.GetComponent<Turret>();
if (turret != null)
{
foreach (var linkedItem in item.GetLinkedEntities<Item>())
{
var container = linkedItem.GetComponent<ItemContainer>();
if (container == null) { continue; }
for (int i = 0; i < container.Inventory.Capacity; i++)
{
if (container.Inventory.Items[i] != null) { continue; }
if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab i && container.CanBeContained(i) &&
Config.ForbiddenAmmunition.None(id => id.Equals(i.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab)
{
Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, wreck);
if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false))
{
item.Remove();
}
}
}
}
}
}
}
this.brain = brain;
Wreck = wreck;
foreach (var item in Wreck.GetItems(false))
{
var turret = item.GetComponent<Turret>();
if (turret != null)
{
turrets.Add(turret);
}
if (item.HasTag("cellspawnorgan"))
{
if (!spawnOrgans.Contains(item))
{
spawnOrgans.Add(item);
}
}
}
wayPoints.AddRange(Wreck.GetWaypoints(false));
hulls.AddRange(Wreck.GetHulls(false));
IsAlive = true;
}
private readonly List<Item> destroyedOrgans = new List<Item>();
public void Update(float deltaTime)
{
if (!IsAlive || Wreck == null || Wreck.Removed)
{
cells.ForEach(c => c.OnDeath -= OnCellDeath);
return;
}
if (brain == null || brain.Removed || brain.Condition <= 0)
{
Kill();
}
destroyedOrgans.Clear();
foreach (var organ in spawnOrgans)
{
if (organ.Condition <= 0)
{
destroyedOrgans.Add(organ);
}
}
destroyedOrgans.ForEach(o => spawnOrgans.Remove(o));
bool someoneNearby = false;
float minDist = Sonar.DefaultSonarRange * 2.0f;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist)
{
someoneNearby = true;
break;
}
}
foreach (Character c in Character.CharacterList)
{
if (c != Character.Controlled && !c.IsRemotePlayer) { continue; }
if (Vector2.DistanceSquared(c.WorldPosition, Wreck.WorldPosition) < minDist * minDist)
{
someoneNearby = true;
break;
}
}
if (!someoneNearby) { return; }
OperateTurrets(deltaTime);
if (!IsClient)
{
if (!initialCellsSpawned) { SpawnInitialCells(); }
UpdateReinforcements(deltaTime);
}
}
private void SpawnInitialCells()
{
int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom);
if (brain.CurrentHull?.WaterPercentage >= MinWaterLevel)
{
for (int i = 0; i < brainRoomCells; i++)
{
if (!TrySpawnCell(out _, brain.CurrentHull)) { break; }
}
}
int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside);
for (int i = 0; i < cellsInside; i++)
{
if (!TrySpawnCell(out _)) { break; }
}
int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside);
// If we failed to spawn some of the cells in the brainroom/inside, spawn some extra cells outside.
cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - cells.Count, cellsOutside, MaxCellsOutside);
for (int i = 0; i < cellsOutside; i++)
{
ISpatialEntity targetEntity = wayPoints.GetRandom(wp => wp.CurrentHull == null);
if (targetEntity == null) { break; }
if (!TrySpawnCell(out _, targetEntity)) { break; }
}
initialCellsSpawned = true;
}
public void Kill()
{
if (!IsClient)
{
brain.Condition = 0;
}
IsAlive = false;
}
// The client doesn't use these, so we don't have to sync them.
private readonly List<Character> cells = new List<Character>();
// Intentionally contains duplicates.
private readonly List<Hull> populatedHulls = new List<Hull>();
private float cellSpawnTimer;
private float CellSpawnTime => Config.CellSpawnTime;
private float CellSpawnRandomFactor => Config.CellSpawnRandomFactor;
private int MinCellsPerBrainRoom => Config.MinCellsPerBrainRoom;
private int MaxCellsPerRoom => Config.MaxCellsPerRoom;
private int MinCellsOutside => Config.MinCellsOutside;
private int MaxCellsOutside => Config.MaxCellsOutside;
private int MinCellsInside => Config.MinCellsInside;
private int MaxCellsInside => Config.MaxCellsInside;
private int MaxCellCount => Config.MaxCellCount;
private float MinWaterLevel => Config.MinWaterLevel;
void UpdateReinforcements(float deltaTime)
{
if (cells.Count >= MaxCellCount) { return; }
cellSpawnTimer -= deltaTime;
if (cellSpawnTimer < 0)
{
TrySpawnCell(out _, spawnOrgans.GetRandom());
cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor);
}
}
bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null)
{
cell = null;
if (cells.Count >= MaxCellCount) { return false; }
if (targetEntity == null)
{
targetEntity =
wayPoints.GetRandom(wp => wp.CurrentHull != null && populatedHulls.Count(h => h == wp.CurrentHull) < MaxCellsPerRoom && wp.CurrentHull.WaterPercentage >= MinWaterLevel) ??
hulls.GetRandom(h => populatedHulls.Count(h2 => h2 == h) < MaxCellsPerRoom && h.WaterPercentage >= MinWaterLevel) as ISpatialEntity;
}
if (targetEntity == null) { return false; }
if (targetEntity is Hull h)
{
populatedHulls.Add(h);
}
else if (targetEntity is WayPoint wp && wp.CurrentHull != null)
{
populatedHulls.Add(wp.CurrentHull);
}
// Don't add items in the list, because we want to be able to ignore the restrictions for spawner organs.
cell = Character.Create("Leucocyte", targetEntity.WorldPosition, ToolBox.RandomSeed(8), hasAi: true, createNetworkEvent: true);
cells.Add(cell);
cell.OnDeath += OnCellDeath;
cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor);
return true;
}
void OperateTurrets(float deltaTime)
{
foreach (var turret in turrets)
{
// Never target other creatures than humans with the turrets.
turret.ThalamusOperate(deltaTime,
!turret.Item.HasTag("ignorecharacters"),
targetOtherCreatures: false,
!turret.Item.HasTag("ignoresubmarines"),
turret.Item.HasTag("ignoreaimdelay"));
}
}
void OnCellDeath(Character character, CauseOfDeath causeOfDeath)
{
cells.Remove(character);
}
#if SERVER
public void ServerWrite(IWriteMessage msg, Client client, object[] extraData = null)
{
msg.Write(IsAlive);
}
#endif
#if CLIENT
public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime)
{
IsAlive = msg.ReadBoolean();
}
#endif
}
}
@@ -0,0 +1,97 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class WreckAIConfig : ISerializableEntity
{
public string Name => "Wreck AI Config";
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
[Serialize(60f, false)]
public float CellSpawnTime { get; set; }
[Serialize(0.5f, false)]
public float CellSpawnRandomFactor { get; set; }
[Serialize(0, false)]
public int MinCellsPerBrainRoom { get; set; }
[Serialize(3, false)]
public int MaxCellsPerRoom { get; set; }
[Serialize(2, false)]
public int MinCellsOutside { get; set; }
[Serialize(5, false)]
public int MaxCellsOutside { get; set; }
[Serialize(3, false)]
public int MinCellsInside { get; set; }
[Serialize(10, false)]
public int MaxCellsInside { get; set; }
[Serialize(15, false)]
public int MaxCellCount { get; set; }
[Serialize(100f, false)]
public float MinWaterLevel { get; set; }
public readonly string[] ForbiddenAmmunition;
public static List<WreckAIConfig> List
{
get
{
if (paramsList == null)
{
LoadAll();
}
return paramsList;
}
}
private static List<WreckAIConfig> paramsList;
public static WreckAIConfig GetRandom() => List.GetRandom(Rand.RandSync.Server);
public WreckAIConfig(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
ForbiddenAmmunition = XMLExtensions.GetAttributeStringArray(element, "ForbiddenAmmunition", new string[0], convertToLowerInvariant: true);
}
public static void LoadAll()
{
paramsList = new List<WreckAIConfig>();
var files = GameMain.Instance.GetFilesOfType(ContentType.WreckAIConfig);
if (files.None())
{
DebugConsole.ThrowError("Cannot find any Wreck AI config!");
return;
}
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
var mainElement = doc.Root;
if (mainElement.IsOverride())
{
mainElement = doc.Root.FirstElement();
paramsList.Clear();
DebugConsole.NewMessage($"Overriding the wreck ai config with '{file.Path}'", Color.Yellow);
}
else if (paramsList.Any())
{
DebugConsole.NewMessage($"Adding additional wreck ai config from file '{file.Path}'");
}
paramsList.Add(new WreckAIConfig(mainElement));
}
}
}
}
@@ -70,7 +70,7 @@ namespace Barotrauma
}
}
if (IsDead || Vitality <= 0.0f || IsUnconscious || Stun > 0.0f) return;
if (IsDead || Vitality <= 0.0f|| Stun > 0.0f || IsIncapacitated) return;
if (!aiController.Enabled) return;
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) return;
if (Controlled == this) return;
@@ -231,25 +231,12 @@ namespace Barotrauma
}
else
{
Limb refLimb = GetLimb(LimbType.Head);
float refAngle;
if (refLimb == null)
float rotation = MathHelper.WrapAngle(Collider.Rotation);
rotation = MathHelper.ToDegrees(rotation);
if (rotation < 0.0f)
{
refAngle = CurrentAnimationParams.TorsoAngleInRadians;
refLimb = GetLimb(LimbType.Torso);
rotation += 360;
}
else
{
refAngle = CurrentAnimationParams.HeadAngleInRadians;
}
float rotation = refLimb.Rotation;
if (!float.IsNaN(refAngle)) { rotation -= refAngle * Dir; }
rotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(rotation));
if (rotation < 0.0f) rotation += 360;
if (rotation > 20 && rotation < 160)
{
TargetDir = Direction.Left;
@@ -347,9 +334,23 @@ namespace Barotrauma
target.AnimController.Collider.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + dragForce));
}
//pull the character's mouth to the target character (again with a fluctuating force)
float pullStrength = (float)(Math.Sin(eatTimer) * Math.Max(Math.Sin(eatTimer * 0.5f), 0.0f));
mouthLimb.body.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f * pullStrength, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
if (InWater)
{
//pull the character's mouth to the target character (again with a fluctuating force)
float pullStrength = (float)(Math.Sin(eatTimer) * Math.Max(Math.Sin(eatTimer * 0.5f), 0.0f));
mouthLimb.body.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f * pullStrength, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
else
{
float force = (float)Math.Sin(eatTimer * 100) * mouthLimb.Mass;
mouthLimb.body.ApplyLinearImpulse(Vector2.UnitY * force * 2, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
mouthLimb.body.ApplyTorque(-force * 50);
}
var jaw = GetLimb(LimbType.Jaw);
if (jaw != null)
{
jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25);
}
character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
@@ -439,7 +440,7 @@ namespace Barotrauma
if (CurrentSwimParams.RotateTowardsMovement)
{
Collider.SmoothRotate(movementAngle, CurrentSwimParams.SteerTorque);
Collider.SmoothRotate(movementAngle, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
if (TorsoAngle.HasValue)
{
Limb torso = GetLimb(LimbType.Torso);
@@ -491,11 +492,11 @@ namespace Barotrauma
}
if (mainLimb.type == LimbType.Head && HeadAngle.HasValue)
{
Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque);
Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
else if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
{
Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque);
Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
if (TorsoAngle.HasValue)
{
@@ -515,7 +516,7 @@ namespace Barotrauma
}
var waveLength = Math.Abs(CurrentSwimParams.WaveLength * RagdollParams.JointScale);
var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude);
var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude * character.SpeedMultiplier);
if (waveLength > 0 && waveAmplitude > 0)
{
WalkPos -= transformedMovement.Length() / Math.Abs(waveLength);
@@ -524,6 +525,10 @@ namespace Barotrauma
foreach (var limb in Limbs)
{
if (Math.Abs(limb.Params.ConstantTorque) > 0)
{
limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true);
}
switch (limb.type)
{
case LimbType.LeftFoot:
@@ -548,7 +553,7 @@ namespace Barotrauma
if (Limbs[i].SteerForce <= 0.0f) { continue; }
if (!Collider.PhysEnabled) { continue; }
Vector2 pullPos = Limbs[i].PullJointWorldAnchorA;
Limbs[i].body.ApplyForce(movement * Limbs[i].SteerForce * Limbs[i].Mass, pullPos);
Limbs[i].body.ApplyForce(movement * Limbs[i].SteerForce * Limbs[i].Mass * Math.Max(character.SpeedMultiplier, 1), pullPos);
}
Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition;
@@ -665,6 +670,10 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (Math.Abs(limb.Params.ConstantTorque) > 0)
{
limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true);
}
switch (limb.type)
{
case LimbType.LeftFoot:
@@ -729,7 +738,10 @@ namespace Barotrauma
break;
case LimbType.LeftLeg:
case LimbType.RightLeg:
if (Math.Abs(CurrentGroundedParams.LegTorque) > 0.001f) limb.body.ApplyTorque(limb.Mass * CurrentGroundedParams.LegTorque * Dir);
if (Math.Abs(CurrentGroundedParams.LegTorque) > 0)
{
limb.body.ApplyTorque(limb.Mass * CurrentGroundedParams.LegTorque * Dir);
}
break;
}
}
@@ -900,7 +900,7 @@ namespace Barotrauma
surfaceLimiter = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 0.4f) - surfacePos;
surfaceLimiter = Math.Max(1.0f, surfaceLimiter);
if (surfaceLimiter > 50.0f) return;
if (surfaceLimiter > 50.0f) { return; }
}
Limb leftHand = GetLimb(LimbType.LeftHand);
@@ -928,8 +928,7 @@ namespace Barotrauma
if (!aiming)
{
float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2;
Collider.SmoothRotate(newRotation, 5.0f);
//torso.body.SmoothRotate(newRotation);
Collider.SmoothRotate(newRotation, 5.0f * character.SpeedMultiplier);
}
}
else
@@ -942,13 +941,13 @@ namespace Barotrauma
TargetMovement = new Vector2(0.0f, -0.1f);
float newRotation = MathUtils.VectorToAngle(diff);
Collider.SmoothRotate(newRotation, 5.0f);
Collider.SmoothRotate(newRotation, 5.0f * character.SpeedMultiplier);
}
}
torso.body.MoveToPos(Collider.SimPosition + new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * 0.4f, 5.0f);
if (TargetMovement == Vector2.Zero) return;
if (TargetMovement == Vector2.Zero) { return; }
movement = MathUtils.SmoothStep(movement, TargetMovement, 0.3f);
@@ -1007,7 +1006,7 @@ namespace Barotrauma
var waist = GetLimb(LimbType.Waist);
footPos = waist == null ? Vector2.Zero : waist.SimPosition - new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * (upperLegLength + lowerLegLength);
Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength) * CurrentSwimParams.LegMoveAmount * CurrentAnimationParams.CycleSpeed, 0.0f);
Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength / character.SpeedMultiplier) * CurrentSwimParams.LegMoveAmount, 0.0f);
transformedFootPos = Vector2.Transform(transformedFootPos, Matrix.CreateRotationZ(Collider.Rotation));
if (rightFoot != null && !rightFoot.Disabled)
@@ -1061,7 +1060,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);
HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength);
HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier);
}
if (leftHand != null && !leftHand.Disabled)
@@ -1070,7 +1069,7 @@ namespace Barotrauma
leftHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, leftHandPos.X) : Math.Min(-0.3f, leftHandPos.X);
leftHandPos = Vector2.Transform(leftHandPos, rotationMatrix);
HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength);
HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier);
}
}
@@ -1653,7 +1652,10 @@ namespace Barotrauma
//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)
{
if (character.IsUnconscious || character.Stun > 0.0f) aim = false;
if (character.Stun > 0.0f || character.IsIncapacitated)
{
aim = false;
}
//calculate the handle positions
Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation);
@@ -1677,7 +1679,7 @@ namespace Barotrauma
Holdable holdable = item.GetComponent<Holdable>();
if (!isClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero)
if (!isClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero && !character.IsIncapacitated)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition);
@@ -1764,7 +1766,7 @@ namespace Barotrauma
if (holdable.Pusher != null)
{
if (character.IsUnconscious || character.Stun > 0.0f)
if (character.Stun > 0.0f || character.IsIncapacitated)
{
holdable.Pusher.Enabled = false;
}
@@ -1779,7 +1781,7 @@ namespace Barotrauma
else
{
holdable.Pusher.TargetPosition = currItemPos;
holdable.Pusher.TargetRotation = character.IsUnconscious || character.Stun > 0.0f ? itemAngle : holdAngle * Dir;
holdable.Pusher.TargetRotation = holdAngle * Dir;
holdable.Pusher.MoveToTargetPosition(true);
@@ -1929,7 +1931,7 @@ namespace Barotrauma
float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos);
if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2))
{
TargetMovement = Vector2.Normalize(handWorldPos - character.WorldPosition) * GetCurrentSpeed(false);
TargetMovement = Vector2.Normalize(handWorldPos - character.WorldPosition) * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1);
}
}
@@ -97,8 +97,6 @@ namespace Barotrauma
protected float strongestImpact;
protected double onFloorTimer;
private float splashSoundTimer;
//the movement speed of the ragdoll
@@ -383,7 +381,7 @@ namespace Barotrauma
}
}
if (character.IsHusk)
if (character.IsHusk && character.Params.UseHuskAppendage)
{
var characterPrefab = CharacterPrefab.FindByFilePath(character.ConfigPath);
if (characterPrefab?.XDocument != null)
@@ -732,14 +730,20 @@ namespace Barotrauma
limbJoint.IsSevered = true;
limbJoint.Enabled = false;
Vector2 limbDiff = limbJoint.LimbA.SimPosition - limbJoint.LimbB.SimPosition;
if (limbDiff.LengthSquared() < 0.0001f) { limbDiff = Rand.Vector(1.0f); }
limbDiff = Vector2.Normalize(limbDiff);
float mass = limbJoint.BodyA.Mass + limbJoint.BodyB.Mass;
limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
List<Limb> connectedLimbs = new List<Limb>();
List<LimbJoint> checkedJoints = new List<LimbJoint>();
GetConnectedLimbs(connectedLimbs, checkedJoints, MainLimb);
foreach (Limb limb in Limbs)
{
if (connectedLimbs.Contains(limb)) continue;
if (connectedLimbs.Contains(limb)) { continue; }
limb.IsSevered = true;
}
@@ -1626,14 +1630,14 @@ namespace Barotrauma
float sin = (float)Math.Sin(mouthLimb.Rotation);
Vector2 bodySize = mouthLimb.body.GetSize();
Vector2 offset = new Vector2(mouthLimb.MouthPos.X * bodySize.X / 2, mouthLimb.MouthPos.Y * bodySize.Y / 2);
return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos) * RagdollParams.LimbScale;
return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos) * mouthLimb.Scale * RagdollParams.LimbScale;
}
public Vector2 GetColliderBottom()
{
float offset = 0.0f;
if (!character.IsUnconscious && !character.IsDead && character.Stun <= 0.0f)
if (!character.IsDead && character.Stun <= 0.0f && !character.IsIncapacitated)
{
offset = -ColliderHeightFromFloor;
}
@@ -13,11 +13,12 @@ namespace Barotrauma
public enum AttackContext
{
NotDefined,
Any,
Water,
Ground,
Inside,
Outside
Outside,
NotDefined
}
public enum AttackTarget
@@ -72,13 +73,13 @@ namespace Barotrauma
partial class Attack : ISerializableEntity
{
[Serialize(AttackContext.NotDefined, true, description: "The attack will be used only in this context."), Editable]
[Serialize(AttackContext.Any, true, description: "The attack will be used only in this context."), Editable]
public AttackContext Context { get; private set; }
[Serialize(AttackTarget.Any, true, description: "Does the attack target only specific targets?"), Editable]
public AttackTarget TargetType { get; private set; }
[Serialize(LimbType.None, true, description: "If not defined or set to none, the closest limb is used (default)."), Editable]
[Serialize(LimbType.None, true, description: "To which limb is the attack aimed at? If not defined or set to none, the closest limb is used (default)."), Editable]
public LimbType TargetLimbType { get; private set; }
[Serialize(HitDetection.Distance, true, description: "Collision detection is more accurate, but it only affects targets that are in contact with the limb."), Editable]
@@ -87,9 +88,15 @@ namespace Barotrauma
[Serialize(AIBehaviorAfterAttack.FallBack, true, description: "The preferred AI behavior after the attack."), Editable]
public AIBehaviorAfterAttack AfterAttack { get; set; }
[Serialize(false, true, description: "Should the AI try to reverse when aiming with this attack?"), Editable]
[Serialize(0f, true, description: "A delay before reacting after performing an attack."), Editable]
public float AfterAttackDelay { get; set; }
[Serialize(false, true, description: "Should the AI try to turn around when aiming with this attack?"), Editable]
public bool Reverse { get; private set; }
[Serialize(false, true, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable]
public bool Retreat { get; private set; }
[Serialize(0.0f, true, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)]
public float Range { get; set; }
@@ -147,7 +154,19 @@ namespace Barotrauma
[Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public float Force { get; private set; }
[Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
[Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
public Vector2 RootForceWorldStart { get; private set; }
[Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
public Vector2 RootForceWorldMiddle { get; private set; }
[Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
public Vector2 RootForceWorldEnd { get; private set; }
[Serialize(TransitionMode.Linear, true, description:""), Editable]
public TransitionMode RootTransitionEasing { get; private set; }
[Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)]
public float Torque { get; private set; }
[Serialize(false, true), Editable]
@@ -156,13 +175,13 @@ namespace Barotrauma
[Serialize(0.0f, true, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
public float TargetImpulse { get; private set; }
[Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards)."), Editable]
[Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable]
public Vector2 TargetImpulseWorld { get; private set; }
[Serialize(0.0f, true, description: "Applied to the target the attack hits. The direction of the force is from this limb towards the target (use negative values to pull the target closer)."), Editable(-1000.0f, 1000.0f)]
public float TargetForce { get; private set; }
[Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards)."), Editable]
[Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable]
public Vector2 TargetForceWorld { get; private set; }
[Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed when the target is dead."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
@@ -279,7 +298,7 @@ namespace Barotrauma
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.");
string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.ToLowerInvariant() == afflictionName);
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.");
@@ -289,7 +308,7 @@ namespace Barotrauma
else
{
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier);
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.");
@@ -324,7 +343,7 @@ namespace Barotrauma
AfflictionPrefab afflictionPrefab;
Affliction affliction;
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier);
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab != null)
{
float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
@@ -419,7 +438,10 @@ namespace Barotrauma
public AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound = true)
{
if (targetLimb == null) return new AttackResult();
if (targetLimb == null)
{
return new AttackResult();
}
if (OnlyHumans)
{
@@ -461,6 +483,7 @@ namespace Barotrauma
public float AttackTimer { get; private set; }
public float CoolDownTimer { get; set; }
public float CurrentRandomCoolDown { get; private set; }
public float SecondaryCoolDownTimer { get; set; }
public bool IsRunning { get; private set; }
@@ -492,7 +515,8 @@ namespace Barotrauma
public void SetCoolDown()
{
float randomFraction = CoolDown * CoolDownRandomFactor;
CoolDownTimer = CoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server));
CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server));
CoolDownTimer = CoolDown + CurrentRandomCoolDown;
randomFraction = SecondaryCoolDown * CoolDownRandomFactor;
SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server));
}
@@ -501,11 +525,12 @@ namespace Barotrauma
{
CoolDownTimer = 0;
SecondaryCoolDownTimer = 0;
CurrentRandomCoolDown = 0;
}
partial void DamageParticles(float deltaTime, Vector2 worldPosition);
public bool IsValidContext(AttackContext context) => Context == context || Context == AttackContext.NotDefined;
public bool IsValidContext(AttackContext context) => Context == context || Context == AttackContext.Any || Context == AttackContext.NotDefined;
public bool IsValidContext(IEnumerable<AttackContext> contexts)
{
@@ -559,5 +584,11 @@ namespace Barotrauma
return true;
}
}
public Vector2 CalculateAttackPhase(TransitionMode easing = TransitionMode.Linear)
{
float t = AttackTimer / Duration;
return MathUtils.Bezier(RootForceWorldStart, RootForceWorldMiddle, RootForceWorldEnd, ToolBox.GetEasing(easing, t));
}
}
}
@@ -57,6 +57,9 @@ namespace Barotrauma
public Hull CurrentHull = null;
public bool IsRemotePlayer;
public bool IsPlayer => Controlled == this || IsRemotePlayer;
public readonly Dictionary<string, SerializableProperty> Properties;
public Dictionary<string, SerializableProperty> SerializableProperties
{
@@ -122,19 +125,54 @@ namespace Barotrauma
set => Params.NeedsAir = value;
}
public bool NeedsWater
{
get => Params.NeedsWater;
set => Params.NeedsWater = value;
}
public bool NeedsOxygen => NeedsAir || NeedsWater && !AnimController.InWater;
public float Noise
{
get => Params.Noise;
set => Params.Noise = value;
}
public float Visibility
{
get => Params.Visibility;
set => Params.Visibility = value;
}
public bool IsTraitor;
public string TraitorCurrentObjective = "";
public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase);
private float attackCoolDown;
public Order CurrentOrder { get; private set; }
public Order CurrentOrder
{
get
{
return Info?.CurrentOrder;
}
private set
{
if (Info != null) { Info.CurrentOrder = value; }
}
}
public string CurrentOrderOption
{
get
{
return Info?.CurrentOrderOption;
}
private set
{
if (Info != null) { Info.CurrentOrderOption = value; }
}
}
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
private readonly List<float> speedMultipliers = new List<float>();
@@ -160,7 +198,7 @@ namespace Barotrauma
if (turret != null)
{
viewTargetWorldPos = new Vector2(
targetItem.WorldRect.X + turret.TransformedBarrelPos.X,
targetItem.WorldRect.X + turret.TransformedBarrelPos.X,
targetItem.WorldRect.Y - turret.TransformedBarrelPos.Y);
}
}
@@ -201,7 +239,7 @@ namespace Barotrauma
{
displayName = TextManager.Get($"Character.{SpeciesName}", returnNull: true);
}
return displayName ?? Name;
return string.IsNullOrWhiteSpace(displayName) ? Name : displayName;
}
}
@@ -245,7 +283,7 @@ namespace Barotrauma
//text displayed when the character is highlighted if custom interact is set
public string customInteractHUDText;
private Action<Character, Character> onCustomInteract;
private float lockHandsTimer;
public bool LockHands
{
@@ -261,22 +299,22 @@ namespace Barotrauma
public bool AllowInput
{
get { return !IsUnconscious && Stun <= 0.0f && !IsDead; }
get { return Stun <= 0.0f && !IsDead && !IsIncapacitated; }
}
public bool CanMove
{
get
{
if (!AllowInput) { return false; }
if (!AnimController.InWater && !AnimController.CanWalk) { return false; }
if (!AllowInput) { return false; }
return true;
}
}
public bool CanInteract
{
get { return AllowInput && IsHumanoid && !LockHands && !Removed; }
get { return AllowInput && IsHumanoid && !LockHands && !Removed && !IsIncapacitated; }
}
public Vector2 CursorPosition
@@ -362,12 +400,21 @@ namespace Barotrauma
pressureProtection = MathHelper.Clamp(value, 0.0f, 100.0f);
}
}
private float ragdollingLockTimer;
public bool IsRagdolled;
public bool IsForceRagdolled;
public bool dontFollowCursor;
public bool IsIncapacitated
{
get
{
if (IsUnconscious) { return true; }
return CharacterHealth.Afflictions.Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
}
}
public bool IsUnconscious
{
get { return CharacterHealth.IsUnconscious; }
@@ -491,6 +538,8 @@ namespace Barotrauma
public bool IsDead { get; private set; }
public bool EnableDespawn { get; set; } = true;
public CauseOfDeath CauseOfDeath
{
get;
@@ -513,7 +562,7 @@ namespace Barotrauma
{
if (!canBeDragged) { return false; }
if (Removed || !AnimController.Draggable) { return false; }
return IsDead || Stun > 0.0f || LockHands || IsUnconscious;
return IsDead || Stun > 0.0f || LockHands || IsIncapacitated;
}
set { canBeDragged = value; }
}
@@ -531,7 +580,7 @@ namespace Barotrauma
}
else
{
return (IsDead || Stun > 0.0f || LockHands || IsUnconscious);
return (IsDead || Stun > 0.0f || LockHands || IsIncapacitated);
}
}
set { canInventoryBeAccessed = value; }
@@ -758,7 +807,7 @@ namespace Barotrauma
var matchingAffliction = AfflictionPrefab.List
.Where(p => p.AfflictionType == "huskinfection")
.Select(p => p as AfflictionPrefabHusk)
.FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.InvariantCultureIgnoreCase)));
.FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.OrdinalIgnoreCase)));
string nonHuskedSpeciesName = string.Empty;
if (matchingAffliction == null)
{
@@ -770,10 +819,14 @@ namespace Barotrauma
{
nonHuskedSpeciesName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, matchingAffliction);
}
ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(nonHuskedSpeciesName) : RagdollParams.GetDefaultRagdollParams<FishRagdollParams>(nonHuskedSpeciesName) as RagdollParams;
if (info == null)
if (ragdollParams == null)
{
info = new CharacterInfo(nonHuskedSpeciesName, ragdollParams.FileName);
string name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName;
ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(name) : RagdollParams.GetDefaultRagdollParams<FishRagdollParams>(name) as RagdollParams;
}
if (Params.HasInfo && info == null)
{
info = new CharacterInfo(nonHuskedSpeciesName);
}
}
@@ -844,7 +897,7 @@ namespace Barotrauma
Info.HairElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Hair)));
#if CLIENT
head.LoadHuskSprite();
head.EnableHuskSprite = Params.Husk;
head.LoadHerpesSprite();
head.UpdateWearableTypesToHide();
#endif
@@ -1010,59 +1063,55 @@ namespace Barotrauma
}
else
{
if (IsKeyDown(InputType.Left)) targetMovement.X -= 1.0f;
if (IsKeyDown(InputType.Right)) targetMovement.X += 1.0f;
if (IsKeyDown(InputType.Up)) targetMovement.Y += 1.0f;
if (IsKeyDown(InputType.Down)) targetMovement.Y -= 1.0f;
if (IsKeyDown(InputType.Left)) { targetMovement.X -= 1.0f; }
if (IsKeyDown(InputType.Right)) { targetMovement.X += 1.0f; }
if (IsKeyDown(InputType.Up)) { targetMovement.Y += 1.0f; }
if (IsKeyDown(InputType.Down)) { targetMovement.Y -= 1.0f; }
}
bool run = false;
if ((IsKeyDown(InputType.Run) && AnimController.ForceSelectAnimationType == AnimationType.NotDefined) || ForceRun)
{
run = CanRun;
}
return ApplyMovementLimits(targetMovement, AnimController.GetCurrentSpeed(run));
}
//can't run if
// - dragging someone
// - crouching
// - moving backwards
public bool CanRun => (SelectedCharacter == null || !SelectedCharacter.CanBeDragged) &&
(!(AnimController is HumanoidAnimController) || !((HumanoidAnimController)AnimController).Crouching) &&
!AnimController.IsMovingBackwards;
public Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed)
{
//the vertical component is only used for falling through platforms and climbing ladders when not in water,
//so the movement can't be normalized or the Character would walk slower when pressing down/up
if (AnimController.InWater)
{
float length = targetMovement.Length();
if (length > 0.0f) targetMovement /= length;
if (length > 0.0f)
{
targetMovement /= length;
}
}
bool run = false;
if ((IsKeyDown(InputType.Run) && AnimController.ForceSelectAnimationType == AnimationType.NotDefined) || ForceRun)
{
//can't run if
// - dragging someone
// - crouching
// - moving backwards
run = (SelectedCharacter == null || !SelectedCharacter.CanBeDragged) &&
(!(AnimController is HumanoidAnimController) || !((HumanoidAnimController)AnimController).Crouching) &&
!AnimController.IsMovingBackwards;
}
float currentSpeed = AnimController.GetCurrentSpeed(run);
targetMovement *= currentSpeed;
float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed);
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
//apply speed multiplier if
// a. it's boosting the movement speed and the character is trying to move fast (= running)
// b. it's a debuff that decreases movement speed
float speedMultiplier = SpeedMultiplier;
if (run || speedMultiplier <= 0.0f) targetMovement *= speedMultiplier;
ResetSpeedMultiplier(); // Reset, items will set the value before the next update
SpeedMultiplier = greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier);
targetMovement *= SpeedMultiplier;
// Reset, status effects will set the value before the next update
ResetSpeedMultiplier();
return targetMovement;
}
/// <summary>
/// Can be used to modify the character's speed via StatusEffects
/// </summary>
public float SpeedMultiplier
{
get
{
return greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier);
}
}
public float SpeedMultiplier { get; private set; }
public void StackSpeedMultiplier(float val)
{
@@ -2012,8 +2061,8 @@ namespace Barotrauma
HideFace = false;
UpdateSightRange();
UpdateSoundRange();
UpdateSightRange(deltaTime);
UpdateSoundRange(deltaTime);
if (IsDead) { return; }
@@ -2084,12 +2133,17 @@ namespace Barotrauma
UpdateControlled(deltaTime, cam);
//Health effects
if (NeedsAir) { UpdateOxygen(deltaTime); }
if (NeedsOxygen)
{
UpdateOxygen(deltaTime);
}
CharacterHealth.Update(deltaTime);
if (IsUnconscious)
if (IsIncapacitated)
{
UpdateUnconscious();
Stun = Math.Max(5.0f, Stun);
AnimController.ResetPullJoints();
SelectedConstruction = null;
return;
}
@@ -2162,33 +2216,44 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime, Camera cam);
partial void SetOrderProjSpecific(Order order, string orderOption);
private void UpdateOxygen(float deltaTime)
{
PressureProtection -= deltaTime * 100.0f;
float hullAvailableOxygen = 0.0f;
if (!AnimController.HeadInWater && AnimController.CurrentHull != null)
if (NeedsAir)
{
//don't decrease the amount of oxygen in the hull if the character has more oxygen available than the hull
//(i.e. if the character has some external source of oxygen)
if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage)
PressureProtection -= deltaTime * 100.0f;
}
if (NeedsWater)
{
float waterAvailable = 100;
if (!AnimController.InWater && CurrentHull != null)
{
AnimController.CurrentHull.Oxygen -= Hull.OxygenConsumptionSpeed * deltaTime;
waterAvailable = CurrentHull.WaterPercentage;
}
hullAvailableOxygen = AnimController.CurrentHull.OxygenPercentage;
OxygenAvailable += MathHelper.Clamp(waterAvailable - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f);
}
else
{
float hullAvailableOxygen = 0.0f;
if (!AnimController.HeadInWater && AnimController.CurrentHull != null)
{
//don't decrease the amount of oxygen in the hull if the character has more oxygen available than the hull
//(i.e. if the character has some external source of oxygen)
if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage)
{
AnimController.CurrentHull.Oxygen -= Hull.OxygenConsumptionSpeed * deltaTime;
}
hullAvailableOxygen = AnimController.CurrentHull.OxygenPercentage;
}
OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f);
}
OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f);
}
partial void UpdateOxygenProjSpecific(float prevOxygen);
private void UpdateUnconscious()
{
Stun = Math.Max(5.0f, Stun);
AnimController.ResetPullJoints();
SelectedConstruction = null;
}
/// <summary>
/// How far the character is from the closest human player (including spectators)
/// </summary>
@@ -2221,23 +2286,29 @@ namespace Barotrauma
}
private float despawnTimer;
private const float DespawnDelay = 5.0f * 60.0f; //5 minutes
private void UpdateDespawn(float deltaTime)
{
if (!EnableDespawn) { return; }
//clients don't despawn characters unless the server says so
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
if (!IsDead) { return; }
if (Submarine != null && CharacterList.Count(c => c.IsDead && c.Submarine == Submarine) < GameMain.Config.CorpsesPerSubDespawnThreshold)
{
return;
}
float distToClosestPlayer = GetDistanceToClosestPlayer();
if (distToClosestPlayer > NetConfig.DisableCharacterDist)
{
//despawn in 1 second if very far from all human players
despawnTimer = Math.Max(despawnTimer, DespawnDelay - 1.0f);
//despawn in 1 minute if very far from all human players
despawnTimer = Math.Max(despawnTimer, GameMain.Config.CorpseDespawnDelay - 60.0f);
}
despawnTimer += deltaTime;
if (despawnTimer < DespawnDelay) { return; }
if (despawnTimer < GameMain.Config.CorpseDespawnDelay) { return; }
if (IsHuman)
{
@@ -2264,7 +2335,12 @@ namespace Barotrauma
if (itemContainer == null) { return; }
foreach (Item inventoryItem in Inventory.Items)
{
itemContainer.Inventory.TryPutItem(inventoryItem, user: null);
if (inventoryItem == null) { continue; }
if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null))
{
//if the item couldn't be put inside the despawn container, just drop it
inventoryItem.Drop(dropper: this);
}
}
}
}
@@ -2274,7 +2350,7 @@ namespace Barotrauma
public void DespawnNow()
{
despawnTimer = DespawnDelay;
despawnTimer = GameMain.Config.CorpseDespawnDelay;
}
public static void RemoveByPrefab(CharacterPrefab prefab)
@@ -2290,18 +2366,39 @@ namespace Barotrauma
}
}
private void UpdateSightRange()
private readonly float maxAIRange = 10000;
private readonly float aiTargetChangeSpeed = 5;
private void UpdateSightRange(float deltaTime)
{
if (aiTarget == null) { return; }
float range = (float)Math.Sqrt(Mass) * 250 + AnimController.Collider.LinearVelocity.Length() * 500;
aiTarget.SightRange = MathHelper.Clamp(range, 0, 10000);
float minRange = Math.Clamp((float)Math.Sqrt(Mass) * Visibility, 250, 1000);
float massFactor = (float)Math.Sqrt(Mass / 20);
float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange);
float newRange = MathHelper.SmoothStep(aiTarget.SightRange, targetRange, deltaTime * aiTargetChangeSpeed);
if (!float.IsNaN(newRange))
{
aiTarget.SightRange = newRange;
}
}
private void UpdateSoundRange()
private void UpdateSoundRange(float deltaTime)
{
if (aiTarget == null) { return; }
float range = ((float)Math.Sqrt(Mass) / 3) * (AnimController.TargetMovement.Length() * 2) * Noise;
aiTarget.SoundRange = MathHelper.Clamp(range, 0, 10000);
if (IsDead)
{
aiTarget.SoundRange = 0;
}
else
{
float massFactor = (float)Math.Sqrt(Mass / 10);
float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange);
float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed);
if (!float.IsNaN(newRange))
{
aiTarget.SoundRange = newRange;
}
}
}
public bool CanHearCharacter(Character speaker)
@@ -2316,24 +2413,17 @@ namespace Barotrauma
public void SetOrder(Order order, string orderOption, Character orderGiver, bool speak = true)
{
if (orderGiver != null)
{
//set the character order only if the character is close enough to hear the message
if (!CanHearCharacter(orderGiver)) { return; }
}
//set the character order only if the character is close enough to hear the message
if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
if (AIController is HumanAIController humanAI)
{
humanAI.SetOrder(order, orderOption, orderGiver, speak);
}
#if CLIENT
else
{
GameMain.GameSession?.CrewManager?.DisplayCharacterOrder(this, order, orderOption);
}
#endif
SetOrderProjSpecific(order, orderOption);
CurrentOrder = order;
CurrentOrderOption = orderOption;
}
private readonly List<AIChatMessage> aiChatMessageQueue = new List<AIChatMessage>();
@@ -2469,13 +2559,17 @@ namespace Barotrauma
DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker);
if (limbHit == null) { return new AttackResult(); }
limbHit.body?.ApplyLinearImpulse(attack.TargetImpulseWorld + attack.TargetForceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld;
if (attacker != null)
{
forceWorld.X *= attacker.AnimController.Dir;
}
limbHit.body?.ApplyLinearImpulse(forceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
var mainLimb = limbHit.character.AnimController.MainLimb;
if (limbHit != mainLimb)
{
// Always add force to mainlimb
mainLimb.body?.ApplyLinearImpulse(attack.TargetImpulseWorld + attack.TargetForceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
mainLimb.body?.ApplyLinearImpulse(forceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
#if SERVER
if (attacker is Character attackingCharacter && attackingCharacter.AIController == null)
@@ -2602,6 +2696,7 @@ namespace Barotrauma
mainLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
bool wasDead = IsDead;
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound);
CharacterHealth.ApplyDamage(hitLimb, attackResult);
@@ -2609,6 +2704,10 @@ namespace Barotrauma
{
OnAttacked?.Invoke(attacker, attackResult);
OnAttackedProjSpecific(attacker, attackResult);
if (!wasDead)
{
TryAdjustAttackerSkill(attacker, -attackResult.Damage);
}
};
if (attacker != null && attackResult.Damage > 0.0f)
@@ -2621,6 +2720,30 @@ namespace Barotrauma
partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult);
public void TryAdjustAttackerSkill(Character attacker, float healthChange)
{
if (attacker == null) { return; }
bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID;
if (isEnemy)
{
if (healthChange < 0.0f)
{
float attackerSkillLevel = attacker.GetSkillLevel("weapons");
attacker.Info?.IncreaseSkillLevel("weapons",
-healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f),
attacker.WorldPosition + Vector2.UnitY * 100.0f);
}
}
else if (healthChange > 0.0f)
{
float attackerSkillLevel = attacker.GetSkillLevel("medical");
attacker.Info?.IncreaseSkillLevel("medical",
healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f),
attacker.WorldPosition + Vector2.UnitY * 100.0f);
}
}
public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
@@ -2702,7 +2825,7 @@ namespace Barotrauma
partial void ImplodeFX();
public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false)
public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true)
{
if (IsDead || CharacterHealth.Unkillable) { return; }
@@ -2745,9 +2868,9 @@ namespace Barotrauma
SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath);
KillProjSpecific(causeOfDeath, causeOfDeathAffliction);
KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log);
if (info != null) info.CauseOfDeath = CauseOfDeath;
if (info != null) { info.CauseOfDeath = CauseOfDeath; }
AnimController.movement = Vector2.Zero;
AnimController.TargetMovement = Vector2.Zero;
@@ -2770,7 +2893,7 @@ namespace Barotrauma
GameMain.GameSession.KillCharacter(this);
}
}
partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction);
partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log);
public void Revive()
{
@@ -293,6 +293,9 @@ namespace Barotrauma
private NPCPersonalityTrait personalityTrait;
public Order CurrentOrder { get; set;}
public string CurrentOrderOption { get; set; }
//unique ID given to character infos in MP
//used by clients to identify which infos are the same to prevent duplicate characters in round summary
public ushort ID;
@@ -524,9 +527,11 @@ namespace Barotrauma
}
foreach (XElement subElement in infoElement.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "job") continue;
Job = new Job(subElement);
break;
if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase))
{
Job = new Job(subElement);
break;
}
}
LoadHeadAttachments();
}
@@ -661,7 +666,7 @@ namespace Barotrauma
{
foreach (XElement limbElement in Ragdoll.MainElement.Elements())
{
if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") { continue; }
if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; }
XElement spriteElement = limbElement.Element("sprite");
if (spriteElement == null) { continue; }
@@ -677,7 +682,7 @@ namespace Barotrauma
//go through the files in the directory to find a matching sprite
foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath)))
{
if (!file.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase))
if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -828,6 +833,11 @@ namespace Barotrauma
{
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
if (Job.Prefab.Identifier == "assistant")
{
increase *= SkillSettings.Current.AssistantSkillIncreaseMultiplier;
}
float prevLevel = Job.GetSkillLevel(skillIdentifier);
Job.IncreaseSkillLevel(skillIdentifier, increase);
@@ -955,7 +965,7 @@ namespace Barotrauma
foreach (XElement childInvElement in itemElement.Elements())
{
if (itemContainerIndex >= itemContainers.Count) break;
if (childInvElement.Name.ToString().ToLowerInvariant() != "inventory") continue;
if (!childInvElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; }
SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement);
itemContainerIndex++;
}
@@ -14,8 +14,13 @@ namespace Barotrauma
public Dictionary<string, SerializableProperty> SerializableProperties { get; set; }
protected float _strength;
[Serialize(0f, true), Editable]
public float Strength { get; set; }
public virtual float Strength
{
get { return _strength; }
set { _strength = value; }
}
[Serialize("", true), Editable]
public string Identifier { get; private set; }
@@ -38,7 +43,7 @@ namespace Barotrauma
public Affliction(AfflictionPrefab prefab, float strength)
{
Prefab = prefab;
Strength = strength;
_strength = strength;
Identifier = prefab?.Identifier;
}
@@ -173,11 +178,11 @@ namespace Barotrauma
if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted
{
Strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier;
_strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier;
}
else // Reduce strengthening of afflictions if resistant
{
Strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier));
_strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier));
}
foreach (StatusEffect statusEffect in currentEffect.StatusEffects)
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using System;
namespace Barotrauma
{
@@ -9,7 +9,7 @@ namespace Barotrauma
{
public enum InfectionState
{
Dormant, Transition, Active
Initial, Dormant, Transition, Active, Final
}
private bool subscribedToDeathEvent;
@@ -17,154 +17,157 @@ namespace Barotrauma
private InfectionState state;
private List<Limb> huskAppendage;
private Character character;
private readonly List<Affliction> huskInfection = new List<Affliction>();
[Serialize(0f, true), Editable]
public override float Strength
{
get { return _strength; }
set
{
// Don't allow to set the strength too high (from outside) to avoid rapid transformation into husk when taking lots of damage from husks.
// If the strength is more than the value, this will effectively reset the current strength to the max. That's why we use two steps.
float max = _strength > ActiveThreshold ? ActiveThreshold + 1 : DormantThreshold - 1;
_strength = Math.Clamp(value, 0, max);
}
}
public InfectionState State
{
get { return state; }
private set
{
if (state == value) { return; }
state = value;
if (character != null && character == Character.Controlled)
{
UpdateMessages();
}
}
}
public AfflictionHusk(AfflictionPrefab prefab, float strength) :
base(prefab, strength)
{
}
private float DormantThreshold => Prefab.MaxStrength * 0.5f;
private float ActiveThreshold => Prefab.MaxStrength * 0.75f;
public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength) { }
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
float prevStrength = Strength;
base.Update(characterHealth, targetLimb, deltaTime);
character = characterHealth.Character;
if (character == null) { return; }
if (!subscribedToDeathEvent)
{
characterHealth.Character.OnDeath += CharacterDead;
character.OnDeath += CharacterDead;
subscribedToDeathEvent = true;
}
if (characterHealth.Character == Character.Controlled) UpdateMessages(prevStrength, characterHealth.Character);
if (Strength < Prefab.MaxStrength * 0.5f)
if (Strength < DormantThreshold)
{
UpdateDormantState(deltaTime, characterHealth.Character);
DeactivateHusk();
State = InfectionState.Dormant;
}
else if (Strength < ActiveThreshold)
{
DeactivateHusk();
character.SpeechImpediment = 100;
State = InfectionState.Transition;
}
else if (Strength < Prefab.MaxStrength)
{
characterHealth.Character.SpeechImpediment = 100.0f;
UpdateTransitionState(deltaTime, characterHealth.Character);
if (State != InfectionState.Active)
{
character.SetStun(Rand.Range(2, 4, Rand.RandSync.Server));
}
State = InfectionState.Active;
ActivateHusk();
}
else
{
characterHealth.Character.SpeechImpediment = 100.0f;
UpdateActiveState(deltaTime, characterHealth.Character);
State = InfectionState.Final;
ActivateHusk();
ApplyDamage(deltaTime, applyForce: true);
character.SetStun(1);
}
}
partial void UpdateMessages(float prevStrength, Character character);
partial void UpdateMessages();
private void UpdateDormantState(float deltaTime, Character character)
private void ApplyDamage(float deltaTime, bool applyForce)
{
if (state != InfectionState.Dormant)
{
DeactivateHusk(character);
}
state = InfectionState.Dormant;
}
private void UpdateTransitionState(float deltaTime, Character character)
{
if (state != InfectionState.Transition)
{
DeactivateHusk(character);
}
state = InfectionState.Transition;
}
private void UpdateActiveState(float deltaTime, Character character)
{
if (state != InfectionState.Active)
{
ActivateHusk(character);
state = InfectionState.Active;
}
foreach (Limb limb in character.AnimController.Limbs)
{
float random = Rand.Value(Rand.RandSync.Server);
huskInfection.Clear();
huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * deltaTime / character.AnimController.Limbs.Length));
character.LastDamageSource = null;
character.DamageLimb(
limb.WorldPosition, limb,
new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(0.5f * deltaTime / character.AnimController.Limbs.Length) },
0.0f, false, 0.0f);
float force = applyForce ? random * 0.1f * limb.Mass : 0;
character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, force);
}
}
public void ActivateHusk(Character character)
public void ActivateHusk()
{
if (huskAppendage == null)
if (huskAppendage == null && character.Params.UseHuskAppendage)
{
huskAppendage = AttachHuskAppendage(character, Prefab.Identifier);
if (huskAppendage != null)
{
character.NeedsAir = false;
character.SetStun(0.5f);
}
#if CLIENT
character.AnimController.GetLimb(LimbType.Head).EnableHuskSprite = true;
#endif
}
character.NeedsAir = false;
character.SpeechImpediment = 100;
}
private void DeactivateHusk(Character character)
private void DeactivateHusk()
{
character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false);
if (huskAppendage != null)
{
huskAppendage.ForEach(l => character.AnimController.RemoveLimb(l));
huskAppendage = null;
#if CLIENT
character.AnimController.GetLimb(LimbType.Head).EnableHuskSprite = false;
#endif
}
}
public void Remove(Character character)
public void Remove()
{
DeactivateHusk(character);
if (character != null) character.OnDeath -= CharacterDead;
if (character == null) { return; }
DeactivateHusk();
character.OnDeath -= CharacterDead;
subscribedToDeathEvent = false;
}
private void CharacterDead(Character character, CauseOfDeath causeOfDeath)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (Strength < Prefab.MaxStrength * 0.5f || character.Removed) { return; }
if (Strength < ActiveThreshold || character.Removed) { return; }
//don't turn the character into a husk if any of its limbs are severed
if (character.AnimController?.LimbJoints != null)
{
foreach (var limbJoint in character.AnimController.LimbJoints)
{
if (limbJoint.IsSevered) return;
if (limbJoint.IsSevered) { return; }
}
}
//create the AI husk in a coroutine to ensure that we don't modify the character list while enumerating it
CoroutineManager.StartCoroutine(CreateAIHusk(character));
CoroutineManager.StartCoroutine(CreateAIHusk());
}
private IEnumerable<object> CreateAIHusk(Character character)
private IEnumerable<object> CreateAIHusk()
{
character.Enabled = false;
Entity.Spawner.AddToRemoveQueue(character);
string speciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk);
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName);
string huskedSpeciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk);
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
if (prefab == null)
{
DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found.");
yield return CoroutineStatus.Success;
}
var husk = Character.Create(speciesName, character.WorldPosition, character.Info.Name, character.Info, isRemotePlayer: false, hasAi: true, ragdoll: character.AnimController.RagdollParams);
var husk = Character.Create(huskedSpeciesName, character.WorldPosition, ToolBox.RandomSeed(8), character.Info, isRemotePlayer: false, hasAi: true);
foreach (Limb limb in husk.AnimController.Limbs)
{
@@ -197,6 +200,11 @@ namespace Barotrauma
husk.Inventory.TryPutItem(character.Inventory.Items[i], i, true, false, null);
}
husk.SetStun(5);
yield return new WaitForSeconds(5, false);
#if CLIENT
husk.PlaySound(CharacterSound.SoundType.Idle);
#endif
yield return CoroutineStatus.Success;
}
@@ -114,6 +114,10 @@ namespace Barotrauma
private List<LimbHealth> limbHealths = new List<LimbHealth>();
//non-limb-specific afflictions
private List<Affliction> afflictions = new List<Affliction>();
/// <summary>
/// Note: returns only the non-limb-secific afflictions. Use GetAllAfflictions or some other method for getting also the limb-specific afflictions.
/// </summary>
public IEnumerable<Affliction> Afflictions => afflictions;
private HashSet<Affliction> irremovableAfflictions = new HashSet<Affliction>();
private Affliction bloodlossAffliction;
@@ -160,12 +164,12 @@ namespace Barotrauma
{
get
{
if (!Character.NeedsAir || Unkillable) return 100.0f;
if (!Character.NeedsOxygen || Unkillable) { return 100.0f; }
return -oxygenLowAffliction.Strength + 100;
}
set
{
if (!Character.NeedsAir || Unkillable) return;
if (!Character.NeedsOxygen || Unkillable) { return; }
oxygenLowAffliction.Strength = MathHelper.Clamp(-value + 100, 0.0f, 200.0f);
}
}
@@ -216,7 +220,7 @@ namespace Barotrauma
limbHealths.Clear();
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "limb") continue;
if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; }
limbHealths.Add(new LimbHealth(subElement, this));
}
if (limbHealths.Count == 0)
@@ -269,30 +273,6 @@ namespace Barotrauma
}
}
public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true)
{
foreach (Affliction affliction in afflictions)
{
if (affliction.Prefab.Identifier == identifier) return affliction;
}
if (!allowLimbAfflictions) return null;
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction affliction in limbHealth.Afflictions)
{
if (affliction.Prefab.Identifier == identifier) return affliction;
}
}
return null;
}
public T GetAffliction<T>(string identifier, bool allowLimbAfflictions = true) where T : Affliction
{
return GetAffliction(identifier, allowLimbAfflictions) as T;
}
public IEnumerable<Affliction> GetAfflictionsByType(string afflictionType, Limb limb)
{
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
@@ -304,6 +284,37 @@ namespace Barotrauma
return limbHealths[limb.HealthIndex].Afflictions.Where(a => a.Prefab.AfflictionType == afflictionType);
}
public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true)
=> GetAffliction(a => a.Prefab.Identifier == identifier, allowLimbAfflictions);
public Affliction GetAfflictionOfType(string afflictionType, bool allowLimbAfflictions = true)
=> GetAffliction(a => a.Prefab.AfflictionType == afflictionType, allowLimbAfflictions);
private Affliction GetAffliction(Func<Affliction, bool> predicate, bool allowLimbAfflictions = true)
{
foreach (Affliction affliction in afflictions)
{
if (predicate(affliction)) { return affliction; }
}
if (!allowLimbAfflictions)
{
return null;
}
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction affliction in limbHealth.Afflictions)
{
if (predicate(affliction)) { return affliction; }
}
}
return null;
}
public T GetAffliction<T>(string identifier, bool allowLimbAfflictions = true) where T : Affliction
{
return GetAffliction(identifier, allowLimbAfflictions) as T;
}
public Affliction GetAffliction(string identifier, Limb limb)
{
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
@@ -408,11 +419,10 @@ namespace Barotrauma
return resistance;
}
private List<Affliction> matchingAfflictions = new List<Affliction>();
public void ReduceAffliction(Limb targetLimb, string affliction, float amount)
{
affliction = affliction.ToLowerInvariant();
List<Affliction> matchingAfflictions = new List<Affliction>(afflictions);
matchingAfflictions.Clear();
if (targetLimb != null)
{
@@ -426,8 +436,8 @@ namespace Barotrauma
}
}
matchingAfflictions.RemoveAll(a =>
a.Prefab.Identifier.ToLowerInvariant() != affliction &&
a.Prefab.AfflictionType.ToLowerInvariant() != affliction);
!a.Prefab.Identifier.Equals(affliction, StringComparison.OrdinalIgnoreCase) &&
!a.Prefab.AfflictionType.Equals(affliction, StringComparison.OrdinalIgnoreCase));
if (matchingAfflictions.Count == 0) return;
@@ -526,7 +536,7 @@ namespace Barotrauma
private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction)
{
if (!DoesBleed && newAffliction is AfflictionBleeding) return;
if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
foreach (Affliction affliction in limbHealth.Afflictions)
{
@@ -559,7 +569,7 @@ namespace Barotrauma
private void AddAffliction(Affliction newAffliction)
{
if (!DoesBleed && newAffliction is AfflictionBleeding) return;
if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
if (newAffliction.Prefab.AfflictionType == "huskinfection")
{
var huskPrefab = newAffliction.Prefab as AfflictionPrefabHusk;
@@ -653,7 +663,7 @@ namespace Barotrauma
private void UpdateOxygen(float deltaTime)
{
if (!Character.NeedsAir) return;
if (!Character.NeedsOxygen) { return; }
float prevOxygen = OxygenAmount;
if (IsUnconscious)
@@ -691,13 +701,15 @@ namespace Barotrauma
foreach (Affliction affliction in limbHealth.Afflictions)
{
float vitalityDecrease = affliction.GetVitalityDecrease(this);
if (limbHealth.VitalityMultipliers.ContainsKey(affliction.Prefab.Identifier.ToLowerInvariant()))
string identifier = affliction.Prefab.Identifier.ToLowerInvariant();
string type = affliction.Prefab.AfflictionType.ToLowerInvariant();
if (limbHealth.VitalityMultipliers.ContainsKey(identifier))
{
vitalityDecrease *= limbHealth.VitalityMultipliers[affliction.Prefab.Identifier.ToLowerInvariant()];
vitalityDecrease *= limbHealth.VitalityMultipliers[identifier];
}
if (limbHealth.VitalityTypeMultipliers.ContainsKey(affliction.Prefab.AfflictionType.ToLowerInvariant()))
if (limbHealth.VitalityTypeMultipliers.ContainsKey(type))
{
vitalityDecrease *= limbHealth.VitalityTypeMultipliers[affliction.Prefab.AfflictionType.ToLowerInvariant()];
vitalityDecrease *= limbHealth.VitalityTypeMultipliers[type];
}
vitalityDecrease *= damageResistanceMultiplier;
Vitality -= vitalityDecrease;
@@ -750,6 +762,7 @@ namespace Barotrauma
return new Pair<CauseOfDeathType, Affliction>(causeOfDeath, strongestAffliction);
}
// TODO: this method is called a lot (every half second) -> optimize, don't create new class instances and lists every time!
private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions)
{
List<Affliction> allAfflictions = new List<Affliction>(afflictions);
@@ -791,8 +804,7 @@ namespace Barotrauma
/// </summary>
/// <param name="treatmentSuitability">A dictionary where the key is the identifier of the item and the value the suitability</param>
/// <param name="normalize">If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable.</param>
/// <param name="randomization">Amount of randomization to apply to the values (0 = the values are accurate, 1 = the values are completely random)</param>
/// <param name="randomization">Amount of randomization to apply to the values (0 = the values are accurate, 1 = the values are completely random)</param>
public void GetSuitableTreatments(Dictionary<string, float> treatmentSuitability, bool normalize, float randomization = 0.0f)
{
//key = item identifier
@@ -833,10 +845,18 @@ namespace Barotrauma
}
}
private readonly List<Affliction> activeAfflictions = new List<Affliction>();
private readonly List<Pair<LimbHealth, Affliction>> limbAfflictions = new List<Pair<LimbHealth, Affliction>>();
public void ServerWrite(IWriteMessage msg)
{
List<Affliction> activeAfflictions = afflictions.FindAll(a => a.Strength > 0.0f && a.Strength >= a.Prefab.ActivationThreshold);
activeAfflictions.Clear();
foreach (var affliction in afflictions)
{
if (affliction.Strength > 0.0f && affliction.Strength >= affliction.Prefab.ActivationThreshold)
{
activeAfflictions.Add(affliction);
}
}
msg.Write((byte)activeAfflictions.Count);
foreach (Affliction affliction in activeAfflictions)
{
@@ -846,7 +866,7 @@ namespace Barotrauma
0.0f, affliction.Prefab.MaxStrength, 8);
}
List<Pair<LimbHealth, Affliction>> limbAfflictions = new List<Pair<LimbHealth, Affliction>>();
limbAfflictions.Clear();
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction limbAffliction in limbHealth.Afflictions)
@@ -873,5 +893,11 @@ namespace Barotrauma
}
partial void RemoveProjSpecific();
/// <summary>
/// Automatically filters out buffs.
/// </summary>
public static IEnumerable<Affliction> SortAfflictionsBySeverity(IEnumerable<Affliction> afflictions) =>
afflictions.Where(a => !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength);
}
}
@@ -62,7 +62,7 @@ namespace Barotrauma
skills = new Dictionary<string, Skill>();
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "skill") { continue; }
if (!subElement.Name.ToString().Equals("skill", System.StringComparison.OrdinalIgnoreCase)) { continue; }
string skillIdentifier = subElement.GetAttributeString("identifier", "");
if (string.IsNullOrEmpty(skillIdentifier)) { continue; }
skills.Add(
@@ -146,6 +146,7 @@ namespace Barotrauma
private set;
}
// TODO: not used
[Serialize(10.0f, false)]
public float Commonness
{
@@ -241,7 +242,7 @@ namespace Barotrauma
}
public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(sync);
public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(p => p.Identifier != "watchman", sync);
public static void LoadAll(IEnumerable<ContentFile> files)
{
@@ -262,7 +263,7 @@ namespace Barotrauma
}
foreach (XElement element in mainElement.Elements())
{
if (element.Name.ToString().ToLowerInvariant() == "nojob") { continue; }
if (element.Name.ToString().Equals("nojob", StringComparison.OrdinalIgnoreCase)) { continue; }
if (element.IsOverride())
{
var job = new JobPrefab(element.FirstElement(), file.Path)
@@ -17,7 +17,7 @@ namespace Barotrauma
public enum LimbType
{
None, LeftHand, RightHand, LeftArm, RightArm, LeftForearm, RightForearm,
LeftLeg, RightLeg, LeftFoot, RightFoot, Head, Torso, Tail, Legs, RightThigh, LeftThigh, Waist
LeftLeg, RightLeg, LeftFoot, RightFoot, Head, Torso, Tail, Legs, RightThigh, LeftThigh, Waist, Jaw
};
partial class LimbJoint : RevoluteJoint
@@ -28,6 +28,8 @@ namespace Barotrauma
public readonly Ragdoll ragdoll;
public readonly Limb LimbA, LimbB;
public float Scale => Params.Scale * ragdoll.RagdollParams.JointScale;
public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One)
{
Params = jointParams;
@@ -59,15 +61,15 @@ namespace Barotrauma
}
if (ragdoll.IsFlipped)
{
LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Params.Ragdoll.JointScale);
LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Params.Ragdoll.JointScale);
LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Scale);
LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Scale);
UpperLimit = MathHelper.ToRadians(-Params.LowerLimit);
LowerLimit = MathHelper.ToRadians(-Params.UpperLimit);
}
else
{
LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Params.Ragdoll.JointScale);
LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Params.Ragdoll.JointScale);
LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Scale);
LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Scale);
UpperLimit = MathHelper.ToRadians(Params.UpperLimit);
LowerLimit = MathHelper.ToRadians(Params.LowerLimit);
}
@@ -125,9 +127,29 @@ namespace Barotrauma
private Direction dir;
public int HealthIndex => Params.HealthIndex;
public float Scale => Params.Ragdoll.LimbScale;
public float Scale => Params.Scale * Params.Ragdoll.LimbScale;
public float AttackPriority => Params.AttackPriority;
public bool DoesFlip => Params.Flip;
public bool DoesFlip
{
get
{
if (character.AnimController.CurrentAnimationParams is GroundedMovementParams)
{
switch (type)
{
case LimbType.LeftFoot:
case LimbType.LeftLeg:
case LimbType.LeftThigh:
case LimbType.RightFoot:
case LimbType.RightLeg:
case LimbType.RightThigh:
// Legs always has to flip
return true;
}
}
return Params.Flip;
}
}
public float SteerForce => Params.SteerForce;
@@ -654,33 +676,37 @@ namespace Barotrauma
}
Vector2 diff = attackSimPos - SimPosition;
bool applyForces = (!attack.ApplyForcesOnlyOnce || !wasRunning) && diff.LengthSquared() > 0.00001f;
bool applyForces = !attack.ApplyForcesOnlyOnce || !wasRunning;
if (applyForces)
{
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Count > 0)
{
foreach (int limbIndex in attack.ForceOnLimbIndices)
{
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) continue;
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) { continue; }
Limb limb = character.AnimController.Limbs[limbIndex];
limb.body.ApplyTorque(limb.Mass * character.AnimController.Dir * attack.Torque);
diff = attackSimPos - limb.SimPosition;
if (diff == Vector2.Zero) { continue; }
limb.body.ApplyTorque(limb.Mass * character.AnimController.Dir * attack.Torque * limb.Params.AttackForceMultiplier);
Vector2 forcePos = limb.pullJoint == null ? limb.body.SimPosition : limb.pullJoint.WorldAnchorA;
limb.body.ApplyLinearImpulse(limb.Mass * attack.Force * Vector2.Normalize(attackSimPos - SimPosition), forcePos,
maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
limb.body.ApplyLinearImpulse(limb.Mass * attack.Force * limb.Params.AttackForceMultiplier * Vector2.Normalize(diff), forcePos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
else
else if (diff != Vector2.Zero)
{
body.ApplyTorque(Mass * character.AnimController.Dir * attack.Torque);
body.ApplyTorque(Mass * character.AnimController.Dir * attack.Torque * Params.AttackForceMultiplier);
Vector2 forcePos = pullJoint == null ? body.SimPosition : pullJoint.WorldAnchorA;
body.ApplyLinearImpulse(
Mass * attack.Force * Vector2.Normalize(attackSimPos - SimPosition),
forcePos,
maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
body.ApplyLinearImpulse(Mass * attack.Force * Params.AttackForceMultiplier * Vector2.Normalize(diff), forcePos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
Vector2 forceWorld = attack.CalculateAttackPhase(attack.RootTransitionEasing);
forceWorld.X *= character.AnimController.Dir;
character.AnimController.MainLimb.body.ApplyLinearImpulse(character.Mass * forceWorld, character.SimPosition, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
if (!attack.IsRunning)
{
// Set the main collider where the body lands after the attack
character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: 0);
}
return wasHit;
}
@@ -125,7 +125,7 @@ namespace Barotrauma
public static string GetFolder(XDocument doc, string filePath)
{
var folder = doc.Root?.Element("animations")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default")
if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
folder = Path.Combine(Path.GetDirectoryName(filePath), "Animations");
}
@@ -198,7 +198,7 @@ namespace Barotrauma
}
else
{
selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant() == fileName.ToLowerInvariant());
selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
DebugConsole.ThrowError($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
@@ -28,18 +28,30 @@ namespace Barotrauma
[Serialize(false, true), Editable]
public bool Humanoid { get; private set; }
[Serialize(false, true), Editable]
public bool HasInfo { get; private set; }
[Serialize(false, true), Editable]
public bool Husk { get; private set; }
[Serialize(false, true), Editable]
public bool UseHuskAppendage { get; private set; }
[Serialize(false, true), Editable]
public bool NeedsAir { get; set; }
[Serialize(false, true, description: "Can the creature live without water or does it die on dry land?"), Editable]
public bool NeedsWater { get; set; }
[Serialize(false, true), Editable]
public bool CanSpeak { get; set; }
[Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 1000f)]
public float Noise { get; set; }
[Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 1000f)]
public float Visibility { get; set; }
[Serialize("blood", true), Editable]
public string BloodDecal { get; private set; }
@@ -450,6 +462,12 @@ namespace Barotrauma
[Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable()]
public bool AggressiveBoarding { get; private set; }
[Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable()]
public bool EnforceAggressiveBehaviorForMissions { get; private set; }
[Serialize(false, true, description: "Should the character target or ignore walls when it's inside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()]
public bool TargetInnerWalls { get; private set; }
// TODO: latchonto, swarming
public IEnumerable<TargetParams> Targets => targets;
@@ -538,6 +556,9 @@ namespace Barotrauma
[Serialize(0f, true, description: "Generic distance that can be used for different purposes depending on the state. Eg. in Avoid state this defines the distance that the character tries to keep to the target. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)]
public float ReactDistance { get; set; }
[Serialize(0f, true, description: "Used for defining the attack distance for PassiveAggressive and Aggressive states. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)]
public float AttackDistance { get; set; }
public TargetParams(XElement element, CharacterParams character) : base(element, character) { }
public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { }
@@ -94,8 +94,7 @@ namespace Barotrauma
public static string GetFolder(string speciesName, ContentPackage contentPackage = null)
{
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier.ToLowerInvariant()==speciesName.ToLowerInvariant() &&
(contentPackage==null || p.ContentPackage == contentPackage));
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier.Equals(speciesName, StringComparison.OrdinalIgnoreCase) && (contentPackage == null || p.ContentPackage == contentPackage));
if (prefab?.XDocument == null)
{
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}' (content package {contentPackage?.Name ?? "null"})");
@@ -107,7 +106,7 @@ namespace Barotrauma
public static string GetFolder(XDocument doc, string filePath)
{
var folder = doc.Root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default")
if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
folder = Path.Combine(Path.GetDirectoryName(filePath), "Ragdolls") + Path.DirectorySeparatorChar;
}
@@ -150,7 +149,7 @@ namespace Barotrauma
}
else
{
selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant() == fileName.ToLowerInvariant());
selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.");
@@ -489,6 +488,9 @@ namespace Barotrauma
[Serialize(0.25f, true), Editable]
public float Stiffness { get; set; }
[Serialize(1f, true, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable]
public float Scale { get; set; }
public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
}
@@ -591,6 +593,18 @@ namespace Barotrauma
[Serialize("", true), Editable]
public string Notes { get; set; }
[Serialize(0f, true), Editable]
public float ConstantTorque { get; set; }
[Serialize(0f, true), Editable]
public float ConstantAngle { get; set; }
[Serialize(1f, true), Editable]
public float Scale { get; set; }
[Serialize(1f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)]
public float AttackForceMultiplier { get; set; }
// Non-editable ->
[Serialize(0, true)]
public int HealthIndex { get; set; }
@@ -65,6 +65,37 @@ namespace Barotrauma
set { skillIncreasePerFabricatorRequiredSkill = value; }
}
private float skillIncreasePerHostileDamage;
[Serialize(0.01f, true)]
public float SkillIncreasePerHostileDamage
{
get { return skillIncreasePerHostileDamage * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerHostileDamage = value; }
}
private float skillIncreasePerSecondWhenOperatingTurret;
[Serialize(0.001f, true)]
public float SkillIncreasePerSecondWhenOperatingTurret
{
get { return skillIncreasePerSecondWhenOperatingTurret * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerSecondWhenOperatingTurret = value; }
}
private float skillIncreasePerFriendlyHealed;
[Serialize(0.001f, true)]
public float SkillIncreasePerFriendlyHealed
{
get { return skillIncreasePerFriendlyHealed * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerFriendlyHealed = value; }
}
[Serialize(1.1f, true)]
public float AssistantSkillIncreaseMultiplier
{
get;
set;
}
private SkillSettings(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
@@ -39,7 +39,10 @@ namespace Barotrauma
TraitorMissions,
EventManagerSettings,
Orders,
SkillSettings
SkillSettings,
Wreck,
Corpses,
WreckAIConfig
}
public class ContentPackage
@@ -63,8 +66,10 @@ namespace Barotrauma
ContentType.LevelObjectPrefabs,
ContentType.RuinConfig,
ContentType.Outpost,
ContentType.Wreck,
ContentType.Afflictions,
ContentType.Orders
ContentType.Orders,
ContentType.Corpses
};
//at least one file of each these types is required in core content packages
@@ -75,6 +80,7 @@ namespace Barotrauma
ContentType.Character,
ContentType.Structure,
ContentType.Outpost,
ContentType.Wreck,
ContentType.Text,
ContentType.Executable,
ContentType.ServerExecutable,
@@ -87,7 +93,8 @@ namespace Barotrauma
ContentType.Afflictions,
ContentType.UIStyle,
ContentType.EventManagerSettings,
ContentType.Orders
ContentType.Orders,
ContentType.Corpses
};
public static IEnumerable<ContentType> CorePackageRequiredFiles
@@ -284,6 +291,7 @@ namespace Barotrauma
case ContentType.None:
case ContentType.Outpost:
case ContentType.Submarine:
case ContentType.Wreck:
break;
default:
try
@@ -364,7 +372,10 @@ namespace Barotrauma
{
if (Files.Find(file => file.Path == path && file.Type == type) != null) return null;
ContentFile cf = new ContentFile(path, type);
ContentFile cf = new ContentFile(path, type)
{
ContentPackage = this
};
Files.Add(cf);
return cf;
@@ -460,12 +471,12 @@ namespace Barotrauma
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
var rootElement = doc.Root;
var element = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement;
var ragdollFolder = RagdollParams.GetFolder(doc, file.Path);
var ragdollFolder = RagdollParams.GetFolder(doc, file.Path).CleanUpPathCrossPlatform(true);
if (Directory.Exists(ragdollFolder))
{
Directory.GetFiles(ragdollFolder, "*.xml").ForEach(f => filePaths.Add(f));
}
var animationFolder = AnimationParams.GetFolder(doc, file.Path);
var animationFolder = AnimationParams.GetFolder(doc, file.Path).CleanUpPathCrossPlatform(true);
if (Directory.Exists(animationFolder))
{
Directory.GetFiles(animationFolder, "*.xml").ForEach(f => filePaths.Add(f));
@@ -508,14 +519,22 @@ namespace Barotrauma
return IsModFilePathAllowed(path);
}
/// <summary>
/// Are mods allowed to install a file into the specified path. If a content package XML includes files
/// with a prohibited path, they are treated as references to external files. For example, a mod could include
/// some vanilla files in the XML, in which case the game will simply use the vanilla files present in the game folder.
/// Returns whether mods are allowed to install a file into the specified path.
/// Currently mods are only allowed to install files into the Mods folder.
/// The only exception to this rule is the Vanilla content package.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static bool IsModFilePathAllowed(string path)
{
if (GameMain.VanillaContent.Files.Any(f => string.Equals(System.IO.Path.GetFullPath(f.Path).CleanUpPath(),
System.IO.Path.GetFullPath(path).CleanUpPath(),
StringComparison.InvariantCultureIgnoreCase)))
{
//file is in vanilla package, this is allowed
return true;
}
while (true)
{
string temp = System.IO.Path.GetDirectoryName(path);
@@ -573,7 +592,13 @@ namespace Barotrauma
{
if (System.IO.Path.GetFileName(modDirectory.TrimEnd(System.IO.Path.DirectorySeparatorChar)) == "ExampleMod") { continue; }
string modFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.MetadataFileName);
if (File.Exists(modFilePath))
string copyingFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.CopyIndicatorFileName);
if (File.Exists(copyingFilePath))
{
//this mod didn't clean up its copying file; assume it's corrupted and delete it
Directory.Delete(modDirectory, true);
}
else if (File.Exists(modFilePath))
{
List.Add(new ContentPackage(modFilePath));
}
@@ -186,6 +186,7 @@ namespace Barotrauma
#endif
if (handle.Thread == null)
{
if (handle.AbortRequested) { return true; }
if (handle.Coroutine.Current != null)
{
WaitForSeconds wfs = handle.Coroutine.Current as WaitForSeconds;
@@ -475,10 +475,7 @@ namespace Barotrauma
commands.Add(new Command("teleportcharacter|teleport", "teleport [character name]: Teleport the specified character to the position of the cursor. If the name parameter is omitted, the controlled character will be teleported.", null,
() =>
{
return new string[][]
{
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
};
return new string[][] { ListCharacterNames() };
}, isCheat: true));
commands.Add(new Command("godmode", "godmode: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (string[] args) =>
@@ -531,18 +528,17 @@ namespace Barotrauma
commands.Add(new Command("findentityids", "findentityids [entityname]", (string[] args) =>
{
if (args.Length == 0) return;
args[0] = args[0].ToLowerInvariant();
if (args.Length == 0) { return; }
foreach (MapEntity mapEntity in MapEntity.mapEntityList)
{
if (mapEntity.Name.ToLowerInvariant() == args[0])
if (mapEntity.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase))
{
ThrowError(mapEntity.ID + ": " + mapEntity.Name.ToString());
}
}
foreach (Character character in Character.CharacterList)
{
if (character.Name.ToLowerInvariant() == args[0] || character.SpeciesName.ToLowerInvariant() == args[0])
if (character.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || character.SpeciesName.Equals(args[0], StringComparison.OrdinalIgnoreCase))
{
ThrowError(character.ID + ": " + character.Name.ToString());
}
@@ -554,8 +550,8 @@ namespace Barotrauma
if (args.Length < 2) return;
AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a =>
a.Name.ToLowerInvariant() == args[0].ToLowerInvariant() ||
a.Identifier.ToLowerInvariant() == args[0].ToLowerInvariant());
a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) ||
a.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
ThrowError("Affliction \"" + args[0] + "\" not found.");
@@ -695,18 +691,20 @@ namespace Barotrauma
NewMessage("Level seed: " + Level.Loaded.Seed);
}
},null));
#if DEBUG
commands.Add(new Command("crash", "crash: Crashes the game.", (string[] args) =>
{
throw new Exception("crash command issued");
}));
commands.Add(new Command("teleportsub", "teleportsub [start/end]: Teleport the submarine to the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) =>
{
if (Submarine.MainSub == null || Level.Loaded == null) return;
if (args.Length > 0 && args[0].ToLowerInvariant() == "start")
if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase))
{
#if SERVER
ThrowError("Cannot teleport the sub to the position of the cursor. Use \"start\" or \"end\", or execute the command as a client.");
#else
Submarine.MainSub.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition));
#endif
}
else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
{
Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
}
@@ -714,8 +712,21 @@ namespace Barotrauma
{
Submarine.MainSub.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
}
},
() =>
{
return new string[][]
{
new string[] { "start", "end", "cursor" }
};
}, isCheat: true));
#if DEBUG
commands.Add(new Command("crash", "crash: Crashes the game.", (string[] args) =>
{
throw new Exception("crash command issued");
}));
commands.Add(new Command("removecharacter", "removecharacter [character name]: Immediately deletes the specified character.", (string[] args) =>
{
if (args.Length == 0) { return; }
@@ -751,18 +762,18 @@ namespace Barotrauma
IEnumerable<object> TestLevels()
{
Submarine selectedSub = null;
SubmarineInfo selectedSub = null;
string subName = GameMain.Config.QuickStartSubmarineName;
if (!string.IsNullOrEmpty(subName))
{
selectedSub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name.ToLower() == subName.ToLower());
selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.ToLower() == subName.ToLower());
}
int count = 0;
while (true)
{
var gamesession = new GameSession(
Submarine.SavedSubmarines.GetRandom(s => !s.HasTag(SubmarineTag.HideInMenus)),
SubmarineInfo.SavedSubmarines.GetRandom(s => !s.HasTag(SubmarineTag.HideInMenus)),
"Data/Saves/test.xml",
GameModePreset.List.Find(gm => gm.Identifier == "devsandbox"),
missionPrefab: null);
@@ -776,7 +787,7 @@ namespace Barotrauma
{
if (ruin.Area.Intersects(subWorldRect))
{
ThrowError("Ruins intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Name);
ThrowError("Ruins intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Info.Name);
yield return CoroutineStatus.Success;
}
}
@@ -797,7 +808,7 @@ namespace Barotrauma
(int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y));
if (cellRect.Intersects(subWorldRect))
{
ThrowError("Level cells intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Name);
ThrowError("Level cells intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Info.Name);
yield return CoroutineStatus.Success;
}
}
@@ -816,7 +827,7 @@ namespace Barotrauma
}
#endif
commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) =>
commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) =>
{
foreach (Item it in Item.ItemList)
{
@@ -1097,10 +1108,7 @@ namespace Barotrauma
//TODO: alphabetical order?
commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () =>
{
return new string[][]
{
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
};
return new string[][] { ListCharacterNames() };
}));
commands.Add(new Command("los", "Toggle the line of sight effect on/off (client-only).", null, isCheat: true));
commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true));
@@ -1220,15 +1228,17 @@ namespace Barotrauma
return;
}
if (!splitCommand[0].ToLowerInvariant().Equals("admin"))
string firstCommand = splitCommand[0].ToLowerInvariant();
if (!firstCommand.Equals("admin", StringComparison.OrdinalIgnoreCase))
{
NewMessage(command, Color.White, true);
}
#if CLIENT
if (GameMain.Client != null)
{
Command matchingCommand = commands.Find(c => c.names.Contains(splitCommand[0].ToLowerInvariant()));
Command matchingCommand = commands.Find(c => c.names.Contains(firstCommand));
if (matchingCommand == null)
{
//if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side
@@ -1236,7 +1246,7 @@ namespace Barotrauma
NewMessage("Server command: " + command, Color.Cyan);
return;
}
else if (GameMain.Client.HasConsoleCommandPermission(splitCommand[0].ToLowerInvariant()))
else if (GameMain.Client.HasConsoleCommandPermission(firstCommand))
{
if (matchingCommand.RelayToServer)
{
@@ -1249,13 +1259,20 @@ namespace Barotrauma
}
return;
}
#if !DEBUG
if (!IsCommandPermitted(splitCommand[0].ToLowerInvariant(), GameMain.Client))
{
ThrowError("You're not permitted to use the command \"" + splitCommand[0].ToLowerInvariant() + "\"!");
return;
}
#endif
}
#endif
bool commandFound = false;
foreach (Command c in commands)
{
if (!c.names.Contains(splitCommand[0].ToLowerInvariant())) continue;
if (!c.names.Contains(firstCommand)) { continue; }
c.Execute(splitCommand.Skip(1).ToArray());
commandFound = true;
break;
@@ -1266,7 +1283,9 @@ namespace Barotrauma
ThrowError("Command \"" + splitCommand[0] + "\" not found.");
}
}
private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).Select(c => c.Name).Distinct().ToArray();
private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null)
{
if (args.Length == 0) return null;
@@ -1283,7 +1302,7 @@ namespace Barotrauma
}
var matchingCharacters = Character.CharacterList.FindAll(c =>
c.Name.ToLowerInvariant() == characterName &&
c.Name.Equals(characterName, StringComparison.OrdinalIgnoreCase) &&
(!c.IsRemotePlayer || !ignoreRemotePlayers || allowedRemotePlayer?.Character == c));
if (!matchingCharacters.Any())
@@ -1329,7 +1348,7 @@ namespace Barotrauma
JobPrefab job = null;
if (!JobPrefab.Prefabs.ContainsKey(characterLowerCase))
{
job = JobPrefab.Prefabs.Find(jp => jp.Name?.ToLowerInvariant() == characterLowerCase);
job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(characterLowerCase, StringComparison.OrdinalIgnoreCase));
}
else
{
@@ -1587,12 +1606,7 @@ namespace Barotrauma
return true;
}
public static Command FindCommand(string commandName)
{
commandName = commandName.ToLowerInvariant();
return commands.Find(c => c.names.Any(n => n.ToLowerInvariant() == commandName));
}
public static Command FindCommand(string commandName) => commands.Find(c => c.names.Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase)));
public static void Log(string message)
{
@@ -1610,8 +1624,25 @@ namespace Barotrauma
}
}
System.Diagnostics.Debug.WriteLine(error);
NewMessage(error, Color.Red);
#if CLIENT
if (listBox == null) { NewMessage(error, Color.Red); return; }
var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), style: "InnerFrame", color: Color.White)
{
CanBeFocused = false
};
var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) },
error, textAlignment: Alignment.TopLeft, font: GUI.SmallFont, wrap: true)
{
CanBeFocused = false,
TextColor = Color.Red
};
textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5);
textBlock.SetTextPos();
listBox.UpdateScrollBarSize();
listBox.BarScroll = 1.0f;
if (createMessageBox)
{
CoroutineManager.StartCoroutine(CreateMessageBox(error));
@@ -1620,6 +1651,8 @@ namespace Barotrauma
{
isOpen = true;
}
#else
NewMessage(error, Color.Red);
#endif
}
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma
{
public enum TransitionMode
{
Linear,
Smooth,
Smoother,
EaseIn,
EaseOut,
Exponential
}
}
@@ -1,6 +1,4 @@
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -53,7 +51,7 @@ namespace Barotrauma
public override void Init(bool affectSubImmediately)
{
spawnPos = Level.Loaded.GetRandomItemPos(
(Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < 0.5f) ? Level.PositionType.MainPath : Level.PositionType.Cave | Level.PositionType.Ruin,
(Rand.Value(Rand.RandSync.Server) < 0.5f) ? Level.PositionType.MainPath : Level.PositionType.Cave | Level.PositionType.Ruin,
500.0f, 10000.0f, 30.0f);
spawnPending = true;
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -219,13 +218,44 @@ namespace Barotrauma
private void CreateEvents(ScriptedEventSet eventSet)
{
if (eventSet.ChooseRandom)
int applyCount = 1;
if (eventSet.PerRuin)
{
if (eventSet.EventPrefabs.Count > 0)
applyCount = Level.Loaded.Ruins.Count();
}
else if (eventSet.PerWreck)
{
applyCount = Submarine.Loaded.Count(s => s.Info.IsWreck && (s.ThalamusAI == null || !s.ThalamusAI.IsAlive));
}
for (int i = 0; i < applyCount; i++)
{
if (eventSet.ChooseRandom)
{
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
var eventPrefab = ToolBox.SelectWeightedRandom(eventSet.EventPrefabs, eventSet.EventPrefabs.Select(e => e.Commonness).ToList(), rand);
if (eventPrefab != null)
if (eventSet.EventPrefabs.Count > 0)
{
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
var eventPrefab = ToolBox.SelectWeightedRandom(eventSet.EventPrefabs, eventSet.EventPrefabs.Select(e => e.Commonness).ToList(), rand);
if (eventPrefab != null)
{
var newEvent = eventPrefab.CreateInstance();
newEvent.Init(true);
DebugConsole.Log("Initialized event " + newEvent.ToString());
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<ScriptedEvent>());
}
selectedEvents[eventSet].Add(newEvent);
}
}
if (eventSet.ChildSets.Count > 0)
{
var newEventSet = SelectRandomEvents(eventSet.ChildSets);
if (newEventSet != null) { CreateEvents(newEventSet); }
}
}
else
{
foreach (ScriptedEventPrefab eventPrefab in eventSet.EventPrefabs)
{
var newEvent = eventPrefab.CreateInstance();
newEvent.Init(true);
@@ -236,30 +266,11 @@ namespace Barotrauma
}
selectedEvents[eventSet].Add(newEvent);
}
}
if (eventSet.ChildSets.Count > 0)
{
var newEventSet = SelectRandomEvents(eventSet.ChildSets);
if (newEventSet != null) { CreateEvents(newEventSet); }
}
}
else
{
foreach (ScriptedEventPrefab eventPrefab in eventSet.EventPrefabs)
{
var newEvent = eventPrefab.CreateInstance();
newEvent.Init(true);
DebugConsole.Log("Initialized event " + newEvent.ToString());
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<ScriptedEvent>());
}
selectedEvents[eventSet].Add(newEvent);
}
foreach (ScriptedEventSet childEventSet in eventSet.ChildSets)
{
CreateEvents(childEventSet);
foreach (ScriptedEventSet childEventSet in eventSet.ChildSets)
{
CreateEvents(childEventSet);
}
}
}
}
@@ -296,11 +307,14 @@ namespace Barotrauma
0.0f, 1.0f);
//don't create new events if within 50 meters of the start/end of the level
if (distanceTraveled <= 0.0f ||
distFromStart * Physics.DisplayToRealWorldRatio < 50.0f ||
distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f)
if (!eventSet.AllowAtStart)
{
return false;
if (distanceTraveled <= 0.0f ||
distFromStart * Physics.DisplayToRealWorldRatio < 50.0f ||
distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f)
{
return false;
}
}
if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) &&
@@ -368,17 +382,15 @@ namespace Barotrauma
pendingEventSets.RemoveAt(i);
if (!selectedEvents.ContainsKey(eventSet))
if (selectedEvents.ContainsKey(eventSet))
{
//no events selected from this event set
continue;
//start events in this set
foreach (ScriptedEvent scriptedEvent in selectedEvents[eventSet])
{
activeEvents.Add(scriptedEvent);
}
}
//start events in this set
foreach (ScriptedEvent scriptedEvent in selectedEvents[eventSet])
{
activeEvents.Add(scriptedEvent);
}
//add child event sets to pending
foreach (ScriptedEventSet childEventSet in eventSet.ChildSets)
{
@@ -431,7 +443,7 @@ namespace Barotrauma
enemyDanger = 0.0f;
foreach (Character character in Character.CharacterList)
{
if (character.IsDead || character.IsUnconscious || !character.Enabled) continue;
if (character.IsDead || character.IsIncapacitated || !character.Enabled) continue;
EnemyAIController enemyAI = character.AIController as EnemyAIController;
if (enemyAI == null) continue;
@@ -458,7 +470,7 @@ namespace Barotrauma
int hullCount = 0;
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine == null || hull.Submarine.IsOutpost) { continue; }
if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
hullCount++;
foreach (Gap gap in hull.ConnectedGaps)
{
@@ -69,7 +69,7 @@ namespace Barotrauma
return;
}
WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, true);
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");
@@ -44,6 +44,11 @@ namespace Barotrauma
}
}
public override int TeamCount
{
get { return 2; }
}
public CombatMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
{
@@ -113,7 +118,7 @@ namespace Barotrauma
{
for (int i = 0; i < 2; i++)
{
if (wifiComponent.Item.Submarine == subs[i] || subs[i].DockedTo.Contains(wifiComponent.Item.Submarine))
if (wifiComponent.Item.Submarine == subs[i] || subs[i].ConnectedDockingPorts.ContainsKey(wifiComponent.Item.Submarine))
{
wifiComponent.TeamID = subs[i].TeamID;
}
@@ -74,6 +74,11 @@ namespace Barotrauma
get { return true; }
}
public virtual int TeamCount
{
get { return 1; }
}
public virtual IEnumerable<Vector2> SonarPositions
{
get { return Enumerable.Empty<Vector2>(); }
@@ -136,7 +141,7 @@ namespace Barotrauma
}
else
{
allowedMissions.AddRange(MissionPrefab.List.Where(m => ((int)(missionType & m.type)) != 0));
allowedMissions.AddRange(MissionPrefab.List.Where(m => ((int)(missionType & m.Type)) != 0));
}
allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
@@ -168,10 +173,9 @@ namespace Barotrauma
public virtual void Update(float deltaTime) { }
public virtual bool AssignTeamIDs(List<Networking.Client> clients)
public virtual void AssignTeamIDs(List<Networking.Client> clients)
{
clients.ForEach(c => c.TeamID = Character.TeamType.Team1);
return false;
}
protected void ShowMessage(int missionState)
@@ -31,7 +31,7 @@ namespace Barotrauma
private readonly ConstructorInfo constructor;
public readonly MissionType type;
public readonly MissionType Type;
public readonly bool MultiplayerOnly, SingleplayerOnly;
@@ -154,18 +154,18 @@ namespace Barotrauma
}
string missionTypeName = element.GetAttributeString("type", "");
if (!Enum.TryParse(missionTypeName, out type))
if (!Enum.TryParse(missionTypeName, out Type))
{
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
return;
}
if (type == MissionType.None)
if (Type == MissionType.None)
{
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be none.");
return;
}
constructor = missionClasses[type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) });
constructor = missionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) });
InitProjSpecific(element);
}
@@ -176,11 +176,11 @@ namespace Barotrauma
{
foreach (Pair<string, string> allowedLocationType in AllowedLocationTypes)
{
if (allowedLocationType.First.ToLowerInvariant() == "any" ||
allowedLocationType.First.ToLowerInvariant() == from.Type.Identifier.ToLowerInvariant())
if (allowedLocationType.First.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedLocationType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase))
{
if (allowedLocationType.Second.ToLowerInvariant() == "any" ||
allowedLocationType.Second.ToLowerInvariant() == to.Type.Identifier.ToLowerInvariant())
if (allowedLocationType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedLocationType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase))
{
return true;
}
@@ -189,7 +189,7 @@ namespace Barotrauma
return false;
}
public Mission Instantiate(Location[] locations)
{
return constructor?.Invoke(new object[] { this, locations }) as Mission;
@@ -2,9 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -113,7 +110,25 @@ namespace Barotrauma
private void InitializeMonsters(IEnumerable<Character> monsters)
{
monsters.ForEach(m => m.Enabled = false);
foreach (var monster in monsters)
{
monster.Enabled = false;
if (monster.Params.AI.EnforceAggressiveBehaviorForMissions)
{
foreach (var targetParam in monster.Params.AI.Targets)
{
switch (targetParam.State)
{
case AIState.Avoid:
case AIState.Escape:
case AIState.Flee:
case AIState.PassiveAggressive:
targetParam.State = AIState.Attack;
break;
}
}
}
}
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
foreach (Character monster in monsters)
{
@@ -191,7 +206,10 @@ namespace Barotrauma
completed = true;
}
public bool IsEliminated(Character enemy) => enemy.Removed || enemy.IsDead || enemy.AIController is EnemyAIController ai && ai.State == AIState.Flee;
public bool IsEliminated(Character enemy) =>
enemy == null ||
enemy.Removed ||
enemy.IsDead ||
enemy.AIController is EnemyAIController ai && ai.State == AIState.Flee;
}
}
@@ -13,6 +13,14 @@ namespace Barotrauma
private Item item;
private readonly Level.PositionType spawnPositionType;
private readonly string containerTag;
private readonly string existingItemTag;
private bool usedExistingItem;
private readonly bool showMessageWhenPickedUp;
public override IEnumerable<Vector2> SonarPositions
{
@@ -24,7 +32,7 @@ namespace Barotrauma
}
else
{
yield return ConvertUnits.ToDisplayUnits(item.SimPosition);
yield return item.WorldPosition;
}
}
}
@@ -32,6 +40,8 @@ namespace Barotrauma
public SalvageMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
{
containerTag = prefab.ConfigElement.GetAttributeString("containertag", "");
if (prefab.ConfigElement.Attribute("itemname") != null)
{
DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item.");
@@ -52,6 +62,9 @@ namespace Barotrauma
}
}
existingItemTag = prefab.ConfigElement.GetAttributeString("existingitemtag", "");
showMessageWhenPickedUp = prefab.ConfigElement.GetAttributeBool("showmessagewhenpickedup", false);
string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) ||
!Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType))
@@ -64,20 +77,66 @@ namespace Barotrauma
{
if (!IsClient)
{
//ruin items are allowed to spawn close to the sub
float minDistance = spawnPositionType == Level.PositionType.Ruin ? 0.0f : Level.Loaded.Size.X * 0.3f;
//ruin/wreck items are allowed to spawn close to the sub
float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Wreck ?
0.0f : Level.Loaded.Size.X * 0.3f;
Vector2 position = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f);
item = new Item(itemPrefab, position, null);
item.body.FarseerBody.BodyType = BodyType.Kinematic;
if (item.HasTag("alien"))
if (!string.IsNullOrEmpty(existingItemTag))
{
var suitableItems = Item.ItemList.Where(it => it.HasTag(existingItemTag));
switch (spawnPositionType)
{
case Level.PositionType.Cave:
case Level.PositionType.MainPath:
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 != SubmarineInfo.SubmarineType.Wreck) { continue; }
Rectangle worldBorders = it.Submarine.Borders;
worldBorders.Location += it.Submarine.WorldPosition.ToPoint();
if (Submarine.RectContains(worldBorders, it.WorldPosition))
{
item = it;
usedExistingItem = true;
break;
}
}
break;
}
}
if (item == null)
{
item = new Item(itemPrefab, position, null);
item.body.FarseerBody.BodyType = BodyType.Kinematic;
item.FindHull();
}
//try to find a container and place the item inside it
if (!string.IsNullOrEmpty(containerTag) && item.ParentInventory == null)
{
//try to find an artifact holder and place the artifact inside it
foreach (Item it in Item.ItemList)
{
if (it.Submarine != null || !it.HasTag("artifactholder")) continue;
if (!it.HasTag(containerTag)) { continue; }
switch (spawnPositionType)
{
case Level.PositionType.Cave:
case Level.PositionType.MainPath:
if (it.Submarine != null || it.ParentRuin != null) { continue; }
break;
case Level.PositionType.Ruin:
if (it.ParentRuin == null) { continue; }
break;
case Level.PositionType.Wreck:
if (it.Submarine == null || it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Wreck) { continue; }
break;
}
var itemContainer = it.GetComponent<Items.Components.ItemContainer>();
if (itemContainer == null) { continue; }
if (itemContainer.Combine(item, user: null)) { break; } // Placement successful
@@ -97,14 +156,21 @@ namespace Barotrauma
{
case 0:
if (item.ParentInventory != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; }
if (item.CurrentHull?.Submarine == null) { return; }
if (showMessageWhenPickedUp)
{
if (!(item.ParentInventory?.Owner is Character)) { return; }
}
else
{
if (item.CurrentHull?.Submarine == null || item.CurrentHull.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { return; }
}
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; }
State = 2;
break;
}
}
}
public override void End()
@@ -1,50 +1,46 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
namespace Barotrauma
{
class MonsterEvent : ScriptedEvent
{
private string speciesName;
private int minAmount, maxAmount;
private readonly string speciesName;
private readonly int minAmount, maxAmount;
private List<Character> monsters;
private bool spawnDeep;
private readonly bool spawnDeep;
private Vector2? spawnPos;
private bool disallowed;
private Level.PositionType spawnPosType;
private readonly bool disallowed;
private readonly Level.PositionType spawnPosType;
private bool spawnPending;
private string characterFileName;
public override Vector2 DebugDrawPos
{
get { return spawnPos.HasValue ? spawnPos.Value : Vector2.Zero; }
get { return spawnPos ?? Vector2.Zero; }
}
public override string ToString()
{
if (maxAmount <= 1)
{
return "MonsterEvent (" + characterFileName + ")";
return "MonsterEvent (" + speciesName + ")";
}
else if (minAmount < maxAmount)
{
return "MonsterEvent (" + characterFileName + " x" + minAmount + "-" + maxAmount + ")";
return "MonsterEvent (" + speciesName + " x" + minAmount + "-" + maxAmount + ")";
}
else
{
return "MonsterEvent (" + characterFileName + " x" + maxAmount + ")";
return "MonsterEvent (" + speciesName + " x" + maxAmount + ")";
}
}
@@ -76,7 +72,6 @@ namespace Barotrauma
}
spawnDeep = prefab.ConfigElement.GetAttributeBool("spawndeep", false);
characterFileName = Path.GetFileName(Path.GetDirectoryName(speciesName)).ToLower();
if (GameMain.NetworkMember != null)
{
@@ -85,7 +80,10 @@ namespace Barotrauma
if (!string.IsNullOrWhiteSpace(tryKey))
{
if (!GameMain.NetworkMember.ServerSettings.MonsterEnabled[tryKey]) disallowed = true; //spawn was disallowed by host
if (!GameMain.NetworkMember.ServerSettings.MonsterEnabled[tryKey])
{
disallowed = true; //spawn was disallowed by host
}
}
}
}
@@ -106,18 +104,8 @@ namespace Barotrauma
public override bool CanAffectSubImmediately(Level level)
{
float maxRange = Items.Components.Sonar.DefaultSonarRange * 0.8f;
List<Vector2> positions = GetAvailableSpawnPositions();
foreach (Vector2 position in positions)
{
if (Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition) < maxRange * maxRange)
{
return true;
}
}
return false;
float maxRange = Sonar.DefaultSonarRange * 0.8f;
return GetAvailableSpawnPositions().Any(p => Vector2.DistanceSquared(p.Position.ToVector2(), Submarine.MainSub.WorldPosition) < maxRange * maxRange);
}
public override void Init(bool affectSubImmediately)
@@ -128,28 +116,44 @@ namespace Barotrauma
}
}
private List<Vector2> GetAvailableSpawnPositions()
private List<Level.InterestingPosition> GetAvailableSpawnPositions()
{
var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType));
List<Vector2> positions = new List<Vector2>();
foreach (var allowedPosition in availablePositions)
var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType) && !Level.Loaded.UsedPositions.Contains(p));
var removals = new List<Level.InterestingPosition>();
foreach (var position in availablePositions)
{
if (Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(allowedPosition.Position.ToVector2())))) { continue; }
positions.Add(allowedPosition.Position.ToVector2());
}
if (spawnDeep)
{
for (int i = 0; i < positions.Count; i++)
if (position.Submarine != null)
{
positions[i] = new Vector2(positions[i].X, positions[i].Y - Level.Loaded.Size.Y);
if (position.Submarine.ThalamusAI != null && position.Submarine.ThalamusAI.IsAlive)
{
removals.Add(position);
}
else
{
continue;
}
}
if (position.PositionType != Level.PositionType.MainPath) { continue; }
if (Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(position.Position.ToVector2()))))
{
removals.Add(position);
}
if (spawnDeep)
{
for (int i = 0; i < availablePositions.Count; i++)
{
var pos = availablePositions[i].Position;
pos = new Point(pos.X, pos.Y - Level.Loaded.Size.Y);
availablePositions[i] = new Level.InterestingPosition(pos, availablePositions[i].PositionType);
}
}
if (position.Position.Y < Level.Loaded.GetBottomPosition(position.Position.X).Y)
{
removals.Add(position);
}
}
positions.RemoveAll(pos => pos.Y < Level.Loaded.GetBottomPosition(pos.X).Y);
return positions;
removals.ForEach(r => availablePositions.Remove(r));
return availablePositions;
}
private void FindSpawnPosition(bool affectSubImmediately)
@@ -158,67 +162,98 @@ namespace Barotrauma
spawnPos = Vector2.Zero;
var availablePositions = GetAvailableSpawnPositions();
if (affectSubImmediately && spawnPosType != Level.PositionType.Ruin)
var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false);
var removedPositions = new List<Level.InterestingPosition>();
foreach (var position in availablePositions)
{
if (availablePositions.Count == 0)
if (Rand.Value(Rand.RandSync.Server) > prefab.SpawnProbability)
{
removedPositions.Add(position);
if (prefab.AllowOnlyOnce)
{
Level.Loaded.UsedPositions.Add(position);
}
}
}
removedPositions.ForEach(p => availablePositions.Remove(p));
bool isSubOrWreck = spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Wreck;
if (affectSubImmediately && !isSubOrWreck)
{
if (availablePositions.None())
{
//no suitable position found, disable the event
Finished();
return;
}
float closestDist = float.PositiveInfinity;
//find the closest spawnposition that isn't too close to any of the subs
foreach (Vector2 position in availablePositions)
foreach (var position in availablePositions)
{
float dist = Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition);
Vector2 pos = position.Position.ToVector2();
float dist = Vector2.DistanceSquared(pos, Submarine.MainSub.WorldPosition);
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.IsOutpost) { continue; }
if (sub.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
float minDistToSub = GetMinDistanceToSub(sub);
if (dist > minDistToSub * minDistToSub && dist < closestDist)
{
closestDist = dist;
spawnPos = position;
chosenPosition = position;
}
}
}
//only found a spawnpos that's very far from the sub, pick one that's closer
//and wait for the sub to move further before spawning
if (closestDist > 15000.0f * 15000.0f)
{
foreach (Vector2 position in availablePositions)
foreach (var position in availablePositions)
{
float dist = Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition);
float dist = Vector2.DistanceSquared(position.Position.ToVector2(), Submarine.MainSub.WorldPosition);
if (dist < closestDist)
{
closestDist = dist;
spawnPos = position;
chosenPosition = position;
}
}
}
}
else
{
float minDist = spawnPosType == Level.PositionType.Ruin ? 0.0f : 20000.0f;
availablePositions.RemoveAll(p => Vector2.Distance(Submarine.MainSub.WorldPosition, p) < minDist);
if (availablePositions.Count == 0)
if (!isSubOrWreck)
{
float minDistance = 20000;
availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
}
if (availablePositions.None())
{
//no suitable position found, disable the event
Finished();
return;
}
spawnPos = availablePositions[Rand.Int(availablePositions.Count, Rand.RandSync.Server)];
chosenPosition = availablePositions.GetRandom();
}
if (chosenPosition.IsValid)
{
spawnPos = chosenPosition.Position.ToVector2();
if (chosenPosition.Submarine != null || chosenPosition.Ruin != null)
{
var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine, useSyncedRand: false);
if (spawnPoint != null)
{
spawnPos = spawnPoint.WorldPosition;
}
}
spawnPending = true;
if (prefab.AllowOnlyOnce)
{
Level.Loaded.UsedPositions.Add(chosenPosition);
}
}
spawnPending = true;
}
private float GetMinDistanceToSub(Submarine submarine)
{
//9000 units is slightly less than the default range of the sonar
return Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), 9000.0f);
return Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), Sonar.DefaultSonarRange * 0.9f);
}
public override void Update(float deltaTime)
@@ -243,9 +278,38 @@ namespace Barotrauma
//wait until there are no submarines at the spawnpos
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.IsOutpost) { continue; }
if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
float minDist = GetMinDistanceToSub(submarine);
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) return;
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; }
}
//if spawning in a ruin/cave, wait for someone to be close to it to spawning
//unnecessary monsters in places the players might never visit during the round
if (spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Cave || spawnPosType == Level.PositionType.Wreck)
{
bool someoneNearby = false;
float minDist = Sonar.DefaultSonarRange * 0.8f;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist)
{
someoneNearby = true;
break;
}
}
foreach (Character c in Character.CharacterList)
{
if (c == Character.Controlled || c.IsRemotePlayer)
{
if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < minDist * minDist)
{
someoneNearby = true;
break;
}
}
}
if (!someoneNearby) { return; }
}
spawnPending = false;
@@ -280,7 +344,7 @@ namespace Barotrauma
Entity targetEntity = Submarine.FindClosest(GameMain.GameScreen.Cam.WorldViewCenter);
#if CLIENT
if (Character.Controlled != null) targetEntity = (Entity)Character.Controlled;
if (Character.Controlled != null) { targetEntity = Character.Controlled; }
#endif
bool monstersDead = true;
@@ -297,7 +361,7 @@ namespace Barotrauma
}
}
if (monstersDead) Finished();
if (monstersDead) { Finished(); }
}
}
}
@@ -1,6 +1,5 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -8,7 +7,7 @@ namespace Barotrauma
{
protected bool isFinished;
private readonly ScriptedEventPrefab prefab;
protected readonly ScriptedEventPrefab prefab;
public bool IsFinished
{
@@ -7,12 +7,11 @@ namespace Barotrauma
{
class ScriptedEventPrefab
{
public readonly XElement ConfigElement;
public readonly Type EventType;
public readonly XElement ConfigElement;
public readonly Type EventType;
public readonly string MusicType;
public readonly float SpawnProbability;
public readonly bool AllowOnlyOnce;
public float Commonness;
public ScriptedEventPrefab(XElement element)
@@ -34,6 +33,8 @@ namespace Barotrauma
DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\".");
}
Commonness = element.GetAttributeFloat("commonness", 1.0f);
SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1);
AllowOnlyOnce = element.GetAttributeBool("allowonlyonce", false);
}
public ScriptedEvent CreateInstance()
@@ -25,6 +25,11 @@ namespace Barotrauma
//the events in this set are delayed if the current EventManager intensity is not between these values
public readonly float MinIntensity, MaxIntensity;
public readonly bool AllowAtStart;
public readonly bool PerRuin;
public readonly bool PerWreck;
public readonly Dictionary<string, float> Commonness;
public readonly List<ScriptedEventPrefab> EventPrefabs;
@@ -54,6 +59,10 @@ namespace Barotrauma
MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f);
MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f);
AllowAtStart = element.GetAttributeBool("allowatstart", false);
PerRuin = element.GetAttributeBool("perruin", false);
PerWreck = element.GetAttributeBool("perwreck", false);
Commonness[""] = 1.0f;
foreach (XElement subElement in element.Elements())
{
@@ -63,7 +72,7 @@ namespace Barotrauma
Commonness[""] = subElement.GetAttributeFloat("commonness", 0.0f);
foreach (XElement overrideElement in subElement.Elements())
{
if (overrideElement.Name.ToString().ToLowerInvariant() == "override")
if (overrideElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase))
{
string levelType = overrideElement.GetAttributeString("leveltype", "");
if (!Commonness.ContainsKey(levelType))
@@ -116,7 +125,7 @@ namespace Barotrauma
int i = 0;
foreach (XElement element in doc.Root.Elements())
{
if (element.Name.ToString().ToLowerInvariant() != "eventset") { continue; }
if (!element.Name.ToString().Equals("eventset", StringComparison.OrdinalIgnoreCase)) { continue; }
List.Add(new ScriptedEventSet(element, i.ToString()));
i++;
}
@@ -7,24 +7,23 @@ namespace Barotrauma.Extensions
public static class IEnumerableExtensions
{
/// <summary>
/// Randomizes the collection and returns it.
/// Randomizes the collection (using OrderBy) and returns it.
/// </summary>
public static IOrderedEnumerable<T> Randomize<T>(this IEnumerable<T> source)
public static IOrderedEnumerable<T> Randomize<T>(this IEnumerable<T> source, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
return source.OrderBy(i => Rand.Value());
return source.OrderBy(i => Rand.Value(randSync));
}
/// <summary>
/// Randomizes the list in place.
/// Randomizes the list in place without creating a new collection, using a Fisher-Yates-based algorithm.
/// </summary>
public static void RandomizeList<T>(this List<T> list)
public static void Shuffle<T>(this IList<T> list, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
//Fisher-Yates shuffle
int n = list.Count;
while (n > 1)
{
n--;
int k = Rand.Int(n + 1);
int k = Rand.Int(n + 1, randSync);
T value = list[k];
list[k] = list[n];
list[n] = value;
@@ -90,6 +89,11 @@ namespace Barotrauma.Extensions
return source.Count(predicate) > 1;
}
}
public static IEnumerable<T> ToEnumerable<T>(this T item)
{
yield return item;
}
// source: https://stackoverflow.com/questions/19237868/get-all-children-to-one-list-recursive-c-sharp
public static IEnumerable<T> SelectManyRecursive<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> selector)
@@ -57,5 +57,43 @@ namespace Barotrauma.Extensions
var size = rect.MultiplySize(scale);
return new Rectangle(rect.X, rect.Y, size.X, size.Y);
}
public static bool IntersectsWorld(this Rectangle rect, Rectangle value)
{
int bottom = rect.Y - rect.Height;
int otherBottom = value.Y - value.Height;
return value.Left < rect.Right && rect.Left < value.Right &&
value.Top > bottom && rect.Top > otherBottom;
}
/// <summary>
/// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower.
/// </summary>
public static bool ContainsWorld(this Rectangle rect, Rectangle other)
{
return
(rect.X <= other.X) && ((other.X + other.Width) <= (rect.X + rect.Width)) &&
(rect.Y >= other.Y) && ((other.Y - other.Height) >= (rect.Y - rect.Height));
}
/// <summary>
/// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower.
/// </summary>
public static bool ContainsWorld(this Rectangle rect, Vector2 point)
{
return
(rect.X <= point.X) && (point.X < (rect.X + rect.Width)) &&
(rect.Y >= point.Y) && (point.Y > (rect.Y - rect.Height));
}
/// <summary>
/// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower.
/// </summary>
public static bool ContainsWorld(this Rectangle rect, Point point)
{
return
(rect.X <= point.X) && (point.X < (rect.X + rect.Width)) &&
(rect.Y >= point.Y) && (point.Y > (rect.Y - rect.Height));
}
}
}
@@ -23,11 +23,18 @@ namespace Barotrauma
{
if (Submarine.MainSubs[i] == null) { continue; }
List<Submarine> subs = new List<Submarine>() { Submarine.MainSubs[i] };
subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.IsOutpost));
subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.Info.IsOutpost));
Place(subs);
}
if (campaign != null) { campaign.InitialSuppliesSpawned = true; }
}
}
foreach (var wreck in Submarine.Loaded)
{
if (wreck.Info.IsWreck)
{
Place(wreck.ToEnumerable());
}
}
}
private static void Place(IEnumerable<Submarine> subs)
@@ -38,10 +45,10 @@ namespace Barotrauma
return;
}
int sizeApprox = MapEntityPrefab.List.Count() / 3;
var containers = new List<ItemContainer>(100);
var prefabsWithContainer = new List<ItemPrefab>(sizeApprox / 3);
var prefabsWithoutContainer = new List<ItemPrefab>(sizeApprox);
int itemCountApprox = MapEntityPrefab.List.Count() / 3;
var containers = new List<ItemContainer>(70 + 30 * subs.Count());
var prefabsWithContainer = new List<ItemPrefab>(itemCountApprox / 3);
var prefabsWithoutContainer = new List<ItemPrefab>(itemCountApprox);
var removals = new List<ItemPrefab>();
foreach (Item item in Item.ItemList)
@@ -49,6 +56,7 @@ namespace Barotrauma
if (!subs.Contains(item.Submarine)) { continue; }
containers.AddRange(item.GetComponents<ItemContainer>());
}
containers.Shuffle();
foreach (MapEntityPrefab prefab in MapEntityPrefab.List)
{
@@ -66,7 +74,7 @@ namespace Barotrauma
spawnedItems.Clear();
var validContainers = new Dictionary<ItemContainer, PreferredContainer>();
prefabsWithContainer.RandomizeList();
prefabsWithContainer.Shuffle();
// Spawn items that have an ItemContainer component first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc)
for (int i = 0; i < prefabsWithContainer.Count; i++)
{
@@ -82,12 +90,13 @@ namespace Barotrauma
// Another pass for items with containers because also they can spawn inside other items (like smg magazine)
prefabsWithContainer.ForEach(i => SpawnItems(i));
// Spawn items that don't have containers last
prefabsWithoutContainer.RandomizeList();
prefabsWithoutContainer.Shuffle();
prefabsWithoutContainer.ForEach(i => SpawnItems(i));
if (OutputDebugInfo)
{
DebugConsole.NewMessage("Automatically placed items: ");
var subNames = subs.Select(s => s.Info.Name).ToList();
DebugConsole.NewMessage($"Automatically placed items in { string.Join(", ", subNames) }:");
foreach (string itemName in spawnedItems.Select(it => it.Name).Distinct())
{
DebugConsole.NewMessage(" - " + itemName + " x" + spawnedItems.Count(it => it.Name == itemName));
@@ -149,7 +158,12 @@ namespace Barotrauma
private static bool SpawnItem(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer)
{
bool success = false;
if (Rand.Value() > validContainer.Value.SpawnProbability) { return success; }
if (Rand.Value() > validContainer.Value.SpawnProbability) { return false; }
// Don't add dangerously reactive materials in thalamus wrecks
if (validContainer.Key.Item.Submarine.ThalamusAI != null && itemPrefab.Tags.Contains("explodesinwater"))
{
return false;
}
int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1);
for (int i = 0; i < amount; i++)
{
@@ -8,6 +8,7 @@ namespace Barotrauma
{
const float ConversationIntervalMin = 100.0f;
const float ConversationIntervalMax = 180.0f;
const float ConversationIntervalMultiplierMultiplayer = 5.0f;
private float conversationTimer, conversationLineTimer;
private List<Pair<Character, string>> pendingConversationLines = new List<Pair<Character, string>>();
@@ -74,11 +75,17 @@ namespace Barotrauma
private void UpdateConversations(float deltaTime)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.ServerSettings.DisableBotConversations) { return; }
conversationTimer -= deltaTime;
if (conversationTimer <= 0.0f)
{
CreateRandomConversation();
conversationTimer = Rand.Range(ConversationIntervalMin, ConversationIntervalMax);
if (GameMain.NetworkMember != null)
{
conversationTimer *= ConversationIntervalMultiplierMultiplayer;
}
}
if (pendingConversationLines.Count > 0)
@@ -64,7 +64,7 @@ namespace Barotrauma
return Submarine.Loaded.FindAll(s =>
s != leavingSub &&
!leavingSub.DockedTo.Contains(s) &&
s != Level.Loaded.StartOutpost && s != Level.Loaded.EndOutpost &&
s.Info.Type == SubmarineInfo.SubmarineType.Player &&
(s.AtEndPosition != leavingSub.AtEndPosition || s.AtStartPosition != leavingSub.AtStartPosition));
}
@@ -80,7 +80,7 @@ namespace Barotrauma
{
foreach (Structure wall in Structure.WallList)
{
if (wall.Submarine == null || wall.Submarine.IsOutpost) { continue; }
if (wall.Submarine == null || wall.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
if (wall.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(wall.Submarine))
{
for (int i = 0; i < wall.SectionCount; i++)
@@ -95,7 +95,7 @@ namespace Barotrauma
{
foreach (Item item in Item.ItemList)
{
if (item.Submarine == null || item.Submarine.IsOutpost) { continue; }
if (item.Submarine == null || item.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
if (item.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(item.Submarine))
{
if (item.GetComponent<Items.Components.Repairable>() != null)
@@ -44,6 +44,7 @@ namespace Barotrauma
{
#if CLIENT
new GameModePreset("singleplayercampaign", typeof(SinglePlayerCampaign), true);
new GameModePreset("subtest", typeof(SubTestMode), true);
new GameModePreset("tutorial", typeof(TutorialMode), true);
new GameModePreset("devsandbox", typeof(GameMode), true);
#endif
@@ -69,9 +69,18 @@ namespace Barotrauma
#if CLIENT
if (GameMain.Client != null)
{
bool success =
GameMain.Client.ConnectedClients.Any(c => c.Character != null && !c.Character.IsDead);
GameMain.GameSession.EndRound("");
GameMain.GameSession.CrewManager.EndRound();
return;
if (success)
{
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
}
return;
}
#endif
@@ -109,12 +118,6 @@ namespace Barotrauma
}
}
//remove all items that are in someone's inventory
foreach (Character c in Character.CharacterList)
{
c.Inventory?.DeleteAllItems();
}
if (success)
{
bool atEndPosition = Submarine.MainSub.AtEndPosition;
@@ -142,6 +145,8 @@ namespace Barotrauma
}
map.ProgressWorld();
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
}
#endif
@@ -65,44 +65,51 @@ namespace Barotrauma
}
}
public SubmarineInfo SubmarineInfo { get; set; }
public Submarine Submarine { get; set; }
public string SavePath { get; set; }
partial void InitProjSpecific();
public GameSession(Submarine submarine, string savePath, GameModePreset gameModePreset, MissionType missionType = MissionType.None)
: this(submarine, savePath)
public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, MissionType missionType = MissionType.None)
: this(submarineInfo, savePath)
{
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
GameMode = gameModePreset.Instantiate(missionType);
}
public GameSession(Submarine submarine, string savePath, GameModePreset gameModePreset, MissionPrefab missionPrefab)
: this(submarine, savePath)
public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, MissionPrefab missionPrefab)
: this(submarineInfo, savePath)
{
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
GameMode = gameModePreset.Instantiate(missionPrefab);
#if CLIENT
if (GameMode is SubTestMode) { EventManager = null; }
#endif
}
private GameSession(Submarine submarine, string savePath)
private GameSession(SubmarineInfo submarineInfo, string savePath)
{
InitProjSpecific();
Submarine.MainSub = submarine;
this.Submarine = submarine;
SubmarineInfo = submarineInfo;
/*Submarine = new Submarine(submarineInfo);
Submarine.MainSub = Submarine;*/
GameMain.GameSession = this;
EventManager = new EventManager();
this.SavePath = savePath;
}
public GameSession(Submarine selectedSub, string saveFile, XDocument doc)
: this(selectedSub, saveFile)
public GameSession(SubmarineInfo selectedSubInfo, string saveFile, XDocument doc)
: this(selectedSubInfo, saveFile)
{
Submarine.MainSub = Submarine;
GameMain.GameSession = this;
selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name);
//selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name);
foreach (XElement subElement in doc.Root.Elements())
{
@@ -150,14 +157,14 @@ namespace Barotrauma
SaveUtil.LoadGame(SavePath);
}
public void StartRound(string levelSeed, float? difficulty = null, bool loadSecondSub = false)
public void StartRound(string levelSeed, float? difficulty = null)
{
Level randomLevel = Level.CreateRandom(levelSeed, difficulty);
StartRound(randomLevel, true, loadSecondSub);
StartRound(randomLevel);
}
public void StartRound(Level level, bool reloadSub = true, bool loadSecondSub = false, bool mirrorLevel = false)
public void StartRound(Level level, bool mirrorLevel = false)
{
//make sure no status effects have been carried on from the next round
//(they should be stopped in EndRound, this is a safeguard against cases where the round is ended ungracefully)
@@ -169,33 +176,26 @@ namespace Barotrauma
#endif
this.Level = level;
if (Submarine == null)
if (SubmarineInfo == null)
{
DebugConsole.ThrowError("Couldn't start game session, submarine not selected.");
return;
}
if (reloadSub || Submarine.MainSub != Submarine) { Submarine.Load(true); }
Submarine.MainSub = Submarine;
if (loadSecondSub)
{
if (Submarine.MainSubs[1] == null)
{
Submarine.MainSubs[1] = new Submarine(Submarine.MainSub.FilePath, Submarine.MainSub.MD5Hash.Hash, true);
Submarine.MainSubs[1].Load(false);
}
else if (reloadSub)
{
Submarine.MainSubs[1].Load(false);
}
}
if (Submarine.IsFileCorrupted)
if (SubmarineInfo.IsFileCorrupted)
{
DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted.");
return;
}
Submarine.Unload();
Submarine = Submarine.MainSub = new Submarine(SubmarineInfo);
Submarine.MainSub = Submarine;
if (GameMode.Mission != null && GameMode.Mission.TeamCount > 1 && Submarine.MainSubs[1] == null)
{
Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true);
}
if (level != null)
{
level.Generate(mirrorLevel);
@@ -231,7 +231,7 @@ namespace Barotrauma
if (port.Item.WorldPosition.Y < Submarine.WorldPosition.Y) { continue; }
float dist = Vector2.DistanceSquared(port.Item.WorldPosition, level.StartOutpost.WorldPosition);
if (myPort == null || dist < closestDistance)
if (myPort == null || dist < closestDistance || (port.MainDockingPort && !myPort.MainDockingPort))
{
myPort = port;
closestDistance = dist;
@@ -254,14 +254,14 @@ namespace Barotrauma
foreach (var sub in Submarine.Loaded)
{
if (sub.IsOutpost)
if (sub.Info.IsOutpost)
{
sub.DisableObstructedWayPoints();
}
}
Entity.Spawner = new EntitySpawner();
if (GameMode.Mission != null) { Mission = GameMode.Mission; }
if (GameMode != null) { GameMode.Start(); }
if (GameMode.Mission != null)
@@ -277,7 +277,7 @@ namespace Barotrauma
}
}
EventManager.StartRound(level);
EventManager?.StartRound(level);
SteamAchievementManager.OnStartRound();
if (GameMode != null)
@@ -286,8 +286,9 @@ namespace Barotrauma
if (GameMain.NetworkMember == null)
{
//only autoplace items here in single player
//only place items and corpses here in single player
//the server does this after loading the respawn shuttle
Level?.SpawnCorpses();
AutoItemPlacer.PlaceIfNeeded(GameMode);
}
if (GameMode is MultiPlayerCampaign mpCampaign && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
@@ -296,18 +297,18 @@ namespace Barotrauma
}
}
GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Name);
GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(level.Seed));
GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Info.Name);
GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(level?.Seed ?? "[NO_LEVEL]"));
GameAnalyticsManager.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start,
GameMode.Preset.Identifier, (Mission == null ? "None" : Mission.GetType().ToString()));
#if CLIENT
if (GameMode is SinglePlayerCampaign) { SteamAchievementManager.OnBiomeDiscovered(level.Biome); }
RoundSummary = new RoundSummary(this);
if (!(GameMode is SubTestMode)) { RoundSummary = new RoundSummary(this); }
GameMain.GameScreen.ColorFade(Color.Black, Color.TransparentBlack, 5.0f);
if (!(GameMode is TutorialMode))
if (!(GameMode is TutorialMode) && !(GameMode is SubTestMode))
{
GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false);
GUI.AddMessage(level.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, level.Difficulty / 100.0f), 5.0f, playSound: false);
@@ -322,7 +323,7 @@ namespace Barotrauma
public void Update(float deltaTime)
{
EventManager.Update(deltaTime);
EventManager?.Update(deltaTime);
GameMode?.Update(deltaTime);
Mission?.Update(deltaTime);
@@ -352,7 +353,7 @@ namespace Barotrauma
}
#endif
EventManager.EndRound();
EventManager?.EndRound();
SteamAchievementManager.OnRoundEnded(this);
Mission = null;
@@ -451,7 +452,7 @@ namespace Barotrauma
XDocument doc = new XDocument(new XElement("Gamesession"));
doc.Root.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal));
doc.Root.Add(new XAttribute("submarine", Submarine == null ? "" : Submarine.Name));
doc.Root.Add(new XAttribute("submarine", SubmarineInfo == null ? "" : SubmarineInfo.Name));
doc.Root.Add(new XAttribute("mapseed", Map.Seed));
doc.Root.Add(new XAttribute("selectedcontentpackages",
string.Join("|", GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path))));
@@ -27,10 +27,10 @@ namespace Barotrauma
}
public partial class GameSettings
{
const string savePath = "config.xml";
const string playerSavePath = "config_player.xml";
const string vanillaContentPackagePath = "Data/ContentPackages/Vanilla";
{
public const string SavePath = "config.xml";
public const string PlayerSavePath = "config_player.xml";
public const string VanillaContentPackagePath = "Data/ContentPackages/Vanilla";
public int GraphicsWidth { get; set; }
public int GraphicsHeight { get; set; }
@@ -138,6 +138,13 @@ namespace Barotrauma
public bool CrewMenuOpen { get; set; } = true;
public bool ChatOpen { get; set; } = true;
public float CorpseDespawnDelay { get; set; } = 10.0f * 60.0f;
/// <summary>
/// How many corpses there can be in a sub before they start to get despawned
/// </summary>
public int CorpsesPerSubDespawnThreshold { get; set; } = 5;
private string overrideSaveFolder, overrideMultiplayerSaveFolder;
private bool unsavedSettings;
@@ -198,7 +205,7 @@ namespace Barotrauma
{
voiceChatVolume = MathHelper.Clamp(value, 0.0f, 1.0f);
#if CLIENT
GameMain.SoundManager?.SetCategoryGainMultiplier("voip", voiceChatVolume * 30.0f, 0);
GameMain.SoundManager?.SetCategoryGainMultiplier("voip", voiceChatVolume, 0);
#endif
}
}
@@ -236,6 +243,7 @@ namespace Barotrauma
#if DEBUG
public bool AutomaticQuickStartEnabled { get; set; }
public bool TextManagerDebugModeEnabled { get; set; }
#endif
private FileSystemWatcher modsFolderWatcher;
@@ -311,7 +319,7 @@ namespace Barotrauma
ref shouldRefreshAfflictions);
if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); }
if (shouldRefreshSubs) { Submarine.RefreshSavedSubs(); }
if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); }
if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); }
if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); }
if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); }
@@ -359,7 +367,7 @@ namespace Barotrauma
ref shouldRefreshAfflictions);
if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); }
if (shouldRefreshSubs) { Submarine.RefreshSavedSubs(); }
if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); }
if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); }
if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); }
if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); }
@@ -408,7 +416,7 @@ namespace Barotrauma
ref shouldRefreshAfflictions);
if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); }
if (shouldRefreshSubs) { Submarine.RefreshSavedSubs(); }
if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); }
if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); }
if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); }
if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); }
@@ -496,10 +504,10 @@ namespace Barotrauma
shouldRefreshSoundPlayer = true;
break;
case ContentType.Particles:
GameMain.ParticleManager.LoadPrefabsFromFile(file);
GameMain.ParticleManager?.LoadPrefabsFromFile(file);
break;
case ContentType.Decals:
GameMain.DecalManager.LoadFromFile(file);
GameMain.DecalManager?.LoadFromFile(file);
break;
#endif
}
@@ -579,10 +587,10 @@ namespace Barotrauma
shouldRefreshSoundPlayer = true;
break;
case ContentType.Particles:
GameMain.ParticleManager.RemovePrefabsByFile(file.Path);
GameMain.ParticleManager?.RemovePrefabsByFile(file.Path);
break;
case ContentType.Decals:
GameMain.DecalManager.RemoveByFile(file.Path);
GameMain.DecalManager?.RemoveByFile(file.Path);
break;
#endif
}
@@ -615,6 +623,7 @@ namespace Barotrauma
case ContentType.Particles:
case ContentType.Decals:
case ContentType.Outpost:
case ContentType.Wreck:
case ContentType.BackgroundCreaturePrefabs:
case ContentType.ServerExecutable:
case ContentType.None:
@@ -643,7 +652,7 @@ namespace Barotrauma
ItemAssemblyPrefab.Prefabs.SortAll();
StructurePrefab.Prefabs.SortAll();
Submarine.RefreshSavedSubs();
SubmarineInfo.RefreshSavedSubs();
ItemPrefab.InitFabricationRecipes();
RuinGeneration.RuinGenerationParams.ClearAll();
ScriptedEventSet.LoadPrefabs();
@@ -757,7 +766,7 @@ namespace Barotrauma
private void OnModFolderUpdate(object sender, FileSystemEventArgs e)
{
if (SuppressModFolderWatcher || !(GameMain.NetworkMember?.IsClient ?? false)) { return; }
if (SuppressModFolderWatcher || (GameMain.NetworkMember?.IsClient ?? false)) { return; }
switch (e.ChangeType)
{
case WatcherChangeTypes.Created:
@@ -818,7 +827,7 @@ namespace Barotrauma
private void LoadDefaultConfig(bool setLanguage = true)
{
XDocument doc = XMLExtensions.TryLoadXml(savePath);
XDocument doc = XMLExtensions.TryLoadXml(SavePath);
if (doc == null)
{
GraphicsWidth = 1024;
@@ -931,7 +940,7 @@ namespace Barotrauma
foreach (ContentPackage contentPackage in SelectedContentPackages)
{
if (contentPackage.Path.Contains(vanillaContentPackagePath))
if (contentPackage.Path.Contains(VanillaContentPackagePath))
{
doc.Root.Add(new XElement("contentpackage", new XAttribute("path", contentPackage.Path)));
break;
@@ -986,7 +995,7 @@ namespace Barotrauma
try
{
using (var writer = XmlWriter.Create(savePath, settings))
using (var writer = XmlWriter.Create(SavePath, settings))
{
doc.WriteTo(writer);
writer.Flush();
@@ -1021,7 +1030,7 @@ namespace Barotrauma
/// </summary>
private bool LoadPlayerConfigInternal()
{
XDocument doc = XMLExtensions.LoadXml(playerSavePath);
XDocument doc = XMLExtensions.LoadXml(PlayerSavePath);
if (doc == null || doc.Root == null)
{
ShowUserStatisticsPrompt = true;
@@ -1199,9 +1208,12 @@ namespace Barotrauma
new XAttribute("crewmenuopen", CrewMenuOpen),
new XAttribute("campaigndisclaimershown", CampaignDisclaimerShown),
new XAttribute("editordisclaimershown", EditorDisclaimerShown),
new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning)
new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning),
new XAttribute("corpsedespawndelay", CorpseDespawnDelay),
new XAttribute("corpsespersubdespawnthreshold", CorpsesPerSubDespawnThreshold)
#if DEBUG
, new XAttribute("automaticquickstartenabled", AutomaticQuickStartEnabled)
, new XAttribute("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled)
#endif
);
@@ -1349,7 +1361,7 @@ namespace Barotrauma
try
{
using (var writer = XmlWriter.Create(playerSavePath, settings))
using (var writer = XmlWriter.Create(PlayerSavePath, settings))
{
doc.WriteTo(writer);
writer.Flush();
@@ -1382,11 +1394,14 @@ namespace Barotrauma
EnableMouseLook = doc.Root.GetAttributeBool("enablemouselook", EnableMouseLook);
CrewMenuOpen = doc.Root.GetAttributeBool("crewmenuopen", CrewMenuOpen);
ChatOpen = doc.Root.GetAttributeBool("chatopen", ChatOpen);
CorpseDespawnDelay = doc.Root.GetAttributeInt("corpsedespawndelay", 10 * 60);
CorpsesPerSubDespawnThreshold = doc.Root.GetAttributeInt("corpsespersubdespawnthreshold", 5);
CampaignDisclaimerShown = doc.Root.GetAttributeBool("campaigndisclaimershown", CampaignDisclaimerShown);
EditorDisclaimerShown = doc.Root.GetAttributeBool("editordisclaimershown", EditorDisclaimerShown);
ShowTutorialSkipWarning = doc.Root.GetAttributeBool("tutorialskipwarning", true);
#if DEBUG
AutomaticQuickStartEnabled = doc.Root.GetAttributeBool("automaticquickstartenabled", AutomaticQuickStartEnabled);
TextManagerDebugModeEnabled = doc.Root.GetAttributeBool("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled);
#endif
XElement gameplayElement = doc.Root.Element("gameplay");
jobPreferences = new List<Pair<string, int>>();
@@ -1560,6 +1575,8 @@ namespace Barotrauma
InventoryScale = 1;
AutoUpdateWorkshopItems = true;
CampaignDisclaimerShown = false;
CorpseDespawnDelay = 10 * 60;
CorpsesPerSubDespawnThreshold = 5;
if (resetLanguage)
{
Language = "English";
@@ -73,7 +73,7 @@ namespace Barotrauma
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "item") continue;
if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; }
string itemIdentifier = subElement.GetAttributeString("identifier", "");
ItemPrefab itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
@@ -58,6 +58,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, false, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")]
public bool MainDockingPort
{
get;
set;
}
public DockingPort DockingTarget { get; private set; }
public bool Docked
@@ -173,8 +180,8 @@ namespace Barotrauma.Items.Components
if (!item.linkedTo.Contains(target.item)) item.linkedTo.Add(target.item);
if (!target.item.linkedTo.Contains(item)) target.item.linkedTo.Add(item);
if (!target.item.Submarine.DockedTo.Contains(item.Submarine)) target.item.Submarine.DockedTo.Add(item.Submarine);
if (!item.Submarine.DockedTo.Contains(target.item.Submarine)) item.Submarine.DockedTo.Add(target.item.Submarine);
if (!target.item.Submarine.DockedTo.Contains(item.Submarine)) target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target);
if (!item.Submarine.DockedTo.Contains(target.item.Submarine)) item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this);
DockingTarget = target;
DockingTarget.DockingTarget = this;
@@ -234,12 +241,12 @@ namespace Barotrauma.Items.Components
Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass ||
DockingTarget.item.Submarine.IsOutpost)
DockingTarget.item.Submarine.Info.IsOutpost)
{
item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff));
}
else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass ||
item.Submarine.IsOutpost)
item.Submarine.Info.IsOutpost)
{
DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff));
}
@@ -703,8 +710,8 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnSecondaryUse, 1.0f);
DockingTarget.item.Submarine.DockedTo.Remove(item.Submarine);
item.Submarine.DockedTo.Remove(DockingTarget.item.Submarine);
DockingTarget.item.Submarine.ConnectedDockingPorts.Remove(item.Submarine);
item.Submarine.ConnectedDockingPorts.Remove(DockingTarget.item.Submarine);
if (door != null && DockingTarget.door != null)
{
@@ -951,12 +958,12 @@ namespace Barotrauma.Items.Components
if (docked)
{
if (item.Submarine != null && DockingTarget?.item?.Submarine != null)
GameServer.Log(sender.LogName + " docked " + item.Submarine.Name + " to " + DockingTarget.item.Submarine.Name, ServerLog.MessageType.ItemInteraction);
GameServer.Log(sender.LogName + " docked " + item.Submarine.Info.Name + " to " + DockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction);
}
else
{
if (item.Submarine != null && prevDockingTarget?.item?.Submarine != null)
GameServer.Log(sender.LogName + " undocked " + item.Submarine.Name + " from " + prevDockingTarget.item.Submarine.Name, ServerLog.MessageType.ItemInteraction);
GameServer.Log(sender.LogName + " undocked " + item.Submarine.Info.Name + " from " + prevDockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction);
}
}
#endif
@@ -287,24 +287,21 @@ namespace Barotrauma.Items.Components
public override bool Select(Character character)
{
if (!isBroken)
if (isBroken) { return true; }
bool hasRequiredItems = HasRequiredItems(character, false);
if (HasAccess(character))
{
bool hasRequiredItems = HasRequiredItems(character, false);
if (HasAccess(character))
{
float originalPickingTime = PickingTime;
PickingTime = 0;
ToggleState(ActionType.OnUse, character);
PickingTime = originalPickingTime;
}
#if CLIENT
else if (hasRequiredItems && character != null && character == Character.Controlled)
{
GUI.AddMessage(accessDeniedTxt, GUI.Style.Red);
}
#endif
float originalPickingTime = PickingTime;
PickingTime = 0;
ToggleState(ActionType.OnUse, character);
PickingTime = originalPickingTime;
}
#if CLIENT
else if (hasRequiredItems && character != null && character == Character.Controlled)
{
GUI.AddMessage(accessDeniedTxt, GUI.Style.Red);
}
#endif
return false;
}
@@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components
{
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "attack") { continue; }
if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
attack = new Attack(subElement, item.Name + ", MeleeWeapon");
}
item.IsShootable = true;
@@ -29,6 +29,13 @@ namespace Barotrauma.Items.Components
set { reload = Math.Max(value, 0.0f); }
}
[Serialize(1, false, description: "How projectiles the weapon launches when fired once.")]
public int ProjectileCount
{
get;
set;
}
[Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles when used by a character with sufficient skills to use the weapon (in degrees).")]
public float Spread
{
@@ -110,55 +117,62 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
}
Projectile projectile = FindProjectile(triggerOnUseOnContainers: true);
if (projectile == null) { return true; }
float spread = GetSpread(character);
float rotation = (item.body.Dir == 1.0f) ? item.body.Rotation : item.body.Rotation - MathHelper.Pi;
rotation += spread * Rand.Range(-0.5f, 0.5f);
projectile.User = character;
//add the limbs of the shooter to the list of bodies to be ignored
//so that the player can't shoot himself
projectile.IgnoredBodies = new List<Body>(limbBodies);
Vector2 projectilePos = item.SimPosition;
Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos;
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
//make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel
if (Submarine.PickBody(sourcePos, barrelPos, projectile.IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null)
for (int i = 0; i < ProjectileCount; i++)
{
//no obstacles -> we can spawn the projectile at the barrel
projectilePos = barrelPos;
Projectile projectile = FindProjectile(triggerOnUseOnContainers: true);
if (projectile == null) { return true; }
float spread = GetSpread(character);
float rotation = (item.body.Dir == 1.0f) ? item.body.Rotation : item.body.Rotation - MathHelper.Pi;
rotation += spread * Rand.Range(-0.5f, 0.5f);
projectile.User = character;
//add the limbs of the shooter to the list of bodies to be ignored
//so that the player can't shoot himself
projectile.IgnoredBodies = new List<Body>(limbBodies);
Vector2 projectilePos = item.SimPosition;
Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos;
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
//make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel
if (Submarine.PickBody(sourcePos, barrelPos, projectile.IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null)
{
//no obstacles -> we can spawn the projectile at the barrel
projectilePos = barrelPos;
}
else if ((sourcePos - barrelPos).LengthSquared() > 0.0001f)
{
//spawn the projectile body.GetMaxExtent() away from the position where the raycast hit the obstacle
projectilePos = sourcePos - Vector2.Normalize(barrelPos - projectilePos) * Math.Max(projectile.Item.body.GetMaxExtent(), 0.1f);
}
projectile.Item.body.ResetDynamics();
projectile.Item.SetTransform(projectilePos, rotation);
projectile.Use(deltaTime);
projectile.Item.GetComponent<Rope>()?.Attach(item, projectile.Item);
if (projectile.Item.Removed) { continue; }
projectile.User = character;
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f));
//set the rotation of the projectile again because dropping the projectile resets the rotation
projectile.Item.SetTransform(projectilePos,
rotation + (projectile.Item.body.Dir * projectile.LaunchRotationRadians));
item.RemoveContained(projectile.Item);
if (i == 0)
{
//recoil
item.body.ApplyLinearImpulse(
new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * item.body.Mass * -50.0f,
maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
else if ((sourcePos - barrelPos).LengthSquared() > 0.0001f)
{
//spawn the projectile body.GetMaxExtent() away from the position where the raycast hit the obstacle
projectilePos = sourcePos - Vector2.Normalize(barrelPos - projectilePos) * Math.Max(projectile.Item.body.GetMaxExtent(), 0.1f);
}
projectile.Item.body.ResetDynamics();
projectile.Item.SetTransform(projectilePos, rotation);
projectile.Use(deltaTime);
if (projectile.Item.Removed) { return true; }
projectile.User = character;
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f));
//set the rotation of the projectile again because dropping the projectile resets the rotation
projectile.Item.SetTransform(projectilePos,
rotation + (projectile.Item.body.Dir * projectile.LaunchRotationRadians));
//recoil
item.body.ApplyLinearImpulse(
new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * item.body.Mass * -50.0f,
maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
LaunchProjSpecific();
item.RemoveContained(projectile.Item);
return true;
}
@@ -578,13 +578,7 @@ namespace Barotrauma.Items.Components
{
character.SetInput(InputType.Aim, false, true);
}
bool isAiming = false;
var holdable = item.GetComponent<Holdable>();
if (holdable != null)
{
isAiming = holdable.ControlPose;
}
sinTime = isAiming ? sinTime + deltaTime * 5 : 0;
sinTime += deltaTime * 5;
}
// Press the trigger only when the tool is approximately facing the target.
Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition;
@@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components
if (!picker.IsKeyDown(InputType.Aim) && !throwing) { throwPos = 0.0f; }
bool aim = picker.IsKeyDown(InputType.Aim) && (picker.SelectedConstruction == null || picker.SelectedConstruction.GetComponent<Ladder>() != null);
if (picker.IsUnconscious || picker.IsDead || !picker.AllowInput)
if (picker.IsDead || !picker.AllowInput)
{
throwing = false;
aim = false;
@@ -68,7 +68,6 @@ namespace Barotrauma.Items.Components
protected const float CorrectionDelay = 1.0f;
protected CoroutineHandle delayedCorrectionCoroutine;
protected float correctionTimer;
[Editable, Serialize(0.0f, false, description: "How long it takes to pick up the item (in seconds).")]
public float PickingTime
@@ -81,7 +80,6 @@ namespace Barotrauma.Items.Components
public Action<bool> OnActiveStateChanged;
public float IsActiveTimer;
public virtual bool IsActive
{
get { return isActive; }
@@ -222,7 +220,6 @@ namespace Barotrauma.Items.Components
set;
}
/// <summary>
/// How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).
/// </summary>
@@ -632,7 +629,7 @@ namespace Barotrauma.Items.Components
}
/// <summary>
/// Only checks the id card(s). Much simpler and a bit different than HasRequiredItems.
/// Only checks if any of the Picked requirements are matched (used for checking id card(s)). Much simpler and a bit different than HasRequiredItems.
/// </summary>
public bool HasAccess(Character character)
{
@@ -641,7 +638,7 @@ namespace Barotrauma.Items.Components
foreach (Item item in character.Inventory.Items)
{
if (item?.Prefab.Identifier == "idcard" && requiredItems.Any(ri => ri.Value.Any(r => r.MatchesItem(item))))
if (requiredItems.Any(ri => ri.Value.Any(r => r.Type == RelatedItem.RelationType.Picked && r.MatchesItem(item))))
{
return true;
}
@@ -741,14 +738,14 @@ namespace Barotrauma.Items.Components
{
foreach (XAttribute attribute in componentElement.Attributes())
{
if (!SerializableProperties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) continue;
if (!SerializableProperties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) { continue; }
property.TrySetValue(this, attribute.Value);
}
ParseMsg();
OverrideRequiredItems(componentElement);
}
if (item.Submarine != null) { SerializableProperty.UpgradeGameVersion(this, originalElement, item.Submarine.GameVersion); }
if (item.Submarine != null) { SerializableProperty.UpgradeGameVersion(this, originalElement, item.Submarine.Info.GameVersion); }
}
/// <summary>
@@ -870,6 +867,7 @@ namespace Barotrauma.Items.Components
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "requireditem":
case "requireditems":
RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmptyRequirements, item.Name);
if (newRequiredItem == null) continue;
@@ -52,6 +52,9 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, false)]
public bool AccessOnlyWhenBroken { get; set; }
[Serialize(5, false, description: "How many inventory slots the inventory has per row.")]
public int SlotsPerRow { get; set; }
@@ -197,10 +200,21 @@ namespace Barotrauma.Items.Components
}
}
public override bool HasRequiredItems(Character character, bool addMessage, string msg = null)
{
return (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
}
public override bool Select(Character character)
{
if (item.Container != null) { return false; }
if (AccessOnlyWhenBroken)
{
if (item.Condition > 0)
{
return false;
}
}
if (AutoInteractWithContained && character.SelectedConstruction == null)
{
foreach (Item contained in Inventory.Items)
@@ -218,6 +232,13 @@ namespace Barotrauma.Items.Components
public override bool Pick(Character picker)
{
if (AccessOnlyWhenBroken)
{
if (item.Condition > 0)
{
return false;
}
}
if (AutoInteractWithContained)
{
foreach (Item contained in Inventory.Items)
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma.Items.Components
{
@@ -209,7 +210,7 @@ namespace Barotrauma.Items.Components
return true;
}
public override bool SecondaryUse(float deltaTime, Character character = null)
{
if (this.user != character)
@@ -55,6 +55,15 @@ namespace Barotrauma.Items.Components
get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / MinVoltage, 1.0f))); }
}
public float CurrentBrokenVolume
{
get
{
if (item.ConditionPercentage > 10.0f) { return 0.0f; }
return Math.Abs(targetForce / 100.0f) * (1.0f - item.ConditionPercentage / 10.0f);
}
}
public Engine(Item item, XElement element)
: base(item, element)
{
@@ -22,6 +22,30 @@ namespace Barotrauma.Items.Components
private ItemContainer inputContainer, outputContainer;
private enum FabricatorState
{
Active = 1,
Paused = 2,
Stopped = 0
}
private FabricatorState state;
private FabricatorState State
{
get
{
return state;
}
set
{
if (state == value) { return; }
state = value;
#if SERVER
item.CreateServerEvent(this);
#endif
}
}
public ItemContainer InputContainer
{
get { return inputContainer; }
@@ -39,7 +63,7 @@ namespace Barotrauma.Items.Components
{
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() == "fabricableitem")
if (subElement.Name.ToString().Equals("fabricableitem", StringComparison.OrdinalIgnoreCase))
{
DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator.");
break;
@@ -61,6 +85,8 @@ namespace Barotrauma.Items.Components
}
}
state = FabricatorState.Stopped;
InitProjSpecific();
}
@@ -147,12 +173,15 @@ namespace Barotrauma.Items.Components
currPowerConsumption = powerConsumption;
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
if (GameMain.NetworkMember?.IsServer ?? true)
{
State = FabricatorState.Active;
}
#if SERVER
if (user != null)
{
GameServer.Log(user.LogName + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction);
}
item.CreateServerEvent(this);
#endif
}
@@ -180,12 +209,15 @@ namespace Barotrauma.Items.Components
inputContainer.Inventory.Locked = false;
outputContainer.Inventory.Locked = false;
if (GameMain.NetworkMember?.IsServer ?? true)
{
State = FabricatorState.Stopped;
}
#if SERVER
if (user != null)
{
GameServer.Log(user.LogName + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction);
}
item.CreateServerEvent(this);
#endif
}
@@ -199,8 +231,25 @@ namespace Barotrauma.Items.Components
progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime;
hasPower = Voltage >= MinVoltage;
if (!hasPower) { return; }
if (GameMain.NetworkMember?.IsClient ?? false)
{
hasPower = State != FabricatorState.Paused;
if (!hasPower)
{
return;
}
}
else
{
hasPower = Voltage >= MinVoltage;
if (!hasPower)
{
State = FabricatorState.Paused;
return;
}
State = FabricatorState.Active;
}
var repairable = item.GetComponent<Repairable>();
if (repairable != null)
@@ -37,23 +37,10 @@ namespace Barotrauma.Items.Components
private float currFlow;
public float CurrFlow
{
get
get
{
if (!IsActive) { return 0.0f; }
return Math.Abs(currFlow);
}
}
public override bool IsActive
{
get => base.IsActive;
set
{
base.IsActive = value;
if (!IsActive)
{
powerConsumption = 0;
}
return Math.Abs(currFlow);
}
}
@@ -77,11 +64,6 @@ namespace Barotrauma.Items.Components
float hullPercentage = 0.0f;
if (item.CurrentHull != null) { hullPercentage = (item.CurrentHull.WaterVolume / item.CurrentHull.Volume) * 100.0f; }
FlowPercentage = ((float)targetLevel - hullPercentage) * 10.0f;
if (pumpSpeedLockTimer <= 0.0f)
{
targetLevel = null;
}
}
currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f);
@@ -125,6 +107,7 @@ namespace Barotrauma.Items.Components
if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed))
{
flowPercentage = MathHelper.Clamp(tempSpeed, -100.0f, 100.0f);
targetLevel = null;
pumpSpeedLockTimer = 0.1f;
}
}
@@ -144,7 +127,7 @@ namespace Barotrauma.Items.Components
if (GameMain.Client != null) { return false; }
#endif
if (objective.Option.ToLowerInvariant() == "stoppumping")
if (objective.Option.Equals("stoppumping", StringComparison.OrdinalIgnoreCase))
{
#if SERVER
if (FlowPercentage > 0.0f)
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System.Globalization;
namespace Barotrauma.Items.Components
{
@@ -651,6 +652,20 @@ namespace Barotrauma.Items.Components
unsentChanges = true;
}
break;
case "set_fissionrate":
if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate))
{
FissionRate = newFissionRate;
unsentChanges = true;
}
break;
case "set_turbineoutput":
if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput))
{
TurbineOutput = newTurbineOutput;
unsentChanges = true;
}
break;
}
}
@@ -165,7 +165,6 @@ namespace Barotrauma.Items.Components
#region Docking
public List<DockingPort> DockingSources = new List<DockingPort>();
public DockingPort ActiveDockingSource, DockingTarget;
private bool searchedConnectedDockingPort;
private bool dockingModeEnabled;
@@ -200,7 +199,7 @@ namespace Barotrauma.Items.Components
if (dockingConnection != null)
{
var connectedPorts = item.GetConnectedComponentsRecursive<DockingPort>(dockingConnection);
DockingSources.AddRange(connectedPorts.Where(p => p.Item.Submarine != null && !p.Item.Submarine.IsOutpost));
DockingSources.AddRange(connectedPorts.Where(p => p.Item.Submarine != null && !p.Item.Submarine.Info.IsOutpost));
}
}
#endregion
@@ -344,6 +343,7 @@ namespace Barotrauma.Items.Components
autopilotRecalculatePathTimer = RecalculatePathInterval;
}
if (steeringPath == null) { return; }
steeringPath.CheckProgress(ConvertUnits.ToSimUnits(controlledSub.WorldPosition), 10.0f);
if (autopilotRayCastTimer <= 0.0f && steeringPath.NextNode != null)
@@ -475,6 +475,8 @@ namespace Barotrauma.Items.Components
private void UpdatePath()
{
if (Level.Loaded == null) { return; }
if (pathFinder == null) pathFinder = new PathFinder(WayPoint.WayPointList, false);
Vector2 target;
@@ -536,7 +538,6 @@ namespace Barotrauma.Items.Components
}
}
private bool aiDockingToggled;
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (objective.Override)
@@ -572,7 +573,7 @@ namespace Barotrauma.Items.Components
}
break;
case "navigateback":
if (!aiDockingToggled && DockingSources.Any(d => d.Docked))
if (DockingSources.Any(d => d.Docked))
{
item.SendSignal(0, "1", "toggle_docking", sender: null);
}
@@ -586,7 +587,7 @@ namespace Barotrauma.Items.Components
}
break;
case "navigatetodestination":
if (!aiDockingToggled && DockingSources.Any(d => d.Docked))
if (DockingSources.Any(d => d.Docked))
{
item.SendSignal(0, "1", "toggle_docking", sender: null);
}
@@ -213,7 +213,7 @@ namespace Barotrauma.Items.Components
}
if (HasBeenTuned) { return true; }
if (string.IsNullOrEmpty(objective.Option) || objective.Option.ToLowerInvariant() == "charge")
if (string.IsNullOrEmpty(objective.Option) || objective.Option.Equals("charge", StringComparison.OrdinalIgnoreCase))
{
if (Math.Abs(rechargeSpeed - maxRechargeSpeed * aiRechargeTargetRatio) > 0.05f)
{
@@ -11,7 +11,7 @@ using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class Projectile : ItemComponent
partial class Projectile : ItemComponent, IServerSerializable
{
struct HitscanResult
{
@@ -32,11 +32,13 @@ namespace Barotrauma.Items.Components
{
public Fixture Fixture;
public Vector2 Normal;
public Vector2 LinearVelocity;
public Impact(Fixture fixture, Vector2 normal)
public Impact(Fixture fixture, Vector2 normal, Vector2 velocity)
{
Fixture = fixture;
Normal = normal;
LinearVelocity = velocity;
}
}
@@ -47,13 +49,13 @@ namespace Barotrauma.Items.Components
//a duration during which the projectile won't drop from the body it's stuck to
private const float PersistentStickJointDuration = 1.0f;
private float launchImpulse;
private PrismaticJoint stickJoint;
private Body stickTarget;
private Attack attack;
private readonly Attack attack;
private Vector2 launchPos;
private readonly HashSet<Body> hits = new HashSet<Body>();
public List<Body> IgnoredBodies;
@@ -68,14 +70,15 @@ namespace Barotrauma.Items.Components
}
}
public IEnumerable<Body> Hits
{
get { return hits; }
}
private float persistentStickJointTimer;
[Serialize(10.0f, false, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")]
public float LaunchImpulse
{
get { return launchImpulse; }
set { launchImpulse = value; }
}
public float LaunchImpulse { get; set; }
[Serialize(0.0f, false, description: "The rotation of the item relative to the rotation of the weapon when launched (in degrees).")]
public float LaunchRotation
@@ -98,6 +101,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, false, description: "When set to true, the item won't fall of a target it's stuck to unless removed.")]
public bool StickPermanently
{
get;
set;
}
[Serialize(false, false, description: "Can the item stick to the character it hits.")]
public bool StickToCharacters
{
@@ -136,6 +146,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(1, false, description: "How many targets the projectile can hit before it stops.")]
public int MaxTargetsToHit
{
get;
set;
}
[Serialize(false, false, description: "Should the item be deleted when it hits something.")]
public bool RemoveOnHit
{
@@ -150,6 +167,17 @@ namespace Barotrauma.Items.Components
set;
}
public Body StickTarget
{
get;
private set;
}
public bool IsStuckToTarget
{
get { return StickTarget != null; }
}
public Projectile(Item item, XElement element)
: base (item, element)
{
@@ -157,7 +185,7 @@ namespace Barotrauma.Items.Components
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "attack") continue;
if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
attack = new Attack(subElement, item.Name + ", Projectile");
}
}
@@ -201,7 +229,7 @@ namespace Barotrauma.Items.Components
}
else
{
Launch(launchDir * launchImpulse * item.body.Mass);
Launch(launchDir * LaunchImpulse * item.body.Mass);
}
}
@@ -212,6 +240,9 @@ namespace Barotrauma.Items.Components
private void Launch(Vector2 impulse)
{
hits.Clear();
MaxTargetsToHit = 2;
if (item.AiTarget != null)
{
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
@@ -220,6 +251,8 @@ namespace Barotrauma.Items.Components
item.Drop(null);
launchPos = item.SimPosition;
item.body.Enabled = true;
item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
@@ -233,7 +266,7 @@ namespace Barotrauma.Items.Components
if (stickJoint == null) { return; }
stickTarget = null;
StickTarget = null;
GameMain.World.Remove(stickJoint);
stickJoint = null;
}
@@ -287,7 +320,7 @@ namespace Barotrauma.Items.Components
foreach (HitscanResult h in hits)
{
item.body.SetTransform(h.Point, rotation);
if (HandleProjectileCollision(h.Fixture, h.Normal))
if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero))
{
hitSomething = true;
break;
@@ -321,7 +354,7 @@ namespace Barotrauma.Items.Components
{
//ignore sensors and items
if (fixture?.Body == null || fixture.IsSensor) { return true; }
if (fixture.Body.UserData is Item) { return true; }
if (fixture.Body.UserData is Item item && item.GetComponent<Door>() == null && !item.Prefab.DamagedByProjectiles) { return true; }
if (fixture.Body?.UserData as string == "ruinroom") { return true; }
//ignore everything else than characters, sub walls and level walls
@@ -341,7 +374,7 @@ namespace Barotrauma.Items.Components
//ignore sensors and items
if (fixture?.Body == null || fixture.IsSensor) { return -1; }
if (fixture.Body.UserData is Item item && item.GetComponent<Door>() == null) { return -1; }
if (fixture.Body.UserData is Item item && item.GetComponent<Door>() == null && !item.Prefab.DamagedByProjectiles) { return -1; }
if (fixture.Body?.UserData as string == "ruinroom") { return -1; }
//ignore everything else than characters, sub walls and level walls
@@ -364,7 +397,7 @@ namespace Barotrauma.Items.Components
while (impactQueue.Count > 0)
{
var impact = impactQueue.Dequeue();
HandleProjectileCollision(impact.Fixture, impact.Normal);
HandleProjectileCollision(impact.Fixture, impact.Normal, impact.LinearVelocity);
}
if (item.body != null && item.body.FarseerBody.IsBullet)
@@ -377,7 +410,7 @@ namespace Barotrauma.Items.Components
}
}
if (stickJoint == null) { return; }
if (stickJoint == null || StickPermanently) { return; }
if (persistentStickJointTimer > 0.0f)
{
@@ -385,19 +418,20 @@ namespace Barotrauma.Items.Components
return;
}
if (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f)
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
stickTarget = null;
if (stickJoint != null)
if (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f ||
stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f)
{
if (GameMain.World.JointList.Contains(stickJoint))
{
GameMain.World.Remove(stickJoint);
}
stickJoint = null;
Unstick();
}
if (!item.body.FarseerBody.IsBullet) { IsActive = false; }
}
#if SERVER
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
item.CreateServerEvent(this);
}
#endif
}
}
@@ -405,6 +439,12 @@ namespace Barotrauma.Items.Components
{
if (User != null && User.Removed) { User = null; return false; }
if (IgnoredBodies.Contains(target.Body)) { return false; }
//ignore character colliders (the projectile only hits limbs)
if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
{
return false;
}
if (hits.Contains(target.Body)) { return false; }
if (target.Body.UserData is Submarine sub)
{
Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ?
@@ -415,9 +455,12 @@ namespace Barotrauma.Items.Components
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir,
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir,
collisionCategory: Physics.CollisionWall);
if (wallBody?.FixtureList?.First() != null && wallBody.UserData is Structure structure)
if (wallBody?.FixtureList?.First() != null && wallBody.UserData is Structure structure &&
//ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction
Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0)
{
target = wallBody.FixtureList.First();
if (hits.Contains(target.Body)) { return false; }
}
else
{
@@ -440,18 +483,23 @@ namespace Barotrauma.Items.Components
return false;
}
impactQueue.Enqueue(new Impact(target, contact.Manifold.LocalNormal));
item.body.FarseerBody.OnCollision -= OnProjectileCollision;
return true;
hits.Add(target.Body);
impactQueue.Enqueue(new Impact(target, contact.Manifold.LocalNormal, item.body.LinearVelocity));
if (hits.Count() >= MaxTargetsToHit)
{
item.body.FarseerBody.OnCollision -= OnProjectileCollision;
return true;
}
else
{
return false;
}
}
private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal)
private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity)
{
if (User != null && User.Removed) { User = null; }
if (IgnoredBodies.Contains(target.Body)) { return false; }
//ignore character colliders (the projectile only hits limbs)
if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
{
@@ -472,7 +520,6 @@ namespace Barotrauma.Items.Components
//severed limbs don't deactivate the projectile (but may still slow it down enough to make it inactive)
if (limb.IsSevered)
{
target.Body.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass);
return true;
}
@@ -552,21 +599,30 @@ namespace Barotrauma.Items.Components
}
}
item.body.FarseerBody.OnCollision -= OnProjectileCollision;
target.Body.ApplyLinearImpulse(velocity * item.body.Mass);
item.body.CollisionCategories = Physics.CollisionItem;
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
IgnoredBodies.Clear();
target.Body.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass);
if (hits.Count() >= MaxTargetsToHit)
{
item.body.FarseerBody.OnCollision -= OnProjectileCollision;
if (item.Prefab.DamagedByProjectiles || item.Prefab.DamagedByMeleeWeapons)
{
item.body.CollisionCategories = Physics.CollisionCharacter;
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile;
}
else
{
item.body.CollisionCategories = Physics.CollisionItem;
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
}
IgnoredBodies.Clear();
}
if (attackResult.AppliedDamageModifiers != null &&
attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles))
{
item.body.LinearVelocity *= 0.1f;
}
else if (Vector2.Dot(item.body.LinearVelocity, collisionNormal) < 0.0f &&
else if (Vector2.Dot(velocity, collisionNormal) < 0.0f && hits.Count() >= MaxTargetsToHit &&
(DoesStick ||
(StickToCharacters && target.Body.UserData is Limb) ||
(StickToStructures && target.Body.UserData is Structure) ||
@@ -575,8 +631,24 @@ namespace Barotrauma.Items.Components
Vector2 dir = new Vector2(
(float)Math.Cos(item.body.Rotation),
(float)Math.Sin(item.body.Rotation));
StickToTarget(target.Body, dir);
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (target.Body.UserData is Structure structure && structure.Submarine != item.Submarine && structure.Submarine != null)
{
StickToTarget(structure.Submarine.PhysicsBody.FarseerBody, dir);
}
else
{
StickToTarget(target.Body, dir);
}
}
#if SERVER
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
item.CreateServerEvent(this);
}
#endif
item.body.LinearVelocity *= 0.5f;
return Hitscan;
@@ -608,7 +680,7 @@ namespace Barotrauma.Items.Components
private void StickToTarget(Body targetBody, Vector2 axis)
{
if (stickJoint != null) return;
if (stickJoint != null) { return; }
stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, true)
{
@@ -616,19 +688,38 @@ namespace Barotrauma.Items.Components
MaxMotorForce = 30.0f,
LimitEnabled = true
};
if (item.Sprite != null)
if (StickPermanently)
{
stickJoint.LowerLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * -0.3f);
stickJoint.UpperLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * 0.3f);
stickJoint.LowerLimit = stickJoint.UpperLimit = 0.0f;
}
else if (item.Sprite != null)
{
stickJoint.LowerLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * -0.3f * item.Scale);
stickJoint.UpperLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * 0.3f * item.Scale);
}
persistentStickJointTimer = PersistentStickJointDuration;
stickTarget = targetBody;
StickTarget = targetBody;
GameMain.World.Add(stickJoint);
IsActive = true;
}
private void Unstick()
{
StickTarget = null;
if (stickJoint != null)
{
if (GameMain.World.JointList.Contains(stickJoint))
{
GameMain.World.Remove(stickJoint);
}
stickJoint = null;
}
if (!item.body.FarseerBody.IsBullet) { IsActive = false; }
}
protected override void RemoveComponentSpecific()
{
if (stickJoint != null)
@@ -1,4 +1,5 @@
using Barotrauma.Networking;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Globalization;
@@ -111,7 +112,7 @@ namespace Barotrauma.Items.Components
element.GetAttributeString("name", "");
//backwards compatibility
var showRepairUIAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().ToLowerInvariant() == "showrepairuithreshold");
var showRepairUIAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("showrepairuithreshold", StringComparison.OrdinalIgnoreCase));
if (showRepairUIAttribute != null)
{
float repairThreshold;
@@ -130,7 +131,26 @@ namespace Barotrauma.Items.Components
}
partial void InitProjSpecific(XElement element);
/// <summary>
/// Check if the character manages to succesfully repair the item
/// </summary>
public bool CheckCharacterSuccess(Character character)
{
if (character == null) { return false; }
if (statusEffectLists == null || statusEffectLists.None(s => s.Key == ActionType.OnFailure)) { return true; }
// unpowered (electrical) items can be repaired without a risk of electrical shock
if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase)) &&
item.GetComponent<Powered>() is Powered powered && powered.Voltage < 0.1f) { return true; }
if (Rand.Range(0.0f, 0.5f) < DegreeOfSuccess(character)) { return true; }
ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
return false;
}
public bool StartRepairing(Character character, FixActions action)
{
if (character == null || character.IsDead || action == FixActions.None)
@@ -143,8 +163,15 @@ namespace Barotrauma.Items.Components
#if SERVER
if (CurrentFixer != character || currentFixerAction != action)
{
if (!CheckCharacterSuccess(character))
{
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, character.ID });
return false;
}
item.CreateServerEvent(this);
}
#else
if (GameMain.Client == null && (CurrentFixer != character || currentFixerAction != action) && !CheckCharacterSuccess(character)) { return false; }
#endif
CurrentFixer = character;
CurrentFixerAction = action;
@@ -0,0 +1,252 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Rope : ItemComponent, IServerSerializable
{
private Item source, target;
private float snapTimer;
private const float SnapAnimDuration = 1.0f;
private float raycastTimer;
private const float RayCastInterval = 0.2f;
[Serialize(0.0f, false, description: "How much force is applied to pull the projectile the rope is attached to.")]
public float ProjectilePullForce
{
get;
set;
}
[Serialize(0.0f, false, description: "How much force is applied to pull the target the rope is attached to.")]
public float TargetPullForce
{
get;
set;
}
[Serialize(0.0f, false, description: "How much force is applied to pull the source the rope is attached to.")]
public float SourcePullForce
{
get;
set;
}
[Serialize(1000.0f, false, description: "How far the source item can be from the projectile until the rope breaks.")]
public float MaxLength
{
get;
set;
}
[Serialize(true, false, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")]
public bool SnapOnCollision
{
get;
set;
}
private bool snapped;
public bool Snapped
{
get { return snapped; }
set
{
if (snapped == value) { return; }
if (GameMain.NetworkMember != null)
{
if (GameMain.NetworkMember.IsClient)
{
return;
}
else
{
#if SERVER
item.CreateServerEvent(this);
#endif
}
}
snapped = value;
}
}
public Rope(Item item, XElement element) : base(item, element)
{
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public void Attach(Item source, Item target)
{
System.Diagnostics.Debug.Assert(source != null);
System.Diagnostics.Debug.Assert(target != null);
this.source = source;
this.target = target;
ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition);
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
if (source == null || source.Removed || target == null || target.Removed)
{
IsActive = false;
return;
}
if (Snapped)
{
snapTimer += deltaTime;
if (snapTimer >= SnapAnimDuration)
{
IsActive = false;
}
return;
}
Vector2 diff = target.WorldPosition - source.WorldPosition;
if (diff.LengthSquared() > MaxLength * MaxLength)
{
Snapped = true;
return;
}
if (SnapOnCollision)
{
raycastTimer += deltaTime;
if (raycastTimer > RayCastInterval)
{
if (Submarine.PickBody(ConvertUnits.ToSimUnits(source.WorldPosition), ConvertUnits.ToSimUnits(target.WorldPosition),
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
customPredicate: (Fixture f) =>
{
var projectile = target?.GetComponent<Projectile>();
if (projectile != null)
{
foreach (Body body in projectile.Hits)
{
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; }
}
}
}
Submarine targetSub = target?.GetComponent<Projectile>()?.StickTarget?.UserData as Submarine ?? target.Submarine;
if (f.Body?.UserData is MapEntity mapEntity && mapEntity.Submarine != null)
{
if (mapEntity.Submarine == targetSub || mapEntity.Submarine == source.Submarine)
{
return false;
}
}
else if (f.Body?.UserData is Submarine sub)
{
if (sub == targetSub || sub == source.Submarine)
{
return false;
}
}
return true;
}) != null)
{
Snapped = true;
return;
}
raycastTimer = 0.0f;
}
}
Vector2 forceDir = diff;
if (forceDir.LengthSquared() > 0.01f)
{
forceDir = Vector2.Normalize(forceDir);
}
if (Math.Abs(ProjectilePullForce) > 0.001f)
{
var projectile = target.GetComponent<Projectile>();
projectile?.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce);
}
if (Math.Abs(SourcePullForce) > 0.001f)
{
var sourceBody = GetBodyToPull(source);
if (sourceBody != null)
{
sourceBody.ApplyForce(forceDir * SourcePullForce);
}
}
if (Math.Abs(TargetPullForce) > 0.001f)
{
var targetBody = GetBodyToPull(target);
if (targetBody != null)
{
targetBody.ApplyForce(-forceDir * TargetPullForce);
}
}
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
base.UpdateBroken(deltaTime, cam);
if (Snapped)
{
snapTimer += deltaTime;
if (snapTimer >= SnapAnimDuration)
{
IsActive = false;
}
}
}
private PhysicsBody GetBodyToPull(Item target)
{
if (target.ParentInventory is CharacterInventory characterInventory &&
characterInventory.Owner is Character ownerCharacter)
{
if (ownerCharacter.Removed) { return null; }
return ownerCharacter.AnimController.Collider;
}
var projectile = target.GetComponent<Projectile>();
if (projectile != null)
{
if (projectile.StickTarget?.UserData is Structure structure)
{
return structure.Submarine?.PhysicsBody;
}
else if (projectile.StickTarget?.UserData is Submarine sub)
{
return sub?.PhysicsBody;
}
else if (projectile.StickTarget?.UserData is Character character)
{
return character.AnimController.Collider;
}
return null;
}
if (target.body != null) { return target.body; }
return null;
}
}
}
@@ -88,7 +88,7 @@ namespace Barotrauma.Items.Components
{
foreach (XElement subElement in item.Prefab.ConfigElement.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "connectionpanel") { continue; }
if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; }
foreach (XElement connectionElement in subElement.Elements())
{
@@ -79,6 +79,12 @@ namespace Barotrauma.Items.Components
Wire wire = wireItem.GetComponent<Wire>();
if (wire != null)
{
if (Item.ItemList.Any(it => it != item && (it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false)))
{
if (wire.Item.body != null) { wire.Item.body.Enabled = false; }
wire.IsActive = false;
wire.UpdateSections();
}
DisconnectedWires.Add(wire);
base.IsActive = true;
}
@@ -197,7 +203,7 @@ namespace Barotrauma.Items.Components
float degreeOfSuccess = DegreeOfSuccess(character);
if (Rand.Range(0.0f, 0.5f) < degreeOfSuccess) { return true; }
item.ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
return false;
}
@@ -12,6 +12,7 @@ namespace Barotrauma.Items.Components
public bool ContinuousSignal;
public bool State;
public string ConnectionName;
public string PropertyName;
public Connection Connection;
[Serialize("", false, translationTextTag: "Label.", description: "The text displayed on this button/tickbox."), Editable]
public string Label { get; set; }
@@ -28,11 +29,12 @@ namespace Barotrauma.Items.Components
{
Label = element.GetAttributeString("text", "");
ConnectionName = element.GetAttributeString("connection", "");
PropertyName = element.GetAttributeString("propertyname", "").ToLowerInvariant();
Signal = element.GetAttributeString("signal", "1");
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() == "statuseffect")
if (subElement.Name.ToString().Equals("statuseffect", System.StringComparison.OrdinalIgnoreCase))
{
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName: "custom interface element (label " + Label + ")"));
}
@@ -89,6 +91,7 @@ namespace Barotrauma.Items.Components
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "button":
case "textbox":
var button = new CustomInterfaceElement(subElement)
{
ContinuousSignal = false
@@ -106,7 +109,7 @@ namespace Barotrauma.Items.Components
};
if (string.IsNullOrEmpty(tickBox.Label))
{
tickBox.Label = "Signal out " + customInterfaceElementList.Count(e => !e.ContinuousSignal);
tickBox.Label = "Signal out " + customInterfaceElementList.Count(e => e.ContinuousSignal);
}
customInterfaceElementList.Add(tickBox);
break;
@@ -168,6 +171,18 @@ namespace Barotrauma.Items.Components
tickBoxElement.State = state;
}
private void TextChanged(CustomInterfaceElement textElement, string text)
{
textElement.Signal = text;
foreach (ISerializableEntity e in item.AllPropertyObjects)
{
if (e.SerializableProperties.ContainsKey(textElement.PropertyName))
{
e.SerializableProperties[textElement.PropertyName].TrySetValue(e, text);
}
}
}
public override void Update(float deltaTime, Camera cam)
{
UpdateProjSpecific();
@@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components
private float blinkTimer;
private bool itemLoaded;
private double lastToggleSignalTime;
public PhysicsBody ParentBody;
@@ -78,9 +78,7 @@ namespace Barotrauma.Items.Components
if (IsActive == value) { return; }
IsActive = value;
#if SERVER
if (GameMain.Server != null && itemLoaded) { item.CreateServerEvent(this); }
#endif
OnStateChanged();
}
}
@@ -117,6 +115,13 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(false, false, description: "If enabled, the component will ignore continuous signals received in the toggle input (i.e. a continuous signal will only toggle it once).")]
public bool IgnoreContinuousToggle
{
get;
set;
}
public override void Move(Vector2 amount)
{
#if CLIENT
@@ -158,14 +163,7 @@ namespace Barotrauma.Items.Components
IsActive = IsOn;
item.AddTag("light");
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
itemLoaded = true;
SetLightSourceState(IsActive, lightBrightness);
}
public override void Update(float deltaTime, Camera cam)
{
if (item.AiTarget != null)
@@ -249,15 +247,21 @@ namespace Barotrauma.Items.Components
return true;
}
partial void OnStateChanged();
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
switch (connection.Name)
{
case "toggle":
IsActive = !IsActive;
if (IgnoreContinuousToggle && lastToggleSignalTime < Timing.TotalTime - 0.1)
{
IsOn = !IsOn;
}
lastToggleSignalTime = Timing.TotalTime;
break;
case "set_state":
IsActive = (signal != "0");
IsOn = signal != "0";
break;
case "set_color":
LightColor = XMLExtensions.ParseColor(signal, false);
@@ -265,20 +269,14 @@ namespace Barotrauma.Items.Components
}
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(IsOn);
}
private void UpdateAITarget(AITarget target)
{
target.Enabled = IsActive;
if (!IsActive) { return; }
if (target.MaxSightRange <= 0)
{
target.MaxSightRange = Range * 5;
}
target.SightRange = target.MaxSightRange * lightBrightness;
target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness);
}
partial void SetLightSourceState(bool enabled, float brightness);
@@ -39,6 +39,14 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(false, false, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.")]
public bool AllowCrossTeamCommunication
{
get;
set;
}
[Editable, Serialize(false, false, description: "If enabled, any signals received from another chat-linked wifi component are displayed " +
"as chat messages in the chatbox of the player holding the item.")]
public bool LinkToChat
@@ -84,7 +92,7 @@ namespace Barotrauma.Items.Components
{
if (sender == null || sender.channel != channel) { return false; }
if (sender.TeamID != TeamID)
if (sender.TeamID != TeamID && !AllowCrossTeamCommunication)
{
return false;
}
@@ -34,6 +34,8 @@ namespace Barotrauma.Items.Components
private int failedLaunchAttempts;
private readonly List<Item> activeProjectiles = new List<Item>();
private Character user;
[Serialize("0,0", false, description: "The position of the barrel relative to the upper left corner of the base sprite (in pixels).")]
@@ -72,6 +74,20 @@ namespace Barotrauma.Items.Components
set { reloadTime = value; }
}
[Serialize(1, false, description: "How projectiles the weapon launches when fired once.")]
public int ProjectileCount
{
get;
set;
}
[Serialize(false, false, description: "Can the turret be fired without projectiles (causing it just to execute the OnUse effects and the firing animation without actually firing anything).")]
public bool LaunchWithoutProjectile
{
get;
set;
}
[Editable, Serialize("0.0,0.0", true, description: "The range at which the barrel can rotate. TODO")]
public Vector2 RotationLimits
{
@@ -95,6 +111,13 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles (in degrees).")]
public float Spread
{
get;
set;
}
[Editable(0.0f, 1000.0f, DecimalCount = 2),
Serialize(5.0f, false, description: "How much torque is applied to rotate the barrel when the item is used by a character"
+ " with insufficient skills to operate it. Higher values make the barrel rotate faster.")]
@@ -155,7 +178,21 @@ namespace Barotrauma.Items.Components
UpdateTransformedBarrelPos();
}
}
[Serialize(3000.0f, true, description: "How close to a target the turret has to be for an AI character to fire it.")]
public float AIRange
{
get;
set;
}
[Serialize(-1, true, description: "The turret won't fire additional projectiles if the number of previously fired, still active projectiles reaches this limit. If set to -1, there is no limit to the number of projectiles.")]
public int MaxActiveProjectiles
{
get;
set;
}
public Turret(Item item, XElement element)
: base(item, element)
{
@@ -213,13 +250,13 @@ namespace Barotrauma.Items.Components
{
this.cam = cam;
if (reload > 0.0f) reload -= deltaTime;
if (reload > 0.0f) { reload -= deltaTime; }
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
UpdateProjSpecific(deltaTime);
if (minRotation == maxRotation) return;
if (minRotation == maxRotation) { return; }
float targetMidDiff = MathHelper.WrapAngle(targetRotation - (minRotation + maxRotation) / 2.0f);
@@ -230,12 +267,19 @@ namespace Barotrauma.Items.Components
targetRotation = (targetMidDiff < 0.0f) ? minRotation : maxRotation;
}
float degreeOfSuccess = user == null ? 0.5f : DegreeOfSuccess(user);
if (degreeOfSuccess < 0.5f) degreeOfSuccess *= degreeOfSuccess; //the ease of aiming drops quickly with insufficient skill levels
float degreeOfSuccess = user == null ? 0.5f : DegreeOfSuccess(user);
if (degreeOfSuccess < 0.5f) { degreeOfSuccess *= degreeOfSuccess; } //the ease of aiming drops quickly with insufficient skill levels
float springStiffness = MathHelper.Lerp(SpringStiffnessLowSkill, SpringStiffnessHighSkill, degreeOfSuccess);
float springDamping = MathHelper.Lerp(SpringDampingLowSkill, SpringDampingHighSkill, degreeOfSuccess);
float rotationSpeed = MathHelper.Lerp(RotationSpeedLowSkill, RotationSpeedHighSkill, degreeOfSuccess);
if (user?.Info != null)
{
user.Info.IncreaseSkillLevel("weapons",
SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f),
user.WorldPosition + Vector2.UnitY * 150.0f);
}
angularVelocity +=
(MathHelper.WrapAngle(targetRotation - rotation) * springStiffness - angularVelocity * springDamping) * deltaTime;
angularVelocity = MathHelper.Clamp(angularVelocity, -rotationSpeed, rotationSpeed);
@@ -265,95 +309,110 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character character = null)
{
if (!characterUsable && character != null) return false;
if (!characterUsable && character != null) { return false; }
return TryLaunch(deltaTime, character);
}
private bool TryLaunch(float deltaTime, Character character = null)
private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false)
{
#if CLIENT
if (GameMain.Client != null) return false;
#endif
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
if (reload > 0.0f) return false;
if (reload > 0.0f) { return false; }
if (GetAvailableBatteryPower() < powerConsumption)
if (MaxActiveProjectiles >= 0)
{
#if CLIENT
if (!flashLowPower && character != null && character == Character.Controlled)
activeProjectiles.RemoveAll(it => it.Removed);
if (activeProjectiles.Count >= MaxActiveProjectiles)
{
flashLowPower = true;
GUI.PlayUISound(GUISoundType.PickItemFail);
return false;
}
#endif
return false;
}
foreach (MapEntity e in item.linkedTo)
if (!ignorePower)
{
//use linked projectile containers in case they have to react to the turret being launched somehow
//(play a sound, spawn more projectiles)
if (!(e is Item linkedItem)) continue;
ItemContainer projectileContainer = linkedItem.GetComponent<ItemContainer>();
if (projectileContainer != null)
if (GetAvailableBatteryPower() < powerConsumption)
{
linkedItem.Use(deltaTime, null);
var repairable = linkedItem.GetComponent<Repairable>();
if (repairable != null)
{
repairable.LastActiveTime = (float)Timing.TotalTime + 1.0f;
}
}
}
var projectiles = GetLoadedProjectiles(true);
if (projectiles.Count == 0)
{
//coilguns spawns ammo in the ammo boxes with the OnUse statuseffect when the turret is launched,
//causing a one frame delay before the gun can be launched (or more in multiplayer where there may be a longer delay)
// -> attempt to launch the gun multiple times before showing the "no ammo" flash
failedLaunchAttempts++;
#if CLIENT
if (!flashNoAmmo && character != null && character == Character.Controlled && failedLaunchAttempts > 20)
{
flashNoAmmo = true;
failedLaunchAttempts = 0;
GUI.PlayUISound(GUISoundType.PickItemFail);
}
#endif
return false;
}
failedLaunchAttempts = 0;
var batteries = item.GetConnectedComponents<PowerContainer>();
float neededPower = powerConsumption;
while (neededPower > 0.0001f && batteries.Count > 0)
{
batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f);
float takePower = neededPower / batteries.Count;
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
foreach (PowerContainer battery in batteries)
{
neededPower -= takePower;
battery.Charge -= takePower / 3600.0f;
#if SERVER
if (GameMain.Server != null)
if (!flashLowPower && character != null && character == Character.Controlled)
{
battery.Item.CreateServerEvent(battery);
flashLowPower = true;
GUI.PlayUISound(GUISoundType.PickItemFail);
}
#endif
return false;
}
}
Launch(projectiles[0].Item, character);
Projectile launchedProjectile = null;
for (int i = 0; i < ProjectileCount; i++)
{
foreach (MapEntity e in item.linkedTo)
{
//use linked projectile containers in case they have to react to the turret being launched somehow
//(play a sound, spawn more projectiles)
if (!(e is Item linkedItem)) { continue; }
ItemContainer projectileContainer = linkedItem.GetComponent<ItemContainer>();
if (projectileContainer != null)
{
linkedItem.Use(deltaTime, null);
var repairable = linkedItem.GetComponent<Repairable>();
if (repairable != null)
{
repairable.LastActiveTime = (float)Timing.TotalTime + 1.0f;
}
}
}
var projectiles = GetLoadedProjectiles(true);
if (projectiles.Count == 0 && !LaunchWithoutProjectile)
{
//coilguns spawns ammo in the ammo boxes with the OnUse statuseffect when the turret is launched,
//causing a one frame delay before the gun can be launched (or more in multiplayer where there may be a longer delay)
// -> attempt to launch the gun multiple times before showing the "no ammo" flash
failedLaunchAttempts++;
#if CLIENT
if (!flashNoAmmo && character != null && character == Character.Controlled && failedLaunchAttempts > 20)
{
flashNoAmmo = true;
failedLaunchAttempts = 0;
GUI.PlayUISound(GUISoundType.PickItemFail);
}
#endif
return false;
}
failedLaunchAttempts = 0;
launchedProjectile = projectiles.FirstOrDefault();
if (!ignorePower)
{
var batteries = item.GetConnectedComponents<PowerContainer>();
float neededPower = powerConsumption;
while (neededPower > 0.0001f && batteries.Count > 0)
{
batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f);
float takePower = neededPower / batteries.Count;
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
foreach (PowerContainer battery in batteries)
{
neededPower -= takePower;
battery.Charge -= takePower / 3600.0f;
#if SERVER
battery.Item.CreateServerEvent(battery);
#endif
}
}
}
if (launchedProjectile != null || LaunchWithoutProjectile)
{
Launch(launchedProjectile?.Item, character);
}
}
#if SERVER
if (character != null)
if (character != null && launchedProjectile != null)
{
string msg = character.LogName + " launched " + item.Name + " (projectile: " + projectiles[0].Item.Name;
var containedItems = projectiles[0].Item.ContainedItems;
string msg = character.LogName + " launched " + item.Name + " (projectile: " + launchedProjectile.Item.Name;
var containedItems = launchedProjectile.Item.ContainedItems;
if (containedItems == null || !containedItems.Any())
{
msg += ")";
@@ -373,27 +432,36 @@ namespace Barotrauma.Items.Components
{
reload = reloadTime;
projectile.Drop(null);
projectile.body.Dir = 1.0f;
projectile.body.ResetDynamics();
projectile.body.Enabled = true;
projectile.SetTransform(ConvertUnits.ToSimUnits(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y)), -rotation);
projectile.UpdateTransform();
projectile.Submarine = projectile.body.Submarine;
Projectile projectileComponent = projectile.GetComponent<Projectile>();
if (projectileComponent != null)
if (projectile != null)
{
projectileComponent.Use((float)Timing.Step);
projectileComponent.User = user;
}
activeProjectiles.Add(projectile);
projectile.Drop(null);
if (projectile.body != null)
{
projectile.body.Dir = 1.0f;
projectile.body.ResetDynamics();
projectile.body.Enabled = true;
}
if (projectile.Container != null) projectile.Container.RemoveContained(projectile);
float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f);
projectile.SetTransform(ConvertUnits.ToSimUnits(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y)), -rotation + spread);
projectile.UpdateTransform();
projectile.Submarine = projectile.body?.Submarine;
Projectile projectileComponent = projectile.GetComponent<Projectile>();
if (projectileComponent != null)
{
projectileComponent.Use((float)Timing.Step);
projectile.GetComponent<Rope>()?.Attach(item, projectile);
projectileComponent.User = user;
}
if (projectile.Container != null) { projectile.Container.RemoveContained(projectile); }
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), projectile });
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), projectile });
}
}
ApplyStatusEffects(ActionType.OnUse, 1.0f, user: user);
@@ -402,6 +470,182 @@ namespace Barotrauma.Items.Components
partial void LaunchProjSpecific();
private float waitTimer;
private float disorderTimer;
private float prevTargetRotation;
private float updateTimer;
private bool updatePending;
public void ThalamusOperate(float deltaTime, bool targetHumans, bool targetOtherCreatures, bool targetSubmarines, bool ignoreDelay)
{
IsActive = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
return;
}
if (updatePending)
{
if (updateTimer < 0.0f)
{
#if SERVER
item.CreateServerEvent(this);
#endif
prevTargetRotation = targetRotation;
updateTimer = 0.25f;
}
updateTimer -= deltaTime;
}
if (!ignoreDelay && waitTimer > 0)
{
waitTimer -= deltaTime;
return;
}
Submarine closestSub = null;
float maxDistance = 10000.0f;
float shootDistance = AIRange;
ISpatialEntity target = null;
float closestDist = shootDistance * shootDistance;
if (targetHumans || targetOtherCreatures)
{
foreach (var character in Character.CharacterList)
{
if (character == null || character.Removed || character.IsDead) { continue; }
if (character.Params.Group.Equals("thalamus", StringComparison.OrdinalIgnoreCase)) { continue; }
bool isHuman = character.IsHuman || character.Params.Group.Equals("human", StringComparison.OrdinalIgnoreCase);
if (isHuman)
{
if (!targetHumans)
{
// Don't target humans if not defined to.
continue;
}
}
else if (!targetOtherCreatures)
{
// Don't target other creatures if not defined to.
continue;
}
float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition);
if (dist > closestDist) { continue; }
target = character;
closestDist = dist;
}
}
if (targetSubmarines)
{
if (target == null || target.Submarine != null)
{
closestDist = maxDistance * maxDistance;
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
float dist = Vector2.DistanceSquared(sub.WorldPosition, item.WorldPosition);
if (dist > closestDist) { continue; }
closestSub = sub;
closestDist = dist;
}
closestDist = shootDistance * shootDistance;
if (closestSub != null)
{
foreach (var hull in Hull.hullList)
{
if (!closestSub.IsEntityFoundOnThisSub(hull, true)) { continue; }
float dist = Vector2.DistanceSquared(hull.WorldPosition, item.WorldPosition);
if (dist > closestDist) { continue; }
target = hull;
closestDist = dist;
}
}
}
}
if (!ignoreDelay)
{
if (target == null)
{
// Random movement
waitTimer = Rand.Value(Rand.RandSync.Unsynced) < 0.98f ? 0f : Rand.Range(5f, 20f);
targetRotation = Rand.Range(minRotation, maxRotation);
updatePending = true;
return;
}
if (disorderTimer < 0)
{
// Random disorder
disorderTimer = Rand.Range(0f, 3f);
waitTimer = Rand.Range(0.25f, 1f);
targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-1f, 1f));
updatePending = true;
return;
}
else
{
disorderTimer -= deltaTime;
}
}
if (target == null) { return; }
float angle = -MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition);
targetRotation = MathUtils.WrapAngleTwoPi(angle);
if (Math.Abs(targetRotation - prevTargetRotation) > 0.1f) { updatePending = true; }
if (target is Hull targetHull)
{
Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
if (!MathUtils.GetLineRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _))
{
return;
}
}
else
{
float midRotation = (minRotation + maxRotation) / 2.0f;
while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; }
while (midRotation - angle > MathHelper.Pi) { angle += MathHelper.TwoPi; }
if (angle < minRotation || angle > maxRotation) { return; }
float enemyAngle = MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition);
float turretAngle = -rotation;
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; }
}
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition);
if (target.Submarine != null)
{
start -= target.Submarine.SimPosition;
end -= target.Submarine.SimPosition;
}
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(start, end, null, collisionCategories);
if (pickedBody == null) { return; }
Character targetCharacter = null;
if (pickedBody.UserData is Character c)
{
targetCharacter = c;
}
else if (pickedBody.UserData is Limb limb)
{
targetCharacter = limb.character;
}
if (targetCharacter != null)
{
if (targetCharacter.Params.Group.Equals("thalamus", StringComparison.OrdinalIgnoreCase))
{
// Don't shoot friendly characters
return;
}
}
else if (!(pickedBody.UserData is Structure) && !(pickedBody.UserData is Item))
{
// Hit something else than a wall or an item (probably a level wall)
return;
}
TryLaunch(deltaTime, ignorePower: true);
}
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget &&
@@ -483,7 +727,7 @@ namespace Barotrauma.Items.Components
//enough shells and power
Character closestEnemy = null;
float closestDist = 3000 * 3000;
float closestDist = AIRange * AIRange;
foreach (Character enemy in Character.CharacterList)
{
// Ignore dead, friendly, and those that are inside the same sub
@@ -547,7 +791,7 @@ namespace Barotrauma.Items.Components
return false;
}
if (objective.Option.ToLowerInvariant() == "fireatwill")
if (objective.Option.Equals("fireatwill", StringComparison.OrdinalIgnoreCase))
{
character?.Speak(TextManager.GetWithVariable("DialogFireTurret", "[itemname]", item.Name, true), null, 0.0f, "fireturret", 5.0f);
character.SetInput(InputType.Shoot, true, true);
@@ -556,19 +800,6 @@ namespace Barotrauma.Items.Components
return false;
}
private void GetAvailablePower(out float availableCharge, out float availableCapacity)
{
var batteries = item.GetConnectedComponents<PowerContainer>();
availableCharge = 0.0f;
availableCapacity = 0.0f;
foreach (PowerContainer battery in batteries)
{
availableCharge += battery.Charge;
availableCapacity += battery.Capacity;
}
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
@@ -591,7 +822,7 @@ namespace Barotrauma.Items.Components
foreach (MapEntity e in item.linkedTo)
{
if (e is Item projectileContainer) { CheckProjectileContainer(projectileContainer, projectiles, returnFirst); }
if (returnFirst && projectiles.Any()) return projectiles;
if (returnFirst && projectiles.Any()) { return projectiles; }
}
return projectiles;
@@ -600,27 +831,27 @@ namespace Barotrauma.Items.Components
private void CheckProjectileContainer(Item projectileContainer, List<Projectile> projectiles, bool returnFirst)
{
var containedItems = projectileContainer.ContainedItems;
if (containedItems == null) return;
if (containedItems == null) { return; }
foreach (Item containedItem in containedItems)
{
var projectileComponent = containedItem.GetComponent<Projectile>();
if (projectileComponent != null)
if (projectileComponent != null && projectileComponent.Item.body != null)
{
projectiles.Add(projectileComponent);
if (returnFirst) return;
if (returnFirst) { return; }
}
else
{
//check if the contained item is another itemcontainer with projectiles inside it
if (containedItem.ContainedItems == null) continue;
if (containedItem.ContainedItems == null) { continue; }
foreach (Item subContainedItem in containedItem.ContainedItems)
{
projectileComponent = subContainedItem.GetComponent<Projectile>();
if (projectileComponent != null)
if (projectileComponent != null && projectileComponent.Item.body != null)
{
projectiles.Add(projectileComponent);
if (returnFirst) return;
if (returnFirst) { return; }
}
}
}
@@ -694,7 +925,6 @@ namespace Barotrauma.Items.Components
TryLaunch((float)Timing.Step, sender);
}
break;
case "toggle":
case "toggle_light":
if (lightComponent != null)
{
@@ -706,8 +936,9 @@ namespace Barotrauma.Items.Components
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
Item item = (Item)extraData[2];
msg.Write(item.Removed ? (ushort)0 : item.ID);
Item item = extraData.Length > 2 ? (Item)extraData[2] : null;
msg.Write(item == null || item.Removed ? (ushort)0 : item.ID);
msg.WriteRangedSingle(MathHelper.Clamp(targetRotation, minRotation, maxRotation), minRotation, maxRotation, 8);
}
}
}
@@ -211,6 +211,7 @@ namespace Barotrauma.Items.Components
}
public bool AutoEquipWhenFull { get; private set; }
public bool DisplayContainedStatus { get; private set; }
public readonly int Variants;
@@ -264,6 +265,7 @@ namespace Barotrauma.Items.Components
limbType = new LimbType[spriteCount];
limb = new Limb[spriteCount];
AutoEquipWhenFull = element.GetAttributeBool("autoequipwhenfull", true);
DisplayContainedStatus = element.GetAttributeBool("displaycontainedstatus", false);
int i = 0;
foreach (XElement subElement in element.Elements())
{
@@ -284,7 +286,7 @@ namespace Barotrauma.Items.Components
foreach (XElement lightElement in subElement.Elements())
{
if (lightElement.Name.ToString().ToLowerInvariant() != "lightcomponent") continue;
if (!lightElement.Name.ToString().Equals("lightcomponent", StringComparison.OrdinalIgnoreCase)) { continue; }
wearableSprites[i].LightComponent = new LightComponent(item, lightElement)
{
Parent = this
@@ -197,6 +197,7 @@ namespace Barotrauma
if (item.body != null)
{
item.body.Enabled = false;
item.body.BodyType = FarseerPhysics.BodyType.Dynamic;
}
}
@@ -25,7 +25,8 @@ namespace Barotrauma
OnFire, InWater, NotInWater,
OnImpact,
OnEating,
OnDeath = OnBroken
OnDeath = OnBroken,
OnDamaged
}
partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable
@@ -164,13 +165,6 @@ namespace Barotrauma
get { return description ?? prefab.Description; }
set { description = value; }
}
[Editable, Serialize(false, true)]
public bool HiddenInGame
{
get;
set;
}
[Editable, Serialize(false, true)]
public bool NonInteractable
@@ -290,6 +284,24 @@ namespace Barotrauma
set { /*do nothing*/ }
}
[Serialize("", true)]
/// <summary>
/// Can be used to modify the AITarget's label using status effects
/// </summary>
public string SonarLabel
{
get { return AiTarget?.SonarLabel ?? ""; }
set
{
if (AiTarget != null)
{
AiTarget.SonarLabel = value;
}
}
}
[Serialize(false, false)]
/// <summary>
/// Can be used by status effects or conditionals to check if the physics body of the item is active
@@ -1237,6 +1249,8 @@ namespace Barotrauma
float damageAmount = attack.GetItemDamage(deltaTime);
Condition -= damageAmount;
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
return new AttackResult(damageAmount, null);
}
@@ -1296,6 +1310,7 @@ namespace Barotrauma
#if CLIENT
if (ic.HasSounds)
{
ic.PlaySound(ActionType.Always);
ic.UpdateSounds();
if (!ic.WasUsed)
{
@@ -1343,7 +1358,7 @@ namespace Barotrauma
UpdateTransform();
if (CurrentHull == null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth))
{
Spawner.AddToRemoveQueue(this);
Spawner?.AddToRemoveQueue(this);
return;
}
}
@@ -1726,16 +1741,15 @@ namespace Barotrauma
public bool TryInteract(Character picker, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceActionKey = false)
{
bool hasRequiredSkills = true;
bool picked = false, selected = false;
#if CLIENT
bool hasRequiredSkills = true;
Skill requiredSkill = null;
#endif
foreach (ItemComponent ic in components)
{
bool pickHit = false, selectHit = false;
if (picker.IsKeyDown(InputType.Aim))
{
pickHit = false;
@@ -1779,13 +1793,11 @@ namespace Barotrauma
picker.IsKeyHit(InputType.Select);
}
#endif
if (!pickHit && !selectHit) continue;
if (!ic.HasRequiredSkills(picker, out Skill tempRequiredSkill)) hasRequiredSkills = false;
if (!pickHit && !selectHit) { continue; }
bool showUiMsg = false;
#if CLIENT
if (!ic.HasRequiredSkills(picker, out Skill tempRequiredSkill)) { hasRequiredSkills = false; }
showUiMsg = picker == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen;
#endif
if (!ignoreRequiredItems && !ic.HasRequiredItems(picker, showUiMsg)) continue;
@@ -1795,10 +1807,9 @@ namespace Barotrauma
picked = true;
ic.ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
#if CLIENT
if (picker == Character.Controlled) GUI.ForceMouseOn(null);
if (picker == Character.Controlled) { GUI.ForceMouseOn(null); }
if (tempRequiredSkill != null) { requiredSkill = tempRequiredSkill; }
#endif
if (tempRequiredSkill != null) requiredSkill = tempRequiredSkill;
if (ic.CanBeSelected) selected = true;
}
}
@@ -1839,6 +1850,30 @@ namespace Barotrauma
return true;
}
public float GetContainedItemConditionPercentage()
{
var containedItems = ContainedItems;
if (containedItems != null)
{
float condition = 0f;
float maxCondition = 0f;
foreach (Item item in containedItems)
{
condition += item.condition;
maxCondition += item.MaxCondition;
}
if (maxCondition > 0.0f)
{
return condition / maxCondition;
}
}
return -1;
}
public void Use(float deltaTime, Character character = null, Limb targetLimb = null)
{
if (RequireAimToUse && (character == null || !character.IsKeyDown(InputType.Aim)))
@@ -2333,9 +2368,9 @@ namespace Barotrauma
item.SetActiveSprite();
if (submarine?.GameVersion != null)
if (submarine?.Info.GameVersion != null)
{
SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, submarine.GameVersion);
SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, submarine.Info.GameVersion);
}
foreach (ItemComponent component in item.components)

Some files were not shown because too many files have changed in this diff Show More