Unstable v0.1300.0.0 (February 19th 2021)

This commit is contained in:
Joonas Rikkonen
2021-02-25 13:44:23 +02:00
parent b772654326
commit 24cbef485a
441 changed files with 21343 additions and 8562 deletions
@@ -1,18 +1,17 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using Barotrauma.Items.Components;
using System.Linq;
namespace Barotrauma
{
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow }
abstract partial class AIController : ISteerable
{
public bool Enabled;
public readonly Character Character;
private AIState state;
// Update only when the value changes, not when it keeps the same.
protected AITarget _lastAiTarget;
// Updated each time the value is updated (also when the value is the same).
@@ -74,25 +73,6 @@ namespace Barotrauma
get { return true; }
}
public virtual AIObjectiveManager ObjectiveManager
{
get { return null; }
}
public AIState State
{
get { return state; }
set
{
if (state == value) { return; }
PreviousState = state;
OnStateChanged(state, value);
state = value;
}
}
public AIState PreviousState { get; protected set; }
private IEnumerable<Hull> visibleHulls;
private float hullVisibilityTimer;
const float hullVisibilityInterval = 0.5f;
@@ -112,6 +92,9 @@ namespace Barotrauma
}
}
protected bool HasValidPath(bool requireNonDirty = false) =>
steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable && (!requireNonDirty || !pathSteering.IsPathDirty);
public AIController (Character c)
{
Character = c;
@@ -149,8 +132,177 @@ namespace Barotrauma
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
public bool IsSteeringThroughGap { get; protected set; }
public virtual bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
{
if (wall == null) { return false; }
if (section == null) { return false; }
Gap gap = section.gap;
if (gap == null) { return false; }
float maxDistance = Math.Min(wall.Rect.Width, wall.Rect.Height);
if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance) { return false; }
Hull targetHull = gap.FlowTargetHull;
if (targetHull == null) { return false; }
if (wall.IsHorizontal)
{
targetWorldPos.Y = targetHull.WorldRect.Y - targetHull.Rect.Height / 2;
}
else
{
targetWorldPos.X = targetHull.WorldRect.Center.X;
}
return SteerThroughGap(gap, targetWorldPos, deltaTime, maxDistance: -1);
}
public virtual bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance = -1)
{
Hull targetHull = gap.FlowTargetHull;
if (targetHull == null) { return false; }
if (maxDistance > 0)
{
if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance) { return false; }
}
if (SteeringManager is IndoorsSteeringManager pathSteering)
{
pathSteering.ResetPath();
}
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(targetWorldPos - Character.WorldPosition));
return true;
}
public bool CanPassThroughHole(Structure wall, int sectionIndex, int requiredHoleCount)
{
if (!wall.SectionBodyDisabled(sectionIndex)) { return false; }
int holeCount = 1;
for (int j = sectionIndex - 1; j > sectionIndex - requiredHoleCount; j--)
{
if (wall.SectionBodyDisabled(j))
{
holeCount++;
}
else
{
break;
}
}
for (int j = sectionIndex + 1; j < sectionIndex + requiredHoleCount; j++)
{
if (wall.SectionBodyDisabled(j))
{
holeCount++;
}
else
{
break;
}
}
return holeCount >= requiredHoleCount;
}
protected bool IsWallDisabled(Structure wall)
{
bool isDisabled = true;
for (int i = 0; i < wall.Sections.Length; i++)
{
if (!wall.SectionBodyDisabled(i))
{
isDisabled = false;
break;
}
}
return isDisabled;
}
private readonly HashSet<Item> unequippedItems = new HashSet<Item>();
public bool TakeItem(Item item, Inventory targetInventory, bool equip, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
{
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
if (item.ParentInventory is ItemInventory itemInventory)
{
if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
}
if (equip)
{
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < targetInventory.Capacity; i++)
{
if (targetInventory is CharacterInventory characterInventory)
{
//slot not needed by the item, continue
if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; }
}
targetSlot = i;
//slot free, continue
var otherItem = targetInventory.GetItemAt(i);
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot))
{
if (storeUnequipped && targetInventory.Owner == Character)
{
unequippedItems.Add(otherItem);
}
continue;
}
if (dropOtherIfCannotMove)
{
//if everything else fails, simply drop the existing item
otherItem.Drop(Character);
}
}
}
return targetInventory.TryPutItem(item, targetSlot, allowSwapping, allowCombine: false, Character);
}
else
{
return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot);
}
}
public void UnequipEmptyItems(Item item, bool avoidDroppingInSea = true) => UnequipEmptyItems(Character, item, avoidDroppingInSea);
public static void UnequipEmptyItems(Character character, Item item, bool avoidDroppingInSea = true)
{
if (item.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f))
{
foreach (Item containedItem in item.OwnInventory.AllItemsMod)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
{
if (character.Submarine == null && avoidDroppingInSea)
{
// If we are outside of main sub, try to put the item in the inventory instead dropping it in the sea.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
{
continue;
}
}
containedItem.Drop(character);
}
}
}
}
public void ReequipUnequipped()
{
foreach (var item in unequippedItems)
{
if (item != null && !item.Removed && Character.HasItem(item))
{
TakeItem(item, Character.Inventory, equip: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false);
}
}
unequippedItems.Clear();
}
protected virtual void OnStateChanged(AIState from, AIState to) { }
protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -50,9 +50,9 @@ namespace Barotrauma
/// Returns true if the current or the next node is in ladders.
/// </summary>
public bool InLadders =>
currentPath != null &&
currentPath.CurrentNode != null && (currentPath.CurrentNode.Ladders != null && !currentPath.CurrentNode.Ladders.Item.NonInteractable ||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable));
currentPath != null && currentPath.CurrentNode != null &&
(currentPath.CurrentNode.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character) ||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character)));
/// <summary>
/// Returns true if any node in the path is in stairs
@@ -70,7 +70,7 @@ namespace Barotrauma
if (currentPath.NextNode == null) { return false; }
var currentLadder = currentPath.CurrentNode.Ladders;
if (currentLadder == null) { return false; }
if (currentLadder.Item.NonInteractable) { return false; }
if (!currentLadder.Item.IsInteractable(character)) { return false; }
var nextLadder = GetNextLadder();
return nextLadder != null && nextLadder == currentLadder;
}
@@ -117,13 +117,13 @@ namespace Barotrauma
}
/// <summary>
/// Seeks the ladder from the current and the next two nodes.
/// Seeks the ladder from the next and next + 1 nodes.
/// </summary>
public Ladder GetNextLadder()
{
if (currentPath == null) { return null; }
if (currentPath.NextNode == null) { return null; }
if (currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable)
if (currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character))
{
return currentPath.NextNode.Ladders;
}
@@ -134,7 +134,7 @@ namespace Barotrauma
{
var node = currentPath.Nodes[index];
if (node == null) { return null; }
if (node.Ladders != null && !node.Ladders.Item.NonInteractable)
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
{
return node.Ladders;
}
@@ -146,7 +146,7 @@ namespace Barotrauma
{
node = currentPath.Nodes[index];
if (node == null) { return null; }
if (node.Ladders != null && !node.Ladders.Item.NonInteractable)
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
{
return node.Ladders;
}
@@ -294,10 +294,16 @@ namespace Barotrauma
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
var ladders = GetNextLadder();
Ladder currentLadder = currentPath.CurrentNode.Ladders;
if (currentLadder != null && !currentLadder.Item.IsInteractable(character))
{
currentLadder = null;
}
Ladder nextLadder = GetNextLadder();
var ladders = currentLadder ?? nextLadder;
if (canClimb && !isDiving && ladders != null && character.SelectedConstruction != ladders.Item)
{
if (IsNextNodeLadder || currentPath.CurrentIndex == currentPath.Nodes.Count - 1)
if (IsNextNodeLadder || currentPath.Finished)
{
if (character.CanInteractWith(ladders.Item))
{
@@ -325,7 +331,6 @@ namespace Barotrauma
if (character.IsClimbing && !isDiving)
{
Vector2 diff = currentPath.CurrentNode.SimPosition - pos;
Ladder nextLadder = GetNextLadder();
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
if (nextLadderSameAsCurrent)
{
@@ -341,8 +346,7 @@ namespace Barotrauma
diff.Y = Math.Max(diff.Y, 1.0f);
}
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
float margin = 0.1f;
bool isAboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f;
bool isAboveFloor = heightFromFloor > -0.1f;
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (isAboveFloor && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50))
{
@@ -357,7 +361,7 @@ namespace Barotrauma
nextLadder.Item.TryInteract(character, false, true);
}
}
if (nextLadder != null || isAboveFloor)
if (isAboveFloor || nextLadderSameAsCurrent)
{
currentPath.SkipToNextNode();
}
@@ -383,8 +387,7 @@ namespace Barotrauma
character.SelectedConstruction = null;
}
var door = currentPath.CurrentNode.ConnectedDoor;
bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken;
if (!blockedByDoor)
if (door == null || door.CanBeTraversed)
{
float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
float targetDistance = collider.GetSize().X * multiplier;
@@ -417,10 +420,9 @@ namespace Barotrauma
bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y;
bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight;
var door = currentPath.CurrentNode.ConnectedDoor;
bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1));
float targetDistance = Math.Max(collider.radius * margin, minWidth);
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && !blockedByDoor)
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed))
{
currentPath.SkipToNextNode();
}
@@ -434,18 +436,20 @@ namespace Barotrauma
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsOpen) { return true; }
if (door.Item.NonInteractable) { return false; }
if (CanBreakDoors) { return true; }
if (door.IsStuck || door.IsJammed) { return false; }
if (!canOpenDoors || character.LockHands) { return false; }
if (door.IsOpen || door.IsBroken) { return true; }
if (!door.Item.IsInteractable(character)) { return false; }
if (!CanBreakDoors)
{
if (door.IsStuck || door.IsJammed) { return false; }
if (!canOpenDoors || character.LockHands) { return false; }
}
if (door.HasIntegratedButtons)
{
return door.CanBeOpenedWithoutTools(character);
return door.HasAccess(character) || CanBreakDoors;
}
else
{
return door.Item.GetConnectedComponents<Controller>(true).Any(b => !b.Item.NonInteractable && b.HasAccess(character) && (buttonFilter == null || buttonFilter(b)));
return door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))) || CanBreakDoors;
}
}
@@ -620,18 +624,19 @@ namespace Barotrauma
}
}
bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y;
//non-humanoids can't climb up ladders
if (!(character.AnimController is HumanoidAnimController))
{
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (nextNode.Waypoint.Ladders.Item.NonInteractable || character.LockHands)||
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands)||
(nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y)) //upper node not underwater
nextNodeAboveWaterLevel)) //upper node not underwater
{
return null;
}
}
if (node.Waypoint != null && node.Waypoint.CurrentHull != null)
if (node.Waypoint.CurrentHull != null)
{
var hull = node.Waypoint.CurrentHull;
if (hull.FireSources.Count > 0)
@@ -641,23 +646,26 @@ namespace Barotrauma
penalty += fs.Size.X * 10.0f;
}
}
if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f)
if (character.NeedsAir)
{
if (!HumanAIController.HasDivingSuit(character))
if (hull.WaterVolume / hull.Rect.Width > 100.0f)
{
penalty += 500.0f;
if (!HumanAIController.HasDivingSuit(character))
{
penalty += 500.0f;
}
}
if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
{
penalty += 1000.0f;
}
}
if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
{
penalty += 1000.0f;
}
}
float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y);
if (node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null)
{
penalty += yDist * 10.0f;
float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y);
if (nextNodeAboveWaterLevel && node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null)
{
penalty += yDist * 10.0f;
}
}
return penalty;
@@ -19,6 +19,7 @@ namespace Barotrauma
private Body targetBody;
private Vector2 attachSurfaceNormal;
private Submarine targetSubmarine;
private readonly Character character;
public bool AttachToSub { get; private set; }
public bool AttachToWalls { get; private set; }
@@ -74,22 +75,24 @@ namespace Barotrauma
attachLimb = enemyAI.Character.AnimController.MainLimb;
}
character = enemyAI.Character;
enemyAI.Character.OnDeath += OnCharacterDeath;
}
public void SetAttachTarget(Structure wall, Vector2 attachPos, Vector2 attachSurfaceNormal)
{
if (wall == null) { return; }
var sub = wall.Submarine;
if (sub == null) { return; }
targetWall = wall;
targetBody = wall.Submarine.PhysicsBody.FarseerBody;
targetSubmarine = wall.Submarine;
targetSubmarine = sub;
targetBody = targetSubmarine.PhysicsBody.FarseerBody;
this.attachSurfaceNormal = attachSurfaceNormal;
wallAttachPos = attachPos;
}
public void Update(EnemyAIController enemyAI, float deltaTime)
{
Character character = enemyAI.Character;
if (character.Submarine != null)
{
DeattachFromBody(reset: true);
@@ -160,12 +163,12 @@ namespace Barotrauma
{
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
{
attachSurfaceNormal = edge.GetNormal(cell);
targetBody = cell.Body;
Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection);
float distSqr = Vector2.DistanceSquared(character.SimPosition, wallAttachPos);
float distSqr = Vector2.DistanceSquared(character.SimPosition, potentialAttachPos);
if (distSqr < closestDist)
{
attachSurfaceNormal = edge.GetNormal(cell);
targetBody = cell.Body;
wallAttachPos = potentialAttachPos;
closestDist = distSqr;
}
@@ -183,7 +186,7 @@ namespace Barotrauma
wallAttachPos = Vector2.Zero;
}
if (wallAttachPos == Vector2.Zero)
if (wallAttachPos == Vector2.Zero || targetBody == null)
{
DeattachFromBody(reset: false);
}
@@ -194,7 +197,7 @@ namespace Barotrauma
if (squaredDistance < targetDistance * targetDistance)
{
//close enough to a wall -> attach
AttachToBody(character.AnimController.Collider, attachLimb, targetBody, wallAttachPos);
AttachToBody(wallAttachPos);
enemyAI.SteeringManager.Reset();
}
else
@@ -217,7 +220,7 @@ namespace Barotrauma
{
if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
{
AttachToBody(character.AnimController.Collider, attachLimb, targetBody, transformedAttachPos);
AttachToBody(transformedAttachPos);
}
}
}
@@ -268,9 +271,12 @@ namespace Barotrauma
}
}
private void AttachToBody(PhysicsBody collider, Limb attachLimb, Body targetBody, Vector2 attachPos)
private void AttachToBody(Vector2 attachPos)
{
if (attachLimb == null) { return; }
if (targetBody == null) { return; }
if (attachCooldown > 0) { return; }
var collider = character.AnimController.Collider;
//already attached to something
if (AttachJoints.Count > 0)
{
@@ -178,7 +178,7 @@ namespace Barotrauma
{
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0f &&
speaker?.CurrentHull != null &&
speaker.TeamID == Character.TeamType.FriendlyNPC &&
speaker.TeamID == CharacterTeamType.FriendlyNPC &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
{
currentFlags.Add("EnterOutpost");
@@ -213,7 +213,7 @@ namespace Barotrauma
}
}
if (speaker.TeamID == Character.TeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost)
if (speaker.TeamID == CharacterTeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost)
{
currentFlags.Add("OutpostNPC");
}
@@ -67,6 +67,12 @@ namespace Barotrauma
_abandon = value;
if (_abandon)
{
#if DEBUG
if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
{
throw new Exception("Order abandoned!");
}
#endif
OnAbandon();
}
}
@@ -96,9 +102,21 @@ namespace Barotrauma
return all;
}
/// <summary>
/// A single shot event. Automatically cleared after launching. Use OnCompleted method for implementing (internal) persistent behavior.
/// </summary>
public event Action Completed;
/// <summary>
/// A single shot event. Automatically cleared after launching. Use OnAbandoned method for implementing (internal) persistent behavior.
/// </summary>
public event Action Abandoned;
/// <summary>
/// A single shot event. Automatically cleared after launching. Use OnSelected method for implementing (internal) persistent behavior.
/// </summary>
public event Action Selected;
/// <summary>
/// A single shot event. Automatically cleared after launching. Use OnDeselected method for implementing (internal) persistent behavior.
/// </summary>
public event Action Deselected;
protected HumanAIController HumanAIController => character.AIController as HumanAIController;
@@ -202,7 +220,7 @@ namespace Barotrauma
{
if (!AllowOutsideSubmarine && character.Submarine == null) { return false; }
if (AllowInAnySub) { return true; }
if (AllowInFriendlySubs && character.Submarine.TeamID == Character.TeamType.FriendlyNPC) { return true; }
if (AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) { return true; }
return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID);
}
}
@@ -212,7 +230,7 @@ namespace Barotrauma
/// </summary>
public virtual float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed)
{
Priority = 0;
@@ -221,7 +239,7 @@ namespace Barotrauma
}
if (isOrder)
{
Priority = AIObjectiveManager.OrderPriority;
Priority = objectiveManager.GetOrderPriority(this);
}
else
{
@@ -243,7 +261,7 @@ namespace Barotrauma
public virtual void Update(float deltaTime)
{
if (objectiveManager.CurrentOrder != this && objectiveManager.WaitTimer <= 0)
if (!objectiveManager.IsOrder(this) && objectiveManager.WaitTimer <= 0)
{
UpdateDevotion(deltaTime);
}
@@ -318,22 +336,26 @@ namespace Barotrauma
{
Reset();
Selected?.Invoke();
Selected = null;
}
public virtual void OnDeselected()
{
CumulatedDevotion = 0;
Deselected?.Invoke();
Deselected = null;
}
protected virtual void OnCompleted()
{
Completed?.Invoke();
Completed = null;
}
protected virtual void OnAbandon()
{
Abandoned?.Invoke();
Abandoned = null;
}
public virtual void Reset()
@@ -408,7 +430,14 @@ namespace Barotrauma
subObjectives.Remove(subObjective);
if (AbandonWhenCannotCompleteSubjectives)
{
Abandon = true;
if (objectiveManager.IsOrder(this))
{
Reset();
}
else
{
Abandon = true;
}
}
}
}
@@ -21,7 +21,7 @@ namespace Barotrauma
if (battery == null) { return false; }
var item = battery.Item;
if (item.IgnoreByAI) { return false; }
if (item.NonInteractable) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (item.Submarine == null) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
@@ -64,7 +64,7 @@ namespace Barotrauma
private bool IsReady(PowerContainer battery)
{
if (battery.HasBeenTuned && character.CurrentOrder == null) { return true; }
if (battery.HasBeenTuned && character.IsDismissed) { return true; }
if (Option == "charge")
{
return battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio;
@@ -79,7 +79,7 @@ namespace Barotrauma
new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier)
{
IsLoop = false,
Override = character.CurrentOrder != null,
Override = !character.IsDismissed,
completionCondition = () => IsReady(battery)
};
@@ -48,7 +48,7 @@ namespace Barotrauma
float selectedBonus = isSelected ? 100 - MaxDevotion : 0;
float devotion = (CumulatedDevotion + selectedBonus) / 100;
float reduction = IsPriority ? 1 : isSelected ? 2 : 3;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
float max = AIObjectiveManager.LowestOrderPriority - reduction;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
@@ -58,6 +58,11 @@ namespace Barotrauma
{
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
if (item.IgnoreByAI)
{
Abandon = true;
return;
}
if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer))
{
itemIndex = 0;
@@ -74,6 +79,7 @@ namespace Barotrauma
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
{
Equip = equip,
TakeWholeStack = true,
DropIfFails = true
},
onCompleted: () =>
@@ -12,21 +12,30 @@ namespace Barotrauma
public override bool AllowAutomaticItemUnequipping => false;
public override bool ForceOrderPriority => false;
public readonly Item prioritizedItem;
public readonly List<Item> prioritizedItems = new List<Item>();
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null)
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.prioritizedItem = prioritizedItem;
if (prioritizedItem != null)
{
prioritizedItems.Add(prioritizedItem);
}
}
protected override float TargetEvaluation() => Targets.Any() ? AIObjectiveManager.RunPriority - 1 : 0;
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, IEnumerable<Item> prioritizedItems, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.prioritizedItems.AddRange(prioritizedItems.Where(i => i != null));
}
protected override float TargetEvaluation() => Targets.Any() ? (objectiveManager.IsOrder(this) ? objectiveManager.GetOrderPriority(this) : AIObjectiveManager.RunPriority - 1) : 0;
protected override bool Filter(Item target)
{
// If the target was selected as a valid target, we'll have to accept it so that the objective can be completed.
// The validity changes when a character picks the item up.
if (!IsValidTarget(target, character)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); }
if (!IsValidTarget(target, character, checkInventory: true)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); }
if (target.CurrentHull.FireSources.Count > 0) { return false; }
// Don't repair items in rooms that have enemies inside.
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
@@ -38,7 +47,7 @@ namespace Barotrauma
protected override AIObjective ObjectiveConstructor(Item item)
=> new AIObjectiveCleanupItem(item, character, objectiveManager, priorityModifier: PriorityModifier)
{
IsPriority = prioritizedItem == item
IsPriority = prioritizedItems.Contains(item)
};
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
@@ -56,12 +65,19 @@ namespace Barotrauma
return true;
}
public static bool IsValidTarget(Item item, Character character)
public static bool IsValidContainer(Item item, Character character) =>
!item.IgnoreByAI && item.IsInteractable(character) && item.HasTag("allowcleanup") && item.ParentInventory == null && item.OwnInventory != null && item.OwnInventory.AllItems.Any() && IsItemInsideValidSubmarine(item, character);
public static bool IsValidTarget(Item item, Character character, bool checkInventory)
{
if (item == null) { return false; }
if (item.IgnoreByAI) { return false; }
if (item.NonInteractable) { return false; }
if (item.ParentInventory != null) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (item.SpawnedInOutpost) { return false; }
if (item.ParentInventory != null)
{
if (item.Container == null || !IsValidContainer(item.Container, character)) { return false; }
}
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
@@ -83,20 +99,29 @@ namespace Barotrauma
{
return false;
}
if (!checkInventory)
{
return true;
}
bool canEquip = true;
if (!item.AllowedSlots.Contains(InvSlotType.Any))
{
canEquip = false;
var inv = character.Inventory;
foreach (var allowedSlot in item.AllowedSlots)
{
int slot = character.Inventory.FindLimbSlot(allowedSlot);
if (slot > -1)
foreach (var slotType in inv.SlotTypes)
{
if (character.Inventory.Items[slot] == null)
if (!allowedSlot.HasFlag(slotType)) { continue; }
for (int i = 0; i < inv.Capacity; i++)
{
canEquip = true;
break;
}
if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.GetItemAt(i) != null)
{
canEquip = false;
break;
}
}
}
}
}
@@ -79,11 +79,17 @@ namespace Barotrauma
private float coolDownTimer;
private IEnumerable<Body> myBodies;
private float aimTimer;
private float spreadTimer;
private bool canSeeTarget;
private float visibilityCheckTimer;
private readonly float visibilityCheckInterval = 0.2f;
private float sqrDistance;
private readonly float maxDistance = 2000;
private readonly float distanceCheckInterval = 0.2f;
private float distanceTimer;
/// <summary>
/// Aborts the objective when this condition is true
/// </summary>
@@ -108,9 +114,13 @@ namespace Barotrauma
public CombatMode Mode { get; private set; }
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
private bool TargetEliminated => Enemy == null || Enemy.Removed || Enemy.IsUnconscious;
private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious;
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
private bool EnemyIsClose() => Enemy != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500;
private float AimSpeed => HumanAIController.AimSpeed;
private float AimAccuracy => HumanAIController.AimAccuracy;
private bool EnemyIsClose() => Enemy != null && character.CurrentHull != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500;
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
: base(character, objectiveManager, priorityModifier)
@@ -136,11 +146,12 @@ namespace Barotrauma
{
Mode = CombatMode.Retreat;
}
spreadTimer = Rand.Range(-10, 10);
}
public override float GetPriority()
{
if (character.TeamID == Character.TeamType.FriendlyNPC && Enemy != null)
if (character.TeamID == CharacterTeamType.FriendlyNPC && Enemy != null)
{
if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine))
{
@@ -168,6 +179,12 @@ namespace Barotrauma
{
findSafety.Priority = 0;
}
distanceTimer -= deltaTime;
if (distanceTimer < 0)
{
distanceTimer = distanceCheckInterval;
sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
}
}
protected override bool Check()
@@ -175,9 +192,13 @@ namespace Barotrauma
if (IsOffensiveOrArrest && Mode != initialMode)
{
Abandon = true;
SteeringManager.Reset();
return false;
}
if (sqrDistance > maxDistance * maxDistance)
{
// The target escaped from us.
return true;
}
return IsEnemyDisabled || (!IsOffensiveOrArrest && coolDownTimer <= 0);
}
@@ -186,7 +207,6 @@ namespace Barotrauma
if (abortCondition != null && abortCondition())
{
Abandon = true;
SteeringManager.Reset();
return;
}
if (!IsOffensiveOrArrest)
@@ -238,7 +258,9 @@ namespace Barotrauma
}
}
private bool IsLoaded(ItemComponent weapon) => weapon.HasRequiredContainedItems(character, addMessage: false);
private bool IsLoaded(ItemComponent weapon, bool checkContainedItems = true) =>
weapon.HasRequiredContainedItems(character, addMessage: false) &&
(!checkContainedItems || weapon.Item.OwnInventory == null || weapon.Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
private bool TryArm()
{
@@ -260,21 +282,21 @@ namespace Barotrauma
// No weapons
break;
}
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
if (!character.Inventory.Contains(Weapon) || WeaponComponent == null)
{
// Not in the inventory anymore or cannot find the weapon component
allWeapons.Remove(WeaponComponent);
Weapon = null;
continue;
}
if (IsLoaded(WeaponComponent))
if (IsLoaded(WeaponComponent, checkContainedItems: true))
{
// All good, the weapon is loaded
break;
}
if (Reload(seekAmmo: false))
{
// All good, reloading successful
// All good, we can use the weapon.
break;
}
else
@@ -304,7 +326,7 @@ namespace Barotrauma
}
}
}
bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != Character.TeamType.FriendlyNPC && IsOffensiveOrArrest;
bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest;
if (!isAllowedToSeekWeapons)
{
if (WeaponComponent == null)
@@ -369,7 +391,7 @@ namespace Barotrauma
bool CheckWeapon(bool seekAmmo)
{
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
if (!character.Inventory.Contains(Weapon) || WeaponComponent == null)
{
// Not in the inventory anymore or cannot find the weapon component
return false;
@@ -564,21 +586,20 @@ namespace Barotrauma
container.ContainableItems.Any(containable => containable.Identifiers.Any(id => id.Equals(mobileBatteryTag))));
// If there's no such container, assume that the melee weapon can stun without a battery.
return containers.None() || containers.Any(container =>
(container as ItemContainer)?.Inventory.Items.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
(container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
}
}
private HashSet<ItemComponent> FindWeaponsFromInventory()
{
weapons.Clear();
foreach (var item in character.Inventory.Items)
foreach (var item in character.Inventory.AllItems)
{
if (item == null) { continue; }
if (ignoredWeapons.Contains(item)) { continue; }
GetWeapons(item, weapons);
if (item.OwnInventory != null)
{
item.OwnInventory.Items.ForEach(i => GetWeapons(i, weapons));
item.OwnInventory.AllItems.ForEach(i => GetWeapons(i, weapons));
}
}
return weapons;
@@ -598,7 +619,7 @@ namespace Barotrauma
private void Unequip()
{
if (!character.LockHands && character.SelectedItems.Contains(Weapon))
if (!character.LockHands && character.HeldItems.Contains(Weapon))
{
if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Any }))
{
@@ -617,10 +638,10 @@ namespace Barotrauma
if (!character.HasEquippedItem(Weapon))
{
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.FindAll(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand));
var slots = Weapon.AllowedSlots.Where(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand));
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
aimTimer = Rand.Range(0.5f, 1f);
aimTimer = Rand.Range(1f, 1.5f) / AimSpeed;
}
else
{
@@ -651,7 +672,7 @@ namespace Barotrauma
}
else
{
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls, allowChangingTheSubmarine: character.TeamID != Character.TeamType.FriendlyNPC);
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls, allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
@@ -705,11 +726,7 @@ namespace Barotrauma
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = Enemy.DisplayName
},
onAbandon: () =>
{
Abandon = true;
SteeringManager.Reset();
});
onAbandon: () => Abandon = true);
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.Stun > 2)
{
@@ -724,7 +741,7 @@ namespace Barotrauma
}
else
{
if (character.TeamID == Character.TeamType.FriendlyNPC)
if (character.TeamID == CharacterTeamType.FriendlyNPC)
{
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs");
if (prefab != null)
@@ -769,9 +786,9 @@ namespace Barotrauma
#endif
}
// Confiscate stolen goods.
foreach (var item in Enemy.Inventory.Items)
foreach (var item in Enemy.Inventory.AllItemsMod)
{
if (item == null || item == handCuffs) { continue; }
if (item == handCuffs) { continue; }
if (item.StolenDuringRound)
{
item.Drop(character);
@@ -814,33 +831,32 @@ namespace Barotrauma
/// </summary>
private bool Reload(bool seekAmmo)
{
if (WeaponComponent == null) { return false; }
if (!WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return false; }
var containedItems = Weapon.OwnInventory?.Items;
if (containedItems == null) { return true; }
// Drop empty ammo
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0)
{
containedItem.Drop(character);
}
}
if (WeaponComponent == null) { return false; }
if (Weapon.OwnInventory == null) { return true; }
// Eject empty ammo
HumanAIController.UnequipEmptyItems(Weapon);
RelatedItem item = null;
Item ammunition = null;
string[] ammunitionIdentifiers = null;
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
{
ammunition = containedItems.FirstOrDefault(it => it != null && it.Condition > 0 && requiredItem.MatchesItem(it));
if (ammunition != null)
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
{
// Ammunition still remaining
return true;
ammunition = Weapon.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it));
if (ammunition != null)
{
// Ammunition still remaining
return true;
}
item = requiredItem;
ammunitionIdentifiers = requiredItem.Identifiers;
}
item = requiredItem;
ammunitionIdentifiers = requiredItem.Identifiers;
}
else if (WeaponComponent is MeleeWeapon meleeWeapon)
{
ammunitionIdentifiers = meleeWeapon.PreferredContainedItems;
}
// No ammo
if (ammunition == null)
{
@@ -851,22 +867,13 @@ namespace Barotrauma
if (ammunition != null)
{
var container = Weapon.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
if (!container.Inventory.TryPutItem(ammunition, null))
{
if (!container.Inventory.CanBePut(ammunition))
{
return false;
}
character.Inventory.RemoveItem(ammunition);
if (!container.Inventory.TryPutItem(ammunition, null))
if (ammunition.ParentInventory == character.Inventory)
{
ammunition.Drop(character);
}
}
else
{
container.Combine(ammunition, character);
}
}
}
}
@@ -884,6 +891,15 @@ namespace Barotrauma
private void Attack(float deltaTime)
{
character.CursorPosition = Enemy.WorldPosition;
if (AimAccuracy < 1)
{
spreadTimer += deltaTime * Rand.Range(0.01f, 1f);
float shake = Rand.Range(0.95f, 1.05f);
float offsetAmount = (1 - AimAccuracy) * Rand.Range(300f, 500f);
float distanceFactor = MathUtils.InverseLerp(0, 1000 * 1000, sqrDistance);
float offset = (float)Math.Sin(spreadTimer * shake) * offsetAmount * distanceFactor;
character.CursorPosition += new Vector2(0, offset);
}
if (character.Submarine != null)
{
character.CursorPosition -= character.Submarine.Position;
@@ -894,7 +910,11 @@ namespace Barotrauma
canSeeTarget = character.CanSeeTarget(Enemy);
visibilityCheckTimer = visibilityCheckInterval;
}
if (!canSeeTarget) { return; }
if (!canSeeTarget)
{
aimTimer = Rand.Range(0.2f, 1f) / AimSpeed;
return;
}
if (Weapon.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
@@ -945,14 +965,12 @@ namespace Barotrauma
}
if (closeEnough)
{
SteeringManager.Reset();
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
UseWeapon(deltaTime);
}
else if (!character.IsFacing(Enemy.WorldPosition))
{
// Don't do the facing check if we are close to the target, because it easily causes the character to get stuck here when it flips around.
aimTimer = Rand.Range(1f, 1.5f);
aimTimer = Rand.Range(1f, 1.5f) / AimSpeed;
}
}
else
@@ -961,14 +979,15 @@ namespace Barotrauma
{
if (sqrDist > repairTool.Range * repairTool.Range) { return; }
}
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4)
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor)
{
if (myBodies == null)
{
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories);
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories, allowInsideFixture: true);
if (pickedBody != null)
{
Character target = null;
@@ -982,24 +1001,29 @@ namespace Barotrauma
}
if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target)))
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
float reloadTime = 0;
if (WeaponComponent is RangedWeapon rangedWeapon)
{
reloadTime = rangedWeapon.Reload;
}
if (WeaponComponent is MeleeWeapon mw)
{
reloadTime = mw.Reload;
}
aimTimer = reloadTime * Rand.Range(1f, 1.5f);
UseWeapon(deltaTime);
}
}
}
}
}
private void UseWeapon(float deltaTime)
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
float reloadTime = 0;
if (WeaponComponent is RangedWeapon rangedWeapon)
{
reloadTime = rangedWeapon.Reload;
}
if (WeaponComponent is MeleeWeapon mw)
{
reloadTime = mw.Reload;
}
aimTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.5f) / AimSpeed);
}
protected override void OnCompleted()
{
base.OnCompleted();
@@ -1007,6 +1031,23 @@ namespace Barotrauma
{
Unequip();
}
if (!HoldPosition)
{
SteeringManager.Reset();
}
}
protected override void OnAbandon()
{
base.OnAbandon();
if (Weapon != null)
{
Unequip();
}
if (!HoldPosition)
{
SteeringManager.Reset();
}
}
public override void Reset()
@@ -21,7 +21,8 @@ namespace Barotrauma
//can either be a tag or an identifier
public readonly string[] itemIdentifiers;
public readonly ItemContainer container;
public readonly Item item;
private readonly Item item;
public Item ItemToContain { get; private set; }
private AIObjectiveGetItem getItemObjective;
private AIObjectiveGoTo goToObjective;
@@ -30,10 +31,13 @@ namespace Barotrauma
public bool AllowToFindDivingGear { get; set; } = true;
public bool AllowDangerousPressure { get; set; }
public float ConditionLevel { get; set; }
public float ConditionLevel { get; set; } = 1;
public bool Equip { get; set; }
public bool RemoveEmpty { get; set; } = true;
public bool MoveWholeStack { get; set; }
public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
@@ -66,14 +70,14 @@ namespace Barotrauma
}
if (item != null)
{
return container.Inventory.Items.Contains(item);
return container.Inventory.Contains(item);
}
else
{
int containedItemCount = 0;
foreach (Item i in container.Inventory.Items)
foreach (Item it in container.Inventory.AllItems)
{
if (i != null && CheckItem(i))
if (CheckItem(it))
{
containedItemCount++;
}
@@ -82,7 +86,7 @@ namespace Barotrauma
}
}
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage > ConditionLevel;
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI();
protected override void Act(float deltaTime)
{
@@ -91,58 +95,44 @@ namespace Barotrauma
Abandon = true;
return;
}
Item itemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true);
if (itemToContain != null)
ItemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true);
if (ItemToContain != null)
{
if (!character.CanInteractWith(itemToContain))
if (!character.CanInteractWith(ItemToContain, checkLinked: false))
{
Abandon = true;
return;
}
if (character.CanInteractWith(container.Item, out _, checkLinked: false))
if (character.CanInteractWith(container.Item, checkLinked: false))
{
if (RemoveEmpty)
{
foreach (var emptyItem in container.Inventory.Items)
{
if (emptyItem == null) { continue; }
if (emptyItem.Condition <= 0)
{
emptyItem.Drop(character);
}
}
HumanAIController.UnequipEmptyItems(container.Item);
}
// Contain the item
if (itemToContain.ParentInventory == character.Inventory)
Inventory originalInventory = ItemToContain.ParentInventory;
var slots = originalInventory?.FindIndices(ItemToContain);
if (container.Inventory.TryPutItem(ItemToContain, null))
{
if (!container.Inventory.CanBePut(itemToContain))
if (MoveWholeStack && slots != null)
{
Abandon = true;
}
else
{
character.Inventory.RemoveItem(itemToContain);
if (container.Inventory.TryPutItem(itemToContain, null))
foreach (int slot in slots)
{
IsCompleted = true;
}
else
{
itemToContain.Drop(character);
Abandon = true;
foreach (Item item in originalInventory.GetItemsAt(slot).ToList())
{
container.Inventory.TryPutItem(item, null);
}
}
IsCompleted = true;
}
}
else
{
if (container.Combine(itemToContain, character))
if (ItemToContain.ParentInventory == character.Inventory)
{
IsCompleted = true;
}
else
{
Abandon = true;
ItemToContain.Drop(character);
}
Abandon = true;
}
}
else
@@ -151,7 +141,7 @@ namespace Barotrauma
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = container.Item.Name,
abortCondition = () => !itemToContain.IsOwnedBy(character)
abortCondition = obj => !ItemToContain.IsOwnedBy(character)
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -22,8 +22,13 @@ namespace Barotrauma
public AIObjectiveGetItem GetItemObjective => getItemObjective;
public AIObjectiveContainItem ContainObjective => containObjective;
public Item TargetItem => targetItem;
public ItemContainer TargetContainer => targetContainer;
public bool Equip { get; set; }
public bool TakeWholeStack { get; set; }
/// <summary>
/// If true drops the item when containing the item fails.
/// In both cases abandons the objective.
@@ -58,12 +63,17 @@ namespace Barotrauma
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);
Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI), recursive: false);
if (itemToDecontain == null)
{
Abandon = true;
return;
}
if (itemToDecontain.IgnoreByAI)
{
Abandon = true;
return;
}
if (targetContainer == null)
{
if (sourceContainer == null)
@@ -77,7 +87,7 @@ namespace Barotrauma
return;
}
}
else if (targetContainer.Inventory.Items.Contains(itemToDecontain))
else if (targetContainer.Inventory.Contains(itemToDecontain))
{
IsCompleted = true;
return;
@@ -85,7 +95,7 @@ namespace Barotrauma
if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character))
{
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip),
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip) { TakeWholeStack = this.TakeWholeStack },
onAbandon: () => Abandon = true);
return;
}
@@ -94,6 +104,7 @@ namespace Barotrauma
TryAddSubObjective(ref containObjective,
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager)
{
MoveWholeStack = TakeWholeStack,
Equip = Equip,
RemoveEmpty = false,
GetItemPriority = GetItemPriority,
@@ -35,7 +35,7 @@ namespace Barotrauma
Abandon = true;
return Priority;
}
bool isOrder = objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>();
bool isOrder = objectiveManager.HasOrder<AIObjectiveExtinguishFires>();
if (!isOrder && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
// Don't go into rooms with any enemies, unless it's an order
@@ -78,14 +78,17 @@ namespace Barotrauma
{
TryAddSubObjective(ref getExtinguisherObjective, () =>
{
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher", allowBroken: false))
{
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
}
var getItemObjective = new AIObjectiveGetItem(character, "fireextinguisher", objectiveManager, equip: true)
{
AllowStealing = true,
// If the item is inside an unsafe hull, decrease the priority
GetItemPriority = i => HumanAIController.UnsafeHulls.Contains(i.CurrentHull) ? 0.1f : 1
};
if (objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>())
if (objectiveManager.HasOrder<AIObjectiveExtinguishFires>())
{
getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindfireextinguisher"), null, 0.0f, "dialogcannotfindfireextinguisher", 10.0f);
};
@@ -105,9 +108,13 @@ namespace Barotrauma
}
foreach (FireSource fs in targetHull.FireSources)
{
bool inRange = fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range));
bool move = !inRange || !HumanAIController.VisibleHulls.Contains(fs.Hull);
if (inRange || useExtinquisherTimer > 0.0f)
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange;
float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y);
bool inRange = xDist + yDist < extinguisher.Range;
// Use the hull position, because the fire x pos is sometimes inside a wall -> the bot can't ever see it and continues running towards the wall.
ISpatialEntity lookTarget = character.CurrentHull == targetHull || character.CurrentHull.linkedTo.Contains(targetHull) ? targetHull : fs as ISpatialEntity;
bool move = !inRange || !character.CanSeeTarget(lookTarget);
if ((inRange && character.CanSeeTarget(lookTarget)) || useExtinquisherTimer > 0)
{
useExtinquisherTimer += deltaTime;
if (useExtinquisherTimer > 2.0f)
@@ -121,19 +128,7 @@ namespace Barotrauma
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
if (extinguisherItem.RequireAimToUse)
{
bool isOperatingButtons = false;
if (SteeringManager == PathSteering)
{
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen && !door.IsBroken)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
}
if (!isOperatingButtons)
{
character.SetInput(InputType.Aim, false, true);
}
character.SetInput(InputType.Aim, false, true);
sinTime += deltaTime * 10;
}
character.SetInput(extinguisherItem.IsShootable ? InputType.Shoot : InputType.Use, false, true);
@@ -142,15 +137,11 @@ namespace Barotrauma
{
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, true), null, 0, "putoutfire", 10.0f);
}
if (!character.CanSeeTarget(fs))
{
move = true;
}
}
if (move)
{
//go to the first firesource
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2)
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: Math.Max(fs.DamageRange, extinguisher.Range * 0.7f))
{
DialogueIdentifier = "dialogcannotreachfire",
TargetName = fs.Hull.DisplayName
@@ -158,7 +149,7 @@ namespace Barotrauma
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
{
gotoObjective.requiredCondition = () => HumanAIController.VisibleHulls.Contains(fs.Hull);
gotoObjective.requiredCondition = () => targetHull == null || character.CanSeeTarget(targetHull);
}
}
else
@@ -38,11 +38,19 @@ namespace Barotrauma
public static bool IsValidTarget(Hull hull, Character character)
{
if (hull == null) { return false; }
if (hull.IgnoreByAI) { return false; }
if (hull.FireSources.None()) { return false; }
if (hull.Submarine == null) { return false; }
if (character.Submarine == null) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(hull, includingConnectedSubs: true)) { return false; }
if (hull.BallastFlora != null) { return false; }
foreach (var ballastFlora in MapCreatures.Behavior.BallastFloraBehavior.EntityList)
{
if (ballastFlora.Parent?.Submarine != character.Submarine) { continue; }
if (ballastFlora.Branches.Any(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull))
{
return false;
}
}
return true;
}
}
@@ -26,7 +26,7 @@ namespace Barotrauma
protected override AIObjective ObjectiveConstructor(Character target)
{
var combatObjective = new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
if (character.TeamID == Character.TeamType.FriendlyNPC && target.TeamID == Character.TeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
@@ -50,14 +50,11 @@ namespace Barotrauma
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (target == character) { return false; }
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (target.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine == null) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null)
{
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
}
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
return true;
}
}
@@ -1,5 +1,7 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -36,11 +38,11 @@ namespace Barotrauma
return;
}
targetItem = character.Inventory.FindItemByTag(gearTag, true);
if (targetItem == null || !character.HasEquippedItem(targetItem))
if (targetItem == null || !character.HasEquippedItem(targetItem) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0))
{
TryAddSubObjective(ref getDivingGear, () =>
{
if (targetItem == null)
if (targetItem == null && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
}
@@ -56,7 +58,7 @@ namespace Barotrauma
}
else
{
if (!DropEmptyTanks(character, targetItem, out Item[] containedItems))
if (!EjectEmptyTanks(character, targetItem, out var containedItems))
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + targetItem + "\" has no proper inventory");
@@ -64,14 +66,25 @@ namespace Barotrauma
Abandon = true;
return;
}
if (containedItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > MIN_OXYGEN))
float min = character.Submarine == null ? 0.01f : MIN_OXYGEN;
if (containedItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > min))
{
// No valid oxygen source loaded.
// Seek oxygen that has min 10% condition left.
// Seek oxygen that has at least 10% condition left.
TryAddSubObjective(ref getOxygen, () =>
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC)
if (character.IsOnPlayerTeam)
{
if (HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: min))
{
character.Speak(TextManager.Get("dialogswappingoxygentank"), null, 0, "swappingoxygentank", 30.0f);
}
else
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
}
}
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
@@ -80,20 +93,45 @@ namespace Barotrauma
},
onAbandon: () =>
{
int remainingTanks = ReportOxygenTankCount();
// Try to seek any oxygen sources.
TryAddSubObjective(ref getOxygen, () =>
{
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC)
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
ConditionLevel = 0
AllowDangerousPressure = true
};
},
onAbandon: () => Abandon = true,
onAbandon: () =>
{
Abandon = true;
if (remainingTanks > 0 && !HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 0.01f))
{
character.Speak(TextManager.Get("dialogcantfindtoxygen"), null, 0, "cantfindoxygen", 30.0f);
}
},
onCompleted: () => RemoveSubObjective(ref getOxygen));
},
onCompleted: () => RemoveSubObjective(ref getOxygen));
onCompleted: () =>
{
RemoveSubObjective(ref getOxygen);
ReportOxygenTankCount();
});
int ReportOxygenTankCount()
{
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("oxygensource") && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfOxygenTanks"), null, 0.0f, "outofoxygentanks", 30.0f);
}
else if (remainingOxygenTanks < 10)
{
character.Speak(TextManager.Get("DialogLowOnOxygenTanks"), null, 0.0f, "lowonoxygentanks", 30.0f);
}
return remainingOxygenTanks;
}
}
}
}
@@ -101,21 +139,11 @@ namespace Barotrauma
/// <summary>
/// Returns false only when no inventory can be found from the item.
/// </summary>
public static bool DropEmptyTanks(Character actor, Item target, out Item[] containedItems)
public static bool EjectEmptyTanks(Character actor, Item target, out IEnumerable<Item> containedItems)
{
containedItems = target.OwnInventory?.Items;
if (containedItems == null)
{
return false;
}
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
{
containedItem.Drop(actor);
}
}
containedItems = target.OwnInventory?.AllItems;
if (containedItems == null) { return false; }
AIController.UnequipEmptyItems(actor, target);
return true;
}
@@ -46,19 +46,24 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo && HumanAIController.HasDivingSuit(character) ? 0 : 100;
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.HasActiveObjective<AIObjectiveCombat>()) && HumanAIController.HasDivingSuit(character) ? 0 : 100;
}
else
{
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character))
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN))
{
Priority = 100;
}
else if (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() && character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
{
// Ordered to follow/hold position inside a hostile sub -> ignore find safety unless we need to find a diving gear
Priority = 0;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
{
// Boost the priority while seeking the diving gear
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.OrderPriority + 20, 100));
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.HighestOrderPriority + 20, 100));
}
}
return Priority;
@@ -168,7 +173,7 @@ namespace Barotrauma
{
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull;
currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != Character.TeamType.FriendlyNPC);
currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _);
if (currentSafeHull == null)
{
@@ -359,7 +364,7 @@ namespace Barotrauma
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
// Intentionally exclude wrecks from this check
if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != Character.TeamType.FriendlyNPC)
if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
{
hullSafety /= 10;
}
@@ -52,7 +52,7 @@ namespace Barotrauma
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float reduction = isPriority ? 1 : 2;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
float max = AIObjectiveManager.LowestOrderPriority - reduction;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
@@ -64,10 +64,10 @@ namespace Barotrauma
var weldingTool = character.Inventory.FindItemByTag("weldingequipment", true);
if (weldingTool == null)
{
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
onAbandon: () =>
{
if (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>())
if (character.IsOnPlayerTeam && objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>())
{
character.Speak(TextManager.Get("dialogcannotfindweldingequipment"), null, 0.0f, "dialogcannotfindweldingequipment", 10.0f);
}
@@ -78,8 +78,7 @@ namespace Barotrauma
}
else
{
var containedItems = weldingTool.OwnInventory?.Items;
if (containedItems == null)
if (weldingTool.OwnInventory == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory");
@@ -88,19 +87,34 @@ namespace Barotrauma
return;
}
// Drop empty tanks
foreach (Item containedItem in containedItems)
HumanAIController.UnequipEmptyItems(weldingTool);
if (weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
onAbandon: () =>
{
Abandon = true;
ReportWeldingFuelTankCount();
},
onCompleted: () =>
{
RemoveSubObjective(ref refuelObjective);
ReportWeldingFuelTankCount();
});
void ReportWeldingFuelTankCount()
{
containedItem.Drop(character);
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("weldingfuel") && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfWeldingFuel"), null, 0.0f, "outofweldingfuel", 30.0f);
}
else if (remainingOxygenTanks < 4)
{
character.Speak(TextManager.Get("DialogLowOnWeldingFuel"), null, 0.0f, "lowonweldingfuel", 30.0f);
}
}
}
if (containedItems.None(i => i != null && i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref refuelObjective));
return;
}
}
@@ -42,7 +42,7 @@ namespace Barotrauma
if (totalLeaks == 0) { return 0; }
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated, onlyBots: true);
bool anyFixers = otherFixers > 0;
if (objectiveManager.CurrentOrder == this)
if (objectiveManager.IsOrder(this))
{
float ratio = anyFixers ? totalLeaks / (float)otherFixers : 1;
return Targets.Sum(t => GetLeakSeverity(t)) * ratio;
@@ -72,7 +72,11 @@ namespace Barotrauma
{
if (gap == null) { return false; }
// Don't fix a leak on a wall section set to be ignored
if (gap.ConnectedWall?.Sections?.Any(s => s.gap == gap && s.IgnoreByAI) ?? false) { return false; }
if (gap.ConnectedWall != null)
{
if (gap.ConnectedWall.Sections.Any(s => s.gap == gap && s.IgnoreByAI)) { return false; }
if (gap.ConnectedWall.MaxHealth <= 0.0f) { return false; }
}
if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; }
if (gap.Submarine == null || character.Submarine == null) { return false; }
// Don't allow going into another sub, unless it's connected and of the same team and type.
@@ -44,6 +44,8 @@ namespace Barotrauma
/// </summary>
public bool AllowStealing { get; set; }
public bool TakeWholeStack { get; set; }
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
@@ -177,7 +179,7 @@ namespace Barotrauma
}
else if (moveToTarget is Item parentItem)
{
canInteract = character.CanInteractWith(parentItem, out _, checkLinked: false);
canInteract = character.CanInteractWith(parentItem, checkLinked: false);
}
if (canInteract)
{
@@ -191,8 +193,20 @@ namespace Barotrauma
return;
}
Inventory itemInventory = targetItem.ParentInventory;
var slots = itemInventory?.FindIndices(targetItem);
if (HumanAIController.TakeItem(targetItem, character.Inventory, equip, storeUnequipped: true))
{
if (TakeWholeStack && slots != null)
{
foreach (int slot in slots)
{
foreach (Item item in itemInventory.GetItemsAt(slot).ToList())
{
HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true);
}
}
}
IsCompleted = true;
}
else
@@ -211,7 +225,16 @@ namespace Barotrauma
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach)
{
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
abortCondition = () => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
abortCondition = obj =>
{
bool abort = targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget;
if (abort)
{
// Fail silently if someone takes the suit.
obj.speakIfFails = false;
}
return abort;
},
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString()
};
@@ -256,7 +279,7 @@ namespace Barotrauma
if (mySub == null) { continue; }
if (!AllowStealing)
{
if (character.TeamID == Character.TeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; }
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; }
}
if (!CheckItem(item)) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
@@ -276,6 +299,10 @@ namespace Barotrauma
itemPriority = GetItemPriority(item);
}
Entity rootInventoryOwner = item.GetRootInventoryOwner();
if (rootInventoryOwner is Item ownerItem)
{
if (!ownerItem.IsInteractable(character)) { continue; }
}
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
@@ -308,7 +335,7 @@ namespace Barotrauma
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) =>
{
targetItem = spawnedItem;
if (character.TeamID == Character.TeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
{
spawnedItem.SpawnedInOutpost = true;
}
@@ -347,7 +374,7 @@ namespace Barotrauma
private bool CheckItem(Item item)
{
if (item.NonInteractable) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (item.IsThisOrAnyContainerIgnoredByAI()) { return false; }
if (ignoredItems.Contains(item)) { return false; };
if (item.Condition < TargetCondition) { return false; }
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -22,18 +23,22 @@ namespace Barotrauma
/// <summary>
/// Aborts the objective when this condition is true
/// </summary>
public Func<bool> abortCondition;
public Func<AIObjectiveGoTo, bool> abortCondition;
public Func<PathNode, bool> endNodeFilter;
public Func<float> priorityGetter;
public bool followControlledCharacter;
public bool mimic;
public bool speakIfFails = true;
public float extraDistanceWhileSwimming;
public float extraDistanceOutsideSub;
private float _closeEnough = 50;
private readonly float minDistance = 50;
private readonly float seekGapsInterval = 1;
private float seekGapsTimer;
/// <summary>
/// Display units
/// </summary>
@@ -76,7 +81,7 @@ namespace Barotrauma
public override float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed)
{
Priority = 0;
@@ -110,12 +115,14 @@ namespace Barotrauma
}
else
{
Priority = isOrder ? AIObjectiveManager.OrderPriority : 10;
Priority = isOrder ? objectiveManager.GetOrderPriority(this) : 10;
}
}
return Priority;
}
private readonly float avoidLookAheadDistance = 5;
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
: base(character, objectiveManager, priorityModifier)
{
@@ -140,10 +147,11 @@ namespace Barotrauma
private void SpeakCannotReach()
{
if (!character.IsOnPlayerTeam) { return; }
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
#endif
if (objectiveManager.CurrentOrder != null && DialogueIdentifier != null)
if (objectiveManager.HasOrders() && DialogueIdentifier != null && speakIfFails)
{
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character));
if (msg != null)
@@ -157,10 +165,9 @@ namespace Barotrauma
{
if (followControlledCharacter)
{
if (Character.Controlled == null)
if (Character.Controlled == null || !HumanAIController.IsFriendly(Character.Controlled))
{
Abandon = true;
SteeringManager.Reset();
return;
}
Target = Character.Controlled;
@@ -181,7 +188,6 @@ namespace Barotrauma
if (e.Removed)
{
Abandon = true;
SteeringManager.Reset();
return;
}
else
@@ -193,7 +199,7 @@ namespace Barotrauma
if (!followControlledCharacter)
{
// Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls.
bool containsUnsafeNodes = HumanAIController.CurrentOrder == null && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
bool containsUnsafeNodes = character.IsDismissed && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
&& PathSteering != null && PathSteering.CurrentPath != null
&& PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull))
@@ -208,14 +214,14 @@ namespace Barotrauma
{
Abandon = true;
}
else if (waitUntilPathUnreachable < 0)
else if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty)
{
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty)
SteeringManager.Reset();
if (waitUntilPathUnreachable < 0)
{
if (repeat)
{
SpeakCannotReach();
SteeringManager.Reset();
}
else
{
@@ -223,12 +229,7 @@ namespace Barotrauma
}
}
}
if (Abandon)
{
SpeakCannotReach();
SteeringManager.Reset();
}
else
if (!Abandon)
{
if (getDivingGearIfNeeded && !character.LockHands)
{
@@ -248,13 +249,14 @@ namespace Barotrauma
}
}
bool needsEquipment = false;
float minOxygen = character.Submarine == null ? 0 : AIObjectiveFindDivingGear.MIN_OXYGEN;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (needsEquipment)
{
@@ -286,52 +288,134 @@ namespace Barotrauma
}
}
}
if (!character.AnimController.InWater)
if (character.AnimController.InWater)
{
useScooter = false;
checkScooterTimer = 0;
}
else if (checkScooterTimer <= 0)
{
useScooter = false;
checkScooterTimer = checkScooterTime;
string scooterTag = "scooter";
string batteryTag = "mobilebattery";
Item scooter = null;
bool isScooterEquipped = false;
float closeEnough = 250;
float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition);
bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic ||
(Target is Character targetCharacter && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2));
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, batteryTag, requireEquipped: true))
if (character.CurrentHull == null)
{
scooter = equippedScooters.FirstOrDefault();
isScooterEquipped = scooter != null;
}
else if (shouldUseScooter && HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> scooters, batteryTag, requireEquipped: false))
{
scooter = scooters.FirstOrDefault();
if (scooter != null)
if (seekGapsTimer > 0)
{
isScooterEquipped = HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
}
}
if (scooter != null && isScooterEquipped)
{
if (shouldUseScooter)
{
useScooter = true;
seekGapsTimer -= deltaTime;
}
else
{
// Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.anySlot);
SeekGaps(maxDistance: 500);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
if (TargetGap != null)
{
// Check that nothing is blocking the way
Vector2 rayStart = character.SimPosition;
Vector2 rayEnd = TargetGap.SimPosition;
if (TargetGap.Submarine != null && character.Submarine == null)
{
rayStart -= TargetGap.Submarine.SimPosition;
}
else if (TargetGap.Submarine == null && character.Submarine != null)
{
rayEnd -= character.Submarine.SimPosition;
}
var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
if (closestBody != null)
{
TargetGap = null;
}
}
}
}
else
{
TargetGap = null;
}
if (TargetGap != null)
{
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, TargetGap.FlowTargetHull.WorldPosition, deltaTime))
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
return;
}
else
{
TargetGap = null;
}
}
if (checkScooterTimer <= 0)
{
useScooter = false;
checkScooterTimer = checkScooterTime;
string scooterTag = "scooter";
string batteryTag = "mobilebattery";
Item scooter = null;
float closeEnough = 250;
float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition);
bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic ||
(Target is Character targetCharacter && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2));
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
// Currently equipped scooter
scooter = equippedScooters.FirstOrDefault();
}
else if (shouldUseScooter)
{
bool hasBattery = false;
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
{
// Non-equipped scooter with a battery
scooter = nonEquippedScooters.FirstOrDefault();
hasBattery = true;
}
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
{
// Non-equipped scooter without a battery
scooter = _nonEquippedScooters.FirstOrDefault();
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
}
if (scooter != null && hasBattery)
{
// Equip only if we have a battery available
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
}
}
bool isScooterEquipped = scooter != null && character.HasEquippedItem(scooter);
if (scooter != null && isScooterEquipped)
{
if (shouldUseScooter)
{
useScooter = true;
// Check the battery
if (scooter.ContainedItems.None(i => i.Condition > 0))
{
// Try to switch batteries
if (HumanAIController.HasItem(character, batteryTag, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
{
scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.anySlot));
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
{
useScooter = false;
}
}
else
{
useScooter = false;
}
}
}
if (!useScooter)
{
// Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.anySlot);
}
}
}
else
{
checkScooterTimer -= deltaTime;
}
}
else
{
checkScooterTimer -= deltaTime;
TargetGap = null;
useScooter = false;
checkScooterTimer = 0;
}
if (SteeringManager == PathSteering)
{
@@ -347,7 +431,7 @@ namespace Barotrauma
nodeFilter,
CheckVisibility);
if (!isInside && PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable)
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
{
if (useScooter)
{
@@ -358,7 +442,7 @@ namespace Barotrauma
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 2);
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2);
}
}
}
@@ -378,7 +462,7 @@ namespace Barotrauma
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
}
}
}
@@ -439,6 +523,27 @@ namespace Barotrauma
return null;
}
public Gap TargetGap { get; private set; }
private void SeekGaps(float maxDistance)
{
Gap selectedGap = null;
float selectedDistance = -1;
foreach (Gap gap in Gap.GapList)
{
if (gap.Open < 1) { continue; }
if (gap.FlowTargetHull == null) { continue; }
if (gap.Submarine != Target.Submarine) { continue; }
float distance = Vector2.DistanceSquared(character.WorldPosition, gap.WorldPosition);
if (distance > maxDistance * maxDistance) { continue; }
if (selectedGap == null || distance < selectedDistance)
{
selectedGap = gap;
selectedDistance = distance;
}
}
TargetGap = selectedGap;
}
public bool IsCloseEnough
{
get
@@ -465,7 +570,7 @@ namespace Barotrauma
Abandon = true;
return false;
}
if (abortCondition != null && abortCondition())
if (abortCondition != null && abortCondition(this))
{
Abandon = true;
return false;
@@ -507,12 +612,13 @@ namespace Barotrauma
{
PathSteering.ResetPath();
}
SpeakCannotReach();
base.OnAbandon();
}
private void StopMovement()
{
character.AIController.SteeringManager.Reset();
SteeringManager.Reset();
if (Target != null)
{
character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
@@ -530,6 +636,8 @@ namespace Barotrauma
{
base.Reset();
findDivingGear = null;
seekGapsTimer = 0;
TargetGap = null;
}
}
}
@@ -21,9 +21,9 @@ namespace Barotrauma
set
{
behavior = value;
if (behavior == BehaviorType.StayInHull && character.TeamID != Character.TeamType.FriendlyNPC)
if (behavior == BehaviorType.StayInHull && TargetHull == null)
{
DebugConsole.NewMessage($"AIObjectiveIdle.BehaviorType.StayInHull is implemented only for outpost NPCs. Using passive behavior for {character.Name} ({character.Info.Job.Prefab.Identifier})", color: Color.Red);
DebugConsole.AddWarning($"Trying to set a character's behavior type to StayInHull, but target hull is not set. {character.Name} ({character.Info.Job.Prefab.Identifier})");
behavior = BehaviorType.Passive;
}
switch (behavior)
@@ -203,7 +203,7 @@ namespace Barotrauma
if (currentTarget != null && !currentTargetIsInvalid)
{
if (character.TeamID == Character.TeamType.FriendlyNPC)
if (character.TeamID == CharacterTeamType.FriendlyNPC)
{
if (currentTarget.Submarine.TeamID != character.TeamID)
{
@@ -260,7 +260,7 @@ namespace Barotrauma
{
//choose a random available hull
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isInWrongSub = character.TeamID == Character.TeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID;
bool isInWrongSub = character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID;
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node =>
{
@@ -319,14 +319,14 @@ namespace Barotrauma
public void Wander(float deltaTime)
{
if (character.IsClimbing) { return; }
if (!character.AnimController.InWater)
var currentHull = character.CurrentHull;
if (!character.AnimController.InWater && currentHull != null)
{
standStillTimer -= deltaTime;
if (standStillTimer > 0.0f)
{
walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
var currentHull = character.CurrentHull;
if (currentHull != null && currentHull.Rect.Width > IndoorsSteeringManager.smallRoomSize / 2 && tooCloseCharacter == null)
if (currentHull.Rect.Width > IndoorsSteeringManager.smallRoomSize / 2 && tooCloseCharacter == null)
{
foreach (Character c in Character.CharacterList)
{
@@ -402,6 +402,14 @@ namespace Barotrauma
PathSteering.Wander(deltaTime);
}
public void FaceTargetAndWait(ISpatialEntity target, float waitTime)
{
standStillTimer = waitTime;
HumanAIController.FaceTarget(target);
currentTarget = null;
SetTargetTimerHigh();
}
private void FindTargetHulls()
{
targetHulls.Clear();
@@ -411,7 +419,7 @@ namespace Barotrauma
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
if (hull.Submarine == null) { continue; }
if (character.Submarine == null) { break; }
if (character.TeamID == Character.TeamType.FriendlyNPC)
if (character.TeamID == CharacterTeamType.FriendlyNPC)
{
if (hull.Submarine.TeamID != character.TeamID)
{
@@ -487,7 +495,7 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveCleanupItems.IsValidTarget(item, character) && !ignoredItems.Contains(item))
if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true) && !ignoredItems.Contains(item))
{
itemsToClean.Add(item);
}
@@ -139,13 +139,13 @@ namespace Barotrauma
}
else
{
if (objectiveManager.CurrentOrder == this)
if (objectiveManager.IsOrder(this))
{
Priority = ForceOrderPriority ? AIObjectiveManager.OrderPriority : targetValue;
Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue;
}
else
{
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
float max = AIObjectiveManager.LowestOrderPriority - 1;
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
}
@@ -167,7 +167,7 @@ namespace Barotrauma
foreach (T target in GetList())
{
// The bots always find targets when the objective is an order.
if (objectiveManager.CurrentOrder != this)
if (!objectiveManager.IsOrder(this))
{
// Battery or pump states cannot currently be reported (not implemented) and therefore we must ignore them -> the bots always know if they require attention.
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater;
@@ -1,6 +1,6 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Barotrauma.Networking; // used by the server
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -10,8 +10,8 @@ namespace Barotrauma
{
class AIObjectiveManager
{
// TODO: expose
public const float OrderPriority = 70;
public const float HighestOrderPriority = 70;
public const float LowestOrderPriority = 60;
public const float RunPriority = 50;
// Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 5;
@@ -25,7 +25,6 @@ namespace Barotrauma
public HumanAIController HumanAIController => character.AIController as HumanAIController;
private float _waitTimer;
/// <summary>
/// When set above zero, the character will stand still doing nothing until the timer runs out. Does not affect orders, find safety or combat.
@@ -39,26 +38,25 @@ namespace Barotrauma
}
}
public AIObjective CurrentOrder { get; private set; }
public List<OrderInfo> CurrentOrders { get; } = new List<OrderInfo>();
/// <summary>
/// The AIObjective in <see cref="CurrentOrders"/> with the highest <see cref="AIObjective.Priority"/>
/// </summary>
public AIObjective CurrentOrder
{
get
{
return ForcedOrder ?? currentOrder;
}
private set
{
currentOrder = value;
}
}
private AIObjective currentOrder;
public AIObjective ForcedOrder { get; private set; }
public AIObjective CurrentObjective { get; private set; }
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
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);
public AIObjectiveManager(Character character)
{
this.character = character;
@@ -127,10 +125,14 @@ namespace Barotrauma
{
var orderPrefab = Order.GetPrefab(autonomousObjective.identifier);
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); }
var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID)?.GetRandom() : null;
Item item = null;
if (orderPrefab.MustSetTarget)
{
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandom();
}
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != Character.TeamType.FriendlyNPC) { continue; }
if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { continue; }
var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier);
if (objective != null && objective.CanBeCompleted)
{
@@ -196,21 +198,34 @@ namespace Barotrauma
public void UpdateObjectives(float deltaTime)
{
if (CurrentOrder != null)
UpdateOrderObjective(ForcedOrder);
if (CurrentOrders.Any())
{
foreach(var order in CurrentOrders)
{
var orderObjective = order.Objective;
UpdateOrderObjective(orderObjective);
}
}
void UpdateOrderObjective(AIObjective orderObjective)
{
if (orderObjective == null) { return; }
#if DEBUG
// Note: don't automatically remove orders here. Removing orders needs to be done via dismissing.
if (CurrentOrder.IsCompleted)
if (orderObjective.IsCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red);
}
else if (!CurrentOrder.CanBeCompleted)
else if (!orderObjective.CanBeCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag}, CANNOT BE COMPLETED.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag}, CANNOT BE COMPLETED.", Color.Red);
}
#endif
CurrentOrder.Update(deltaTime);
orderObjective.Update(deltaTime);
}
if (WaitTimer > 0)
{
WaitTimer -= deltaTime;
@@ -244,7 +259,29 @@ namespace Barotrauma
public void SortObjectives()
{
CurrentOrder?.GetPriority();
ForcedOrder?.GetPriority();
AIObjective orderWithHighestPriority = null;
float highestPriority = 0;
foreach (var currentOrder in CurrentOrders)
{
var orderObjective = currentOrder.Objective;
if (orderObjective == null) { continue; }
orderObjective.GetPriority();
if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority)
{
orderWithHighestPriority = orderObjective;
highestPriority = orderObjective.Priority;
}
}
#if SERVER
if (orderWithHighestPriority != null && orderWithHighestPriority != currentOrder)
{
GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.ObjectiveManagerOrderState });
}
#endif
CurrentOrder = orderWithHighestPriority;
for (int i = Objectives.Count - 1; i >= 0; i--)
{
Objectives[i].GetPriority();
@@ -253,6 +290,7 @@ namespace Barotrauma
{
Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
}
GetCurrentObjective()?.SortSubObjectives();
}
@@ -268,12 +306,18 @@ namespace Barotrauma
}
}
public void SetOrder(AIObjective objective)
public void SetForcedOrder(AIObjective objective)
{
CurrentOrder = objective;
ForcedOrder = objective;
}
public void SetOrder(Order order, string option, Character orderGiver)
public void ClearForcedOrder()
{
ForcedOrder = null;
}
private CoroutineHandle speakRoutine;
public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak)
{
if (character.IsDead)
{
@@ -284,8 +328,53 @@ namespace Barotrauma
#endif
}
ClearIgnored();
CurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false);
if (CurrentOrder == null)
if (order == null || order.Identifier == "dismissed")
{
if (!string.IsNullOrEmpty(option))
{
if (CurrentOrders.Any(o => o.MatchesDismissedOrder(option)))
{
var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(option));
CurrentOrders.Remove(dismissedOrderInfo);
}
}
else
{
CurrentOrders.Clear();
}
}
// Make sure the order priorities reflect those set by the player
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
{
var currentOrder = CurrentOrders[i];
if (currentOrder.Objective == null || currentOrder.MatchesOrder(order, option))
{
CurrentOrders.RemoveAt(i);
continue;
}
var currentOrderInfo = character.GetCurrentOrder(currentOrder.Order, currentOrder.OrderOption);
if (currentOrderInfo.HasValue)
{
int currentPriority = currentOrderInfo.Value.ManualPriority;
if (currentOrder.ManualPriority != currentPriority)
{
CurrentOrders[i] = new OrderInfo(currentOrder, currentPriority);
}
}
else
{
CurrentOrders.RemoveAt(i);
}
}
var newCurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false);
if (newCurrentOrder != null)
{
CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder));
}
if (!HasOrders())
{
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
CreateAutonomousObjectives();
@@ -293,13 +382,57 @@ namespace Barotrauma
else
{
// This should be redundant, because all the objectives are reset when they are selected as active.
CurrentOrder.Reset();
newCurrentOrder?.Reset();
if (speak && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
//if (speakRoutine != null)
//{
// CoroutineManager.StopCoroutines(speakRoutine);
//}
//speakRoutine = CoroutineManager.InvokeAfter(() =>
//{
// if (GameMain.GameSession == null || Level.Loaded == null) { return; }
// if (newCurrentOrder != null && character.SpeechImpediment < 100.0f)
// {
// if (newCurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets");
// }
// else if (newCurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries");
// }
// else if (newCurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire");
// }
// else if (newCurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks");
// }
// else if (newCurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies");
// }
// else if (newCurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
// }
// else if (newCurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps");
// }
// }
//}, 3);
}
}
}
public AIObjective CreateObjective(Order order, string option, Character orderGiver, bool isAutonomous, float priorityModifier = 1)
{
if (order == null) { return null; }
if (order == null || order.Identifier == "dismissed") { return null; }
AIObjective newObjective;
switch (order.Identifier.ToLowerInvariant())
{
@@ -320,8 +453,7 @@ namespace Barotrauma
case "wait":
newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
{
AllowGoingOutside = order.TargetSpatialEntity == null ? character.CurrentHull == null :
character.Submarine == null || character.Submarine != order.TargetSpatialEntity.Submarine
AllowGoingOutside = character.Submarine == null || (order.TargetSpatialEntity != null && character.Submarine != order.TargetSpatialEntity.Submarine)
};
break;
case "fixleaks":
@@ -345,7 +477,7 @@ namespace Barotrauma
case "pumpwater":
if (order.TargetItemComponent is Pump targetPump)
{
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier)
{
IsLoop = true,
@@ -370,7 +502,7 @@ namespace Barotrauma
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; }
if (order.TargetItemComponent == null) { return null; }
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
@@ -383,7 +515,7 @@ namespace Barotrauma
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, false, priorityModifier: priorityModifier)
{
IsLoop = false,
Override = character.CurrentOrder != null,
Override = !character.IsDismissed,
completionCondition = () =>
{
if (float.TryParse(option, out float pct))
@@ -403,11 +535,26 @@ namespace Barotrauma
};
break;
case "cleanupitems":
newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier, order.TargetEntity as Item);
if (order.TargetEntity is Item targetItem)
{
if (targetItem.HasTag("allowcleanup") && targetItem.ParentInventory == null && targetItem.OwnInventory != null)
{
// Target all items inside the container
newObjective = new AIObjectiveCleanupItems(character, this, targetItem.OwnInventory.AllItems, priorityModifier);
}
else
{
newObjective = new AIObjectiveCleanupItems(character, this, targetItem, priorityModifier);
}
}
else
{
newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier: priorityModifier);
}
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
@@ -421,21 +568,9 @@ namespace Barotrauma
return newObjective;
}
private void DismissSelf()
{
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
{
GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), null, character);
}
#else
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), null, null, character, character));
#endif
}
private bool IsAllowedToWait()
{
if (CurrentOrder != null) { return false; }
if (HasOrders()) { return false; }
if (CurrentObjective is AIObjectiveCombat || CurrentObjective is AIObjectiveFindSafety) { return false; }
if (character.AnimController.InWater) { return false; }
if (character.IsClimbing) { return false; }
@@ -446,5 +581,61 @@ namespace Barotrauma
if (AIObjectiveIdle.IsForbidden(character.CurrentHull)) { return false; }
return true;
}
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
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);
public bool IsOrder(AIObjective objective)
{
return objective == ForcedOrder || CurrentOrders.Any(o => o.Objective == objective);
}
public bool HasOrders()
{
return ForcedOrder != null || CurrentOrders.Any();
}
public bool HasOrder<T>() where T : AIObjective
{
return ForcedOrder is T || CurrentOrders.Any(o => o.Objective is T);
}
public float GetOrderPriority(AIObjective objective)
{
if (objective == ForcedOrder) { return HighestOrderPriority; }
var currentOrder = CurrentOrders.FirstOrDefault(o => o.Objective == objective);
if (currentOrder.Objective == null)
{
return HighestOrderPriority;
}
else if (currentOrder.ManualPriority > 0)
{
return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority));
}
#if DEBUG
DebugConsole.AddWarning("Error in order priority: shouldn't return 0!");
#endif
return 0;
}
public OrderInfo? GetCurrentOrderInfo()
{
if (currentOrder == null) { return null; }
return CurrentOrders.FirstOrDefault(o => o.Objective == CurrentOrder);
}
}
}
@@ -36,7 +36,7 @@ namespace Barotrauma
public override float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed || character.LockHands)
{
Priority = 0;
@@ -51,7 +51,7 @@ namespace Barotrauma
{
if (isOrder)
{
Priority = AIObjectiveManager.OrderPriority;
Priority = objectiveManager.GetOrderPriority(this);
}
ItemComponent target = GetTarget();
Item targetItem = target?.Item;
@@ -69,7 +69,7 @@ namespace Barotrauma
{
if (!isOrder)
{
if (reactor.LastUserWasPlayer && character.TeamID != Character.TeamType.FriendlyNPC ||
if (reactor.LastUserWasPlayer && character.TeamID != CharacterTeamType.FriendlyNPC ||
HumanAIController.IsTrueForAnyCrewMember(c =>
c.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() == target))
{
@@ -89,11 +89,16 @@ namespace Barotrauma
case "powerup":
// Check that we don't already have another order that is targeting the same item.
// Without this the autonomous objective will tell the bot to turn the reactor on again.
if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target && operateOrder.Option != Option)
if (IsAnotherOrderTargetingSameItem(objectiveManager.ForcedOrder) || objectiveManager.CurrentOrders.Any(o => IsAnotherOrderTargetingSameItem(o.Objective)))
{
Priority = 0;
return Priority;
}
bool IsAnotherOrderTargetingSameItem(AIObjective objective)
{
return objective is AIObjectiveOperateItem operateObjective && operateObjective != this && operateObjective.GetTarget() == target && operateObjective.Option != Option;
}
break;
}
}
@@ -101,20 +106,30 @@ namespace Barotrauma
targetItem.Submarine != character.Submarine && !isOrder ||
targetItem.CurrentHull.FireSources.Any() ||
HumanAIController.IsItemOperatedByAnother(target, out _) ||
Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))
|| component.Item.IgnoreByAI || (useController && controller.Item.IgnoreByAI))
{
Priority = 0;
}
else
{
float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier);
float max = isOrder ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1;
if (!isOrder && reactor != null && reactor.PowerOn && Option == "powerup")
if (isOrder)
{
// Decrease the priority when targeting a reactor that is already on.
value /= 2;
float max = objectiveManager.GetOrderPriority(this);
float value = CumulatedDevotion + (max * PriorityModifier);
Priority = MathHelper.Clamp(value, 0, max);
}
else
{
float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier);
float max = AIObjectiveManager.LowestOrderPriority - 1;
if (reactor != null && reactor.PowerOn && Option == "powerup")
{
// Decrease the priority when targeting a reactor that is already on.
value /= 2;
}
Priority = MathHelper.Clamp(value, 0, max);
}
Priority = MathHelper.Clamp(value, 0, max);
}
}
return Priority;
@@ -137,7 +152,7 @@ namespace Barotrauma
throw new Exception("target null");
#endif
}
else if (target.Item.NonInteractable)
else if (!target.Item.IsInteractable(character))
{
Abandon = true;
}
@@ -153,25 +168,13 @@ namespace Barotrauma
ItemComponent target = GetTarget();
if (useController && controller == null)
{
character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f);
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f);
}
Abandon = true;
return;
}
// If this is not an order...
if (objectiveManager.CurrentOrder != this)
{
// Don't allow to operate an item that someone with a better skills already operates
if (HumanAIController.IsItemOperatedByAnother(target, out _))
{
// Don't abandon
return;
}
if (component.Item.IgnoreByAI || (useController && controller.Item.IgnoreByAI))
{
Abandon = true;
return;
}
}
if (operateTarget != null)
{
if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
@@ -215,7 +218,7 @@ namespace Barotrauma
Abandon = true;
return;
}
else if (!character.Inventory.Items.Contains(component.Item))
else if (!character.Inventory.Contains(component.Item))
{
TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true),
onAbandon: () => Abandon = true,
@@ -241,13 +244,14 @@ namespace Barotrauma
continue;
}
//equip slot already taken
if (character.Inventory.Items[i] != null)
var existingItem = character.Inventory.GetItemAt(i);
if (existingItem != null)
{
//try to put the item in an Any slot, and drop it if that fails
if (!character.Inventory.Items[i].AllowedSlots.Contains(InvSlotType.Any) ||
!character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List<InvSlotType>() { InvSlotType.Any }))
if (!existingItem.AllowedSlots.Contains(InvSlotType.Any) ||
!character.Inventory.TryPutItem(existingItem, character, new List<InvSlotType>() { InvSlotType.Any }))
{
character.Inventory.Items[i].Drop(character);
existingItem.Drop(character);
}
}
if (character.Inventory.TryPutItem(component.Item, i, true, false, character))
@@ -28,7 +28,7 @@ namespace Barotrauma
{
if (pump == null) { return false; }
if (pump.Item.IgnoreByAI) { return false; }
if (pump.Item.NonInteractable) { return false; }
if (!pump.Item.IsInteractable(character)) { return false; }
if (pump.Item.HasTag("ballast")) { return false; }
if (pump.Item.Submarine == null) { return false; }
if (pump.Item.CurrentHull == null) { return false; }
@@ -33,10 +33,14 @@ namespace Barotrauma
public override float GetPriority()
{
if (!IsAllowed)
if (!IsAllowed || Item.IgnoreByAI)
{
Priority = 0;
Abandon = true;
if (IsRepairing())
{
Item.Repairables.ForEach(r => r.StopRepairing(character));
}
return Priority;
}
// TODO: priority list?
@@ -55,13 +59,13 @@ namespace Barotrauma
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 4000, dist));
}
float requiredSuccessFactor = objectiveManager.IsCurrentOrder<AIObjectiveRepairItems>() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor;
float requiredSuccessFactor = objectiveManager.HasOrder<AIObjectiveRepairItems>() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor;
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100;
bool isSelected = IsRepairing();
float selectedBonus = isSelected ? 100 - MaxDevotion : 0;
float devotion = (CumulatedDevotion + selectedBonus) / 100;
float reduction = isPriority ? 1 : isSelected ? 2 : 3;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
float max = AIObjectiveManager.LowestOrderPriority - reduction;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
@@ -70,7 +74,7 @@ namespace Barotrauma
protected override bool Check()
{
IsCompleted = Item.IsFullCondition;
if (IsCompleted && IsRepairing())
if (character.IsOnPlayerTeam && IsCompleted && IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
}
@@ -93,7 +97,10 @@ namespace Barotrauma
var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true);
if (objectiveManager.IsCurrentOrder<AIObjectiveRepairItems>())
{
getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair"), null, 0.0f, "dialogcannotfindrequireditemtorepair", 10.0f);
if (character.IsOnPlayerTeam)
{
getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair"), null, 0.0f, "dialogcannotfindrequireditemtorepair", 10.0f);
}
}
subObjectives.Add(getItemObjective);
}
@@ -107,8 +114,7 @@ namespace Barotrauma
}
if (repairTool != null)
{
var containedItems = repairTool.Item.OwnInventory?.Items;
if (containedItems == null)
if (repairTool.Item.OwnInventory == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory");
@@ -116,27 +122,20 @@ namespace Barotrauma
Abandon = true;
return;
}
// Drop empty tanks
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
{
containedItem.Drop(character);
}
}
// Eject empty tanks
HumanAIController.UnequipEmptyItems(repairTool.Item);
RelatedItem item = null;
Item fuel = null;
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
{
item = requiredItem;
fuel = containedItems.FirstOrDefault(it => it != null && it.Condition > 0.0f && requiredItem.MatchesItem(it));
fuel = repairTool.Item.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it));
if (fuel != null) { break; }
}
if (fuel == null)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
onCompleted: () => RemoveSubObjective(ref refuelObjective),
onAbandon: () => Abandon = true);
return;
@@ -178,7 +177,7 @@ namespace Barotrauma
}
if (Abandon)
{
if (IsRepairing())
if (character.IsOnPlayerTeam && IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
@@ -213,7 +212,7 @@ namespace Barotrauma
onAbandon: () =>
{
Abandon = true;
if (IsRepairing())
if (character.IsOnPlayerTeam && IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
@@ -229,7 +228,7 @@ namespace Barotrauma
{
foreach (RelatedItem requiredItem in kvp.Value)
{
foreach (var item in character.Inventory.Items)
foreach (var item in character.Inventory.AllItems)
{
if (requiredItem.MatchesItem(item))
{
@@ -104,7 +104,7 @@ namespace Barotrauma
}
bool anyFixers = otherFixers > 0;
float ratio = anyFixers ? items / (float)otherFixers : 1;
if (objectiveManager.CurrentOrder == this)
if (objectiveManager.IsOrder(this))
{
return Targets.Sum(t => 100 - t.ConditionPercentage);
}
@@ -149,7 +149,7 @@ namespace Barotrauma
{
if (item == null) { return false; }
if (item.IgnoreByAI) { return false; }
if (item.NonInteractable) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (item.IsFullCondition) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null || character.Submarine == null) { return false; }
@@ -85,7 +85,7 @@ namespace Barotrauma
Item suit = suits.FirstOrDefault();
if (suit != null)
{
AIObjectiveFindDivingGear.DropEmptyTanks(character, suit, out _);
AIObjectiveFindDivingGear.EjectEmptyTanks(character, suit, out _);
}
}
else if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out IEnumerable<Item> masks, requireEquipped: true))
@@ -93,7 +93,7 @@ namespace Barotrauma
Item mask = masks.FirstOrDefault();
if (mask != null)
{
AIObjectiveFindDivingGear.DropEmptyTanks(character, mask, out _);
AIObjectiveFindDivingGear.EjectEmptyTanks(character, mask, out _);
}
}
bool ShouldRemoveDivingSuit() => targetCharacter.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && targetCharacter.CurrentHull?.LethalPressure <= 0;
@@ -101,7 +101,7 @@ namespace Barotrauma
{
suits.ForEach(suit => suit.Drop(character));
}
else if (suits.Any() && suits.None(s => s.OwnInventory?.Items != null && s.OwnInventory.Items.Any(it => it != null && it.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && it.ConditionPercentage > 0)))
else if (suits.Any() && suits.None(s => s.OwnInventory?.AllItems != null && s.OwnInventory.AllItems.Any(it => it.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && it.ConditionPercentage > 0)))
{
// The target has a suit equipped with an empty oxygen tank.
// Can't remove the suit, because the target needs it.
@@ -322,7 +322,7 @@ namespace Barotrauma
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
}
if (targetCharacter != character)
if (targetCharacter != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" },
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
@@ -331,9 +331,16 @@ namespace Barotrauma
character.DeselectCharacter();
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
onCompleted: () => RemoveSubObjective(ref getItemObjective),
onAbandon: () => RemoveSubObjective(ref getItemObjective));
onAbandon: () =>
{
Abandon = true;
if (character != targetCharacter && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f);
}
});
}
}
}
@@ -380,7 +387,7 @@ namespace Barotrauma
return false;
}
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
if (isCompleted && targetCharacter != character)
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
@@ -427,6 +434,13 @@ namespace Barotrauma
replaceOxygenObjective = null;
safeHull = null;
ignoreOxygen = false;
character.SelectedCharacter = null;
}
public override void OnDeselected()
{
character.SelectedCharacter = null;
base.OnDeselected();
}
}
}
@@ -14,7 +14,7 @@ namespace Barotrauma
public override bool AllowInAnySub => true;
private const float vitalityThreshold = 75;
private const float vitalityThresholdForOrders = 85;
private const float vitalityThresholdForOrders = 90;
public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
{
if (manager == null)
@@ -23,7 +23,10 @@ namespace Barotrauma
}
else
{
return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? vitalityThresholdForOrders : vitalityThreshold;
// When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored.
// If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players.
// On the other hand, if the bots too eagerly heal characters when it's not nevessary, it's inefficient and can feel frustrating, because it can't be controlled.
return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
}
}
@@ -37,7 +40,7 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
if (Targets.None()) { return 100; }
if (objectiveManager.CurrentOrder != this)
if (!objectiveManager.IsOrder(this))
{
if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious))
{
@@ -19,27 +19,62 @@ namespace Barotrauma
struct OrderInfo
{
public string ComponentIdentifier { get; set; }
public Order Order { get; private set; }
public string OrderOption { get; private set; }
public Order Order { get; }
public string OrderOption { get; }
public int ManualPriority { get; }
public OrderType Type { get; }
public AIObjective Objective { get; }
public bool IsCurrentOrder => Type == OrderType.Current;
public OrderInfo(Order order, string orderOption)
public enum OrderType
{
Current,
Previous
}
private OrderInfo(Order order, string orderOption, int manualPriority, OrderType orderType, AIObjective objective)
{
ComponentIdentifier = "currentorder";
Order = order;
OrderOption = orderOption;
ManualPriority = Math.Min(manualPriority, CharacterInfo.HighestManualOrderPriority);
Type = orderType;
Objective = objective;
}
public OrderInfo(OrderInfo orderInfo)
{
ComponentIdentifier = "previousorder";
Order = orderInfo.Order;
OrderOption = orderInfo.OrderOption;
}
public OrderInfo(Order order, string orderOption, int manualPriority) : this(order, orderOption, manualPriority, OrderType.Current, null) { }
public OrderInfo(Order order, string orderOption, int manualPriority, AIObjective objective) : this(order, orderOption, manualPriority, OrderType.Current, objective) { }
public OrderInfo(OrderInfo orderInfo, int manualPriority) : this(orderInfo.Order, orderInfo.OrderOption, manualPriority, orderInfo.Type, orderInfo.Objective) { }
public OrderInfo(OrderInfo orderInfo, OrderType type) : this(orderInfo.Order, orderInfo.OrderOption, orderInfo.ManualPriority, type, orderInfo.Objective) { }
public bool MatchesOrder(string orderIdentifier, string orderOption) =>
(orderIdentifier == Order?.Identifier || (string.IsNullOrEmpty(orderIdentifier) && string.IsNullOrEmpty(Order?.Identifier))) &&
(orderOption == OrderOption || (string.IsNullOrEmpty(orderOption) && string.IsNullOrEmpty(OrderOption)));
public bool MatchesOrder(Order order, string option) =>
order.Identifier == Order.Identifier &&
option == OrderOption;
MatchesOrder(order?.Identifier, option);
public bool MatchesOrder(OrderInfo orderInfo) =>
MatchesOrder(orderInfo.Order?.Identifier, orderInfo.OrderOption);
public bool MatchesDismissedOrder(string dismissOrderOption)
{
string[] dismissedOrder = dismissOrderOption?.Split('.');
if (dismissedOrder != null && dismissedOrder.Length > 0)
{
string dismissedOrderIdentifier = dismissedOrder.Length > 0 ? dismissedOrder[0] : null;
if (dismissedOrderIdentifier == null || dismissedOrderIdentifier != Order?.Identifier) { return false; }
string dismissedOrderOption = dismissedOrder.Length > 1 ? dismissedOrder[1] : null;
if (dismissedOrderOption == null && string.IsNullOrEmpty(OrderOption)) { return true; }
return dismissedOrderOption == OrderOption;
}
else
{
return false;
}
}
}
class Order
@@ -59,6 +94,10 @@ namespace Barotrauma
public Order Prefab { get; private set; }
public readonly string Name;
/// <summary>
/// Name that can be used with the contextual version of the order
/// </summary>
public readonly string ContextualName;
public readonly Sprite SymbolSprite;
@@ -97,7 +136,6 @@ namespace Barotrauma
public bool TargetAllCharacters { get; }
public bool IsReport => TargetAllCharacters && !MustSetTarget;
public readonly float FadeOutTime;
public Entity TargetEntity;
@@ -119,9 +157,9 @@ namespace Barotrauma
private readonly Dictionary<string, Sprite> minimapIcons;
public Dictionary<string, Sprite> MinimapIcons => IsPrefab ? minimapIcons : Prefab.minimapIcons;
public readonly float Weight;
public readonly bool MustSetTarget;
public readonly string AppropriateSkill;
public readonly bool Hidden;
public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1;
public bool IsPrefab { get; private set; }
@@ -159,6 +197,11 @@ namespace Barotrauma
public int? WallSectionIndex { get; }
public bool IsIgnoreOrder { get; }
/// <summary>
/// Should the order icon be drawn when the order target is inside a container
/// </summary>
public bool DrawIconWhenContained { get; }
public static void Init()
{
Prefabs = new Dictionary<string, Order>();
@@ -239,7 +282,8 @@ namespace Barotrauma
private Order(XElement orderElement)
{
Identifier = orderElement.GetAttributeString("identifier", "");
Name = TextManager.Get("OrderName." + Identifier, true) ?? "Name not found";
Name = TextManager.Get("OrderName." + Identifier, returnNull: true) ?? "Name not found";
ContextualName = TextManager.Get("OrderNameContextual." + Identifier, returnNull: true) ?? Name;
string targetItemType = orderElement.GetAttributeString("targetitemtype", "");
if (!string.IsNullOrWhiteSpace(targetItemType))
@@ -267,6 +311,7 @@ namespace Barotrauma
if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null);
Hidden = orderElement.GetAttributeBool("hidden", false);
var optionNames = TextManager.Get("OrderOptions." + Identifier, true)?.Split(',', '') ??
orderElement.GetAttributeStringArray("optionnames", new string[0]);
@@ -315,6 +360,7 @@ namespace Barotrauma
IsPrefab = true;
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis";
DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false);
}
/// <summary>
@@ -324,23 +370,26 @@ namespace Barotrauma
{
Prefab = prefab.Prefab ?? prefab;
Name = prefab.Name;
Identifier = prefab.Identifier;
ItemComponentType = prefab.ItemComponentType;
CanTypeBeSubclass = prefab.CanTypeBeSubclass;
TargetItems = prefab.TargetItems;
Options = prefab.Options;
SymbolSprite = prefab.SymbolSprite;
Color = prefab.Color;
UseController = prefab.UseController;
TargetAllCharacters = prefab.TargetAllCharacters;
AppropriateJobs = prefab.AppropriateJobs;
FadeOutTime = prefab.FadeOutTime;
MustSetTarget = prefab.MustSetTarget;
AppropriateSkill = prefab.AppropriateSkill;
Category = prefab.Category;
MustManuallyAssign = prefab.MustManuallyAssign;
IsIgnoreOrder = prefab.IsIgnoreOrder;
Name = prefab.Name;
ContextualName = prefab.ContextualName;
Identifier = prefab.Identifier;
ItemComponentType = prefab.ItemComponentType;
CanTypeBeSubclass = prefab.CanTypeBeSubclass;
TargetItems = prefab.TargetItems;
Options = prefab.Options;
SymbolSprite = prefab.SymbolSprite;
Color = prefab.Color;
UseController = prefab.UseController;
TargetAllCharacters = prefab.TargetAllCharacters;
AppropriateJobs = prefab.AppropriateJobs;
FadeOutTime = prefab.FadeOutTime;
MustSetTarget = prefab.MustSetTarget;
AppropriateSkill = prefab.AppropriateSkill;
Category = prefab.Category;
MustManuallyAssign = prefab.MustManuallyAssign;
IsIgnoreOrder = prefab.IsIgnoreOrder;
DrawIconWhenContained = prefab.DrawIconWhenContained;
Hidden = prefab.Hidden;
OrderGiver = orderGiver;
TargetEntity = targetEntity;
@@ -351,9 +400,7 @@ namespace Barotrauma
ConnectedController = targetItem.Item?.FindController();
if (ConnectedController == null)
{
#if DEBUG
throw new Exception("Tried to use controller, but couldn't find one");
#endif
DebugConsole.AddWarning("AI: Tried to use a controller for operating an item, but couldn't find any.");
UseController = false;
}
}
@@ -400,7 +447,7 @@ namespace Barotrauma
orderOption ??= "";
string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + Identifier;
if (!string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; }
if (Identifier != "dismissed" && !string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; }
if (targetCharacterName == null) { targetCharacterName = ""; }
if (targetRoomName == null) { targetRoomName = ""; }
@@ -433,7 +480,8 @@ namespace Barotrauma
return firstMatchingComponent != null;
}
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, Character.TeamType? requiredTeam = null)
/// <param name="interactableFor">Only returns items which are interactable for this character</param>
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null)
{
List<Item> matchingItems = new List<Item>();
if (submarine == null) { return matchingItems; }
@@ -456,16 +504,23 @@ namespace Barotrauma
{
matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType) && !i.TryFindController(out _));
}
if (interactableFor != null)
{
matchingItems.RemoveAll(it => !it.IsInteractable(interactableFor) ||
(UseController && it.FindController() is Controller c && !c.Item.IsInteractable(interactableFor)));
}
}
return matchingItems;
}
public List<Item> GetMatchingItems(bool mustBelongToPlayerSub)
/// <param name="interactableFor">Only returns items which are interactable for this character</param>
public List<Item> GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null)
{
Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1 ?
Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == CharacterTeamType.Team2 && Submarine.MainSubs.Length > 1 ?
Submarine.MainSubs[1] :
Submarine.MainSub;
return GetMatchingItems(submarine, mustBelongToPlayerSub);
return GetMatchingItems(submarine, mustBelongToPlayerSub, interactableFor: interactableFor);
}
public string GetOptionName(string id)
@@ -478,5 +533,23 @@ namespace Barotrauma
if (index < 0 || index >= Options.Length) { return null; }
return GetOptionName(Options[index]);
}
/// <summary>
/// Used to create the order option for the Dismiss order to know which order it targets
/// </summary>
/// <param name="orderInfo">The order to target with the dismiss order</param>
public static string GetDismissOrderOption(OrderInfo orderInfo)
{
if (orderInfo.Order != null)
{
string option = orderInfo.Order.Identifier;
if (!string.IsNullOrEmpty(orderInfo.OrderOption))
{
option += $".{orderInfo.OrderOption}";
}
return option;
}
return "";
}
}
}
@@ -254,11 +254,8 @@ namespace Barotrauma
{
if (AiController.Character.Inventory != null)
{
var items = AiController.Character.Inventory.Items;
for (int i = 0; i < items.Length; i++)
foreach (Item item in AiController.Character.Inventory.AllItems)
{
var item = items[i];
if (item == null) { continue; }
var tag = item.GetComponent<NameTag>();
if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName))
{
@@ -358,7 +355,7 @@ namespace Barotrauma
XElement petElement = new XElement("pet",
new XAttribute("speciesname", c.SpeciesName),
new XAttribute("ownerid", petBehavior.Owner?.ID ?? Entity.NullEntityID),
new XAttribute("ownerhash", petBehavior.Owner?.Info?.GetIdentifier() ?? 0),
new XAttribute("seed", c.Seed));
var petBehaviorElement = new XElement("petbehavior",
@@ -387,16 +384,19 @@ namespace Barotrauma
{
string speciesName = subElement.GetAttributeString("speciesname", "");
string seed = subElement.GetAttributeString("seed", "123");
ushort ownerID = (ushort)subElement.GetAttributeInt("ownerid", 0);
int ownerHash = subElement.GetAttributeInt("ownerhash", 0);
Vector2 spawnPos = Vector2.Zero;
Character owner = Entity.FindEntityByID(ownerID) as Character;
if (owner != null)
Character owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == ownerHash);
if (owner != null && owner.Submarine?.Info.Type == SubmarineType.Player)
{
spawnPos = owner.WorldPosition;
}
else
{
var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandom();
//try to find a spawnpoint in the main sub
var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandom();
//if not found, try any player sub (shuttle/drone etc)
spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandom();
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
}
var pet = Character.Create(speciesName, spawnPos, seed);
@@ -94,20 +94,24 @@ namespace Barotrauma
{
Vector2 targetVel = target - host.SimPosition;
if (targetVel.LengthSquared() < 0.00001f) return Vector2.Zero;
if (targetVel.LengthSquared() < 0.00001f) { return Vector2.Zero; }
targetVel = Vector2.Normalize(targetVel) * weight;
Vector2 newSteering = targetVel - host.Steering;
// TODO: the code below doesn't quite work as it should, and I'm not sure what the purpose of it is/was.
// So, we'll just return the targetVel for now, as it produces smooth results.
return targetVel;
if (newSteering == Vector2.Zero) return Vector2.Zero;
//Vector2 newSteering = targetVel - host.Steering;
float steeringSpeed = (newSteering + host.Steering).Length();
if (steeringSpeed > Math.Abs(weight))
{
newSteering = Vector2.Normalize(newSteering) * Math.Abs(weight);
}
//if (newSteering == Vector2.Zero) return Vector2.Zero;
return newSteering;
//float steeringSpeed = (newSteering + host.Steering).Length();
//if (steeringSpeed > Math.Abs(weight))
//{
// newSteering = Vector2.Normalize(newSteering) * Math.Abs(weight);
//}
//return newSteering;
}
protected virtual Vector2 DoSteeringWander(float weight)
@@ -35,7 +35,7 @@ namespace Barotrauma
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, string tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.prefab != null && IsThalamus(e.prefab, tag));
private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.Category == MapEntityCategory.Thalamus || entityPrefab.Tags.Contains(tag);
private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
public WreckAI(Submarine wreck)
{
@@ -131,7 +131,7 @@ namespace Barotrauma
if (container == null) { continue; }
for (int i = 0; i < container.Inventory.Capacity; i++)
{
if (container.Inventory.Items[i] != null) { continue; }
if (container.Inventory.GetItemAt(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)
{
@@ -246,7 +246,7 @@ namespace Barotrauma
initialCellsSpawned = true;
}
private void Kill()
public void Kill()
{
thalamusItems.ForEach(i => i.Condition = 0);
foreach (var turret in turrets)
@@ -319,25 +319,33 @@ namespace Barotrauma
private readonly List<Hull> populatedHulls = new List<Hull>();
private float cellSpawnTimer;
private float CellSpawnTime => Config.AgentSpawnDelay;
private float CellSpawnRandomFactor => Config.AgentSpawnDelayRandomFactor;
private int MinCellsPerBrainRoom => Config.MinAgentsPerBrainRoom;
private int MaxCellsPerRoom => Config.MaxAgentsPerRoom;
private int MinCellsOutside => Config.MinAgentsOutside;
private int MaxCellsOutside => Config.MaxAgentsOutside;
private int MinCellsInside => Config.MinAgentsInside;
private int MaxCellsInside => Config.MaxAgentsInside;
private int MaxCellCount => Config.MaxAgentCount;
private int MinCellsPerBrainRoom => CalculateCellCount(0, Config.MinAgentsPerBrainRoom);
private int MaxCellsPerRoom => CalculateCellCount(1, Config.MaxAgentsPerRoom);
private int MinCellsOutside => CalculateCellCount(0, Config.MinAgentsOutside);
private int MaxCellsOutside => CalculateCellCount(0, Config.MaxAgentsOutside);
private int MinCellsInside => CalculateCellCount(2, Config.MinAgentsInside);
private int MaxCellsInside => CalculateCellCount(3, Config.MaxAgentsInside);
private int MaxCellCount => CalculateCellCount(5, Config.MaxAgentCount);
private float MinWaterLevel => Config.MinWaterLevel;
private int CalculateCellCount(int minValue, int maxValue)
{
if (maxValue == 0) { return 0; }
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, Level.Loaded.Difficulty * 0.01f * Config.AgentSpawnCountDifficultyMultiplier));
}
private float GetSpawnTime() =>
Math.Max(Config.AgentSpawnDelay * Rand.Range(Config.AgentSpawnDelayRandomFactor, 1 + Config.AgentSpawnDelayRandomFactor)
/ (Math.Max(Level.Loaded.Difficulty, 1) * 0.01f * Config.AgentSpawnDelayDifficultyMultiplier), Config.AgentSpawnDelay);
void UpdateReinforcements(float deltaTime)
{
if (protectiveCells.Count >= MaxCellCount || spawnOrgans.Count == 0) { return; }
if (spawnOrgans.Count == 0) { return; }
cellSpawnTimer -= deltaTime;
if (cellSpawnTimer < 0)
{
TrySpawnCell(out _, spawnOrgans.GetRandom());
cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor);
cellSpawnTimer = GetSpawnTime();
}
}
@@ -364,7 +372,7 @@ namespace Barotrauma
cell = Character.Create(Config.DefensiveAgent, targetEntity.WorldPosition, ToolBox.RandomSeed(8), hasAi: true, createNetworkEvent: true);
protectiveCells.Add(cell);
cell.OnDeath += OnCellDeath;
cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor);
cellSpawnTimer = GetSpawnTime();
return true;
}
@@ -42,6 +42,12 @@ namespace Barotrauma
[Serialize(0.5f, false)]
public float AgentSpawnDelayRandomFactor { get; private set; }
[Serialize(1f, false)]
public float AgentSpawnDelayDifficultyMultiplier { get; private set; }
[Serialize(1f, false)]
public float AgentSpawnCountDifficultyMultiplier { get; private set; }
[Serialize(0, false)]
public int MinAgentsPerBrainRoom { get; private set; }
@@ -1,18 +1,9 @@
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
partial class AICharacter : Character
{
//characters that are further than this from the camera (and all clients)
//have all their limb physics bodies disabled
const float EnableSimplePhysicsDist = 6000.0f;
const float DisableSimplePhysicsDist = EnableSimplePhysicsDist * 0.9f;
const float EnableSimplePhysicsDistSqr = EnableSimplePhysicsDist * EnableSimplePhysicsDist;
const float DisableSimplePhysicsDistSqr = DisableSimplePhysicsDist * DisableSimplePhysicsDist;
{
private AIController aiController;
public override AIController AIController
@@ -20,8 +11,8 @@ namespace Barotrauma
get { return aiController; }
}
public AICharacter(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
: base(speciesName, position, seed, characterInfo, id: Entity.NullEntityID, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll)
public AICharacter(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
: base(prefab, speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll)
{
InitProjSpecific();
}
@@ -62,21 +53,12 @@ namespace Barotrauma
if (!IsRemotePlayer && !(AIController is HumanAIController))
{
float characterDist = float.MaxValue;
#if CLIENT
characterDist = Vector2.DistanceSquared(cam.GetPosition(), WorldPosition);
#elif SERVER
if (GameMain.Server != null)
{
characterDist = GetClosestDistance();
}
#endif
if (characterDist > EnableSimplePhysicsDistSqr)
float characterDistSqr = GetDistanceSqrToClosestPlayer();
if (characterDistSqr > MathUtils.Pow2(Params.DisableDistance * 0.5f))
{
AnimController.SimplePhysicsEnabled = true;
}
else if (characterDist < DisableSimplePhysicsDistSqr)
else if (characterDistSqr < MathUtils.Pow2(Params.DisableDistance * 0.5f * 0.9f))
{
AnimController.SimplePhysicsEnabled = false;
}
@@ -90,50 +72,5 @@ namespace Barotrauma
aiController.Update(deltaTime);
}
}
#if SERVER
// Gets the closest distance, either an active player character or spectator
private float GetClosestDistance()
{
float minDist = float.MaxValue;
for (int i = 0; i < GameMain.Server.ConnectedClients.Count; i++)
{
var spectatePos = GameMain.Server.ConnectedClients[i].SpectatePos;
if (spectatePos != null)
{
float dist = Vector2.DistanceSquared(spectatePos.Value, WorldPosition);
if (dist < minDist)
{
minDist = dist;
}
if (dist < DisableSimplePhysicsDistSqr)
{
return dist;
}
}
}
foreach (Character c in CharacterList)
{
if (c != this && c.IsRemotePlayer)
{
float dist = Vector2.DistanceSquared(c.WorldPosition, WorldPosition);
if (dist < minDist)
{
minDist = dist;
}
if (dist < DisableSimplePhysicsDistSqr)
{
return dist;
}
}
}
return minDist;
}
#endif
}
}
@@ -1,6 +1,5 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
@@ -23,7 +22,11 @@ namespace Barotrauma
{
if (_ragdollParams == null)
{
_ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.SpeciesName);
_ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.VariantOf ?? character.SpeciesName);
if (character.VariantOf != null)
{
_ragdollParams.ApplyVariantScale(character.Params.VariantFile);
}
}
return _ragdollParams;
}
@@ -338,9 +341,21 @@ namespace Barotrauma
float dragForce = MathHelper.Clamp(eatSpeed * 10, 0, 40);
if (dragForce > 0.1f)
{
target.AnimController.MainLimb.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + dragForce));
Vector2 targetPos = mouthPos;
if (target.Submarine != null && character.Submarine == null)
{
targetPos -= target.Submarine.SimPosition;
}
else if (target.Submarine == null && character.Submarine != null)
{
targetPos += character.Submarine.SimPosition;
}
target.AnimController.MainLimb.body.SmoothRotate(mouthLimb.Rotation, dragForce * 2);
target.AnimController.Collider.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + dragForce));
if (!target.AnimController.SimplePhysicsEnabled)
{
target.AnimController.MainLimb.MoveToPos(targetPos, (float)(Math.Sin(eatTimer) + dragForce));
}
target.AnimController.Collider.MoveToPos(targetPos, (float)(Math.Sin(eatTimer) + dragForce));
}
if (InWater)
@@ -408,23 +423,26 @@ namespace Barotrauma
if (CurrentSwimParams == null) { return; }
movement = TargetMovement;
bool isMoving = movement.LengthSquared() > 0.00001f;
var mainLimb = MainLimb;
if (isMoving)
{
float t = 0.5f;
if (CurrentSwimParams.RotateTowardsMovement && VectorExtensions.Angle(VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2), movement) > MathHelper.PiOver2)
if (!SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement)
{
// Reduce the linear movement speed when not facing the movement direction
t /= 5;
float offset = mainLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
Vector2 forward = VectorExtensions.Forward(mainLimb.body.TransformedRotation - offset * Character.AnimController.Dir);
float dot = Vector2.Dot(forward, Vector2.Normalize(movement));
if (dot < 0)
{
// Reduce the linear movement speed when not facing the movement direction
t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f);
}
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
}
//limbs are disabled when simple physics is enabled, no need to move them
if (SimplePhysicsEnabled) { return; }
var mainLimb = MainLimb;
mainLimb.PullJointEnabled = true;
//mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
if (!isMoving)
{
WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5);
@@ -630,7 +648,7 @@ namespace Barotrauma
}
if (limb.Params.BlinkFrequency > 0)
{
limb.Blink(deltaTime, MainLimb.Rotation);
limb.UpdateBlink(deltaTime, MainLimb.Rotation);
}
}
@@ -772,7 +790,7 @@ namespace Barotrauma
}
if (limb.Params.BlinkFrequency > 0)
{
limb.Blink(deltaTime, MainLimb.Rotation);
limb.UpdateBlink(deltaTime, MainLimb.Rotation);
}
switch (limb.type)
{
@@ -26,7 +26,7 @@ namespace Barotrauma
{
if (_ragdollParams == null)
{
_ragdollParams = RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(character.SpeciesName);
_ragdollParams = RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(character.VariantOf ?? character.SpeciesName);
}
return _ragdollParams;
}
@@ -201,6 +201,8 @@ namespace Barotrauma
public float LegBendTorque => CurrentGroundedParams.LegBendTorque * RagdollParams.JointScale;
public Vector2 HandMoveOffset => CurrentGroundedParams.HandMoveOffset * RagdollParams.JointScale;
public float LockFlippingUntil;
public override Vector2 AimSourceSimPos
{
get
@@ -518,7 +520,7 @@ namespace Barotrauma
break;
}
if (TargetDir != dir && !IsStuck)
if (Timing.TotalTime > LockFlippingUntil && TargetDir != dir && !IsStuck)
{
Flip();
}
@@ -1315,16 +1317,23 @@ namespace Barotrauma
var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh);
if (thigh == null) { continue; }
if (thigh.IsSevered) { continue; }
float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation));
float thighTorque = thighDiff * thigh.Mass * Math.Sign(torso.Rotation - thigh.Rotation) * 5.0f;
thigh.body.ApplyTorque(thighTorque * strength);
float diff = torso.Rotation - thigh.Rotation;
if (MathUtils.IsValid(diff))
{
float thighTorque = thighDiff * thigh.Mass * Math.Sign(diff) * 5.0f;
thigh.body.ApplyTorque(thighTorque * strength);
}
var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg);
if (leg == null || leg.IsSevered) { continue; }
float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation));
float legTorque = legDiff * leg.Mass * Math.Sign(torso.Rotation - leg.Rotation) * 5.0f;
leg.body.ApplyTorque(legTorque * strength);
diff = torso.Rotation - leg.Rotation;
if (MathUtils.IsValid(diff))
{
float legTorque = legDiff * leg.Mass * Math.Sign(diff) * 5.0f;
leg.body.ApplyTorque(legTorque * strength);
}
}
}
@@ -1452,7 +1461,7 @@ namespace Barotrauma
target.CharacterHealth.CalculateVitality();
if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f)
{
character.Info.IncreaseSkillLevel("medical", SkillSettings.Current.SkillIncreasePerCprRevive, character.WorldPosition + Vector2.UnitY * 150.0f);
character.Info.IncreaseSkillLevel("medical", SkillSettings.Current.SkillIncreasePerCprRevive, character.Position + Vector2.UnitY * 150.0f);
SteamAchievementManager.OnCharacterRevived(target, character);
lastReviveTime = (float)Timing.TotalTime;
#if SERVER
@@ -1460,7 +1469,7 @@ namespace Barotrauma
#endif
//reset attacker, we don't want the character to start attacking us
//because we caused a bit of damage to them during CPR
if (target.LastAttacker == character) { target.LastAttacker = null; }
target.ForgiveAttacker(character);
}
}
}
@@ -1764,13 +1773,13 @@ namespace Barotrauma
Vector2 transformedHoldPos = rightShoulder.WorldAnchorA;
if (itemPos == Vector2.Zero || isClimbing || usingController)
{
if (character.SelectedItems[0] == item)
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item)
{
if (rightHand == null || rightHand.IsSevered) { return; }
transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0];
itemAngle = (rightHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir);
}
else if (character.SelectedItems[1] == item)
else if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item)
{
if (leftHand == null || leftHand.IsSevered) { return; }
transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1];
@@ -1779,13 +1788,13 @@ namespace Barotrauma
}
else
{
if (character.SelectedItems[0] == item)
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item)
{
if (rightHand == null || rightHand.IsSevered) { return; }
transformedHoldPos = rightShoulder.WorldAnchorA;
rightHand.Disabled = true;
}
if (character.SelectedItems[1] == item)
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item)
{
if (leftHand == null || leftHand.IsSevered) { return; }
transformedHoldPos = leftShoulder.WorldAnchorA;
@@ -1798,7 +1807,7 @@ namespace Barotrauma
item.body.ResetDynamics();
Vector2 currItemPos = (character.SelectedItems[0] == item) ?
Vector2 currItemPos = (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item) ?
rightHand.PullJointWorldAnchorA - transformedHandlePos[0] :
leftHand.PullJointWorldAnchorA - transformedHandlePos[1];
@@ -1846,15 +1855,14 @@ namespace Barotrauma
}
}
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false);
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false);
if (!isClimbing && !character.IsIncapacitated)
if (!isClimbing && !character.IsIncapacitated && itemPos != Vector2.Zero)
{
for (int i = 0; i < 2; i++)
{
if (character.SelectedItems[i] != item || itemPos == Vector2.Zero) { continue; }
Limb hand = (i == 0) ? rightHand : leftHand;
HandIK(hand, transformedHoldPos + transformedHandlePos[i]);
if (!character.Inventory.IsInLimbSlot(item, i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand)) { continue; }
HandIK(i == 0 ? rightHand : leftHand, transformedHoldPos + transformedHandlePos[i]);
}
}
}
@@ -2025,16 +2033,13 @@ namespace Barotrauma
Matrix torsoTransform = Matrix.CreateRotationZ(torso.Rotation);
for (int i = 0; i < character.SelectedItems.Length; i++)
foreach (Item heldItem in character.HeldItems)
{
if (i == 1 && character.SelectedItems[0] == character.SelectedItems[1])
if (heldItem?.body != null && !heldItem.Removed && heldItem.GetComponent<Holdable>() != null)
{
break;
}
if (character.SelectedItems[i]?.body != null && !character.SelectedItems[i].Removed && character.SelectedItems[i].GetComponent<Holdable>() != null)
{
character.SelectedItems[i].FlipX(relativeToSub: false);
heldItem.FlipX(relativeToSub: false);
}
heldItem.FlipX(relativeToSub: false);
}
foreach (Limb limb in Limbs)
@@ -281,7 +281,7 @@ namespace Barotrauma
}
}
public const float MAX_SPEED = 15;
public const float MAX_SPEED = 30;
public Vector2 TargetMovement
{
@@ -472,7 +472,7 @@ namespace Barotrauma
if (joint == null) { continue; }
float angle = (joint.LowerLimit + joint.UpperLimit) / 2.0f;
joint.LimbB?.body?.SetTransform(
(joint.WorldAnchorA - MathUtils.RotatePointAroundTarget(joint.LocalAnchorB, Vector2.Zero, MathHelper.ToDegrees(joint.BodyA.Rotation + angle), true)),
(joint.WorldAnchorA - MathUtils.RotatePointAroundTarget(joint.LocalAnchorB, Vector2.Zero, joint.BodyA.Rotation + angle, true)),
joint.BodyA.Rotation + angle);
}
}
@@ -758,11 +758,11 @@ namespace Barotrauma
limb.IsSevered = true;
if (limb.type == LimbType.RightHand)
{
character.SelectedItems[0]?.Drop(character);
character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.Drop(character);
}
else if (limb.type == LimbType.LeftHand)
{
character.SelectedItems[1]?.Drop(character);
character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.Drop(character);
}
}
@@ -1120,6 +1120,32 @@ namespace Barotrauma
splashSoundTimer -= deltaTime;
if (character.Submarine == null && Level.Loaded != null)
{
if (Collider.SimPosition.Y > Level.Loaded.TopBarrier.Position.Y)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, Math.Min(Collider.LinearVelocity.Y, -1));
}
else if (Collider.SimPosition.Y < Level.Loaded.BottomBarrier.Position.Y)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X,
MathHelper.Clamp(Collider.LinearVelocity.Y, Level.Loaded.BottomBarrier.Position.Y - Collider.SimPosition.Y, 10.0f));
}
foreach (Limb limb in Limbs)
{
if (limb.SimPosition.Y > Level.Loaded.TopBarrier.Position.Y)
{
limb.body.LinearVelocity = new Vector2(limb.LinearVelocity.X, Math.Min(limb.LinearVelocity.Y, -1));
}
else if (limb.SimPosition.Y < Level.Loaded.BottomBarrier.Position.Y)
{
limb.body.LinearVelocity = new Vector2(
limb.LinearVelocity.X,
MathHelper.Clamp(limb.LinearVelocity.Y, Level.Loaded.BottomBarrier.Position.Y - limb.SimPosition.Y, 10.0f));
}
}
}
if (forceStanding)
{
inWater = false;
@@ -1562,7 +1588,7 @@ namespace Barotrauma
}
}
public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true)
public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false)
{
if (!MathUtils.IsValid(simPosition))
{
@@ -1575,8 +1601,7 @@ namespace Barotrauma
}
if (MainLimb == null) { return; }
Vector2 limbMoveAmount = simPosition - Collider.SimPosition;
Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition;
if (lerp)
{
Collider.TargetPosition = simPosition;
@@ -1587,13 +1612,15 @@ namespace Barotrauma
Collider.SetTransform(simPosition, Collider.Rotation);
}
foreach (Limb limb in Limbs)
if (!MathUtils.NearlyEqual(limbMoveAmount, Vector2.Zero))
{
if (limb.IsSevered) { continue; }
//check visibility from the new position of the collider to the new position of this limb
Vector2 movePos = limb.SimPosition + limbMoveAmount;
TrySetLimbPosition(limb, simPosition, movePos, lerp, ignorePlatforms);
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
//check visibility from the new position of the collider to the new position of this limb
Vector2 movePos = limb.SimPosition + limbMoveAmount;
TrySetLimbPosition(limb, simPosition, movePos, lerp, ignorePlatforms);
}
}
}
@@ -1644,7 +1671,7 @@ namespace Barotrauma
if (distSqrd > resetDist * resetDist)
{
//ragdoll way too far, reset position
SetPosition(Collider.SimPosition, true);
SetPosition(Collider.SimPosition, true, forceMainLimbToCollider: true);
}
if (distSqrd > allowedDist * allowedDist)
{
@@ -121,11 +121,29 @@ namespace Barotrauma
[Serialize(false, true), Editable]
public bool FullSpeedAfterAttack { get; private set; }
private float _structureDamage;
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float StructureDamage { get; set; }
public float StructureDamage
{
get => _structureDamage * DamageMultiplier;
set => _structureDamage = value;
}
private float _itemDamage;
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float ItemDamage
{
get =>_itemDamage * DamageMultiplier;
set => _itemDamage = value;
}
/// <summary>
/// Currently only used with variants. Used for multiplying all the damage.
/// </summary>
public float DamageMultiplier { get; set; } = 1;
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float ItemDamage { get; set; }
public float LevelWallDamage { get; set; }
[Serialize(false, true)]
public bool Ranged { get; set; }
@@ -199,6 +217,9 @@ namespace Barotrauma
[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(1.0f, true, description: "Affects the strength of the impact effects the limb causes when it hits a submarine."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float SubmarineImpactMultiplier { get; private set; }
[Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float SeverLimbsProbability { get; set; }
@@ -210,6 +231,9 @@ namespace Barotrauma
[Serialize(0.0f, true, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float Priority { get; private set; }
[Serialize(false, true, description: ""), Editable]
public bool Blink { get; private set; }
public IEnumerable<StatusEffect> StatusEffects
{
get { return statusEffects; }
@@ -260,6 +284,11 @@ namespace Barotrauma
return (Duration == 0.0f) ? StructureDamage : StructureDamage * deltaTime;
}
public float GetLevelWallDamage(float deltaTime)
{
return (Duration == 0.0f) ? LevelWallDamage : LevelWallDamage * deltaTime;
}
public float GetItemDamage(float deltaTime)
{
return (Duration == 0.0f) ? ItemDamage : ItemDamage * deltaTime;
@@ -272,7 +301,7 @@ namespace Barotrauma
{
totalDamage += affliction.GetVitalityDecrease(null);
}
return totalDamage;
return totalDamage * DamageMultiplier;
}
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f)
@@ -283,7 +312,7 @@ namespace Barotrauma
Range = range;
DamageRange = range;
StructureDamage = structureDamage;
StructureDamage = LevelWallDamage = structureDamage;
ItemDamage = itemDamage;
}
@@ -299,6 +328,13 @@ namespace Barotrauma
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. <Affliction identifier=\"internaldamage\" strength=\"10\" />).");
}
//if level wall damage is not defined, default to the structure damage
if (element.Attribute("LevelWallDamage") == null &&
element.Attribute("levelwalldamage") == null)
{
LevelWallDamage = StructureDamage;
}
InitProjSpecific(element);
foreach (XElement subElement in element.Elements())
File diff suppressed because it is too large Load Diff
@@ -171,11 +171,8 @@ namespace Barotrauma
if (Character.Inventory != null)
{
int cardSlotIndex = Character.Inventory.FindLimbSlot(InvSlotType.Card);
if (cardSlotIndex < 0) return disguiseName;
var idCard = Character.Inventory.Items[cardSlotIndex];
if (idCard == null) return disguiseName;
var idCard = Character.Inventory.GetItemInLimbSlot(InvSlotType.Card);
if (idCard == null) { return disguiseName; }
//Disguise as the ID card name if it's equipped
string[] readTags = idCard.Tags.Split(',');
@@ -294,19 +291,15 @@ namespace Barotrauma
if (Character.Inventory != null)
{
int cardSlotIndex = Character.Inventory.FindLimbSlot(InvSlotType.Card);
if (cardSlotIndex >= 0)
idCard = Character.Inventory.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent<IdCard>();
if (idCard != null)
{
idCard = Character.Inventory.Items[cardSlotIndex].GetComponent<IdCard>();
if (idCard != null)
{
#if CLIENT
GetDisguisedSprites(idCard);
GetDisguisedSprites(idCard);
#endif
return;
}
return;
}
}
}
@@ -352,13 +345,13 @@ namespace Barotrauma
public CauseOfDeath CauseOfDeath;
public Character.TeamType TeamID;
public CharacterTeamType TeamID;
private readonly NPCPersonalityTrait personalityTrait;
public Order CurrentOrder { get; set; }
public string CurrentOrderOption { get; set; }
public bool IsDismissed => CurrentOrder == null || CurrentOrder.Identifier.Equals("dismissed", StringComparison.OrdinalIgnoreCase);
public const int MaxCurrentOrders = 3;
public static int HighestManualOrderPriority => MaxCurrentOrders;
public List<OrderInfo> CurrentOrders { get; } = new List<OrderInfo>();
//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
@@ -445,6 +438,7 @@ namespace Barotrauma
{
if (ragdoll == null)
{
// TODO: support for variants
string speciesName = SpeciesName;
bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase));
ragdoll = isHumanoid
@@ -472,6 +466,7 @@ namespace Barotrauma
XDocument doc = CharacterPrefab.FindBySpeciesName(_speciesName)?.XDocument;
if (doc == null) { return; }
CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
// TODO: support for variants
head = new HeadInfo();
HasGenders = CharacterConfigElement.GetAttributeBool("genders", false);
if (HasGenders)
@@ -540,6 +535,7 @@ namespace Barotrauma
doc = XMLExtensions.TryLoadXml(file);
}
if (doc == null) { return; }
// TODO: support for variants
CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
HasGenders = CharacterConfigElement.GetAttributeBool("genders", false);
if (HasGenders && gender == Gender.None)
@@ -906,7 +902,7 @@ namespace Barotrauma
return (int)(salary * Job.Prefab.PriceMultiplier);
}
public void IncreaseSkillLevel(string skillIdentifier, float increase, Vector2 worldPos)
public void IncreaseSkillLevel(string skillIdentifier, float increase, Vector2 pos)
{
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
@@ -920,15 +916,10 @@ namespace Barotrauma
float newLevel = Job.GetSkillLevel(skillIdentifier);
OnSkillChanged(skillIdentifier, prevLevel, newLevel, worldPos);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && !MathUtils.NearlyEqual(newLevel, prevLevel))
{
GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateSkills });
}
OnSkillChanged(skillIdentifier, prevLevel, newLevel, pos);
}
public void SetSkillLevel(string skillIdentifier, float level, Vector2 worldPos)
public void SetSkillLevel(string skillIdentifier, float level, Vector2 pos)
{
if (Job == null) { return; }
@@ -936,13 +927,13 @@ namespace Barotrauma
if (skill == null)
{
Job.Skills.Add(new Skill(skillIdentifier, level));
OnSkillChanged(skillIdentifier, 0.0f, level, worldPos);
OnSkillChanged(skillIdentifier, 0.0f, level, pos);
}
else
{
float prevLevel = skill.Level;
skill.Level = level;
OnSkillChanged(skillIdentifier, prevLevel, skill.Level, worldPos);
OnSkillChanged(skillIdentifier, prevLevel, skill.Level, pos);
}
}
@@ -1013,13 +1004,9 @@ namespace Barotrauma
faceAttachments = null;
}
/// <summary>
/// Reset order data so it doesn't carry into further rounds, as the AI is "recreated" always in between rounds anyway.
/// </summary>
public void ResetCurrentOrder()
public void ClearCurrentOrders()
{
CurrentOrder = null;
CurrentOrderOption = "";
CurrentOrders.Clear();
}
public void Remove()
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
@@ -25,11 +23,12 @@ namespace Barotrauma
public string Name { get; private set; }
public string Identifier { get; private set; }
public string FilePath { get; private set; }
public string VariantOf { get; private set; }
public ContentPackage ContentPackage { get; private set; }
public XDocument XDocument { get; private set; }
public static IEnumerable<string> ConfigFilePaths => Prefabs.Select(p => p.FilePath);
public static IEnumerable<XDocument> ConfigFiles => Prefabs.Select(p => p.XDocument);
@@ -80,22 +79,30 @@ namespace Barotrauma
DebugConsole.ThrowError($"Duplicate path: {filePath}");
return false;
}
XElement mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
var name = mainElement.GetAttributeString("name", null);
if (name != null)
XElement mainElement = doc.Root;
if (doc.Root.IsCharacterVariant())
{
DebugConsole.NewMessage($"Error in {filePath}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange);
if (!CheckSpeciesName(mainElement, filePath, out string n)) { return false; }
string inherit = mainElement.GetAttributeString("inherit", null);
string id = n.ToLowerInvariant();
Prefabs.Add(new CharacterPrefab
{
Name = n,
OriginalName = n,
Identifier = id,
FilePath = filePath,
ContentPackage = contentPackage,
XDocument = doc,
VariantOf = inherit
}, isOverride: false);
return true;
}
else
else if (doc.Root.IsOverride())
{
name = mainElement.GetAttributeString("speciesname", string.Empty);
mainElement = doc.Root.FirstElement();
}
if (string.IsNullOrWhiteSpace(name))
{
DebugConsole.ThrowError($"No species name defined for: {filePath}");
return false;
}
var identifier = name.ToLowerInvariant();
if (!CheckSpeciesName(mainElement, filePath, out string name)) { return false; }
string identifier = name.ToLowerInvariant();
Prefabs.Add(new CharacterPrefab
{
Name = name,
@@ -109,6 +116,25 @@ namespace Barotrauma
return true;
}
public static bool CheckSpeciesName(XElement mainElement, string filePath, out string name)
{
name = mainElement.GetAttributeString("name", null);
if (name != null)
{
DebugConsole.NewMessage($"Error in {filePath}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange);
}
else
{
name = mainElement.GetAttributeString("speciesname", string.Empty);
}
if (string.IsNullOrWhiteSpace(name))
{
DebugConsole.ThrowError($"No species name defined for: {filePath}");
return false;
}
return true;
}
public static void LoadAll()
{
foreach (ContentFile file in ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Character))
@@ -2,6 +2,7 @@
using System.Linq;
using System.Xml.Linq;
using System;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -73,7 +74,10 @@ namespace Barotrauma
else if (Strength < ActiveThreshold)
{
DeactivateHusk();
character.SpeechImpediment = 100;
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: false })
{
character.SpeechImpediment = 100;
}
State = InfectionState.Transition;
}
else if (Strength < Prefab.MaxStrength)
@@ -98,7 +102,7 @@ namespace Barotrauma
private void ApplyDamage(float deltaTime, bool applyForce)
{
int limbCount = character.AnimController.Limbs.Count(l => !l.IgnoreCollisions && !l.IsSevered);
int limbCount = character.AnimController.Limbs.Count(l => !l.IgnoreCollisions && !l.IsSevered && !l.Hidden);
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
@@ -118,13 +122,25 @@ namespace Barotrauma
{
huskAppendage = AttachHuskAppendage(character, Prefab.Identifier);
}
character.NeedsAir = false;
character.SpeechImpediment = 100;
if (Prefab is AfflictionPrefabHusk { NeedsAir: false })
{
character.NeedsAir = false;
}
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: false })
{
character.SpeechImpediment = 100;
}
}
private void DeactivateHusk()
{
character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false);
if (Prefab is AfflictionPrefabHusk { NeedsAir: false })
{
character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false);
}
if (huskAppendage != null)
{
huskAppendage.ForEach(l => character.AnimController.RemoveLimb(l));
@@ -160,6 +176,13 @@ namespace Barotrauma
private IEnumerable<object> CreateAIHusk()
{
//character already in remove queue (being removed by something else, for example a modded affliction that uses AfflictionHusk as the base)
// -> don't spawn the AI husk
if (Entity.Spawner.IsInRemoveQueue(character))
{
yield return CoroutineStatus.Success;
}
character.Enabled = false;
Entity.Spawner.AddToRemoveQueue(character);
@@ -179,7 +202,7 @@ namespace Barotrauma
if (husk.Info != null)
{
husk.Info.Character = husk;
husk.Info.TeamID = Character.TeamType.None;
husk.Info.TeamID = CharacterTeamType.None;
}
foreach (Limb limb in husk.AnimController.Limbs)
@@ -201,17 +224,16 @@ namespace Barotrauma
if (character.Inventory != null && husk.Inventory != null)
{
if (character.Inventory.Items.Length != husk.Inventory.Items.Length)
if (character.Inventory.Capacity != husk.Inventory.Capacity)
{
string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
yield return CoroutineStatus.Success;
}
for (int i = 0; i < character.Inventory.Items.Length && i < husk.Inventory.Items.Length; i++)
for (int i = 0; i < character.Inventory.Capacity && i < husk.Inventory.Capacity; i++)
{
if (character.Inventory.Items[i] == null) continue;
husk.Inventory.TryPutItem(character.Inventory.Items[i], i, true, false, null);
character.Inventory.GetItemsAt(i).ForEachMod(item => husk.Inventory.TryPutItem(item, i, true, false, null));
}
}
@@ -90,6 +90,10 @@ namespace Barotrauma
AttachLimbName = null;
AttachLimbType = LimbType.None;
}
SendMessages = element.GetAttributeBool("sendmessages", true);
CauseSpeechImpediment = element.GetAttributeBool("causespeechimpediment", true);
NeedsAir = element.GetAttributeBool("needsair", false);
}
// Use any of these to define which limb the appendage is attached to.
@@ -101,9 +105,13 @@ namespace Barotrauma
public readonly string HuskedSpeciesName;
public readonly string[] TargetSpecies;
public const string Tag = "[speciesname]";
public readonly bool SendMessages;
public readonly bool CauseSpeechImpediment;
public readonly bool NeedsAir;
}
class AfflictionPrefab : IPrefab, IDisposable
class AfflictionPrefab : IPrefab, IDisposable, IHasUintIdentifier
{
public class Effect
{
@@ -220,6 +228,7 @@ namespace Barotrauma
public static AfflictionPrefab Bloodloss;
public static AfflictionPrefab Pressure;
public static AfflictionPrefab Stun;
public static AfflictionPrefab RadiationSickness;
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
@@ -248,7 +257,7 @@ namespace Barotrauma
/// Unique identifier that's generated by hashing the prefab's string identifier.
/// Used to reduce the amount of bytes needed to write affliction data into network messages in multiplayer.
/// </summary>
public uint UIntIdentifier;
public uint UIntIdentifier { get; set; }
// Arbitrary string that is used to identify the type of the affliction.
public readonly string AfflictionType;
@@ -265,6 +274,7 @@ namespace Barotrauma
public ContentPackage ContentPackage { get; private set; }
public readonly string Name, Description;
public readonly string TranslationOverride;
public readonly bool IsBuff;
public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription;
@@ -329,6 +339,7 @@ namespace Barotrauma
Bloodloss = null;
Pressure = null;
Stun = null;
RadiationSickness = null;
#if CLIENT
CharacterHealth.DamageOverlay?.Remove();
CharacterHealth.DamageOverlay = null;
@@ -353,6 +364,7 @@ namespace Barotrauma
if (Bloodloss == null) { DebugConsole.ThrowError("Affliction \"Bloodloss\" not defined in the affliction prefabs."); }
if (Pressure == null) { DebugConsole.ThrowError("Affliction \"Pressure\" not defined in the affliction prefabs."); }
if (Stun == null) { DebugConsole.ThrowError("Affliction \"Stun\" not defined in the affliction prefabs."); }
if (RadiationSickness == null) { DebugConsole.ThrowError("Affliction \"RadiationSickness\" not defined in the affliction prefabs."); }
}
public static void LoadFromFile(ContentFile file)
@@ -490,26 +502,16 @@ namespace Barotrauma
case "stun":
Stun = prefab;
break;
case "radiationsickness":
RadiationSickness = prefab;
break;
}
if (ImpactDamage == null) { ImpactDamage = InternalDamage; }
if (prefab != null)
{
Prefabs.Add(prefab, isOverride);
}
}
using MD5 md5 = MD5.Create();
foreach (AfflictionPrefab prefab in Prefabs)
{
prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5);
//it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small
var collision = Prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier);
if (collision != null)
{
DebugConsole.ThrowError("Hashing collision when generating uint identifiers for Afflictions: " + prefab.Identifier + " has the same identifier as " + collision.Identifier + " (" + prefab.UIntIdentifier + ")");
collision.UIntIdentifier++;
prefab.CalculatePrefabUIntIdentifier(Prefabs);
}
}
}
@@ -541,8 +543,10 @@ namespace Barotrauma
Identifier = element.GetAttributeString("identifier", "");
AfflictionType = element.GetAttributeString("type", "");
Name = TextManager.Get("AfflictionName." + Identifier, true) ?? element.GetAttributeString("name", "");
Description = TextManager.Get("AfflictionDescription." + Identifier, true) ?? element.GetAttributeString("description", "");
TranslationOverride = element.GetAttributeString("translationoverride", null);
string translationId = TranslationOverride ?? Identifier;
Name = TextManager.Get("AfflictionName." + translationId, true) ?? element.GetAttributeString("name", "");
Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", "");
IsBuff = element.GetAttributeBool("isbuff", false);
LimbSpecific = element.GetAttributeBool("limbspecific", false);
@@ -567,12 +571,12 @@ namespace Barotrauma
KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f);
CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + Identifier, true) ?? element.GetAttributeString("causeofdeathdescription", "");
SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + Identifier, true) ?? element.GetAttributeString("selfcauseofdeathdescription", "");
CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + translationId, true) ?? element.GetAttributeString("causeofdeathdescription", "");
SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + translationId, true) ?? element.GetAttributeString("selfcauseofdeathdescription", "");
IconColors = element.GetAttributeColorArray("iconcolors", null);
AchievementOnRemoved = element.GetAttributeString("achievementonremoved", "");
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -214,7 +214,7 @@ namespace Barotrauma
InitProjSpecific(null, character);
}
public CharacterHealth(XElement element, Character character)
public CharacterHealth(XElement element, Character character, XElement limbHealthElement = null)
{
this.Character = character;
InitIrremovableAfflictions();
@@ -224,7 +224,8 @@ namespace Barotrauma
minVitality = character.IsHuman ? -100.0f : 0.0f;
limbHealths.Clear();
foreach (XElement subElement in element.Elements())
limbHealthElement ??= element;
foreach (XElement subElement in limbHealthElement.Elements())
{
if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; }
limbHealths.Add(new LimbHealth(subElement, this));
@@ -685,12 +686,12 @@ namespace Barotrauma
for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--)
{
var affliction = limbHealths[i].Afflictions[j];
Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == i);
Limb targetLimb = Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == i);
affliction.Update(this, targetLimb, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
if (affliction is AfflictionBleeding)
if (affliction is AfflictionBleeding bleeding)
{
UpdateBleedingProjSpecific((AfflictionBleeding)affliction, targetLimb, deltaTime);
UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime);
}
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
@@ -86,6 +86,11 @@ namespace Barotrauma
private void ParseAfflictionTypes()
{
if (string.IsNullOrWhiteSpace(rawAfflictionTypeString))
{
parsedAfflictionTypes = new string[0];
return;
}
string[] splitValue = rawAfflictionTypeString.Split(',', '');
for (int i = 0; i < splitValue.Length; i++)
{
@@ -96,6 +101,11 @@ namespace Barotrauma
private void ParseAfflictionIdentifiers()
{
if (string.IsNullOrWhiteSpace(rawAfflictionIdentifierString))
{
parsedAfflictionIdentifiers = new string[0];
return;
}
string[] splitValue = rawAfflictionIdentifierString.Split(',', '');
for (int i = 0; i < splitValue.Length; i++)
{
@@ -20,6 +20,12 @@ namespace Barotrauma
[Serialize(1f, false)]
public float HealthMultiplier { get; protected set; }
[Serialize(1f, false)]
public float AimSpeed { get; protected set; }
[Serialize(1f, false)]
public float AimAccuracy { get; protected set; }
private readonly HashSet<string> moduleFlags = new HashSet<string>();
[Serialize("", true, "What outpost module tags does the NPC prefer to spawn in.")]
@@ -67,6 +73,9 @@ namespace Barotrauma
[Serialize(AIObjectiveIdle.BehaviorType.Passive, false)]
public AIObjectiveIdle.BehaviorType Behavior { get; protected set; }
[Serialize(float.PositiveInfinity, false)]
public float ReportRange { get; protected set; }
public List<string> PreferredOutpostModuleTypes { get; protected set; }
public string OriginalName { get { return Identifier; } }
@@ -105,16 +114,50 @@ namespace Barotrauma
return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync);
}
public void GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced)
public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null)
{
npc.CharacterHealth.MaxVitality *= HealthMultiplier;
var humanAI = npc.AIController as HumanAIController;
if (humanAI != null)
{
var idleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
if (positionToStayIn != null && Behavior == AIObjectiveIdle.BehaviorType.StayInHull)
{
idleObjective.TargetHull = AIObjectiveGoTo.GetTargetHull(positionToStayIn);
idleObjective.Behavior = AIObjectiveIdle.BehaviorType.StayInHull;
}
else
{
idleObjective.Behavior = Behavior;
foreach (string moduleType in PreferredOutpostModuleTypes)
{
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
}
}
humanAI.ReportRange = ReportRange;
humanAI.AimSpeed = AimSpeed;
humanAI.AimAccuracy = AimAccuracy;
}
if (CampaignInteractionType != CampaignMode.InteractionType.None)
{
(GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, CampaignInteractionType);
if (positionToStayIn != null && humanAI != null)
{
humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200));
}
}
}
public void GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true)
{
var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), randSync);
foreach (XElement itemElement in spawnItems.GetChildElements("item"))
{
InitializeItems(character, itemElement, submarine);
InitializeItems(character, itemElement, submarine, createNetworkEvents: createNetworkEvents);
}
}
private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null)
private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null, bool createNetworkEvents = true)
{
ItemPrefab itemPrefab;
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
@@ -126,7 +169,7 @@ namespace Barotrauma
}
Item item = new Item(itemPrefab, character.Position, null);
#if SERVER
if (GameMain.Server != null && Entity.Spawner != null)
if (GameMain.Server != null && Entity.Spawner != null && createNetworkEvents)
{
if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item))
{
@@ -187,7 +230,7 @@ namespace Barotrauma
}
foreach (XElement childItemElement in itemElement.Elements())
{
InitializeItems(character, childItemElement, submarine, item);
InitializeItems(character, childItemElement, submarine, item, createNetworkEvents);
}
}
}
@@ -203,7 +203,7 @@ namespace Barotrauma
partial class Limb : ISerializableEntity, ISpatialEntity
{
//how long it takes for severed limbs to fade out
public float SeveredFadeOutTime => Params.SeveredFadeOutTime;
public float SeveredFadeOutTime { get; private set; } = 10;
public readonly Character character;
/// <summary>
@@ -308,6 +308,12 @@ namespace Barotrauma
set
{
if (isSevered == value) { return; }
if (value == true)
{
// If any of the connected limbs have a longer fade out time, use that
var connectedLimbs = GetConnectedLimbs();
SeveredFadeOutTime = Math.Max(Params.SeveredFadeOutTime, connectedLimbs.Any() ? connectedLimbs.Max(l => l.SeveredFadeOutTime) : 0);
}
isSevered = value;
if (isSevered)
{
@@ -330,7 +336,7 @@ namespace Barotrauma
}
}
public Submarine Submarine => character.Submarine;
public Submarine Submarine => character?.Submarine;
public bool Hidden
{
@@ -340,7 +346,7 @@ namespace Barotrauma
public Vector2 WorldPosition
{
get { return character.Submarine == null ? Position : Position + character.Submarine.Position; }
get { return character?.Submarine == null ? Position : Position + character.Submarine.Position; }
}
public Vector2 Position
@@ -622,6 +628,14 @@ namespace Barotrauma
}
attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange);
}
if (character.VariantOf != null && character.Params.VariantFile != null)
{
var attackElement = character.Params.VariantFile.Root.GetChildElement("attack");
if (attackElement != null)
{
attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f);
}
}
break;
case "damagemodifier":
DamageModifiers.Add(new DamageModifier(subElement, character.Name));
@@ -669,7 +683,7 @@ namespace Barotrauma
private readonly List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
private readonly List<DamageModifier> tempModifiers = new List<DamageModifier>();
private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound)
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound, float damageMultiplier = 1)
{
appliedDamageModifiers.Clear();
afflictionsCopy.Clear();
@@ -709,7 +723,7 @@ namespace Barotrauma
}
}
}
float finalDamageModifier = 1.0f;
float finalDamageModifier = damageMultiplier;
foreach (DamageModifier damageModifier in tempModifiers)
{
finalDamageModifier *= damageModifier.DamageMultiplier;
@@ -853,6 +867,23 @@ namespace Barotrauma
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
bool wasRunning = attack.IsRunning;
attack.UpdateAttackTimer(deltaTime, character);
if (attack.Blink)
{
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Any())
{
foreach (int limbIndex in attack.ForceOnLimbIndices)
{
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) { continue; }
Limb limb = character.AnimController.Limbs[limbIndex];
if (limb.IsSevered) { continue; }
limb.Blink();
}
}
else
{
Blink();
}
}
bool wasHit = false;
Body structureBody = null;
@@ -1095,7 +1126,7 @@ namespace Barotrauma
{
targets.Clear();
statusEffect.GetNearbyTargets(WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, character, targets);
statusEffect.Apply(actionType, deltaTime, character, targets);
}
else
{
@@ -1103,7 +1134,40 @@ namespace Barotrauma
{
statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition);
}
statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition);
else if (statusEffect.targetLimbs != null)
{
foreach (var limbType in statusEffect.targetLimbs)
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
// Target all matching limbs
foreach (var limb in ragdoll.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.type == limbType)
{
statusEffect.Apply(actionType, deltaTime, character, limb);
}
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
{
// Target just the first matching limb
Limb limb = ragdoll.GetLimb(limbType);
statusEffect.Apply(actionType, deltaTime, character, limb);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb))
{
// Target just the last matching limb
Limb limb = ragdoll.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden);
statusEffect.Apply(actionType, deltaTime, character, limb);
}
}
}
else
{
statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition);
}
}
}
}
@@ -1113,7 +1177,12 @@ namespace Barotrauma
private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime;
public void Blink(float deltaTime, float referenceRotation)
public void Blink()
{
blinkTimer = -TotalBlinkDurationOut;
}
public void UpdateBlink(float deltaTime, float referenceRotation)
{
if (blinkTimer > -TotalBlinkDurationOut)
{
@@ -1147,6 +1216,26 @@ namespace Barotrauma
}
}
public IEnumerable<LimbJoint> GetConnectedJoints() => ragdoll.LimbJoints.Where(j => !j.IsSevered && (j.LimbA == this || j.LimbB == this));
public IEnumerable<Limb> GetConnectedLimbs()
{
var connectedJoints = GetConnectedJoints();
var connectedLimbs = new HashSet<Limb>();
foreach (Limb limb in ragdoll.Limbs)
{
var otherJoints = limb.GetConnectedJoints();
foreach (LimbJoint connectedJoint in connectedJoints)
{
if (otherJoints.Contains(connectedJoint))
{
connectedLimbs.Add(limb);
}
}
}
return connectedLimbs;
}
public void Remove()
{
body?.Remove();
@@ -54,7 +54,7 @@ namespace Barotrauma
abstract class SwimParams : AnimationParams
{
[Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 500, ValueStep = 1)]
[Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float SteerTorque { get; set; }
}
@@ -66,8 +66,13 @@ namespace Barotrauma
protected static Dictionary<string, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<string, Dictionary<string, AnimationParams>>();
private float _movementSpeed;
[Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
public float MovementSpeed { get; set; }
public float MovementSpeed
{
get => _movementSpeed;
set => _movementSpeed = value;
}
[Serialize(1.0f, true, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"),
Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)]
@@ -110,11 +115,10 @@ namespace Barotrauma
[Serialize(AnimationType.NotDefined, true), Editable]
public virtual AnimationType AnimationType { get; protected set; }
public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType.ToString()}";
public static string GetDefaultFile(string speciesName, AnimationType animType, ContentPackage contentPackage = null)
=> Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName, animType)}.xml");
public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType}";
public static string GetDefaultFile(string speciesName, AnimationType animType) => Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml");
public static string GetFolder(string speciesName, ContentPackage contentPackage = null)
public static string GetFolder(string speciesName)
{
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (prefab?.XDocument == null)
@@ -132,7 +136,7 @@ namespace Barotrauma
{
folder = Path.Combine(Path.GetDirectoryName(filePath), "Animations");
}
return folder;
return folder.CleanUpPathCrossPlatform(true);
}
/// <summary>
@@ -163,7 +167,16 @@ namespace Barotrauma
return Enum.TryParse(typeString, out AnimationType fileType) && fileType == type;
}
public static T GetDefaultAnimParams<T>(string speciesName, AnimationType animType) where T : AnimationParams, new() => GetAnimParams<T>(speciesName, animType, GetDefaultFileName(speciesName, animType));
public static T GetDefaultAnimParams<T>(Character character, AnimationType animType) where T : AnimationParams, new()
{
string speciesName = character.VariantOf ?? character.SpeciesName;
if (character.VariantOf != null && character.Params.VariantFile?.Root?.GetChildElement("animations")?.GetAttributeString("folder", null) != null)
{
// Use the overridden animations defined in the variant definition file.
speciesName = character.SpeciesName;
}
return GetAnimParams<T>(speciesName, animType, GetDefaultFileName(speciesName, animType));
}
/// <summary>
/// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path!
@@ -7,11 +7,11 @@ namespace Barotrauma
{
public static FishWalkParams GetDefaultAnimParams(Character character)
{
return Check(character) ? GetDefaultAnimParams<FishWalkParams>(character.SpeciesName, AnimationType.Walk) : Empty;
return Check(character) ? GetDefaultAnimParams<FishWalkParams>(character, AnimationType.Walk) : Empty;
}
public static FishWalkParams GetAnimParams(Character character, string fileName = null)
{
return Check(character) ? GetAnimParams<FishWalkParams>(character.SpeciesName, AnimationType.Walk, fileName) : Empty;
return Check(character) ? GetAnimParams<FishWalkParams>(character.VariantOf ?? character.SpeciesName, AnimationType.Walk, fileName) : Empty;
}
protected static FishWalkParams Empty = new FishWalkParams();
@@ -23,11 +23,11 @@ namespace Barotrauma
{
public static FishRunParams GetDefaultAnimParams(Character character)
{
return Check(character) ? GetDefaultAnimParams<FishRunParams>(character.SpeciesName, AnimationType.Run) : Empty;
return Check(character) ? GetDefaultAnimParams<FishRunParams>(character, AnimationType.Run) : Empty;
}
public static FishRunParams GetAnimParams(Character character, string fileName = null)
{
return Check(character) ? GetAnimParams<FishRunParams>(character.SpeciesName, AnimationType.Run, fileName) : Empty;
return Check(character) ? GetAnimParams<FishRunParams>(character.VariantOf ?? character.SpeciesName, AnimationType.Run, fileName) : Empty;
}
protected static FishRunParams Empty = new FishRunParams();
@@ -37,10 +37,10 @@ namespace Barotrauma
class FishSwimFastParams : FishSwimParams
{
public static FishSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimFastParams>(character.SpeciesName, AnimationType.SwimFast);
public static FishSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimFastParams>(character, AnimationType.SwimFast);
public static FishSwimFastParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<FishSwimFastParams>(character.SpeciesName, AnimationType.SwimFast, fileName);
return GetAnimParams<FishSwimFastParams>(character.VariantOf ?? character.SpeciesName, AnimationType.SwimFast, fileName);
}
public override void StoreSnapshot() => StoreSnapshot<FishSwimFastParams>();
@@ -48,10 +48,10 @@ namespace Barotrauma
class FishSwimSlowParams : FishSwimParams
{
public static FishSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow);
public static FishSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimSlowParams>(character, AnimationType.SwimSlow);
public static FishSwimSlowParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<FishSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow, fileName);
return GetAnimParams<FishSwimSlowParams>(character.VariantOf ?? character.SpeciesName, AnimationType.SwimSlow, fileName);
}
public override void StoreSnapshot() => StoreSnapshot<FishSwimSlowParams>();
@@ -173,13 +173,13 @@ namespace Barotrauma
[Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")]
public bool RotateTowardsMovement { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
[Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)]
public float TorsoTorque { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
[Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)]
public float HeadTorque { get; set; }
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)]
public float TailTorque { get; set; }
[Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)]
@@ -4,7 +4,7 @@ namespace Barotrauma
{
class HumanWalkParams : HumanGroundedParams
{
public static HumanWalkParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanWalkParams>(character.SpeciesName, AnimationType.Walk);
public static HumanWalkParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanWalkParams>(character, AnimationType.Walk);
public static HumanWalkParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<HumanWalkParams>(character.SpeciesName, AnimationType.Walk, fileName);
@@ -15,7 +15,7 @@ namespace Barotrauma
class HumanRunParams : HumanGroundedParams
{
public static HumanRunParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanRunParams>(character.SpeciesName, AnimationType.Run);
public static HumanRunParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanRunParams>(character, AnimationType.Run);
public static HumanRunParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<HumanRunParams>(character.SpeciesName, AnimationType.Run, fileName);
@@ -26,7 +26,7 @@ namespace Barotrauma
class HumanSwimFastParams: HumanSwimParams
{
public static HumanSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanSwimFastParams>(character.SpeciesName, AnimationType.SwimFast);
public static HumanSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanSwimFastParams>(character, AnimationType.SwimFast);
public static HumanSwimFastParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<HumanSwimFastParams>(character.SpeciesName, AnimationType.SwimFast, fileName);
@@ -38,7 +38,7 @@ namespace Barotrauma
class HumanSwimSlowParams : HumanSwimParams
{
public static HumanSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow);
public static HumanSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanSwimSlowParams>(character, AnimationType.SwimSlow);
public static HumanSwimSlowParams GetAnimParams(Character character, string fileName = null)
{
return GetAnimParams<HumanSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow, fileName);
@@ -64,6 +64,9 @@ namespace Barotrauma
[Serialize("waterblood", true), Editable]
public string BleedParticleWater { get; private set; }
[Serialize(1f, true), Editable]
public float BleedParticleMultiplier { get; private set; }
[Serialize(10f, true, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)]
public float EatingSpeed { get; set; }
@@ -76,8 +79,13 @@ namespace Barotrauma
[Serialize(0f, true), Editable]
public float SonarDisruption { get; set; }
[Serialize(25000f, true, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)]
public float DisableDistance { get; set; }
public readonly string File;
public XDocument VariantFile { get; private set; }
public readonly List<SubParam> SubParams = new List<SubParam>();
public readonly List<SoundParams> Sounds = new List<SoundParams>();
public readonly List<ParticleParams> BloodEmitters = new List<ParticleParams>();
@@ -100,6 +108,33 @@ namespace Barotrauma
public bool Load()
{
bool success = base.Load(File);
if (doc.Root.IsCharacterVariant())
{
VariantFile = doc;
var original = CharacterPrefab.FindBySpeciesName(doc.Root.GetAttributeString("inherit", string.Empty));
success = Load(original.FilePath);
CreateSubParams();
TryLoadOverride(this, VariantFile.Root, SerializableProperties);
foreach (XElement subElement in VariantFile.Root.Elements())
{
var matchingParams = SubParams.FirstOrDefault(p => p.Name.Equals(subElement.Name.ToString(), StringComparison.OrdinalIgnoreCase));
if (matchingParams != null)
{
TryLoadOverride(matchingParams, subElement, matchingParams.SerializableProperties);
// TODO: Make recursive? In practice we don't have to go deeper than this, but the implementation would be a lot cleaner with recursion.
foreach (XElement subSubElement in subElement.Elements())
{
if (subSubElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; }
var matchingSubParams = matchingParams.SubParams.FirstOrDefault(p => p.Name.Equals(subSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase));
if (matchingSubParams != null)
{
TryLoadOverride(matchingSubParams, subSubElement, matchingSubParams.SerializableProperties);
}
}
}
}
return success;
}
if (string.IsNullOrEmpty(SpeciesName) && MainElement != null)
{
//backwards compatibility
@@ -111,6 +146,8 @@ namespace Barotrauma
public bool Save(string fileNameWithoutExtension = null)
{
// Disable saving variants for now. Making it work probably requires more work.
if (VariantFile != null) { return false; }
Serialize();
return base.Save(fileNameWithoutExtension, new XmlWriterSettings
{
@@ -181,7 +218,19 @@ namespace Barotrauma
}
}
public bool Deserialize(XElement element = null, bool alsoChildren = true, bool recursive = true)
private void TryLoadOverride(object parentObject, XElement element, Dictionary<string, SerializableProperty> properties)
{
foreach (var property in properties)
{
var matchingAttribute = element.GetAttribute(property.Key);
if (matchingAttribute != null)
{
property.Value.TrySetValue(parentObject, matchingAttribute.Value);
}
}
}
public bool Deserialize(XElement element = null, bool alsoChildren = true, bool recursive = true, bool loadDefaultValues = true)
{
if (base.Deserialize(element))
{
@@ -480,23 +529,42 @@ namespace Barotrauma
[Serialize(20f, true, description: "How long the creature flees before returning to normal state. When the creature sees the target or is being chased, it will always flee, if it's in the flee state."), Editable(minValue: 0f, maxValue: 100f)]
public float MinFleeTime { get; private set; }
[Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable()]
[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()]
[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(true, true, description: "Should the character target or ignore walls when it's outside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()]
[Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine."), Editable]
public bool TargetOuterWalls { get; private set; }
[Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable()]
[Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable]
public bool RandomAttack { get; private set; }
[Serialize(false, true, description:"Can the character open doors and hatches without a proper id card? Only applies on humanoids."), Editable]
public bool Infiltrate { get; private set; }
[Serialize(true, true, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable]
public bool AvoidAbyss { get; set; }
[Serialize(true, true, "Does the creature try to keep in the abyss? Has effect only when AvoidAbyss is false."), Editable]
public bool StayInAbyss { get; set; }
[Serialize(0f, true, description: ""), Editable]
public float StartAggression { get; private set; }
[Serialize(100f, true, description: ""), Editable]
public float MaxAggression { get; private set; }
[Serialize(0f, true, description: ""), Editable]
public float AggressionCumulation { get; private set; }
public IEnumerable<TargetParams> Targets => targets;
protected readonly List<TargetParams> targets = new List<TargetParams>();
public AIParams(XElement element, CharacterParams character) : base(element, character)
{
if (element == null) { return; }
element.GetChildElements("target").ForEach(t => TryAddTarget(t, out _));
element.GetChildElements("targetpriority").ForEach(t => TryAddTarget(t, out _));
}
@@ -588,11 +656,24 @@ namespace Barotrauma
public bool IgnoreContained { get; set; }
[Serialize(false, true, description: "Should the target be ignored while the creature is inside. Doesn't matter where the target is."), Editable]
public bool IgnoreWhileInside { get; set; }
public bool IgnoreInside { get; set; }
[Serialize(false, true, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable]
public bool IgnoreWhileOutside { get; set; }
public bool IgnoreOutside { get; set; }
[Serialize(false, true, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable]
public bool IgnoreIfNotInSameSub { get; set; }
[Serialize(false, true), Editable]
public bool IgnoreIncapacitated { get; set; }
[Serialize(0f, true, description: "How much damage the protected target should take from an attacker before the creature starts defending it."), Editable]
public float DamageThreshold { get; private set; }
[Serialize(AttackPattern.Straight, true), Editable]
public AttackPattern AttackPattern { get; set; }
#region Sweep
[Serialize(0f, true, description: "Use to define a distance at which the creature starts the sweeping movement."), Editable(MinValueFloat = 0, MaxValueFloat = 10000, ValueStep = 1, DecimalCount = 0)]
public float SweepDistance { get; private set; }
@@ -601,6 +682,21 @@ namespace Barotrauma
[Serialize(1f, true, description: "How quickly the sweep direction changes. Uses the sine wave pattern."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)]
public float SweepSpeed { get; private set; }
#endregion
#region Circle
[Serialize(5000f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)]
public float CircleStartDistance { get; private set; }
[Serialize(1f, true), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)]
public float CircleRotationSpeed { get; private set; }
[Serialize(5f, true), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)]
public float CircleStrikeDistanceMultiplier { get; private set; }
[Serialize(0f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)]
public float CircleMaxRandomOffset { get; private set; }
#endregion
public TargetParams(XElement element, CharacterParams character) : base(element, character) { }
@@ -44,14 +44,14 @@ namespace Barotrauma
protected virtual bool Deserialize(XElement element = null)
{
element = element ?? MainElement;
element ??= MainElement;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
return SerializableProperties != null;
}
protected virtual bool Serialize(XElement element = null)
{
element = element ?? MainElement;
element ??= MainElement;
if (element == null)
{
DebugConsole.ThrowError("[EditableParams] The XML element is null!");
@@ -110,7 +110,7 @@ namespace Barotrauma
{
folder = Path.Combine(Path.GetDirectoryName(filePath), "Ragdolls") + Path.DirectorySeparatorChar;
}
return folder;
return folder.CleanUpPathCrossPlatform(correctFilenameCase: true);
}
public static T GetDefaultRagdollParams<T>(string speciesName) where T : RagdollParams, new() => GetRagdollParams<T>(speciesName, GetDefaultFileName(speciesName));
@@ -136,7 +136,7 @@ namespace Barotrauma
string folder = GetFolder(speciesName);
if (Directory.Exists(folder))
{
var files = Directory.GetFiles(folder);
List<string> files = Directory.GetFiles(folder).ToList();
if (files.None())
{
DebugConsole.ThrowError($"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.");
@@ -364,6 +364,21 @@ namespace Barotrauma
}
}
#endif
private bool variantScaleApplied;
public void ApplyVariantScale(XDocument variantFile)
{
if (variantScaleApplied) { return; }
if (variantFile == null) { return; }
var scaleMultiplier = variantFile.Root.GetChildElement("ragdoll")?.GetAttributeFloat("scalemultiplier", 1f);
if (scaleMultiplier.HasValue)
{
JointScale *= scaleMultiplier.Value;
LimbScale *= scaleMultiplier.Value;
}
variantScaleApplied = true;
}
#endregion
#region Memento
@@ -584,7 +599,7 @@ namespace Barotrauma
[Serialize(0f, true, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Width { get; set; }
[Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
[Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
public float Density { get; set; }
[Serialize(false, true), Editable]
@@ -57,13 +57,13 @@ namespace Barotrauma
{
public static string Folder = "Data/ContentPackages/";
private static List<ContentPackage> regularPackages = new List<ContentPackage>();
private static readonly List<ContentPackage> regularPackages = new List<ContentPackage>();
public static IReadOnlyList<ContentPackage> RegularPackages
{
get { return regularPackages; }
}
private static List<ContentPackage> corePackages = new List<ContentPackage>();
private static readonly List<ContentPackage> corePackages = new List<ContentPackage>();
public static IReadOnlyList<ContentPackage> CorePackages
{
get { return corePackages; }
@@ -105,7 +105,7 @@ namespace Barotrauma
};
//at least one file of each these types is required in core content packages
private static HashSet<ContentType> corePackageRequiredFiles = new HashSet<ContentType>
private static readonly HashSet<ContentType> corePackageRequiredFiles = new HashSet<ContentType>
{
ContentType.Jobs,
ContentType.Item,
@@ -141,8 +141,8 @@ namespace Barotrauma
}
public static bool IngameModSwap = false;
public string Name { get; set; }
public string Name { get; set; } = string.Empty;
public string Path
{
@@ -208,9 +208,9 @@ namespace Barotrauma
}
private List<ContentFile> files;
private List<ContentFile> filesToAdd;
private List<ContentFile> filesToRemove;
private readonly List<ContentFile> files;
private readonly List<ContentFile> filesToAdd;
private readonly List<ContentFile> filesToRemove;
public IReadOnlyList<ContentFile> Files
@@ -238,6 +238,12 @@ namespace Barotrauma
get { return Files.Any(f => MultiplayerIncompatibleContent.Contains(f.Type)); }
}
public bool IsCorrupt
{
get;
private set;
}
private ContentPackage()
{
files = new List<ContentFile>();
@@ -256,7 +262,8 @@ namespace Barotrauma
if (doc?.Root == null)
{
DebugConsole.ThrowError("Couldn't load content package \"" + filePath + "\"!");
DebugConsole.ThrowError("Couldn't load content package \"" + filePath + "\"!");
IsCorrupt = true;
return;
}
@@ -621,14 +628,12 @@ namespace Barotrauma
{
case ContentType.Character:
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
var rootElement = doc.Root;
var element = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement;
var ragdollFolder = RagdollParams.GetFolder(doc, file.Path).CleanUpPathCrossPlatform(true);
var ragdollFolder = RagdollParams.GetFolder(doc, file.Path);
if (Directory.Exists(ragdollFolder))
{
Directory.GetFiles(ragdollFolder, "*.xml").ForEach(f => filePaths.Add(f));
}
var animationFolder = AnimationParams.GetFolder(doc, file.Path).CleanUpPathCrossPlatform(true);
var animationFolder = AnimationParams.GetFolder(doc, file.Path);
if (Directory.Exists(animationFolder))
{
Directory.GetFiles(animationFolder, "*.xml").ForEach(f => filePaths.Add(f));
@@ -764,7 +769,8 @@ namespace Barotrauma
foreach (string filePath in files)
{
AddPackage(new ContentPackage(filePath));
var newPackage = new ContentPackage(filePath);
if (!newPackage.IsCorrupt) { AddPackage(newPackage); }
}
IEnumerable<string> modDirectories = Directory.GetDirectories("Mods");
@@ -780,21 +786,25 @@ namespace Barotrauma
}
else if (File.Exists(modFilePath))
{
AddPackage(new ContentPackage(modFilePath));
var newPackage = new ContentPackage(modFilePath);
if (!newPackage.IsCorrupt)
{
AddPackage(newPackage);
}
}
}
SortContentPackages(p => prevRegularPackages.IndexOf(p.Name.ToLowerInvariant()));
GameMain.Config?.SortContentPackages();
}
public static void SortContentPackages<T>(Func<ContentPackage, T> order, bool refreshAll = false)
public static void SortContentPackages<T>(Func<ContentPackage, T> order, bool refreshAll = false, GameSettings config = null)
{
var ordered = regularPackages
.OrderBy(p => order(p))
.ThenBy(p => regularPackages.IndexOf(p))
.ToList();
regularPackages.Clear(); regularPackages.AddRange(ordered);
GameMain.Config?.SortContentPackages(refreshAll);
(config ?? GameMain.Config)?.SortContentPackages(refreshAll);
}
public void Delete()
@@ -106,24 +106,7 @@ namespace Barotrauma
{
lock (Coroutines)
{
Coroutines.ForEach(c =>
{
if (c.Name == name)
{
c.AbortRequested = true;
if (c.Thread != null)
{
bool joined = false;
while (!joined)
{
#if CLIENT
CrossThread.ProcessTasks();
#endif
joined = c.Thread.Join(TimeSpan.FromMilliseconds(500));
}
}
}
});
HandleCoroutineStopping(c => c.Name == name);
Coroutines.RemoveAll(c => c.Name == name);
}
}
@@ -132,10 +115,33 @@ namespace Barotrauma
{
lock (Coroutines)
{
HandleCoroutineStopping(c => c == handle);
Coroutines.RemoveAll(c => c == handle);
}
}
private static void HandleCoroutineStopping(Func<CoroutineHandle, bool> filter)
{
foreach (CoroutineHandle coroutine in Coroutines)
{
if (filter(coroutine))
{
coroutine.AbortRequested = true;
if (coroutine.Thread != null)
{
bool joined = false;
while (!joined)
{
#if CLIENT
CrossThread.ProcessTasks();
#endif
joined = coroutine.Thread.Join(TimeSpan.FromMilliseconds(500));
}
}
}
}
}
public static void ExecuteCoroutineThread(CoroutineHandle handle)
{
try
@@ -733,6 +733,11 @@ namespace Barotrauma
if (newEvent != null)
{
var @event = newEvent.CreateInstance();
if (newEvent == null)
{
NewMessage($"Could not initialize event {args[0]} because level did not meet requirements");
return;
}
GameMain.GameSession.EventManager.ActiveEvents.Add(@event);
@event.Init(true);
NewMessage($"Initialized event {newEvent.Identifier}", Color.Aqua);
@@ -816,7 +821,7 @@ namespace Barotrauma
NewMessage(Hull.EditFire ? "Fire spawning on" : "Fire spawning off", Color.White);
}, isCheat: true));
commands.Add(new Command("explosion", "explosion [range] [force] [damage] [structuredamage] [item damage] [emp strength]: Creates an explosion at the position of the cursor.", null, isCheat: true));
commands.Add(new Command("explosion", "explosion [range] [force] [damage] [structuredamage] [item damage] [emp strength] [ballast flora strength]: Creates an explosion at the position of the cursor.", null, isCheat: true));
commands.Add(new Command("showseed|showlevelseed", "showseed: Show the seed of the current level.", (string[] args) =>
{
@@ -827,6 +832,8 @@ namespace Barotrauma
else
{
NewMessage("Level seed: " + Level.Loaded.Seed);
NewMessage("Level size: " + Level.Loaded.Size.X+"x"+ Level.Loaded.Size.Y);
NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown"));
}
},null));
@@ -1166,6 +1173,14 @@ namespace Barotrauma
c.SetAllDamage(200.0f, 0.0f, 0.0f);
}
}
foreach (Hull hull in Hull.hullList)
{
hull.BallastFlora?.Kill();
}
foreach (Submarine sub in Submarine.Loaded)
{
sub.WreckAI?.Kill();
}
}, null, isCheat: true));
commands.Add(new Command("setclientcharacter", "setclientcharacter [client name] [character name]: Gives the client control of the specified character.", null,
@@ -1287,6 +1302,7 @@ namespace Barotrauma
{
if (item.CurrentHull != null && item.HasTag("ballast") && item.GetComponent<Pump>() is { } pump)
{
if (item.CurrentHull.BallastFlora != null) { continue; }
pumps.Add(pump);
}
}
@@ -1301,8 +1317,8 @@ namespace Barotrauma
}
Pump random = pumps.GetRandom();
random.InfectBallast(prefab.Identifier);
NewMessage($"Infected {random.Name} with {prefab.Identifier}.", Color.Green);
random.InfectBallast(prefab.Identifier, allowMultiplePerShip: true);
NewMessage($"Infected {random.Name} with {prefab.Identifier} in {random.Item.CurrentHull.DisplayName}.", Color.Green);
return;
}
@@ -1448,6 +1464,28 @@ namespace Barotrauma
NewMessage("Set packet duplication to " + (int)(duplicates * 100) + "%.", Color.White);
}));
#if DEBUG
commands.Add(new Command("storeinfo", "", (string[] args) =>
{
if (GameMain.GameSession?.Map?.CurrentLocation is Location location)
{
var msg = "--- Location: " + location.Name + " ---";
msg += "\nBalance: " + location.StoreCurrentBalance;
msg += "\nPrice modifier: " + location.StorePriceModifier + "%";
msg += "\nDaily specials:";
location.DailySpecials.ForEach(i => msg += "\n - " + i.Name);
msg += "\nRequested goods:";
location.RequestedGoods.ForEach(i => msg += "\n - " + i.Name);
NewMessage(msg);
}
else
{
NewMessage("No current location set, can't show store info.");
}
}));
#endif
//"dummy commands" that only exist so that the server can give clients permissions to use them
//TODO: alphabetical order?
commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () =>
@@ -1458,6 +1496,7 @@ namespace Barotrauma
commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true));
commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true));
commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null, isCheat: true));
commands.Add(new Command("togglevoicechatfilters", "Toggle the radio/muffle filters in the voice chat (client-only).", null, isCheat: false));
commands.Add(new Command("togglehud|hud", "Toggle the character HUD (inventories, icons, buttons, etc) on/off (client-only).", null));
commands.Add(new Command("toggleupperhud", "Toggle the upper part of the ingame HUD (chatbox, crewmanager) on/off (client-only).", null));
commands.Add(new Command("toggleitemhighlights", "Toggle the item highlight effect on/off (client-only).", null));
@@ -1755,7 +1794,7 @@ namespace Barotrauma
if (GameMain.GameSession != null)
{
//TODO: a way to select which team to spawn to?
spawnedCharacter.TeamID = Character.Controlled != null ? Character.Controlled.TeamID : Character.TeamType.Team1;
spawnedCharacter.TeamID = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1;
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter);
#endif
@@ -1971,13 +2010,21 @@ namespace Barotrauma
{
if (e != null)
{
error += " {" + e.Message + "}\n" + e.StackTrace.CleanupStackTrace();
error += " {" + e.Message + "}\n";
if (e.StackTrace != null)
{
error += e.StackTrace.CleanupStackTrace();
}
if (e.InnerException != null)
{
error += "\n\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace();
error += "\n\nInner exception: " + e.InnerException.Message + "\n";
if (e.InnerException.StackTrace != null)
{
error += e.InnerException.StackTrace.CleanupStackTrace(); ;
}
}
}
else if (appendStackTrace)
else if (appendStackTrace && Environment.StackTrace != null)
{
error += "\n" + Environment.StackTrace.CleanupStackTrace();
}
@@ -59,7 +59,7 @@ namespace Barotrauma
(Rand.Value(Rand.RandSync.Server) < 0.5f) ?
Level.PositionType.MainPath | Level.PositionType.SidePath :
Level.PositionType.Cave | Level.PositionType.Ruin,
500.0f, 10000.0f, 30.0f);
500.0f, 10000.0f, 30.0f, SpawnPosFilter);
spawnPending = true;
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma
@@ -11,6 +12,8 @@ namespace Barotrauma
public EventPrefab Prefab => prefab;
public Func<Level.InterestingPosition, bool> SpawnPosFilter;
public bool IsFinished
{
get { return isFinished; }
@@ -56,5 +59,10 @@ namespace Barotrauma
{
return true;
}
public virtual bool LevelMeetsRequirements()
{
return true;
}
}
}
@@ -0,0 +1,59 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
internal class CheckAfflictionAction : BinaryOptionAction
{
[Serialize("", true)]
public string Identifier { get; set; } = "";
[Serialize("", true)]
public string TargetTag { get; set; } = "";
[Serialize(LimbType.None, true, "Only check afflictions on the specified limb type")]
public LimbType TargetLimb { get; set; }
[Serialize(true, true, "When set to false when TargetLimb is not specified prevent checking limb-specific afflictions")]
public bool AllowLimbAfflictions { get; set; }
public CheckAfflictionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
if (string.IsNullOrWhiteSpace(Identifier) || string.IsNullOrWhiteSpace(TargetTag)) { return false; }
List<Character> targets = ParentEvent.GetTargets(TargetTag).OfType<Character>().ToList();
if (!(targets.FirstOrDefault() is { } target)) { return false; }
if (TargetLimb == LimbType.None)
{
Affliction? affliction = target.CharacterHealth?.GetAffliction(Identifier, AllowLimbAfflictions);
return affliction != null;
}
if (target.CharacterHealth == null) { return false; }
IEnumerable<Affliction> afflictions = target.CharacterHealth.GetAllAfflictions().Where(affliction =>
{
LimbType? limbType = target.CharacterHealth.GetAfflictionLimb(affliction)?.type;
if (limbType == null) { return false; }
return limbType == TargetLimb || true;
});
return afflictions.Any(a => a.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase));
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckAfflictionAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
$"AfflictionIdentifier: {Identifier.ColorizeObject()}, " +
$"TargetLimb: {TargetLimb.ColorizeObject()}, " +
$"Succeeded: {succeeded.ColorizeObject()})";
}
}
}
@@ -1,4 +1,5 @@
#nullable enable
using System;
using System.Xml.Linq;
namespace Barotrauma
@@ -11,6 +12,12 @@ namespace Barotrauma
[Serialize("", true)]
public string Condition { get; set; } = null!;
[Serialize(false, true, "Forces the comparison to use string instead of attempting to parse it as a boolean or a float first")]
public bool ForceString { get; set; }
[Serialize(false, true, "Performs the comparison against a metadata by identifier instead of a constant value")]
public bool CheckAgainstMetadata { get; set; }
protected object? value2;
protected object? value1;
@@ -41,13 +48,52 @@ namespace Barotrauma
Operator = PropertyConditional.GetOperatorType(op);
if (Operator == PropertyConditional.OperatorType.None) { return false; }
bool? tryBoolean = TryBoolean(campaignMode, value);
if (tryBoolean != null) { return tryBoolean; }
if (CheckAgainstMetadata)
{
object? metadata1 = campaignMode.CampaignMetadata.GetValue(Identifier);
object? metadata2 = campaignMode.CampaignMetadata.GetValue(value);
bool? tryFloat = TryFloat(campaignMode, value);
if (tryFloat != null) { return tryFloat; }
if (metadata1 == null || metadata2 == null)
{
return Operator switch
{
PropertyConditional.OperatorType.Equals => metadata1 == metadata2,
PropertyConditional.OperatorType.NotEquals => metadata1 != metadata2,
_ => false
};
}
if (!ForceString)
{
switch (metadata1)
{
case bool bool1 when metadata2 is bool bool2:
return CompareBool(bool1, bool2) ?? false;
case float float1 when metadata2 is float float2:
return CompareFloat(float1, float2) ?? false;
}
}
if (metadata1 is string string1 && metadata2 is string string2)
{
return CompareString(string1, string2) ?? false;
}
return false;
}
if (!ForceString)
{
bool? tryBoolean = TryBoolean(campaignMode, value);
if (tryBoolean != null) { return tryBoolean; }
bool? tryFloat = TryFloat(campaignMode, value);
if (tryFloat != null) { return tryFloat; }
}
bool? tryString = TryString(campaignMode, value);
if (tryString != null) { return tryString; }
DebugConsole.ThrowError($"{value2} ({Condition}) did not match a boolean or a float.");
return false;
}
@@ -55,53 +101,85 @@ namespace Barotrauma
{
if (bool.TryParse(value, out bool b))
{
bool target = GetBool(campaignMode);
value1 = target;
value2 = b;
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
return target == b;
case PropertyConditional.OperatorType.NotEquals:
return target != b;
default:
DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a boolean (was {Operator} for {value}).");
return false;
}
return CompareBool(GetBool(campaignMode), b);
}
DebugConsole.Log($"{value} != bool");
return null;
}
private bool? CompareBool(bool val1, bool val2)
{
value1 = val1;
value2 = val2;
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
return val1 == val2;
case PropertyConditional.OperatorType.NotEquals:
return val1 != val2;
default:
DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a boolean (was {Operator} for {val2}).");
return false;
}
}
private bool? TryFloat(CampaignMode campaignMode, string value)
{
if (float.TryParse(value, out float f))
{
float target = GetFloat(campaignMode);
value1 = target;
value2 = f;
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
return MathUtils.NearlyEqual(target, f);
case PropertyConditional.OperatorType.GreaterThan:
return target > f;
case PropertyConditional.OperatorType.GreaterThanEquals:
return target >= f;
case PropertyConditional.OperatorType.LessThan:
return target < f;
case PropertyConditional.OperatorType.LessThanEquals:
return target <= f;
case PropertyConditional.OperatorType.NotEquals:
return !MathUtils.NearlyEqual(target, f);
}
return CompareFloat(GetFloat(campaignMode), f);
}
DebugConsole.Log($"{value} != float");
return null;
}
private bool? CompareFloat(float val1, float val2)
{
value1 = val1;
value2 = val2;
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
return MathUtils.NearlyEqual(val1, val2);
case PropertyConditional.OperatorType.GreaterThan:
return val1 > val2;
case PropertyConditional.OperatorType.GreaterThanEquals:
return val1 >= val2;
case PropertyConditional.OperatorType.LessThan:
return val1 < val2;
case PropertyConditional.OperatorType.LessThanEquals:
return val1 <= val2;
case PropertyConditional.OperatorType.NotEquals:
return !MathUtils.NearlyEqual(val1, val2);
}
return null;
}
private bool? TryString(CampaignMode campaignMode, string value)
{
return CompareString(GetString(campaignMode), value);
}
private bool? CompareString(string val1, string val2)
{
value1 = val1;
value2 = val2;
bool equals = string.Equals(val1, val2, StringComparison.OrdinalIgnoreCase);
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
return equals;
case PropertyConditional.OperatorType.NotEquals:
return !equals;
default:
DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a string (was {Operator} for {val2}).");
return null;
}
}
protected virtual bool GetBool(CampaignMode campaignMode)
{
return campaignMode.CampaignMetadata.GetBoolean(Identifier);
@@ -112,6 +190,11 @@ namespace Barotrauma
return campaignMode.CampaignMetadata.GetFloat(Identifier);
}
private string GetString(CampaignMode campaignMode)
{
return campaignMode.CampaignMetadata.GetString(Identifier);
}
public override string ToDebugString()
{
string condition = "?";
@@ -0,0 +1,38 @@
using System.Xml.Linq;
using NLog.Targets;
namespace Barotrauma
{
class ClearTagAction : EventAction
{
[Serialize("", true)]
public string Tag { get; set; }
private bool isFinished;
public ClearTagAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
public override bool IsFinished(ref string goToLabel) => isFinished;
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
if (!string.IsNullOrWhiteSpace(Tag) && ParentEvent.Targets.ContainsKey(Tag))
{
ParentEvent.Targets.Remove(Tag);
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(ClearTagAction)} -> (Tag: {Tag.ColorizeObject()})";
}
}
}
@@ -1,3 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
@@ -40,9 +41,15 @@ namespace Barotrauma
[Serialize(true, true)]
public bool WaitForInteraction { get; set; }
[Serialize("", true, "Tag to assign to whoever invokes the conversation")]
public string InvokerTag { get; set; }
[Serialize(false, true)]
public bool FadeToBlack { get; set; }
[Serialize(true, true, "Should the event end if the conversations is interrupted (e.g. if the speaker dies or falls unconscious mid-conversation). Defaults to true.")]
public bool EndEventIfInterrupted { get; set; }
[Serialize("", true)]
public string EventSprite { get; set; }
@@ -54,7 +61,6 @@ namespace Barotrauma
private Character speaker;
private OrderInfo? prevSpeakerOrder;
private AIObjective prevIdleObjective, prevGotoObjective;
public List<SubactionGroup> Options { get; private set; }
@@ -104,19 +110,26 @@ namespace Barotrauma
{
#if CLIENT
dialogBox?.Close();
GUIMessageBox.MessageBoxes.ForEachMod(mb =>
{
if (mb.UserData as string == "ConversationAction")
{
(mb as GUIMessageBox)?.Close();
}
});
#else
foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.InGame && c.Character != null) { ServerWrite(speaker, c); }
}
# endif
#endif
ResetSpeaker();
dialogOpened = false;
}
if (Interrupted == null)
{
goTo = "_end";
if (EndEventIfInterrupted) { goTo = "_end"; }
return true;
}
else
@@ -171,16 +184,9 @@ namespace Barotrauma
GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
#endif
var humanAI = speaker.AIController as HumanAIController;
if (humanAI != null)
if (humanAI != null && !speaker.IsDead && !speaker.Removed)
{
if (prevSpeakerOrder != null)
{
humanAI.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false);
}
else
{
humanAI.SetOrder(null, string.Empty, orderGiver: null, speak: false);
}
humanAI.ClearForcedOrder();
if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); }
if (prevGotoObjective != null) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); }
}
@@ -255,7 +261,12 @@ namespace Barotrauma
}
else
{
if (Options.Any())
if (ShouldInterrupt())
{
ResetSpeaker();
interrupt = true;
}
else if (Options.Any())
{
Options[selectedOption].Update(deltaTime);
}
@@ -305,16 +316,11 @@ namespace Barotrauma
if (speaker?.AIController is HumanAIController humanAI)
{
prevSpeakerOrder = null;
if (humanAI.CurrentOrder != null)
{
prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption);
}
prevIdleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
prevGotoObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveGoTo>();
humanAI.SetOrder(
Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)),
option: string.Empty, orderGiver: null, speak: false);
humanAI.SetForcedOrder(
Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)),
option: string.Empty, orderGiver: null);
if (targets.Any())
{
Entity closestTarget = null;
@@ -335,6 +341,11 @@ namespace Barotrauma
}
}
if (targetCharacter != null && !string.IsNullOrWhiteSpace(InvokerTag))
{
ParentEvent.AddTarget(InvokerTag, targetCharacter);
}
ShowDialog(speaker, targetCharacter);
dialogOpened = true;
@@ -42,7 +42,7 @@ namespace Barotrauma
var targets = ParentEvent.GetTargets(TargetTag).Where(e => e is Character).Select(e => e as Character);
foreach (var target in targets)
{
target.Info?.IncreaseSkillLevel(Skill?.ToLowerInvariant(), Amount, target.WorldPosition + Vector2.UnitY * 150.0f);
target.Info?.IncreaseSkillLevel(Skill?.ToLowerInvariant(), Amount, target.Position + Vector2.UnitY * 150.0f);
}
isFinished = true;
}
@@ -68,6 +68,9 @@ namespace Barotrauma
}
}
[Serialize(false, true, description: "Should the AI ignore this item. This will prevent outpost NPCs cleaning up or otherwise using important items intended to be left for the players.")]
public bool IgnoreByAI { get; set; }
private bool spawned;
private Entity spawnedEntity;
@@ -106,38 +109,17 @@ namespace Barotrauma
ISpatialEntity spawnPos = GetSpawnPos();
Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter =>
{
newCharacter.TeamID = Character.TeamType.FriendlyNPC;
newCharacter.TeamID = CharacterTeamType.FriendlyNPC;
newCharacter.EnableDespawn = false;
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine);
if (LootingIsStealing)
{
foreach (Item item in newCharacter.Inventory.Items)
foreach (Item item in newCharacter.Inventory.AllItems)
{
if (item != null) { item.SpawnedInOutpost = true; }
}
}
newCharacter.CharacterHealth.MaxVitality *= humanPrefab.HealthMultiplier;
var humanAI = newCharacter.AIController as HumanAIController;
if (humanAI != null)
{
var idleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
if (idleObjective != null)
{
idleObjective.Behavior = humanPrefab.Behavior;
foreach (string moduleType in humanPrefab.PreferredOutpostModuleTypes)
{
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
}
}
}
if (humanPrefab.CampaignInteractionType != CampaignMode.InteractionType.None)
{
(GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(newCharacter, humanPrefab.CampaignInteractionType);
if (spawnPos != null && humanAI != null)
{
humanAI.ObjectiveManager.SetOrder(new AIObjectiveGoTo(spawnPos, newCharacter, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200));
item.SpawnedInOutpost = true;
}
}
humanPrefab.InitializeCharacter(newCharacter, spawnPos);
if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null)
{
ParentEvent.AddTarget(TargetTag, newCharacter);
@@ -197,9 +179,16 @@ namespace Barotrauma
}
void onSpawned(Item newItem)
{
if (!string.IsNullOrEmpty(TargetTag) && newItem != null)
if (newItem != null)
{
ParentEvent.AddTarget(TargetTag, newItem);
if (!string.IsNullOrEmpty(TargetTag))
{
ParentEvent.AddTarget(TargetTag, newItem);
}
if (IgnoreByAI)
{
newItem.AddTag("ignorebyai");
}
}
spawnedEntity = newItem;
}
@@ -12,6 +12,9 @@ namespace Barotrauma
[Serialize("", true)]
public string Tag { get; set; }
[Serialize(true, true)]
public bool IgnoreIncapacitatedCharacters { get; set; }
private bool isFinished = false;
public TagAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
@@ -27,12 +30,26 @@ namespace Barotrauma
private void TagPlayers()
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer);
if (IgnoreIncapacitatedCharacters)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer && !c.IsIncapacitated);
}
else
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer);
}
}
private void TagBots()
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot);
if (IgnoreIncapacitatedCharacters)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && !c.IsIncapacitated);
}
else
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot);
}
}
private void TagCrew()
@@ -24,9 +24,12 @@ namespace Barotrauma
[Serialize(0.0f, true, description: "Range both entities must be within to activate the trigger.")]
public float Radius { get; set; }
[Serialize(true, true, description: "If true, characters who are being targeted by some enemy cannot trigger the event.")]
[Serialize(true, true, description: "If true, characters who are being targeted by some enemy cannot trigger the action.")]
public bool DisableInCombat { get; set; }
[Serialize(true, true, description: "If true, dead/unconscious characters cannot trigger the action.")]
public bool DisableIfTargetIncapacitated { get; set; }
private float distance;
public TriggerAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
@@ -59,6 +62,7 @@ namespace Barotrauma
foreach (Entity e1 in targets1)
{
if (DisableInCombat && IsInCombat(e1)) { continue; }
if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated)) { continue; }
if (!string.IsNullOrEmpty(TargetModuleType))
{
if (IsCloseEnoughToHull(e1, out Hull hull))
@@ -75,6 +79,7 @@ namespace Barotrauma
{
if (e1 == e2) { continue; }
if (DisableInCombat && IsInCombat(e2)) { continue; }
if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated)) { continue; }
Vector2 pos1 = e1.WorldPosition;
Vector2 pos2 = e2.WorldPosition;
@@ -33,7 +33,11 @@ namespace Barotrauma
}
else
{
GameMain.GameSession.EventManager.QueuedEvents.Enqueue(eventPrefab.CreateInstance());
var ev = eventPrefab.CreateInstance();
if (ev != null)
{
GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev);
}
}
}
@@ -3,6 +3,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -51,6 +52,12 @@ namespace Barotrauma
private float roundDuration;
private bool isCrewAway;
//how long it takes after the crew returns for the event manager to resume normal operation
const float CrewAwayResetDelay = 60.0f;
private float crewAwayResetTimer;
private float crewAwayDuration;
private readonly List<EventSet> pendingEventSets = new List<EventSet>();
private readonly Dictionary<EventSet, List<Event>> selectedEvents = new Dictionary<EventSet, List<Event>>();
@@ -86,6 +93,8 @@ namespace Barotrauma
public void StartRound(Level level)
{
this.level = level;
if (isClient) { return; }
pendingEventSets.Clear();
@@ -100,7 +109,6 @@ namespace Barotrauma
totalPathLength = steeringPath.TotalLength;
}
this.level = level;
SelectSettings();
var initialEventSet = SelectRandomEvents(EventSet.List);
@@ -144,6 +152,9 @@ namespace Barotrauma
PreloadContent(GetFilesToPreload());
roundDuration = 0.0f;
isCrewAway = false;
crewAwayDuration = 0.0f;
crewAwayResetTimer = 0.0f;
intensityUpdateTimer = 0.0f;
CalculateCurrentIntensity(0.0f);
currentIntensity = targetIntensity;
@@ -258,26 +269,23 @@ namespace Barotrauma
var doc = characterPrefab.XDocument;
var rootElement = doc.Root;
var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement;
foreach (var soundElement in mainElement.GetChildElements("sound"))
{
var sound = Submarine.LoadRoundSound(soundElement);
}
string speciesName = mainElement.GetAttributeString("speciesname", null);
if (string.IsNullOrWhiteSpace(speciesName))
{
speciesName = mainElement.GetAttributeString("name", null);
if (!string.IsNullOrWhiteSpace(speciesName))
{
DebugConsole.NewMessage($"Error in {file.Path}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange);
}
else
{
throw new Exception($"Species name null in {file.Path}");
}
}
mainElement.GetChildElements("sound").ForEach(e => Submarine.LoadRoundSound(e));
if (!CharacterPrefab.CheckSpeciesName(mainElement, file.Path, out string speciesName)) { continue; }
bool humanoid = mainElement.GetAttributeBool("humanoid", false);
CharacterPrefab originalCharacter;
if (characterPrefab.VariantOf != null)
{
originalCharacter = CharacterPrefab.FindBySpeciesName(characterPrefab.VariantOf);
var originalRoot = originalCharacter.XDocument.Root;
var originalMainElement = originalRoot.IsOverride() ? originalRoot.FirstElement() : originalRoot;
originalMainElement.GetChildElements("sound").ForEach(e => Submarine.LoadRoundSound(e));
if (!CharacterPrefab.CheckSpeciesName(mainElement, file.Path, out string name)) { continue; }
speciesName = name;
if (mainElement.Attribute("humanoid") == null)
{
humanoid = originalMainElement.GetAttributeBool("humanoid", false);
}
}
RagdollParams ragdollParams;
if (humanoid)
{
@@ -335,13 +343,31 @@ namespace Barotrauma
{
if (level == null) { return; }
int applyCount = 1;
List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>();
if (eventSet.PerRuin)
{
applyCount = Level.Loaded.Ruins.Count();
foreach (var ruin in Level.Loaded.Ruins)
{
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Ruin == ruin; });
}
}
else if (eventSet.PerCave)
{
applyCount = Level.Loaded.Caves.Count();
foreach (var cave in Level.Loaded.Caves)
{
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Cave == cave; });
}
}
else if (eventSet.PerWreck)
{
applyCount = Submarine.Loaded.Count(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive));
var wrecks = Submarine.Loaded.Where(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive));
applyCount = wrecks.Count();
foreach (var wreck in wrecks)
{
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Submarine == wreck; });
}
}
for (int i = 0; i < applyCount; i++)
{
@@ -356,7 +382,9 @@ namespace Barotrauma
if (eventPrefab != null)
{
var newEvent = eventPrefab.First.CreateInstance();
if (newEvent == null) { continue; }
newEvent.Init(true);
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
DebugConsole.Log("Initialized event " + newEvent.ToString());
if (!selectedEvents.ContainsKey(eventSet))
{
@@ -378,6 +406,7 @@ namespace Barotrauma
foreach (Pair<EventPrefab, float> eventPrefab in eventSet.EventPrefabs)
{
var newEvent = eventPrefab.First.CreateInstance();
if (newEvent == null) { continue; }
newEvent.Init(true);
DebugConsole.Log("Initialized event " + newEvent.ToString());
if (!selectedEvents.ContainsKey(eventSet))
@@ -402,10 +431,11 @@ namespace Barotrauma
var allowedEventSets =
eventSets.Where(es => level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty && level.LevelData.Type == es.LevelType);
if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.Map?.CurrentLocation?.Type != null)
LocationType locationType = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation?.Type ?? level?.StartLocation?.Type;
if (locationType != null)
{
allowedEventSets = allowedEventSets.Where(set => set.LocationTypeIdentifiers == null || set.LocationTypeIdentifiers.Any(identifier => string.Equals(identifier, campaign.Map.CurrentLocation.Type.Identifier, StringComparison.OrdinalIgnoreCase)));
allowedEventSets = allowedEventSets.Where(set => set.LocationTypeIdentifiers == null || set.LocationTypeIdentifiers.Any(identifier => string.Equals(identifier, locationType.Identifier, StringComparison.OrdinalIgnoreCase)));
}
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
@@ -440,6 +470,14 @@ namespace Barotrauma
}
}
if (eventSet.DelayWhenCrewAway)
{
if ((isCrewAway && crewAwayDuration < settings.FreezeDurationWhenCrewAway) || crewAwayResetTimer > 0.0f)
{
return false;
}
}
if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) &&
roundDuration < eventSet.MinMissionTime)
{
@@ -491,6 +529,25 @@ namespace Barotrauma
}
}
if (IsCrewAway())
{
isCrewAway = true;
crewAwayResetTimer = CrewAwayResetDelay;
crewAwayDuration += deltaTime;
}
else if (crewAwayResetTimer > 0.0f)
{
isCrewAway = false;
crewAwayResetTimer -= deltaTime;
}
else
{
isCrewAway = false;
crewAwayDuration = 0.0f;
eventThreshold += settings.EventThresholdIncrease * deltaTime;
eventCoolDown -= deltaTime;
}
calculateDistanceTraveledTimer -= deltaTime;
if (calculateDistanceTraveledTimer <= 0.0f)
{
@@ -498,9 +555,6 @@ namespace Barotrauma
calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval;
}
eventThreshold += settings.EventThresholdIncrease * deltaTime;
eventCoolDown -= deltaTime;
if (currentIntensity < eventThreshold)
{
bool recheck = false;
@@ -524,7 +578,10 @@ namespace Barotrauma
{
activeEvents.Add(ev);
eventThreshold = settings.DefaultEventThreshold;
eventCoolDown = settings.EventCooldown;
if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown))
{
eventCoolDown = settings.EventCooldown;
}
}
}
@@ -561,7 +618,7 @@ namespace Barotrauma
int characterCount = 0;
foreach (Character character in Character.CharacterList)
{
if (character.IsDead || character.TeamID == Character.TeamType.FriendlyNPC) { continue; }
if (character.IsDead || character.TeamID == CharacterTeamType.FriendlyNPC) { continue; }
if (character.AIController is HumanAIController || character.IsRemotePlayer)
{
avgCrewHealth += character.Vitality / character.MaxVitality * (character.IsUnconscious ? 0.5f : 1.0f);
@@ -584,9 +641,8 @@ namespace Barotrauma
{
if (character.IsDead || character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; }
EnemyAIController enemyAI = character.AIController as EnemyAIController;
if (enemyAI == null) continue;
if (!(character.AIController is EnemyAIController enemyAI)) { continue; }
if (character.CurrentHull?.Submarine != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
{
@@ -679,7 +735,6 @@ namespace Barotrauma
}
}
/// <summary>
/// Finds all actions in a ScriptedEvent
/// </summary>
@@ -748,5 +803,74 @@ namespace Barotrauma
#endif
return refEntity;
}
private bool IsCrewAway()
{
#if CLIENT
return Character.Controlled != null && IsCharacterAway(Character.Controlled);
#else
int playerCount = 0;
int awayPlayerCount = 0;
foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients)
{
if (client.Character == null || client.Character.IsDead || client.Character.IsIncapacitated) { continue; }
playerCount++;
if (IsCharacterAway(client.Character)) { awayPlayerCount++; }
}
return playerCount > 0 && awayPlayerCount / (float)playerCount > 0.5f;
#endif
}
private bool IsCharacterAway(Character character)
{
if (character.Submarine != null)
{
switch (character.Submarine.Info.Type)
{
case SubmarineType.Player:
case SubmarineType.Outpost:
case SubmarineType.OutpostModule:
return false;
case SubmarineType.Wreck:
case SubmarineType.BeaconStation:
return true;
}
}
const int maxDist = 1000;
if (Level.Loaded != null)
{
foreach (var ruin in Level.Loaded.Ruins)
{
Rectangle area = ruin.Area;
area.Inflate(maxDist, maxDist);
if (area.Contains(character.WorldPosition)) { return true; }
}
foreach (var cave in Level.Loaded.Caves)
{
Rectangle area = cave.Area;
area.Inflate(maxDist, maxDist);
if (area.Contains(character.WorldPosition)) { return true; }
}
}
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.Info.Type != SubmarineType.BeaconStation && sub.Info.Type != SubmarineType.Wreck) { continue; }
Rectangle worldBorders = new Rectangle(
sub.Borders.X + (int)sub.WorldPosition.X - maxDist,
sub.Borders.Y + (int)sub.WorldPosition.Y + maxDist,
sub.Borders.Width + maxDist * 2,
sub.Borders.Height + maxDist * 2);
if (Submarine.RectContains(worldBorders, character.WorldPosition))
{
return true;
}
}
return false;
}
}
}
@@ -24,6 +24,8 @@ namespace Barotrauma
public readonly float MinLevelDifficulty = 0.0f;
public readonly float MaxLevelDifficulty = 100.0f;
public readonly float FreezeDurationWhenCrewAway = 60.0f * 10.0f;
public static void Init()
{
List.Clear();
@@ -77,6 +79,8 @@ namespace Barotrauma
MinLevelDifficulty = element.GetAttributeFloat("MinLevelDifficulty", 0.0f);
MaxLevelDifficulty = element.GetAttributeFloat("MaxLevelDifficulty", 100.0f);
FreezeDurationWhenCrewAway = element.GetAttributeFloat("FreezeDurationWhenCrewAway", 10.0f * 60.0f);
}
}
}
@@ -7,9 +7,9 @@ namespace Barotrauma
class EventPrefab
{
public readonly XElement ConfigElement;
public readonly Type EventType;
public readonly string MusicType;
public readonly Type EventType;
public readonly float SpawnProbability;
public readonly bool TriggerEventCooldown;
public float Commonness;
public string Identifier;
@@ -17,8 +17,6 @@ namespace Barotrauma
{
ConfigElement = element;
MusicType = element.GetAttributeString("musictype", "default");
try
{
EventType = Type.GetType("Barotrauma." + ConfigElement.Name, true, true);
@@ -35,6 +33,7 @@ namespace Barotrauma
Identifier = ConfigElement.GetAttributeString("identifier", string.Empty);
Commonness = element.GetAttributeFloat("commonness", 1.0f);
SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
}
public Event CreateInstance()
@@ -50,6 +49,9 @@ namespace Barotrauma
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
}
Event ev = (Event)instance;
if (!ev.LevelMeetsRequirements()) { return null; }
return (Event)instance;
}
}
@@ -83,11 +83,14 @@ namespace Barotrauma
public readonly bool IgnoreCoolDown;
public readonly bool PerRuin;
public readonly bool PerWreck;
public readonly bool PerRuin, PerCave, PerWreck;
public readonly bool OncePerOutpost;
public readonly bool DelayWhenCrewAway;
public readonly bool TriggerEventCooldown;
public readonly Dictionary<string, float> Commonness;
//Pair.First: event prefab, Pair.Second: commonness
@@ -133,10 +136,13 @@ namespace Barotrauma
MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f);
AllowAtStart = element.GetAttributeBool("allowatstart", false);
IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? false);
PerRuin = element.GetAttributeBool("perruin", false);
PerCave = element.GetAttributeBool("percave", false);
PerWreck = element.GetAttributeBool("perwreck", false);
IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck));
DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck);
OncePerOutpost = element.GetAttributeBool("perwreck", false);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
Commonness[""] = 1.0f;
foreach (XElement subElement in element.Elements())
@@ -0,0 +1,112 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
partial class AbandonedOutpostMission : Mission
{
private readonly XElement characterConfig;
private readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
private readonly string itemTag;
private Item itemToDestroy;
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations) :
base(prefab, locations)
{
characterConfig = prefab.ConfigElement.Element("Characters");
itemTag = prefab.ConfigElement.GetAttributeString("targetitem", "");
if (string.IsNullOrEmpty(itemTag))
{
DebugConsole.ThrowError($"Error in mission prefab \"{prefab.Identifier}\". Target item not defined.");
}
}
protected override void StartMissionSpecific(Level level)
{
itemToDestroy = null;
itemToDestroy = Item.ItemList.Find(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag));
if (itemToDestroy == null)
{
DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".");
}
if (!IsClient)
{
InitCharacters();
}
}
private void InitCharacters()
{
characters.Clear();
characterItems.Clear();
if (characterConfig == null) { return; }
var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub;
if (submarine.Info.Type == SubmarineType.Outpost)
{
submarine.TeamID = CharacterTeamType.None;
}
foreach (XElement element in characterConfig.Elements())
{
string characterIdentifier = element.GetAttributeString("identifier", "");
string characterFrom = element.GetAttributeString("from", "");
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
if (humanPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn character for abandoned outpost mission: character prefab \"" + characterIdentifier + "\" not found");
return;
}
string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null);
string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human,
moduleFlags ?? humanPrefab.GetModuleFlags(),
spawnPointTags ?? humanPrefab.GetSpawnPointTags());
if (spawnPos == null)
{
spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
}
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server);
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, spawnPos.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
spawnedCharacter.TeamID = CharacterTeamType.None;
humanPrefab.InitializeCharacter(spawnedCharacter, spawnPos);
humanPrefab.GiveItems(spawnedCharacter, Submarine.MainSub, Rand.RandSync.Server, createNetworkEvents: false);
characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
}
}
public override void Update(float deltaTime)
{
if (State == 0 && itemToDestroy != null && itemToDestroy.Condition <= 0.0f)
{
State = 1;
}
}
public override void End()
{
completed = itemToDestroy == null || itemToDestroy.Condition <= 0.0f;
if (completed)
{
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
GiveReward();
}
}
}
}
@@ -1,5 +1,3 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -11,11 +9,9 @@ namespace Barotrauma
partial class BeaconMission : Mission
{
private bool swarmSpawned;
private string monsterSpeciesName;
private readonly string monsterSpeciesName;
private Point monsterCountRange;
private Level level;
private Location[] locations;
private string sonarLabel;
private readonly string sonarLabel;
public BeaconMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations)
{
@@ -34,8 +30,6 @@ namespace Barotrauma
monsterCountRange = new Point(min, max);
this.locations = locations;
sonarLabel = TextManager.Get("beaconstationsonarlabel");
}
@@ -51,27 +45,58 @@ namespace Barotrauma
{
get
{
yield return level.BeaconStation.WorldPosition;
if (level.BeaconStation == null)
{
yield break;
}
yield return level.BeaconStation.WorldPosition;
}
}
public override void Start(Level level)
{
this.level = level;
}
public override void Update(float deltaTime)
{
if (IsClient) { return; }
if (!swarmSpawned && level.CheckBeaconActive())
{
State = 1;
Vector2 spawnPos = level.BeaconStation.WorldPosition;
spawnPos.Y += level.BeaconStation.GetDockedBorders().Height * 1.5f;
var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p =>
p.PositionType == Level.PositionType.MainPath ||
p.PositionType == Level.PositionType.SidePath);
availablePositions.RemoveAll(p => Level.Loaded.ExtraWalls.Any(w => w.IsPointInside(p.Position.ToVector2())));
availablePositions.RemoveAll(p => Submarine.FindContaining(p.Position.ToVector2()) != null);
if (availablePositions.Any())
{
Level.InterestingPosition? closestPos = null;
float closestDist = float.PositiveInfinity;
foreach (var pos in availablePositions)
{
float dist = Vector2.DistanceSquared(pos.Position.ToVector2(), level.BeaconStation.WorldPosition);
if (dist < closestDist)
{
closestDist = dist;
closestPos = pos;
}
}
if (closestPos.HasValue)
{
spawnPos = closestPos.Value.Position.ToVector2();
}
}
int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1);
for (int i = 0; i < amount; i++)
{
Entity.Spawner.AddToSpawnQueue(monsterSpeciesName, spawnPos);
CoroutineManager.InvokeAfter(() =>
{
//round ended before the coroutine finished
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
Entity.Spawner.AddToSpawnQueue(monsterSpeciesName, spawnPos);
}, Rand.Range(0f, amount));
}
swarmSpawned = true;
}
@@ -82,13 +107,15 @@ namespace Barotrauma
completed = level.CheckBeaconActive();
if (completed)
{
if (GameMain.GameSession.GameMode is CampaignMode)
if (Prefab.LocationTypeChangeOnCompleted != null)
{
int naturalFormationIndex = locations[0].Type.Identifier.Equals("None", StringComparison.OrdinalIgnoreCase) ? 0 : 1;
var upgradeLocation = locations[naturalFormationIndex];
upgradeLocation.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals("Explored", StringComparison.OrdinalIgnoreCase)));
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
GiveReward();
if (level?.LevelData != null)
{
level.LevelData.IsBeaconActive = true;
}
}
}
@@ -94,7 +94,10 @@ namespace Barotrauma
cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.Server),
cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2);
var item = new Item(itemPrefab, position, cargoRoom.Submarine);
var item = new Item(itemPrefab, position, cargoRoom.Submarine)
{
SpawnedInOutpost = true
};
item.FindHull();
items.Add(item);
@@ -115,7 +118,7 @@ namespace Barotrauma
}
}
public override void Start(Level level)
protected override void StartMissionSpecific(Level level)
{
items.Clear();
parentInventoryIDs.Clear();
@@ -135,6 +138,10 @@ namespace Barotrauma
{
GiveReward();
completed = true;
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
}
}
@@ -16,11 +16,11 @@ namespace Barotrauma
get { return false; }
}
private Character.TeamType Winner
private CharacterTeamType Winner
{
get
{
if (GameMain.GameSession?.WinningTeam == null) { return Character.TeamType.None; }
if (GameMain.GameSession?.WinningTeam == null) { return CharacterTeamType.None; }
return GameMain.GameSession.WinningTeam.Value;
}
}
@@ -29,14 +29,14 @@ namespace Barotrauma
{
get
{
if (Winner == Character.TeamType.None || string.IsNullOrEmpty(base.SuccessMessage)) { return ""; }
if (Winner == CharacterTeamType.None || string.IsNullOrEmpty(base.SuccessMessage)) { return ""; }
//disable success message for now if it hasn't been translated
if (!TextManager.ContainsTag("MissionSuccess." + Prefab.TextIdentifier)) { return ""; }
var loser = Winner == Character.TeamType.Team1 ?
Character.TeamType.Team2 :
Character.TeamType.Team1;
var loser = Winner == CharacterTeamType.Team1 ?
CharacterTeamType.Team2 :
CharacterTeamType.Team1;
return base.SuccessMessage
.Replace("[loser]", GetTeamName(loser))
@@ -44,11 +44,6 @@ namespace Barotrauma
}
}
public override int TeamCount
{
get { return 2; }
}
public CombatMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
{
@@ -74,13 +69,13 @@ namespace Barotrauma
};
}
public static string GetTeamName(Character.TeamType teamID)
public static string GetTeamName(CharacterTeamType teamID)
{
if (teamID == Character.TeamType.Team1)
if (teamID == CharacterTeamType.Team1)
{
return teamNames.Length > 0 ? teamNames[0] : "Team 1";
}
else if (teamID == Character.TeamType.Team2)
else if (teamID == CharacterTeamType.Team2)
{
return teamNames.Length > 1 ? teamNames[1] : "Team 2";
}
@@ -91,11 +86,11 @@ namespace Barotrauma
public bool IsInWinningTeam(Character character)
{
return character != null &&
Winner != Character.TeamType.None &&
Winner != CharacterTeamType.None &&
Winner == character.TeamID;
}
public override void Start(Level level)
protected override void StartMissionSpecific(Level level)
{
if (GameMain.NetworkMember == null)
{
@@ -104,7 +99,7 @@ namespace Barotrauma
}
subs = new Submarine[] { Submarine.MainSubs[0], Submarine.MainSubs[1] };
subs[0].TeamID = Character.TeamType.Team1; subs[1].TeamID = Character.TeamType.Team2;
subs[0].TeamID = CharacterTeamType.Team1; subs[1].TeamID = CharacterTeamType.Team2;
subs[0].NeutralizeBallast(); subs[1].NeutralizeBallast();
subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition));
subs[1].FlipX();
@@ -120,9 +115,9 @@ namespace Barotrauma
public override void End()
{
if (GameMain.NetworkMember == null) return;
if (GameMain.NetworkMember == null) { return; }
if (Winner != Character.TeamType.None)
if (Winner != CharacterTeamType.None)
{
GiveReward();
completed = true;
@@ -14,6 +14,8 @@ namespace Barotrauma
private Dictionary<string, Item[]> RelevantLevelResources { get; } = new Dictionary<string, Item[]>();
private List<Tuple<string, Vector2>> MissionClusterPositions { get; } = new List<Tuple<string, Vector2>>();
private readonly HashSet<Level.Cave> caves = new HashSet<Level.Cave>();
public override IEnumerable<Vector2> SonarPositions
{
get
@@ -42,17 +44,72 @@ namespace Barotrauma
}
}
public override void Start(Level level)
protected override void StartMissionSpecific(Level level)
{
if (SpawnedResources.Any())
{
#if DEBUG
throw new Exception($"SpawnedResources.Count > 0 ({SpawnedResources.Count})");
#else
DebugConsole.AddWarning("Spawned resources list was not empty at the start of a mineral mission. The mission instance may not have been ended correctly on previous rounds.");
SpawnedResources.Clear();
#endif
}
if (RelevantLevelResources.Any())
{
#if DEBUG
throw new Exception($"RelevantLevelResources.Count > 0 ({RelevantLevelResources.Count})");
#else
DebugConsole.AddWarning("Relevant level resources list was not empty at the start of a mineral mission. The mission instance may not have been ended correctly on previous rounds.");
RelevantLevelResources.Clear();
#endif
}
if (MissionClusterPositions.Any())
{
#if DEBUG
throw new Exception($"MissionClusterPositions.Count > 0 ({MissionClusterPositions.Count})");
#else
DebugConsole.AddWarning("Mission cluster positions list was not empty at the start of a mineral mission. The mission instance may not have been ended correctly on previous rounds.");
MissionClusterPositions.Clear();
#endif
}
caves.Clear();
if (IsClient) { return; }
foreach (var kvp in ResourceClusters)
{
var prefab = ItemPrefab.Find(null, kvp.Key);
if (prefab == null) { continue; }
if (prefab == null)
{
DebugConsole.ThrowError("Error in MineralMission - " +
"couldn't find an item prefab with the identifier " + kvp.Key);
continue;
}
var spawnedResources = level.GenerateMissionResources(prefab, kvp.Value.First, out float rotation);
if (spawnedResources.Count < kvp.Value.First)
{
DebugConsole.ThrowError("Error in MineralMission - " +
"spawned " + spawnedResources.Count + "/" + kvp.Value.First + " of " + prefab.Name);
}
if (spawnedResources.None()) { continue; }
SpawnedResources.Add(kvp.Key, spawnedResources);
kvp.Value.Second = rotation;
foreach (Level.Cave cave in Level.Loaded.Caves)
{
foreach (Item spawnedResource in spawnedResources)
{
if (cave.Area.Contains(spawnedResource.WorldPosition))
{
cave.DisplayOnSonar = true;
caves.Add(cave);
break;
}
}
}
}
CalculateMissionClusterPositions();
FindRelevantLevelResources();
@@ -76,9 +133,29 @@ namespace Barotrauma
public override void End()
{
if (!EnoughHaveBeenCollected()) { return; }
GiveReward();
completed = true;
if (EnoughHaveBeenCollected())
{
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
GiveReward();
completed = true;
}
foreach (var kvp in SpawnedResources)
{
foreach (var i in kvp.Value)
{
if (i != null && !i.Removed && !HasBeenCollected(i))
{
i.Remove();
}
}
}
SpawnedResources.Clear();
RelevantLevelResources.Clear();
MissionClusterPositions.Clear();
failed = !completed && state > 0;
}
private void FindRelevantLevelResources()
@@ -1,9 +1,7 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Barotrauma
{
@@ -11,6 +9,9 @@ namespace Barotrauma
{
public readonly MissionPrefab Prefab;
protected bool completed, failed;
protected Level level;
protected int state;
public int State
{
@@ -21,7 +22,7 @@ namespace Barotrauma
{
state = value;
#if SERVER
GameMain.Server?.UpdateMissionState(state);
GameMain.Server?.UpdateMissionState(this, state);
#endif
ShowMessage(State);
}
@@ -85,11 +86,6 @@ namespace Barotrauma
get { return true; }
}
public virtual int TeamCount
{
get { return 1; }
}
public virtual IEnumerable<Vector2> SonarPositions
{
get { return Enumerable.Empty<Vector2>(); }
@@ -180,15 +176,23 @@ namespace Barotrauma
return null;
}
public virtual void Start(Level level) { }
public void Start(Level level)
{
foreach (string categoryToShow in Prefab.UnhideEntitySubCategories)
{
foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.prefab.HasSubCategory(categoryToShow)))
{
entityToShow.HiddenInGame = false;
}
}
this.level = level;
StartMissionSpecific(level);
}
protected virtual void StartMissionSpecific(Level level) { }
public virtual void Update(float deltaTime) { }
public virtual void AssignTeamIDs(List<Networking.Client> clients)
{
clients.ForEach(c => c.TeamID = Character.TeamType.Team1);
}
protected void ShowMessage(int missionState)
{
ShowMessageProjSpecific(missionState);
@@ -202,7 +206,10 @@ namespace Barotrauma
public virtual void End()
{
completed = true;
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
GiveReward();
}
@@ -234,6 +241,35 @@ namespace Barotrauma
}
}
protected void ChangeLocationType(LocationTypeChange change)
{
if (change == null) { throw new ArgumentException(); }
if (GameMain.GameSession.GameMode is CampaignMode && !IsClient)
{
int srcIndex = -1;
for (int i = 0; i < Locations.Length; i++)
{
if (Locations[i].Type.Identifier.Equals(change.CurrentType, StringComparison.OrdinalIgnoreCase))
{
srcIndex = i;
break;
}
}
if (srcIndex == -1) { return; }
var location = Locations[srcIndex];
if (change.RequiredDurationRange.X > 0)
{
location.PendingLocationTypeChange = (change, Rand.Range(change.RequiredDurationRange.X, change.RequiredDurationRange.Y), Prefab);
}
else
{
location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase)));
location.LocationTypeChangeCooldown = change.CooldownAfterChange;
}
}
}
public virtual void AdjustLevelData(LevelData levelData) { }
}
}
@@ -18,7 +18,9 @@ namespace Barotrauma
Nest = 0x10,
Mineral = 0x20,
Combat = 0x40,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat
AbandonedOutpost = 0x80,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | AbandonedOutpost
}
partial class MissionPrefab
@@ -33,6 +35,7 @@ namespace Barotrauma
{ MissionType.Beacon, typeof(BeaconMission) },
{ MissionType.Nest, typeof(NestMission) },
{ MissionType.Mineral, typeof(MineralMission) },
{ MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) },
};
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
{
@@ -73,8 +76,26 @@ namespace Barotrauma
public readonly List<string> Headers;
public readonly List<string> Messages;
//the mission can only be received when travelling from Pair.First to Pair.Second
public readonly List<Pair<string, string>> AllowedLocationTypes;
public readonly bool AllowRetry;
public readonly bool IsSideObjective;
/// <summary>
/// The mission can only be received when travelling from Pair.First to Pair.Second
/// </summary>
public readonly List<Pair<string, string>> AllowedConnectionTypes;
/// <summary>
/// The mission can only be received in these location types
/// </summary>
public readonly List<string> AllowedLocationTypes = new List<string>();
/// <summary>
/// Show entities belonging to these sub categories when the mission starts
/// </summary>
public readonly List<string> UnhideEntitySubCategories = new List<string>();
public LocationTypeChange LocationTypeChangeOnCompleted;
public readonly XElement ConfigElement;
@@ -130,7 +151,8 @@ namespace Barotrauma
Name = TextManager.Get("MissionName." + TextIdentifier, true) ?? element.GetAttributeString("name", "");
Description = TextManager.Get("MissionDescription." + TextIdentifier, true) ?? element.GetAttributeString("description", "");
Reward = element.GetAttributeInt("reward", 1);
AllowRetry = element.GetAttributeBool("allowretry", false);
IsSideObjective = element.GetAttributeBool("sideobjective", false);
Commonness = element.GetAttributeInt("commonness", 1);
SuccessMessage = TextManager.Get("MissionSuccess." + TextIdentifier, true) ?? element.GetAttributeString("successmessage", "Mission completed successfully");
@@ -152,9 +174,11 @@ namespace Barotrauma
AchievementIdentifier = element.GetAttributeString("achievementidentifier", "");
UnhideEntitySubCategories = element.GetAttributeStringArray("unhideentitysubcategories", new string[0]).ToList();
Headers = new List<string>();
Messages = new List<string>();
AllowedLocationTypes = new List<Pair<string, string>>();
AllowedConnectionTypes = new List<Pair<string, string>>();
for (int i = 0; i < 100; i++)
{
@@ -183,9 +207,20 @@ namespace Barotrauma
messageIndex++;
break;
case "locationtype":
AllowedLocationTypes.Add(new Pair<string, string>(
subElement.GetAttributeString("from", ""),
subElement.GetAttributeString("to", "")));
case "connectiontype":
if (subElement.Attribute("identifier") != null)
{
AllowedLocationTypes.Add(subElement.GetAttributeString("identifier", ""));
}
else
{
AllowedConnectionTypes.Add(new Pair<string, string>(
subElement.GetAttributeString("from", ""),
subElement.GetAttributeString("to", "")));
}
break;
case "locationtypechange":
LocationTypeChangeOnCompleted = new LocationTypeChange(subElement.GetAttributeString("from", ""), subElement, requireChangeMessages: false, defaultProbability: 1.0f);
break;
case "reputation":
case "reputationreward":
@@ -257,19 +292,32 @@ namespace Barotrauma
public bool IsAllowed(Location from, Location to)
{
foreach (Pair<string, string> allowedLocationType in AllowedLocationTypes)
if (from == to)
{
if (allowedLocationType.First.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedLocationType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase))
return
AllowedLocationTypes.Any(lt => lt.Equals("any", StringComparison.OrdinalIgnoreCase)) ||
AllowedLocationTypes.Any(lt => lt.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase));
}
foreach (Pair<string, string> allowedConnectionType in AllowedConnectionTypes)
{
if (allowedConnectionType.First.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedConnectionType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase))
{
if (allowedLocationType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedLocationType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase))
if (allowedConnectionType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedConnectionType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
if (Type == MissionType.Beacon)
{
var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to));
if (connection?.LevelData == null || !connection.LevelData.HasBeaconStation || connection.LevelData.IsBeaconActive) { return false; }
}
return false;
}
@@ -16,6 +16,7 @@ namespace Barotrauma
private readonly float maxSonarMarkerDistance = 10000.0f;
private readonly Level.PositionType spawnPosType;
public override IEnumerable<Vector2> SonarPositions
{
@@ -52,6 +53,13 @@ namespace Barotrauma
maxSonarMarkerDistance = prefab.ConfigElement.GetAttributeFloat("maxsonarmarkerdistance", 10000.0f);
var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
if (string.IsNullOrWhiteSpace(spawnPosTypeStr) ||
!Enum.TryParse(spawnPosTypeStr, true, out spawnPosType))
{
spawnPosType = Level.PositionType.MainPath | Level.PositionType.SidePath;
}
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
{
speciesName = monsterElement.GetAttributeString("character", string.Empty);
@@ -81,22 +89,32 @@ namespace Barotrauma
TextManager.Get("character." + characterParams.SpeciesName));
}
}
public override void Start(Level level)
protected override void StartMissionSpecific(Level level)
{
if (monsters.Count > 0)
{
#if DEBUG
throw new Exception($"monsters.Count > 0 ({monsters.Count})");
#else
DebugConsole.AddWarning("Monster list was not empty at the start of a monster mission. The mission instance may not have been ended correctly on previous rounds.");
monsters.Clear();
#endif
}
if (tempSonarPositions.Count > 0)
{
#if DEBUG
throw new Exception($"tempSonarPositions.Count > 0 ({tempSonarPositions.Count})");
#else
DebugConsole.AddWarning("Sonar position list was not empty at the start of a monster mission. The mission instance may not have been ended correctly on previous rounds.");
tempSonarPositions.Clear();
#endif
}
if (!IsClient)
{
Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos);
Level.Loaded.TryGetInterestingPosition(true, spawnPosType, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos);
foreach (var monster in monsterPrefabs)
{
int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1);
@@ -115,7 +133,7 @@ namespace Barotrauma
foreach (var monster in monsters)
{
monster.Enabled = false;
if (monster.Params.AI.EnforceAggressiveBehaviorForMissions)
if (monster.Params.AI != null && monster.Params.AI.EnforceAggressiveBehaviorForMissions)
{
foreach (var targetParam in monster.Params.AI.Targets)
{
@@ -203,9 +221,17 @@ namespace Barotrauma
tempSonarPositions.Clear();
monsters.Clear();
if (State < 1) { return; }
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
GiveReward();
completed = true;
if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)))
{
level.LevelData.HasHuntingGrounds = false;
}
}
public bool IsEliminated(Character enemy) =>
@@ -20,7 +20,9 @@ namespace Barotrauma
private readonly float itemSpawnRadius = 800.0f;
private readonly float approachItemsRadius = 1000.0f;
private readonly float nestObjectRadius = 1000.0f;
private readonly float monsterSpawnRadius = 3000.0f;
private readonly int nestObjectAmount = 10;
private readonly bool requireDelivery;
@@ -33,7 +35,14 @@ namespace Barotrauma
{
get
{
yield return nestPosition;
if (State > 0)
{
Enumerable.Empty<Vector2>();
}
else
{
yield return nestPosition;
}
}
}
@@ -46,6 +55,9 @@ namespace Barotrauma
approachItemsRadius = prefab.ConfigElement.GetAttributeFloat("approachitemsradius", itemSpawnRadius * 2.0f);
monsterSpawnRadius = prefab.ConfigElement.GetAttributeFloat("monsterspawnradius", approachItemsRadius * 2.0f);
nestObjectRadius = prefab.ConfigElement.GetAttributeFloat("nestobjectradius", itemSpawnRadius * 2.0f);
nestObjectAmount = prefab.ConfigElement.GetAttributeInt("nestobjectamount", 10);
requireDelivery = prefab.ConfigElement.GetAttributeBool("requiredelivery", false);
string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
@@ -55,7 +67,6 @@ namespace Barotrauma
spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin;
}
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
{
string speciesName = monsterElement.GetAttributeString("character", string.Empty);
@@ -79,8 +90,18 @@ namespace Barotrauma
}
public override void Start(Level level)
protected override void StartMissionSpecific(Level level)
{
if (items.Any())
{
#if DEBUG
throw new Exception($"items.Count > 0 ({items.Count})");
#else
DebugConsole.AddWarning("Item list was not empty at the start of a nest mission. The mission instance may not have been ended correctly on previous rounds.");
items.Clear();
#endif
}
if (!IsClient)
{
//ruin/cave/wreck items are allowed to spawn close to the sub
@@ -90,6 +111,25 @@ namespace Barotrauma
List<GraphEdge> spawnEdges = new List<GraphEdge>();
if (spawnPositionType == Level.PositionType.Cave)
{
Level.Cave closestCave = null;
float closestCaveDist = float.PositiveInfinity;
foreach (var cave in Level.Loaded.Caves)
{
float dist = Vector2.DistanceSquared(nestPosition, cave.Area.Center.ToVector2());
if (dist < closestCaveDist)
{
closestCave = cave;
closestCaveDist = dist;
}
}
if (closestCave != null)
{
closestCave.DisplayOnSonar = true;
SpawnNestObjects(level, closestCave);
#if SERVER
selectedCave = closestCave;
#endif
}
var nearbyCells = Level.Loaded.GetCells(nestPosition, searchDepth: 3);
if (nearbyCells.Any())
{
@@ -171,6 +211,11 @@ namespace Barotrauma
}
}
private void SpawnNestObjects(Level level, Level.Cave cave)
{
level.LevelObjectManager.PlaceNestObjects(level, cave, nestPosition, nestObjectRadius, nestObjectAmount);
}
public override void Update(float deltaTime)
{
if (IsClient)
@@ -258,9 +303,17 @@ namespace Barotrauma
public override void End()
{
if (!AllItemsDestroyedOrRetrieved())
if (AllItemsDestroyedOrRetrieved())
{
return;
GiveReward();
completed = true;
if (completed)
{
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
}
}
foreach (Item item in items)
{
@@ -270,8 +323,6 @@ namespace Barotrauma
}
}
items.Clear();
GiveReward();
completed = true;
failed = !completed && state > 0;
}
}
@@ -1,4 +1,5 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
@@ -101,7 +102,7 @@ namespace Barotrauma
}
}
public override void Start(Level level)
protected override void StartMissionSpecific(Level level)
{
#if SERVER
originalInventoryID = Entity.NullEntityID;
@@ -168,10 +169,11 @@ namespace Barotrauma
//try to find a container and place the item inside it
if (!string.IsNullOrEmpty(containerTag) && item.ParentInventory == null)
{
List<ItemContainer> validContainers = new List<ItemContainer>();
foreach (Item it in Item.ItemList)
{
if (!it.HasTag(containerTag)) { continue; }
if (it.NonInteractable) { continue; }
if (!it.IsPlayerTeamInteractable) { continue; }
switch (spawnPositionType)
{
case Level.PositionType.Cave:
@@ -185,15 +187,18 @@ namespace Barotrauma
if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
break;
}
var itemContainer = it.GetComponent<Items.Components.ItemContainer>();
if (itemContainer == null) { continue; }
if (itemContainer.Combine(item, user: null))
var itemContainer = it.GetComponent<ItemContainer>();
if (itemContainer != null && itemContainer.Inventory.CanBePut(item)) { validContainers.Add(itemContainer); }
}
if (validContainers.Any())
{
var selectedContainer = validContainers.GetRandom();
if (selectedContainer.Combine(item, user: null))
{
#if SERVER
originalInventoryID = it.ID;
originalItemContainerIndex = (byte)it.GetComponentIndex(itemContainer);
originalInventoryID = selectedContainer.Item.ID;
originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer);
#endif
break;
} // Placement successful
}
}
@@ -248,6 +253,11 @@ namespace Barotrauma
return;
}
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
item?.Remove();
item = null;
GiveReward();
@@ -16,8 +16,6 @@ namespace Barotrauma
private readonly float scatter;
private readonly float offset;
private readonly bool spawnDeep;
private Vector2? spawnPos;
private readonly bool disallowed;
@@ -73,14 +71,18 @@ namespace Barotrauma
maxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), minAmount);
var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
if (string.IsNullOrWhiteSpace(spawnPosTypeStr) ||
!Enum.TryParse(spawnPosTypeStr, true, out spawnPosType))
{
spawnPosType = Level.PositionType.MainPath;
}
spawnDeep = prefab.ConfigElement.GetAttributeBool("spawndeep", false);
//backwards compatibility
if (prefab.ConfigElement.GetAttributeBool("spawndeep", false))
{
spawnPosType = Level.PositionType.Abyss;
}
offset = prefab.ConfigElement.GetAttributeFloat("offset", 0);
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 1000), 0, 3000);
@@ -138,6 +140,11 @@ namespace Barotrauma
var removals = new List<Level.InterestingPosition>();
foreach (var position in availablePositions)
{
if (SpawnPosFilter != null && !SpawnPosFilter(position))
{
removals.Add(position);
continue;
}
if (position.Submarine != null)
{
if (position.Submarine.WreckAI != null && position.Submarine.WreckAI.IsAlive)
@@ -154,19 +161,10 @@ namespace Barotrauma
{
continue;
}
if (Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(position.Position.ToVector2()))))
if (Level.Loaded.ExtraWalls.Any(w => w.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);
@@ -180,33 +178,36 @@ namespace Barotrauma
{
if (disallowed) { return; }
if (Rand.Value(Rand.RandSync.Server) > prefab.SpawnProbability)
{
spawnPos = null;
Finished();
return;
}
spawnPos = Vector2.Zero;
var availablePositions = GetAvailableSpawnPositions();
var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false);
var removedPositions = new List<Level.InterestingPosition>();
foreach (var position in availablePositions)
{
if (Rand.Value(Rand.RandSync.Server) > prefab.SpawnProbability)
{
removedPositions.Add(position);
}
}
removedPositions.ForEach(p => availablePositions.Remove(p));
bool isSubOrWreck = spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Wreck;
if (affectSubImmediately && !isSubOrWreck)
if (affectSubImmediately && !isSubOrWreck && spawnPosType != Level.PositionType.Abyss)
{
if (availablePositions.None())
{
//no suitable position found, disable the event
spawnPos = null;
Finished();
return;
}
Submarine refSub = GetReferenceSub();
if (Submarine.MainSubs.Length == 2 && Submarine.MainSubs[1] != null)
{
refSub = Submarine.MainSubs.GetRandom(Rand.RandSync.Unsynced);
}
float closestDist = float.PositiveInfinity;
//find the closest spawnposition that isn't too close to any of the subs
foreach (var position in availablePositions)
{
Vector2 pos = position.Position.ToVector2();
Submarine refSub = GetReferenceSub();
float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition);
foreach (Submarine sub in Submarine.Loaded)
{
@@ -248,7 +249,7 @@ namespace Barotrauma
{
foreach (var position in availablePositions)
{
float dist = Vector2.DistanceSquared(position.Position.ToVector2(), GetReferenceSub().WorldPosition);
float dist = Vector2.DistanceSquared(position.Position.ToVector2(), refSub.WorldPosition);
if (dist < closestDist)
{
closestDist = dist;
@@ -262,11 +263,21 @@ namespace Barotrauma
if (!isSubOrWreck)
{
float minDistance = 20000;
availablePositions.RemoveAll(p => Vector2.DistanceSquared(GetReferenceSub().WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
var refSub = GetReferenceSub();
availablePositions.RemoveAll(p => Vector2.DistanceSquared(refSub.WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
if (Submarine.MainSubs.Length > 1)
{
for (int i = 1; i < Submarine.MainSubs.Length; i++)
{
if (Submarine.MainSubs[i] == null) { continue; }
availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
}
}
}
if (availablePositions.None())
{
//no suitable position found, disable the event
spawnPos = null;
Finished();
return;
}
@@ -335,6 +346,8 @@ namespace Barotrauma
if (spawnPos == null)
{
FindSpawnPosition(affectSubImmediately: true);
//the event gets marked as finished if a spawn point is not found
if (isFinished) { return; }
spawnPending = true;
}
@@ -342,7 +355,7 @@ namespace Barotrauma
if (spawnPending)
{
//wait until there are no submarines at the spawnpos
if (spawnPosType == Level.PositionType.MainPath)
if (spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath || spawnPosType == Level.PositionType.Abyss)
{
foreach (Submarine submarine in Submarine.Loaded)
{
@@ -381,6 +394,19 @@ namespace Barotrauma
if (!someoneNearby) { return; }
}
if (spawnPosType == Level.PositionType.Abyss || spawnPosType == Level.PositionType.AbyssCave)
{
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (submarine.WorldPosition.Y > Level.Loaded.AbyssStart)
{
return;
}
}
}
spawnPending = false;
//+1 because Range returns an integer less than the max value
@@ -412,7 +438,16 @@ namespace Barotrauma
}
}
monsters.Add(Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true));
Character createdCharacter = Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true);
if (GameMain.GameSession.IsCurrentLocationRadiated())
{
AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness;
Affliction affliction = new Affliction(radiationPrefab, radiationPrefab.MaxStrength);
createdCharacter?.CharacterHealth.ApplyAffliction(null, affliction);
// TODO test multiplayer
createdCharacter?.Kill(CauseOfDeathType.Affliction, affliction, log: false);
}
monsters.Add(createdCharacter);
if (monsters.Count == amount)
{
@@ -421,7 +456,7 @@ namespace Barotrauma
//otherwise it'll make the spawned characters act as a swarm
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
}
}, Rand.Range(0f, amount / 2));
}, Rand.Range(0f, amount / 2f));
}
}
@@ -13,6 +13,9 @@ namespace Barotrauma
private int prevEntityCount;
private int prevPlayerCount, prevBotCount;
private readonly string[] requiredDestinationTypes;
public readonly bool RequireBeaconStation;
public int CurrentActionIndex { get; private set; }
public List<EventAction> Actions { get; } = new List<EventAction>();
public Dictionary<string, List<Entity>> Targets { get; } = new Dictionary<string, List<Entity>>();
@@ -39,6 +42,9 @@ namespace Barotrauma
{
DebugConsole.ThrowError($"Scripted event \"{prefab.Identifier}\" has no actions. The event will do nothing.");
}
requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null);
RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false);
}
public void AddTarget(string tag, Entity target)
@@ -199,5 +205,21 @@ namespace Barotrauma
currentAction.Update(deltaTime);
}
}
public override bool LevelMeetsRequirements()
{
if (requiredDestinationTypes == null) { return true; }
var currLocation = GameMain.GameSession?.Campaign?.Map.CurrentLocation;
if (currLocation?.Connections == null) { return true; }
foreach (LocationConnection c in currLocation.Connections)
{
if (RequireBeaconStation && !c.LevelData.HasBeaconStation) { continue; }
if (requiredDestinationTypes.Any(t => c.OtherLocation(currLocation).Type.Identifier.Equals(t, StringComparison.OrdinalIgnoreCase)))
{
return true;
}
}
return false;
}
}
}
@@ -49,6 +49,19 @@ namespace Barotrauma.Extensions
return count == 0 ? default : source.ElementAt(Rand.Range(0, count, randSync));
}
}
public static T GetRandom<T>(this IEnumerable<T> source, Random random)
{
if (source is IList<T> list)
{
int count = list.Count;
return count == 0 ? default : list[random.Next(0, count)];
}
else
{
int count = source.Count();
return count == 0 ? default : source.ElementAt(random.Next(0, count));
}
}
public static T RandomElementByWeight<T>(this IEnumerable<T> source, Func<T, float> weightSelector, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
@@ -14,7 +14,7 @@ namespace Barotrauma
{
try
{
forbiddenWords = File.ReadAllLines(fileListPath).ToHashSet();
forbiddenWords = File.ReadAllLines(fileListPath).Select(s => s.ToLowerInvariant()).ToHashSet();
}
catch (IOException e)
{
@@ -42,16 +42,28 @@ namespace Barotrauma
{
foreach (string word in text.Split(delimiter))
{
words.Add(word);
words.Add(word.ToLowerInvariant());
}
}
foreach (string word in words)
text = text.ToLowerInvariant();
foreach (string forbidden in forbiddenWords)
{
if (forbiddenWords.Any(w => Homoglyphs.Compare(word, w)))
if (forbidden.Contains(' '))
{
forbiddenWord = word;
return true;
if (words.Contains(forbidden.Trim()))
{
forbiddenWord = forbidden.Trim();
return true;
}
}
else
{
if (text.Contains(forbidden))
{
forbiddenWord = forbidden.Trim();
return true;
}
}
}
return false;
@@ -25,11 +25,12 @@ namespace Barotrauma
subs.ForEach(s => s.Info.InitialSuppliesSpawned = true);
}
foreach (var wreck in Submarine.Loaded)
foreach (var sub in Submarine.Loaded)
{
if (wreck.Info.IsWreck)
if (sub.Info.Type == SubmarineType.Wreck ||
sub.Info.Type == SubmarineType.BeaconStation)
{
Place(wreck.ToEnumerable());
Place(sub.ToEnumerable());
}
}
@@ -195,11 +196,12 @@ namespace Barotrauma
int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.Server);
for (int i = 0; i < amount; i++)
{
if (validContainer.Key.Inventory.IsFull())
if (validContainer.Key.Inventory.IsFull(takeStacksIntoAccount: true))
{
containers.Remove(validContainer.Key);
break;
}
if (!validContainer.Key.Inventory.CanBePut(itemPrefab)) { break; }
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine)
{
SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost,
@@ -103,6 +103,10 @@ namespace Barotrauma
public void PurchaseItems(List<PurchasedItem> itemsToPurchase, bool removeFromCrate)
{
// Check all the prices before starting the transaction
// to make sure the modifiers stay the same for the whole transaction
Dictionary<ItemPrefab, int> buyValues = GetBuyValuesAtCurrentLocation(itemsToPurchase.Select(i => i.ItemPrefab));
foreach (PurchasedItem item in itemsToPurchase)
{
// Add to the purchased items
@@ -118,7 +122,7 @@ namespace Barotrauma
}
// Exchange money
var itemValue = GetBuyValueAtCurrentLocation(item);
var itemValue = item.Quantity * buyValues[item.ItemPrefab];
campaign.Money -= itemValue;
Location.StoreCurrentBalance += itemValue;
@@ -136,23 +140,47 @@ namespace Barotrauma
OnPurchasedItemsChanged?.Invoke();
}
public int GetBuyValueAtCurrentLocation(PurchasedItem item) => item?.ItemPrefab != null && Location != null ?
item.Quantity * Location.GetAdjustedItemBuyPrice(item.ItemPrefab) : 0;
public Dictionary<ItemPrefab, int> GetBuyValuesAtCurrentLocation(IEnumerable<ItemPrefab> items)
{
var buyValues = new Dictionary<ItemPrefab, int>();
foreach (var item in items)
{
if (item == null) { continue; }
if (!buyValues.ContainsKey(item))
{
var buyValue = Location?.GetAdjustedItemBuyPrice(item) ?? 0;
buyValues.Add(item, buyValue);
}
}
return buyValues;
}
public int GetSellValueAtCurrentLocation(ItemPrefab itemPrefab, int quantity = 1) => itemPrefab != null && Location != null ?
quantity * Location.GetAdjustedItemSellPrice(itemPrefab) : 0;
public Dictionary<ItemPrefab, int> GetSellValuesAtCurrentLocation(IEnumerable<ItemPrefab> items)
{
var sellValues = new Dictionary<ItemPrefab, int>();
foreach (var item in items)
{
if (item == null) { continue; }
if (!sellValues.ContainsKey(item))
{
var sellValue = Location?.GetAdjustedItemSellPrice(item) ?? 0;
sellValues.Add(item, sellValue);
}
}
return sellValues;
}
public void CreatePurchasedItems()
{
CreateItems(PurchasedItems);
CreateItems(PurchasedItems, Submarine.MainSub);
OnPurchasedItemsChanged?.Invoke();
}
public static void CreateItems(List<PurchasedItem> itemsToSpawn)
public static void CreateItems(List<PurchasedItem> itemsToSpawn, Submarine sub)
{
if (itemsToSpawn.Count == 0) { return; }
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub);
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub);
if (wp == null)
{
DebugConsole.ThrowError("The submarine must have a waypoint marked as Cargo for bought items to be placed correctly!");
@@ -160,85 +188,73 @@ namespace Barotrauma
}
Hull cargoRoom = Hull.FindHull(wp.WorldPosition);
if (cargoRoom == null)
{
DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!");
return;
}
#if CLIENT
new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true), new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "StoreShoppingCrateIcon");
#else
foreach (Client client in GameMain.Server.ConnectedClients)
if (sub == Submarine.MainSub)
{
ChatMessage msg = ChatMessage.Create("", $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}", ChatMessageType.ServerMessageBoxInGame, null);
msg.IconStyle = "StoreShoppingCrateIcon";
GameMain.Server.SendDirectChatMessage(msg, client);
}
#if CLIENT
new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true), new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "StoreShoppingCrateIcon");
#else
foreach (Client client in GameMain.Server.ConnectedClients)
{
ChatMessage msg = ChatMessage.Create("",
TextManager.ContainsTag(cargoRoom.RoomName) ? $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}" : $"CargoSpawnNotification~[roomname]={cargoRoom.RoomName}",
ChatMessageType.ServerMessageBoxInGame, null);
msg.IconStyle = "StoreShoppingCrateIcon";
GameMain.Server.SendDirectChatMessage(msg, client);
}
#endif
}
Dictionary<ItemContainer, int> availableContainers = new Dictionary<ItemContainer, int>();
List<ItemContainer> availableContainers = new List<ItemContainer>();
ItemPrefab containerPrefab = null;
foreach (PurchasedItem pi in itemsToSpawn)
{
float floorPos = cargoRoom.Rect.Y - cargoRoom.Rect.Height;
Vector2 position = GetCargoPos(cargoRoom, pi.ItemPrefab);
Vector2 position = new Vector2(
cargoRoom.Rect.Width > 40 ? Rand.Range(cargoRoom.Rect.X + 20, cargoRoom.Rect.Right - 20) : cargoRoom.Rect.Center.X,
floorPos);
//check where the actual floor structure is in case the bottom of the hull extends below it
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(new Vector2(position.X, cargoRoom.Rect.Y - cargoRoom.Rect.Height / 2)),
ConvertUnits.ToSimUnits(position),
collisionCategory: Physics.CollisionWall) != null)
{
float floorStructurePos = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition.Y);
if (floorStructurePos > floorPos)
{
floorPos = floorStructurePos;
}
}
position.Y = floorPos + pi.ItemPrefab.Size.Y / 2;
ItemContainer itemContainer = null;
if (!string.IsNullOrEmpty(pi.ItemPrefab.CargoContainerIdentifier))
{
itemContainer = availableContainers.Keys.ToList().Find(ac =>
ac.Item.Prefab.Identifier == pi.ItemPrefab.CargoContainerIdentifier ||
ac.Item.Prefab.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant()));
if (itemContainer == null)
{
containerPrefab = ItemPrefab.Prefabs.Find(ep =>
ep.Identifier == pi.ItemPrefab.CargoContainerIdentifier ||
(ep.Tags != null && ep.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant())));
if (containerPrefab == null)
{
DebugConsole.ThrowError("Cargo spawning failed - could not find the item prefab for container \"" + pi.ItemPrefab.CargoContainerIdentifier + "\"!");
continue;
}
Item containerItem = new Item(containerPrefab, position, wp.Submarine);
itemContainer = containerItem.GetComponent<ItemContainer>();
if (itemContainer == null)
{
DebugConsole.ThrowError("Cargo spawning failed - container \"" + containerItem.Name + "\" does not have an ItemContainer component!");
continue;
}
availableContainers.Add(itemContainer, itemContainer.Capacity);
#if SERVER
if (GameMain.Server != null)
{
Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false);
}
#endif
}
}
for (int i = 0; i < pi.Quantity; i++)
{
ItemContainer itemContainer = null;
if (!string.IsNullOrEmpty(pi.ItemPrefab.CargoContainerIdentifier))
{
itemContainer = availableContainers.Find(ac =>
ac.Inventory.CanBePut(pi.ItemPrefab) &&
(ac.Item.Prefab.Identifier == pi.ItemPrefab.CargoContainerIdentifier ||
ac.Item.Prefab.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant())));
if (itemContainer == null)
{
containerPrefab = ItemPrefab.Prefabs.Find(ep =>
ep.Identifier == pi.ItemPrefab.CargoContainerIdentifier ||
(ep.Tags != null && ep.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant())));
if (containerPrefab == null)
{
DebugConsole.ThrowError("Cargo spawning failed - could not find the item prefab for container \"" + pi.ItemPrefab.CargoContainerIdentifier + "\"!");
continue;
}
Item containerItem = new Item(containerPrefab, position, wp.Submarine);
itemContainer = containerItem.GetComponent<ItemContainer>();
if (itemContainer == null)
{
DebugConsole.ThrowError("Cargo spawning failed - container \"" + containerItem.Name + "\" does not have an ItemContainer component!");
continue;
}
availableContainers.Add(itemContainer);
#if SERVER
if (GameMain.Server != null)
{
Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false);
}
#endif
}
}
if (itemContainer == null)
{
//no container, place at the waypoint
@@ -253,20 +269,6 @@ namespace Barotrauma
}
continue;
}
//if the intial container has been removed due to it running out of space, add a new container
//of the same type and begin filling it
if (!availableContainers.ContainsKey(itemContainer))
{
Item containerItemOverFlow = new Item(containerPrefab, position, wp.Submarine);
itemContainer = containerItemOverFlow.GetComponent<ItemContainer>();
availableContainers.Add(itemContainer, itemContainer.Capacity);
#if SERVER
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false);
}
#endif
}
//place in the container
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
@@ -290,23 +292,38 @@ namespace Barotrauma
wifiComponent.TeamID = sub.TeamID;
}
}
}
//reduce the number of available slots in the container
//if there is a container
if (availableContainers.ContainsKey(itemContainer))
{
availableContainers[itemContainer]--;
}
if (availableContainers.ContainsKey(itemContainer) && availableContainers[itemContainer] <= 0)
{
availableContainers.Remove(itemContainer);
}
}
}
}
itemsToSpawn.Clear();
}
public static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab)
{
float floorPos = hull.Rect.Y - hull.Rect.Height;
Vector2 position = new Vector2(
hull.Rect.Width > 40 ? Rand.Range(hull.Rect.X + 20, hull.Rect.Right - 20) : hull.Rect.Center.X,
floorPos);
//check where the actual floor structure is in case the bottom of the hull extends below it
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(new Vector2(position.X, hull.Rect.Y - hull.Rect.Height / 2)),
ConvertUnits.ToSimUnits(position),
collisionCategory: Physics.CollisionWall) != null)
{
float floorStructurePos = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition.Y);
if (floorStructurePos > floorPos)
{
floorPos = floorStructurePos;
}
}
position.Y = floorPos + itemPrefab.Size.Y / 2;
return position;
}
public void SavePurchasedItems(XElement parentElement)
{
var itemsElement = new XElement("cargo");
@@ -48,20 +48,43 @@ namespace Barotrauma
return false;
}
Pair<Order, float?> existingOrder =
ActiveOrders.Find(o => o.First.Prefab == order.Prefab && o.First.TargetEntity == order.TargetEntity &&
(o.First.TargetType != Order.OrderTargetType.WallSection || o.First.WallSectionIndex == order.WallSectionIndex));
// Ignore orders work a bit differently since the "unignore" order counters the "ignore" order
var isUnignoreOrder = order.Identifier == "unignorethis";
var orderPrefab = !isUnignoreOrder ? order.Prefab : Order.GetPrefab("ignorethis");
Pair<Order, float?> existingOrder = ActiveOrders.Find(o =>
o.First.Prefab == orderPrefab && MatchesTarget(o.First.TargetEntity, order.TargetEntity) &&
(o.First.TargetType != Order.OrderTargetType.WallSection || o.First.WallSectionIndex == order.WallSectionIndex));
if (existingOrder != null)
{
existingOrder.Second = fadeOutTime;
return false;
if (!isUnignoreOrder)
{
existingOrder.Second = fadeOutTime;
return false;
}
else
{
ActiveOrders.Remove(existingOrder);
return true;
}
}
else
else if (!isUnignoreOrder)
{
ActiveOrders.Add(new Pair<Order, float?>(order, fadeOutTime));
return true;
}
bool MatchesTarget(Entity existingTarget, Entity newTarget)
{
if (existingTarget == newTarget) { return true; }
if (existingTarget is Hull existingHullTarget && newTarget is Hull newHullTarget)
{
return existingHullTarget.linkedTo.Contains(newHullTarget);
}
return false;
}
return false;
}
public void AddCharacterElements(XElement element)
@@ -122,14 +145,22 @@ namespace Barotrauma
}
#if CLIENT
AddCharacterToCrewList(character);
AddCurrentOrderIcon(character, character.CurrentOrder, character.CurrentOrderOption);
#endif
var idleObjective = character.AIController?.ObjectiveManager?.GetObjective<AIObjectiveIdle>();
if (idleObjective != null)
if (character.CurrentOrders != null)
{
idleObjective.Behavior = character.Info.Job.Prefab.IdleBehavior;
foreach (var order in character.CurrentOrders)
{
AddCurrentOrderIcon(character, order);
}
}
#endif
if (character.AIController is HumanAIController humanAI)
{
var idleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
if (idleObjective != null)
{
idleObjective.Behavior = character.Info.Job.Prefab.IdleBehavior;
}
}
}
public void AddCharacterInfo(CharacterInfo characterInfo)
@@ -150,7 +181,7 @@ namespace Barotrauma
List<WayPoint> spawnWaypoints = null;
List<WayPoint> mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub).ToList();
if (Level.IsLoadedOutpost)
if (Level.IsLoadedOutpost && Submarine.Loaded.Any(s => s.Info.Type == SubmarineType.Outpost && (s.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false)))
{
spawnWaypoints = WayPoint.WayPointList.FindAll(wp =>
wp.SpawnType == SpawnType.Human &&
@@ -177,7 +208,7 @@ namespace Barotrauma
for (int i = 0; i < spawnWaypoints.Count; i++)
{
var info = characterInfos[i];
info.TeamID = Character.TeamType.Team1;
info.TeamID = CharacterTeamType.Team1;
Character character = Character.Create(info, spawnWaypoints[i].WorldPosition, info.Name);
if (character.Info != null)
{
@@ -222,7 +253,8 @@ namespace Barotrauma
{
if (order.Second.HasValue) { order.Second -= deltaTime; }
}
ActiveOrders.RemoveAll(o => o.Second.HasValue && o.Second <= 0.0f);
ActiveOrders.RemoveAll(o => (o.Second.HasValue && o.Second <= 0.0f) ||
(o.First.TargetEntity != null && o.First.TargetEntity.Removed));
UpdateConversations(deltaTime);
UpdateProjectSpecific(deltaTime);
@@ -262,8 +294,8 @@ namespace Barotrauma
{
foreach (Character npc in Character.CharacterList)
{
if (npc.TeamID != Character.TeamType.FriendlyNPC || npc.CurrentHull == null || npc.IsIncapacitated) { continue; }
if (npc.AIController?.ObjectiveManager != null && (npc.AIController.ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>() || npc.AIController.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()))
if (npc.TeamID != CharacterTeamType.FriendlyNPC || npc.CurrentHull == null || npc.IsIncapacitated) { continue; }
if (npc.AIController is HumanAIController humanAI && (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>() || humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()))
{
continue;
}
@@ -7,6 +7,7 @@ namespace Barotrauma
public const float HostileThreshold = 0.1f;
public const float ReputationLossPerNPCDamage = 0.1f;
public const float ReputationLossPerStolenItemPrice = 0.01f;
public const float ReputationLossPerWallDamage = 0.1f;
public const float MinReputationLossPerStolenItem = 0.5f;
public const float MaxReputationLossPerStolenItem = 10.0f;
@@ -11,8 +11,7 @@ namespace Barotrauma
abstract partial class CampaignMode : GameMode
{
const int MaxMoney = int.MaxValue / 2; //about 1 billion
const int InitialMoney = 2500;
public const int MaxInitialSubmarinePrice = 6000;
public const int InitialMoney = 8500;
//duration of the cinematic + credits at the end of the campaign
protected const float EndCinematicDuration = 240.0f;
@@ -32,6 +31,8 @@ namespace Barotrauma
protected XElement petsElement;
private List<Mission> extraMissions = new List<Mission>();
public enum TransitionType
{
None,
@@ -75,11 +76,18 @@ namespace Barotrauma
get { return map; }
}
public override Mission Mission
public override IEnumerable<Mission> Missions
{
get
{
return Map.CurrentLocation?.SelectedMission;
if (Map.CurrentLocation?.SelectedMission != null)
{
yield return Map.CurrentLocation.SelectedMission;
}
foreach (Mission mission in extraMissions)
{
yield return mission;
}
}
}
@@ -146,7 +154,7 @@ namespace Barotrauma
{
for (int i = 0; i < wall.SectionCount; i++)
{
wall.AddDamage(i, -wall.MaxHealth);
wall.SetDamage(i, 0, createNetworkEvent: false);
}
}
}
@@ -186,6 +194,53 @@ namespace Barotrauma
/// </summary>
public event Action BeforeLevelLoading;
public override void AddExtraMissions(LevelData levelData)
{
extraMissions.Clear();
var currentLocation = Map.CurrentLocation;
if (levelData.Type == LevelData.LevelType.Outpost)
{
//if there's an available mission that takes place in the outpost, select it
var availableMissionsInLocation = currentLocation.AvailableMissions.Where(m => m.Locations[0] == currentLocation && m.Locations[1] == currentLocation);
if (availableMissionsInLocation.Any())
{
currentLocation.SelectedMission = availableMissionsInLocation.FirstOrDefault();
}
else
{
currentLocation.SelectedMission = null;
}
}
else
{
//if we had selected a mission that takes place in the outpost, deselect it when leaving the outpost
if (currentLocation.SelectedMission?.Locations[0] == currentLocation &&
currentLocation.SelectedMission?.Locations[1] == currentLocation)
{
currentLocation.SelectedMission = null;
}
if (levelData.HasBeaconStation && !levelData.IsBeaconActive)
{
var beaconMissionPrefab = MissionPrefab.List.Find(m => m.Identifier.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase));
if (beaconMissionPrefab != null && !Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type))
{
extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations));
}
}
if (levelData.HasHuntingGrounds)
{
var huntingGroundsMissionPrefab = MissionPrefab.List.Find(m => m.Identifier.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase));
if (huntingGroundsMissionPrefab != null && !Missions.Any(m => m.Prefab.Type == huntingGroundsMissionPrefab.Type))
{
extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations));
}
}
}
}
public void LoadNewLevel()
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
@@ -287,7 +342,7 @@ namespace Barotrauma
nextLevel = map.StartLocation.LevelData;
return TransitionType.End;
}
if (Level.Loaded.EndLocation != null && Level.Loaded.EndLocation.Type.HasOutpost && Level.Loaded.EndOutpost != null)
if (Level.Loaded.EndLocation != null && Level.Loaded.EndLocation.HasOutpost() && Level.Loaded.EndOutpost != null)
{
nextLevel = Level.Loaded.EndLocation.LevelData;
return TransitionType.ProgressToNextLocation;
@@ -306,13 +361,13 @@ namespace Barotrauma
}
else if (leavingSub.AtStartPosition)
{
if (map.CurrentLocation.Type.HasOutpost && Level.Loaded.StartOutpost != null)
if (map.CurrentLocation.HasOutpost() && Level.Loaded.StartOutpost != null)
{
nextLevel = map.CurrentLocation.LevelData;
return TransitionType.ReturnToPreviousLocation;
}
else if (map.SelectedLocation != null && map.SelectedLocation != map.CurrentLocation && !map.CurrentLocation.Type.HasOutpost &&
(Level.Loaded.LevelData != map.SelectedConnection.LevelData))
else if (map.SelectedLocation != null && map.SelectedLocation != map.CurrentLocation && !map.CurrentLocation.HasOutpost() &&
map.SelectedConnection != null && Level.Loaded.LevelData != map.SelectedConnection.LevelData)
{
nextLevel = map.SelectedConnection.LevelData;
return TransitionType.LeaveLocation;
@@ -481,7 +536,7 @@ namespace Barotrauma
{
CrewManager.RemoveCharacterInfo(ci);
}
ci?.ResetCurrentOrder();
ci?.ClearCurrentOrders();
}
foreach (DockingPort port in DockingPort.List)
@@ -511,7 +566,18 @@ namespace Barotrauma
}
Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation));
Map.SelectLocation(-1);
Map.Radiation.Amount = Map.Radiation.Params.StartingRadiation;
foreach (Location location in Map.Locations)
{
location.TurnsInRadiation = 0;
}
EndCampaignProjSpecific();
if (CampaignMetadata != null)
{
int loops = CampaignMetadata.GetInt("campaign.endings", 0);
CampaignMetadata.SetValue("campaign.endings", loops + 1);
}
}
protected virtual void EndCampaignProjSpecific() { }
@@ -547,18 +613,14 @@ namespace Barotrauma
HumanAIController humanAI = npc.AIController as HumanAIController;
if (humanAI == null) { yield return CoroutineStatus.Failure; }
OrderInfo? prevSpeakerOrder = null;
if (humanAI.CurrentOrder != null)
{
prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption);
}
var waitOrder = Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase));
humanAI.SetOrder(waitOrder, option: string.Empty, orderGiver: null, speak: false);
humanAI.SetForcedOrder(waitOrder, string.Empty, null);
var waitObjective = humanAI.ObjectiveManager.ForcedOrder;
humanAI.FaceTarget(interactor);
while (!npc.Removed && !interactor.Removed &&
Vector2.DistanceSquared(npc.WorldPosition, interactor.WorldPosition) < 300.0f * 300.0f &&
humanAI.CurrentOrder == waitOrder &&
humanAI.ObjectiveManager.ForcedOrder == waitObjective &&
humanAI.AllowCampaignInteraction() &&
!interactor.IsIncapacitated)
{
@@ -569,17 +631,7 @@ namespace Barotrauma
ShowCampaignUI = false;
#endif
if (humanAI.CurrentOrder == waitOrder)
{
if (prevSpeakerOrder != null)
{
humanAI.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false);
}
else
{
humanAI.SetOrder(null, string.Empty, orderGiver: null, speak: false);
}
}
humanAI.ClearForcedOrder();
yield return CoroutineStatus.Success;
}
@@ -694,7 +746,7 @@ namespace Barotrauma
public void OutpostNPCAttacked(Character npc, Character attacker, AttackResult attackResult)
{
if (npc == null || attacker == null || npc.IsDead || npc.IsInstigator) { return; }
if (npc.TeamID != Character.TeamType.FriendlyNPC) { return; }
if (npc.TeamID != CharacterTeamType.FriendlyNPC) { return; }
if (!attacker.IsRemotePlayer && attacker != Character.Controlled) { return; }
Location location = Map?.CurrentLocation;
if (location != null)
@@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
namespace Barotrauma
{
class CoOpMode : MissionMode
{
public CoOpMode(GameModePreset preset, MissionPrefab missionPrefab) : base(preset, ValidateMissionPrefab(missionPrefab, MissionPrefab.CoOpMissionClasses)) { }
public CoOpMode(GameModePreset preset, IEnumerable<MissionPrefab> missionPrefabs) : base(preset, ValidateMissionPrefabs(missionPrefabs, MissionPrefab.CoOpMissionClasses)) { }
public CoOpMode(GameModePreset preset, MissionType missionType, string seed) : base(preset, ValidateMissionType(missionType, MissionPrefab.CoOpMissionClasses), seed) { }
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -16,9 +17,9 @@ namespace Barotrauma
get { return GameMain.GameSession?.CrewManager; }
}
public virtual Mission Mission
public virtual IEnumerable<Mission> Missions
{
get { return null; }
get { return Enumerable.Empty<Mission>(); }
}
public bool IsSinglePlayer
@@ -54,6 +55,8 @@ namespace Barotrauma
}
public virtual void ShowStartMessage() { }
public virtual void AddExtraMissions(LevelData levelData) { }
public virtual void AddToGUIUpdateList()
{
@@ -5,37 +5,43 @@ namespace Barotrauma
{
abstract partial class MissionMode : GameMode
{
private readonly Mission mission;
private readonly List<Mission> missions = new List<Mission>();
public override Mission Mission
public override IEnumerable<Mission> Missions
{
get
{
return mission;
return missions;
}
}
public MissionMode(GameModePreset preset, MissionPrefab missionPrefab)
public MissionMode(GameModePreset preset, IEnumerable<MissionPrefab> missionPrefabs)
: base(preset)
{
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
mission = missionPrefab.Instantiate(locations);
foreach (MissionPrefab missionPrefab in missionPrefabs)
{
missions.Add(missionPrefab.Instantiate(locations));
}
}
public MissionMode(GameModePreset preset, MissionType missionType, string seed)
: base(preset)
{
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
mission = Mission.LoadRandom(locations, seed, false, missionType);
missions.Add(Mission.LoadRandom(locations, seed, false, missionType));
}
protected static MissionPrefab ValidateMissionPrefab(MissionPrefab missionPrefab, Dictionary<MissionType, Type> missionClasses)
protected static IEnumerable<MissionPrefab> ValidateMissionPrefabs(IEnumerable<MissionPrefab> missionPrefabs, Dictionary<MissionType, Type> missionClasses)
{
if (ValidateMissionType(missionPrefab.Type, missionClasses) != missionPrefab.Type)
foreach (MissionPrefab missionPrefab in missionPrefabs)
{
throw new InvalidOperationException("Cannot start gamemode with mission type " + missionPrefab.Type);
if (ValidateMissionType(missionPrefab.Type, missionClasses) != missionPrefab.Type)
{
throw new InvalidOperationException("Cannot start gamemode with mission type " + missionPrefab.Type);
}
}
return missionPrefab;
return missionPrefabs;
}
protected static MissionType ValidateMissionType(MissionType missionType, Dictionary<MissionType, Type> missionClasses)
@@ -8,6 +8,8 @@ namespace Barotrauma
{
partial class MultiPlayerCampaign : CampaignMode
{
public const int MinimumInitialMoney = 500;
private UInt16 lastUpdateID;
public UInt16 LastUpdateID
{
@@ -57,7 +59,7 @@ namespace Barotrauma
InitCampaignData();
}
public static MultiPlayerCampaign StartNew(string mapSeed)
public static MultiPlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub)
{
MultiPlayerCampaign campaign = new MultiPlayerCampaign();
//only the server generates the map, the clients load it from a save file
@@ -96,6 +98,9 @@ namespace Barotrauma
private void Load(XElement element)
{
Money = element.GetAttributeInt("money", 0);
PurchasedLostShuttles = element.GetAttributeBool("purchasedlostshuttles", false);
PurchasedHullRepairs = element.GetAttributeBool("purchasedhullrepairs", false);
PurchasedItemRepairs = element.GetAttributeBool("purchaseditemrepairs", false);
CheatsEnabled = element.GetAttributeBool("cheatsenabled", false);
if (CheatsEnabled)
{
@@ -1,11 +1,49 @@
using System;
using Barotrauma.Networking;
using System.Collections.Generic;
namespace Barotrauma
{
class PvPMode : MissionMode
{
public PvPMode(GameModePreset preset, MissionPrefab missionPrefab) : base(preset, ValidateMissionPrefab(missionPrefab, MissionPrefab.PvPMissionClasses)) { }
public PvPMode(GameModePreset preset, IEnumerable<MissionPrefab> missionPrefabs) : base(preset, ValidateMissionPrefabs(missionPrefabs, MissionPrefab.PvPMissionClasses)) { }
public PvPMode(GameModePreset preset, MissionType missionType, string seed) : base(preset, ValidateMissionType(missionType, MissionPrefab.PvPMissionClasses), seed) { }
public void AssignTeamIDs(IEnumerable<Client> clients)
{
int teamWeight = 0;
List<Client> randList = new List<Client>(clients);
for (int i = 0; i < randList.Count; i++)
{
if (randList[i].PreferredTeam == CharacterTeamType.Team1 ||
randList[i].PreferredTeam == CharacterTeamType.Team2)
{
randList[i].TeamID = randList[i].PreferredTeam;
teamWeight += randList[i].PreferredTeam == CharacterTeamType.Team1 ? -1 : 1;
randList.RemoveAt(i);
i--;
}
}
for (int i = 0; i<randList.Count; i++)
{
Client a = randList[i];
int oi = Rand.Range(0, randList.Count - 1);
Client b = randList[oi];
randList[i] = b;
randList[oi] = a;
}
int halfPlayers = (randList.Count / 2) + teamWeight;
for (int i = 0; i < randList.Count; i++)
{
if (i < halfPlayers)
{
randList[i].TeamID = CharacterTeamType.Team1;
}
else
{
randList[i].TeamID = CharacterTeamType.Team2;
}
}
}
}
}
@@ -22,9 +22,10 @@ namespace Barotrauma
public double RoundStartTime;
public Mission Mission { get; private set; }
private readonly List<Mission> missions = new List<Mission>();
public IEnumerable<Mission> Missions { get { return missions; } }
public Character.TeamType? WinningTeam;
public CharacterTeamType? WinningTeam;
public bool IsRunning { get; private set; }
@@ -107,17 +108,17 @@ namespace Barotrauma
{
this.SavePath = savePath;
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
GameMode = InstantiateGameMode(gameModePreset, seed, missionType: missionType);
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, missionType: missionType);
}
/// <summary>
/// Start a new GameSession with a specific pre-selected mission.
/// </summary>
public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string seed = null, MissionPrefab missionPrefab = null)
public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string seed = null, IEnumerable<MissionPrefab> missionPrefabs = null)
: this(submarineInfo)
{
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
GameMode = InstantiateGameMode(gameModePreset, seed, missionPrefab: missionPrefab);
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, missionPrefabs: missionPrefabs);
}
/// <summary>
@@ -158,28 +159,38 @@ namespace Barotrauma
}
}
private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, MissionPrefab missionPrefab = null, MissionType missionType = MissionType.None)
private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, SubmarineInfo selectedSub, IEnumerable<MissionPrefab> missionPrefabs = null, MissionType missionType = MissionType.None)
{
if (gameModePreset.GameModeType == typeof(CoOpMode))
{
return missionPrefab != null ?
new CoOpMode(gameModePreset, missionPrefab) :
return missionPrefabs != null ?
new CoOpMode(gameModePreset, missionPrefabs) :
new CoOpMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8));
}
else if (gameModePreset.GameModeType == typeof(PvPMode))
{
return missionPrefab != null ?
new PvPMode(gameModePreset, missionPrefab) :
return missionPrefabs != null ?
new PvPMode(gameModePreset, missionPrefabs) :
new PvPMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8));
}
else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign))
{
return MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8));
var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub);
if (campaign != null && selectedSub != null)
{
campaign.Money = Math.Max(MultiPlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price);
}
return campaign;
}
#if CLIENT
else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign))
{
return SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8));
var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub);
if (campaign != null && selectedSub != null)
{
campaign.Money = Math.Max(SinglePlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price);
}
return campaign;
}
else if (gameModePreset.GameModeType == typeof(TutorialMode))
{
@@ -200,7 +211,7 @@ namespace Barotrauma
}
}
private void CreateDummyLocations()
private void CreateDummyLocations(LocationType? forceLocationType = null)
{
dummyLocations = new Location[2];
@@ -217,7 +228,7 @@ namespace Barotrauma
MTRandom rand = new MTRandom(ToolBox.StringToInt(seed));
for (int i = 0; i < 2; i++)
{
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true);
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType: forceLocationType);
}
}
@@ -230,7 +241,7 @@ namespace Barotrauma
/// <summary>
/// Switch to another submarine. The sub is loaded when the next round starts.
/// </summary>
public void SwitchSubmarine(SubmarineInfo newSubmarine, int cost)
public SubmarineInfo SwitchSubmarine(SubmarineInfo newSubmarine, int cost)
{
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
@@ -252,6 +263,7 @@ namespace Barotrauma
Campaign.Money -= cost;
((CampaignMode)GameMode).PendingSubmarineSwitch = newSubmarine;
return newSubmarine;
}
public void PurchaseSubmarine(SubmarineInfo newSubmarine)
@@ -271,9 +283,38 @@ namespace Barotrauma
(OwnedSubmarines != null && OwnedSubmarines.Any(os => os.Name == query.Name));
}
public bool IsCurrentLocationRadiated()
{
if (Map?.CurrentLocation == null || Campaign == null) { return false; }
bool isRadiated = Map.CurrentLocation.IsRadiated();
if (Level.Loaded?.EndLocation is { } endLocation)
{
isRadiated |= endLocation.IsRadiated();
}
return isRadiated;
}
public void StartRound(string levelSeed, float? difficulty = null)
{
StartRound(LevelData.CreateRandom(levelSeed, difficulty));
LevelData randomLevel = null;
foreach (Mission mission in Missions.Union(GameMode.Missions))
{
MissionPrefab missionPrefab = mission.Prefab;
if (missionPrefab != null &&
missionPrefab.AllowedLocationTypes.Any() &&
!missionPrefab.AllowedConnectionTypes.Any())
{
LocationType locationType = LocationType.List.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m.Equals(lt.Identifier, StringComparison.OrdinalIgnoreCase)));
CreateDummyLocations(locationType);
randomLevel = LevelData.CreateRandom(levelSeed, difficulty, requireOutpost: true);
break;
}
}
randomLevel ??= LevelData.CreateRandom(levelSeed, difficulty);
StartRound(randomLevel);
}
public void StartRound(LevelData levelData, bool mirrorLevel = false, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null)
@@ -296,17 +337,11 @@ namespace Barotrauma
LevelData = levelData;
if (GameMode is CampaignMode campaignMode && GameMode.Mission != null &&
LevelData != null && LevelData.Type == LevelData.LevelType.Outpost)
{
campaignMode.Map.CurrentLocation.SelectedMission = null;
}
Submarine.Unload();
Submarine = Submarine.MainSub = new Submarine(SubmarineInfo);
foreach (Submarine sub in Submarine.GetConnectedSubs())
{
sub.TeamID = Character.TeamType.Team1;
sub.TeamID = CharacterTeamType.Team1;
foreach (Item item in Item.ItemList)
{
if (item.Submarine != sub) { continue; }
@@ -316,7 +351,7 @@ namespace Barotrauma
}
}
}
if (GameMode.Mission != null && GameMode.Mission.TeamCount > 1 && Submarine.MainSubs[1] == null)
if (GameMode is PvPMode && Submarine.MainSubs[1] == null)
{
Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true);
}
@@ -329,11 +364,6 @@ namespace Barotrauma
InitializeLevel(level);
GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Info.Name);
GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(levelData?.Seed ?? "[NO_LEVEL]"));
GameAnalyticsManager.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start,
GameMode.Preset.Identifier, (Mission == null ? "None" : Mission.GetType().ToString()));
#if CLIENT
if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
@@ -343,7 +373,7 @@ namespace Barotrauma
existingRoundSummary.ContinueButton.Visible = true;
}
RoundSummary = new RoundSummary(Submarine.Info, GameMode, Mission, StartLocation, EndLocation);
RoundSummary = new RoundSummary(Submarine.Info, GameMode, Missions, StartLocation, EndLocation);
if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode))
{
@@ -352,7 +382,16 @@ namespace Barotrauma
{
GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false);
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false);
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), (Mission == null ? TextManager.Get("None") : Mission.Name)), Color.CadetBlue, playSound: false);
if (missions.Count > 1)
{
string joinedMissionNames = string.Join(", ", missions.Select(m => m.Name));
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), joinedMissionNames), Color.CadetBlue, playSound: false);
}
else
{
var mission = missions.FirstOrDefault();
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), mission?.Name ?? TextManager.Get("None")), Color.CadetBlue, playSound: false);
}
}
else
{
@@ -389,16 +428,18 @@ namespace Barotrauma
Entity.Spawner = new EntitySpawner();
if (GameMode.Mission != null) { Mission = GameMode.Mission; }
if (GameMode != null) { GameMode.Start(); }
if (GameMode.Mission != null)
missions.Clear();
GameMode.AddExtraMissions(LevelData);
missions.AddRange(GameMode.Missions);
GameMode.Start();
foreach (Mission mission in missions)
{
int prevEntityCount = Entity.GetEntities().Count();
Mission.Start(Level.Loaded);
mission.Start(Level.Loaded);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount)
{
DebugConsole.ThrowError(
"Entity count has changed after starting a mission as a client. " +
$"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " +
"The clients should not instantiate entities themselves when starting the mission," +
" but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial.");
}
@@ -438,6 +479,8 @@ namespace Barotrauma
}
}
GameMain.Config.RecentlyEncounteredCreatures.Clear();
GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition;
RoundStartTime = Timing.TotalTime;
GameMain.ResetFrameTime();
@@ -501,7 +544,7 @@ namespace Barotrauma
{
Submarine.SetPosition(spawnPos);
myPort.Dock(outPostPort);
myPort.Lock(true);
myPort.Lock(true, forcePosition: true, applyEffects: false);
}
else
{
@@ -540,21 +583,32 @@ namespace Barotrauma
{
EventManager?.Update(deltaTime);
GameMode?.Update(deltaTime);
Mission?.Update(deltaTime);
foreach (Mission mission in missions)
{
mission.Update(deltaTime);
}
UpdateProjSpecific(deltaTime);
}
public Mission GetMission(int index)
{
if (index < 0 || index >= missions.Count) { return null; }
return missions[index];
}
public int GetMissionIndex(Mission mission)
{
return missions.IndexOf(mission);
}
partial void UpdateProjSpecific(float deltaTime);
public void EndRound(string endMessage, List<TraitorMissionResult> traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None)
{
if (Mission != null) { Mission.End(); }
GameAnalyticsManager.AddProgressionEvent(
(Mission == null || Mission.Completed) ? GameAnalyticsSDK.Net.EGAProgressionStatus.Complete : GameAnalyticsSDK.Net.EGAProgressionStatus.Fail,
GameMode.Preset.Identifier,
Mission == null ? "None" : Mission.GetType().ToString());
foreach (Mission mission in missions)
{
mission.End();
}
#if CLIENT
if (GUI.PauseMenuOpen)
{
@@ -573,14 +627,14 @@ namespace Barotrauma
if (GameMain.NetLobbyScreen != null) GameMain.NetLobbyScreen.OnRoundEnded();
TabMenu.OnRoundEnded();
GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction");
GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb));
#endif
SteamAchievementManager.OnRoundEnded(this);
GameMode?.End(transitionType);
EventManager?.EndRound();
StatusEffect.StopAll();
Mission = null;
missions.Clear();
IsRunning = false;
}
@@ -531,7 +531,7 @@ namespace Barotrauma
List<int> levels = new List<int>();
foreach (XElement subElement in elements)
{
if (!category.CanBeApplied(subElement)) { continue; }
if (!category.CanBeApplied(subElement, prefab)) { continue; }
foreach (XElement component in subElement.Elements())
{

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